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

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

Sonar: various code style cleanup:

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