source: josm/trunk/src/org/openstreetmap/josm/tools/AudioPlayer.java@ 7003

Last change on this file since 7003 was 6830, checked in by Don-vip, 10 years ago

javadoc fixes for jdk8 compatibility

  • Property svn:eol-style set to native
File size: 14.0 KB
RevLine 
[6380]1// License: GPL. For details, see LICENSE file.
[626]2package org.openstreetmap.josm.tools;
3
[547]4import static org.openstreetmap.josm.tools.I18n.tr;
5
[582]6import java.io.IOException;
7import java.net.URL;
8
9import javax.sound.sampled.AudioFormat;
10import javax.sound.sampled.AudioInputStream;
11import javax.sound.sampled.AudioSystem;
12import javax.sound.sampled.DataLine;
[559]13import javax.sound.sampled.SourceDataLine;
[547]14import javax.swing.JOptionPane;
15
16import org.openstreetmap.josm.Main;
[626]17
18/**
19 * Creates and controls a separate audio player thread.
[1169]20 *
[6830]21 * @author David Earl <david@frankieandshadow.com>
[5871]22 * @since 547
[626]23 */
[6362]24public final class AudioPlayer extends Thread {
[626]25
[1169]26 private static AudioPlayer audioPlayer = null;
[547]27
[1169]28 private enum State { INITIALIZING, NOTPLAYING, PLAYING, PAUSED, INTERRUPTED }
29 private State state;
[549]30 private enum Command { PLAY, PAUSE }
[547]31 private enum Result { WAITING, OK, FAILED }
32 private URL playingUrl;
33 private double leadIn; // seconds
[557]34 private double calibration; // ratio of purported duration of samples to true duration
[1169]35 private double position; // seconds
36 private double bytesPerSecond;
37 private static long chunk = 4000; /* bytes */
38 private double speed = 1.0;
[553]39
[1169]40 /**
41 * Passes information from the control thread to the playing thread
42 */
43 private class Execute {
44 private Command command;
45 private Result result;
46 private Exception exception;
47 private URL url;
48 private double offset; // seconds
49 private double speed; // ratio
[626]50
[1169]51 /*
52 * Called to execute the commands in the other thread
53 */
54 protected void play(URL url, double offset, double speed) throws Exception {
55 this.url = url;
56 this.offset = offset;
57 this.speed = speed;
58 command = Command.PLAY;
59 result = Result.WAITING;
60 send();
61 }
62 protected void pause() throws Exception {
63 command = Command.PAUSE;
64 send();
65 }
66 private void send() throws Exception {
67 result = Result.WAITING;
68 interrupt();
69 while (result == Result.WAITING) { sleep(10); /* yield(); */ }
[1865]70 if (result == Result.FAILED)
71 throw exception;
[1169]72 }
73 private void possiblyInterrupt() throws InterruptedException {
74 if (interrupted() || result == Result.WAITING)
75 throw new InterruptedException();
76 }
77 protected void failed (Exception e) {
78 exception = e;
79 result = Result.FAILED;
80 state = State.NOTPLAYING;
81 }
82 protected void ok (State newState) {
83 result = Result.OK;
84 state = newState;
85 }
86 protected double offset() {
87 return offset;
88 }
89 protected double speed() {
90 return speed;
91 }
92 protected URL url() {
93 return url;
94 }
95 protected Command command() {
96 return command;
97 }
98 }
[547]99
[1169]100 private Execute command;
[547]101
[1169]102 /**
103 * Plays a WAV audio file from the beginning. See also the variant which doesn't
104 * start at the beginning of the stream
105 * @param url The resource to play, which must be a WAV file or stream
[5871]106 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format
[1169]107 */
108 public static void play(URL url) throws Exception {
109 AudioPlayer.get().command.play(url, 0.0, 1.0);
110 }
[563]111
[1169]112 /**
113 * Plays a WAV audio file from a specified position.
114 * @param url The resource to play, which must be a WAV file or stream
115 * @param seconds The number of seconds into the audio to start playing
[5871]116 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format
[1169]117 */
118 public static void play(URL url, double seconds) throws Exception {
119 AudioPlayer.get().command.play(url, seconds, 1.0);
120 }
[547]121
[1169]122 /**
123 * Plays a WAV audio file from a specified position at variable speed.
124 * @param url The resource to play, which must be a WAV file or stream
125 * @param seconds The number of seconds into the audio to start playing
[6830]126 * @param speed Rate at which audio playes (1.0 = real time, > 1 is faster)
[5871]127 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format
[1169]128 */
129 public static void play(URL url, double seconds, double speed) throws Exception {
130 AudioPlayer.get().command.play(url, seconds, speed);
131 }
[547]132
[1169]133 /**
134 * Pauses the currently playing audio stream. Does nothing if nothing playing.
[5871]135 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format
[1169]136 */
137 public static void pause() throws Exception {
138 AudioPlayer.get().command.pause();
139 }
[547]140
[1169]141 /**
142 * To get the Url of the playing or recently played audio.
143 * @return url - could be null
144 */
145 public static URL url() {
146 return AudioPlayer.get().playingUrl;
147 }
[547]148
[1169]149 /**
150 * Whether or not we are paused.
151 * @return boolean whether or not paused
152 */
153 public static boolean paused() {
154 return AudioPlayer.get().state == State.PAUSED;
155 }
156
157 /**
158 * Whether or not we are playing.
159 * @return boolean whether or not playing
160 */
161 public static boolean playing() {
162 return AudioPlayer.get().state == State.PLAYING;
163 }
164
165 /**
166 * How far we are through playing, in seconds.
167 * @return double seconds
168 */
169 public static double position() {
170 return AudioPlayer.get().position;
171 }
172
173 /**
174 * Speed at which we will play.
175 * @return double, speed multiplier
176 */
177 public static double speed() {
178 return AudioPlayer.get().speed;
179 }
180
181 /**
182 * gets the singleton object, and if this is the first time, creates it along with
183 * the thread to support audio
184 */
185 private static AudioPlayer get() {
186 if (audioPlayer != null)
187 return audioPlayer;
188 try {
189 audioPlayer = new AudioPlayer();
190 return audioPlayer;
191 } catch (Exception ex) {
192 return null;
193 }
194 }
195
[5871]196 /**
197 * Resets the audio player.
198 */
[1685]199 public static void reset() {
[6310]200 if(audioPlayer != null) {
[1685]201 try {
202 pause();
[6310]203 } catch(Exception e) {
204 Main.warn(e);
205 }
[1685]206 audioPlayer.playingUrl = null;
207 }
208 }
209
[1169]210 private AudioPlayer() {
211 state = State.INITIALIZING;
212 command = new Execute();
213 playingUrl = null;
[5871]214 leadIn = Main.pref.getDouble("audio.leadin", 1.0 /* default, seconds */);
215 calibration = Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */);
[1169]216 start();
217 while (state == State.INITIALIZING) { yield(); }
218 }
219
220 /**
221 * Starts the thread to actually play the audio, per Thread interface
222 * Not to be used as public, though Thread interface doesn't allow it to be made private
223 */
224 @Override public void run() {
225 /* code running in separate thread */
226
227 playingUrl = null;
228 AudioInputStream audioInputStream = null;
229 SourceDataLine audioOutputLine = null;
230 AudioFormat audioFormat = null;
231 byte[] abData = new byte[(int)chunk];
232
233 for (;;) {
234 try {
235 switch (state) {
[1865]236 case INITIALIZING:
237 // we're ready to take interrupts
238 state = State.NOTPLAYING;
239 break;
240 case NOTPLAYING:
241 case PAUSED:
242 sleep(200);
243 break;
244 case PLAYING:
[1169]245 command.possiblyInterrupt();
[1865]246 for(;;) {
247 int nBytesRead = 0;
248 nBytesRead = audioInputStream.read(abData, 0, abData.length);
249 position += nBytesRead / bytesPerSecond;
250 command.possiblyInterrupt();
251 if (nBytesRead < 0) { break; }
252 audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten
253 command.possiblyInterrupt();
254 }
255 // end of audio, clean up
256 audioOutputLine.drain();
257 audioOutputLine.close();
258 audioOutputLine = null;
[5874]259 Utils.close(audioInputStream);
[1865]260 audioInputStream = null;
261 playingUrl = null;
262 state = State.NOTPLAYING;
[1169]263 command.possiblyInterrupt();
[1865]264 break;
[1169]265 }
266 } catch (InterruptedException e) {
267 interrupted(); // just in case we get an interrupt
268 State stateChange = state;
269 state = State.INTERRUPTED;
270 try {
271 switch (command.command()) {
[1865]272 case PLAY:
273 double offset = command.offset();
274 speed = command.speed();
275 if (playingUrl != command.url() ||
276 stateChange != State.PAUSED ||
277 offset != 0.0)
278 {
279 if (audioInputStream != null) {
[5874]280 Utils.close(audioInputStream);
[1865]281 audioInputStream = null;
282 }
283 playingUrl = command.url();
284 audioInputStream = AudioSystem.getAudioInputStream(playingUrl);
285 audioFormat = audioInputStream.getFormat();
286 long nBytesRead = 0;
287 position = 0.0;
288 offset -= leadIn;
289 double calibratedOffset = offset * calibration;
290 bytesPerSecond = audioFormat.getFrameRate() /* frames per second */
[1169]291 * audioFormat.getFrameSize() /* bytes per frame */;
[1865]292 if (speed * bytesPerSecond > 256000.0) {
293 speed = 256000 / bytesPerSecond;
[1169]294 }
[1865]295 if (calibratedOffset > 0.0) {
296 long bytesToSkip = (long)(
297 calibratedOffset /* seconds (double) */ * bytesPerSecond);
298 /* skip doesn't seem to want to skip big chunks, so
299 * reduce it to smaller ones
300 */
301 // audioInputStream.skip(bytesToSkip);
302 while (bytesToSkip > chunk) {
303 nBytesRead = audioInputStream.skip(chunk);
304 if (nBytesRead <= 0)
305 throw new IOException(tr("This is after the end of the recording"));
306 bytesToSkip -= nBytesRead;
307 }
[6246]308 while (bytesToSkip > 0) {
309 long skippedBytes = audioInputStream.skip(bytesToSkip);
310 bytesToSkip -= skippedBytes;
311 if (skippedBytes == 0) {
312 // Avoid inifinite loop
313 Main.warn("Unable to skip bytes from audio input stream");
314 bytesToSkip = 0;
315 }
[1865]316 }
317 position = offset;
318 }
319 if (audioOutputLine != null) {
320 audioOutputLine.close();
321 }
322 audioFormat = new AudioFormat(audioFormat.getEncoding(),
[1169]323 audioFormat.getSampleRate() * (float) (speed * calibration),
324 audioFormat.getSampleSizeInBits(),
325 audioFormat.getChannels(),
326 audioFormat.getFrameSize(),
327 audioFormat.getFrameRate() * (float) (speed * calibration),
328 audioFormat.isBigEndian());
[1865]329 DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
330 audioOutputLine = (SourceDataLine) AudioSystem.getLine(info);
331 audioOutputLine.open(audioFormat);
332 audioOutputLine.start();
333 }
334 stateChange = State.PLAYING;
335 break;
336 case PAUSE:
337 stateChange = State.PAUSED;
338 break;
[1169]339 }
340 command.ok(stateChange);
341 } catch (Exception startPlayingException) {
342 command.failed(startPlayingException); // sets state
343 }
344 } catch (Exception e) {
345 state = State.NOTPLAYING;
346 }
347 }
348 }
349
[5871]350 /**
[6070]351 * Shows a popup audio error message for the given exception.
[5871]352 * @param ex The exception used as error reason. Cannot be {@code null}.
353 */
[1169]354 public static void audioMalfunction(Exception ex) {
[3264]355 String msg = ex.getMessage();
356 if(msg == null)
357 msg = tr("unspecified reason");
358 else
359 msg = tr(msg);
[2017]360 JOptionPane.showMessageDialog(Main.parent,
[3264]361 "<html><p>" + msg + "</p></html>",
[1169]362 tr("Error playing sound"), JOptionPane.ERROR_MESSAGE);
363 }
[547]364}
Note: See TracBrowser for help on using the repository browser.