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

Last change on this file since 12124 was 11746, checked in by Don-vip, 7 years ago

PMD - Strict Exceptions

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