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

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

sonar - squid:S2259 - Null pointers should not be dereferenced

  • Property svn:eol-style set to native
File size: 15.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.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 Exception {
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 Exception {
70 command = Command.PAUSE;
71 send();
72 }
73
74 private void send() throws Exception {
75 result = Result.WAITING;
76 interrupt();
77 while (result == Result.WAITING) {
78 sleep(10);
79 }
80 if (result == Result.FAILED)
81 throw 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 Exception audio fault exception, e.g. can't open stream, unhandleable audio format
124 */
125 public static void play(URL url) throws Exception {
126 AudioPlayer instance = AudioPlayer.getInstance();
127 if (instance != null)
128 instance.command.play(url, 0.0, 1.0);
129 }
130
131 /**
132 * Plays a WAV audio file from a specified position.
133 * @param url The resource to play, which must be a WAV file or stream
134 * @param seconds The number of seconds into the audio to start playing
135 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format
136 */
137 public static void play(URL url, double seconds) throws Exception {
138 AudioPlayer instance = AudioPlayer.getInstance();
139 if (instance != null)
140 instance.command.play(url, seconds, 1.0);
141 }
142
143 /**
144 * Plays a WAV audio file from a specified position at variable speed.
145 * @param url The resource to play, which must be a WAV file or stream
146 * @param seconds The number of seconds into the audio to start playing
147 * @param speed Rate at which audio playes (1.0 = real time, > 1 is faster)
148 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format
149 */
150 public static void play(URL url, double seconds, double speed) throws Exception {
151 AudioPlayer instance = AudioPlayer.getInstance();
152 if (instance != null)
153 instance.command.play(url, seconds, speed);
154 }
155
156 /**
157 * Pauses the currently playing audio stream. Does nothing if nothing playing.
158 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format
159 */
160 public static void pause() throws Exception {
161 AudioPlayer instance = AudioPlayer.getInstance();
162 if (instance != null)
163 instance.command.pause();
164 }
165
166 /**
167 * To get the Url of the playing or recently played audio.
168 * @return url - could be null
169 */
170 public static URL url() {
171 AudioPlayer instance = AudioPlayer.getInstance();
172 return instance == null ? null : instance.playingUrl;
173 }
174
175 /**
176 * Whether or not we are paused.
177 * @return boolean whether or not paused
178 */
179 public static boolean paused() {
180 AudioPlayer instance = AudioPlayer.getInstance();
181 return instance == null ? false : (instance.state == State.PAUSED);
182 }
183
184 /**
185 * Whether or not we are playing.
186 * @return boolean whether or not playing
187 */
188 public static boolean playing() {
189 AudioPlayer instance = AudioPlayer.getInstance();
190 return instance == null ? false : (instance.state == State.PLAYING);
191 }
192
193 /**
194 * How far we are through playing, in seconds.
195 * @return double seconds
196 */
197 public static double position() {
198 AudioPlayer instance = AudioPlayer.getInstance();
199 return instance == null ? -1 : instance.position;
200 }
201
202 /**
203 * Speed at which we will play.
204 * @return double, speed multiplier
205 */
206 public static double speed() {
207 AudioPlayer instance = AudioPlayer.getInstance();
208 return instance == null ? -1 : instance.speed;
209 }
210
211 /**
212 * Returns the singleton object, and if this is the first time, creates it along with
213 * the thread to support audio
214 * @return the unique instance
215 */
216 private static AudioPlayer getInstance() {
217 if (audioPlayer != null)
218 return audioPlayer;
219 try {
220 audioPlayer = new AudioPlayer();
221 return audioPlayer;
222 } catch (RuntimeException ex) {
223 Main.error(ex);
224 return null;
225 }
226 }
227
228 /**
229 * Resets the audio player.
230 */
231 public static void reset() {
232 if (audioPlayer != null) {
233 try {
234 pause();
235 } catch (Exception e) {
236 Main.warn(e);
237 }
238 audioPlayer.playingUrl = null;
239 }
240 }
241
242 private AudioPlayer() {
243 state = State.INITIALIZING;
244 command = new Execute();
245 playingUrl = null;
246 leadIn = Main.pref.getDouble("audio.leadin", 1.0 /* default, seconds */);
247 calibration = Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */);
248 start();
249 while (state == State.INITIALIZING) {
250 yield();
251 }
252 }
253
254 /**
255 * Starts the thread to actually play the audio, per Thread interface
256 * Not to be used as public, though Thread interface doesn't allow it to be made private
257 */
258 @Override public void run() {
259 /* code running in separate thread */
260
261 playingUrl = null;
262 AudioInputStream audioInputStream = null;
263 SourceDataLine audioOutputLine = null;
264 AudioFormat audioFormat;
265 byte[] abData = new byte[(int) chunk];
266
267 for (;;) {
268 try {
269 switch (state) {
270 case INITIALIZING:
271 // we're ready to take interrupts
272 state = State.NOTPLAYING;
273 break;
274 case NOTPLAYING:
275 case PAUSED:
276 sleep(200);
277 break;
278 case PLAYING:
279 command.possiblyInterrupt();
280 for (;;) {
281 int nBytesRead;
282 nBytesRead = audioInputStream.read(abData, 0, abData.length);
283 position += nBytesRead / bytesPerSecond;
284 command.possiblyInterrupt();
285 if (nBytesRead < 0) {
286 break;
287 }
288 audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten
289 command.possiblyInterrupt();
290 }
291 // end of audio, clean up
292 audioOutputLine.drain();
293 audioOutputLine.close();
294 audioOutputLine = null;
295 Utils.close(audioInputStream);
296 audioInputStream = null;
297 playingUrl = null;
298 state = State.NOTPLAYING;
299 command.possiblyInterrupt();
300 break;
301 default: // Do nothing
302 }
303 } catch (InterruptedException e) {
304 interrupted(); // just in case we get an interrupt
305 State stateChange = state;
306 state = State.INTERRUPTED;
307 try {
308 switch (command.command()) {
309 case PLAY:
310 double offset = command.offset();
311 speed = command.speed();
312 if (playingUrl != command.url() ||
313 stateChange != State.PAUSED ||
314 offset != 0) {
315 if (audioInputStream != null) {
316 Utils.close(audioInputStream);
317 }
318 playingUrl = command.url();
319 audioInputStream = AudioSystem.getAudioInputStream(playingUrl);
320 audioFormat = audioInputStream.getFormat();
321 long nBytesRead;
322 position = 0.0;
323 offset -= leadIn;
324 double calibratedOffset = offset * calibration;
325 bytesPerSecond = audioFormat.getFrameRate() /* frames per second */
326 * audioFormat.getFrameSize() /* bytes per frame */;
327 if (speed * bytesPerSecond > 256_000.0) {
328 speed = 256_000 / bytesPerSecond;
329 }
330 if (calibratedOffset > 0.0) {
331 long bytesToSkip = (long) (calibratedOffset /* seconds (double) */ * bytesPerSecond);
332 // skip doesn't seem to want to skip big chunks, so reduce it to smaller ones
333 while (bytesToSkip > chunk) {
334 nBytesRead = audioInputStream.skip(chunk);
335 if (nBytesRead <= 0)
336 throw new IOException(tr("This is after the end of the recording"));
337 bytesToSkip -= nBytesRead;
338 }
339 while (bytesToSkip > 0) {
340 long skippedBytes = audioInputStream.skip(bytesToSkip);
341 bytesToSkip -= skippedBytes;
342 if (skippedBytes == 0) {
343 // Avoid inifinite loop
344 Main.warn("Unable to skip bytes from audio input stream");
345 bytesToSkip = 0;
346 }
347 }
348 position = offset;
349 }
350 if (audioOutputLine != null) {
351 audioOutputLine.close();
352 }
353 audioFormat = new AudioFormat(audioFormat.getEncoding(),
354 audioFormat.getSampleRate() * (float) (speed * calibration),
355 audioFormat.getSampleSizeInBits(),
356 audioFormat.getChannels(),
357 audioFormat.getFrameSize(),
358 audioFormat.getFrameRate() * (float) (speed * calibration),
359 audioFormat.isBigEndian());
360 DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
361 audioOutputLine = (SourceDataLine) AudioSystem.getLine(info);
362 audioOutputLine.open(audioFormat);
363 audioOutputLine.start();
364 }
365 stateChange = State.PLAYING;
366 break;
367 case PAUSE:
368 stateChange = State.PAUSED;
369 break;
370 default: // Do nothing
371 }
372 command.ok(stateChange);
373 } catch (LineUnavailableException | IOException | UnsupportedAudioFileException |
374 SecurityException | IllegalArgumentException startPlayingException) {
375 Main.error(startPlayingException);
376 command.failed(startPlayingException); // sets state
377 }
378 } catch (IOException e) {
379 state = State.NOTPLAYING;
380 Main.error(e);
381 }
382 }
383 }
384
385 /**
386 * Shows a popup audio error message for the given exception.
387 * @param ex The exception used as error reason. Cannot be {@code null}.
388 */
389 public static void audioMalfunction(Exception ex) {
390 String msg = ex.getMessage();
391 if (msg == null)
392 msg = tr("unspecified reason");
393 else
394 msg = tr(msg);
395 Main.error(msg);
396 if (!GraphicsEnvironment.isHeadless()) {
397 JOptionPane.showMessageDialog(Main.parent,
398 "<html><p>" + msg + "</p></html>",
399 tr("Error playing sound"), JOptionPane.ERROR_MESSAGE);
400 }
401 }
402}
Note: See TracBrowser for help on using the repository browser.