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

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

fix Sonar issue squid:S2444 - Lazy initialization of "static" fields should be "synchronized"

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