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

Last change on this file since 8795 was 8510, checked in by Don-vip, 9 years ago

checkstyle: enable relevant whitespace checks and fix them

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