001 // License: GPL. Copyright 2008 by David Earl and others
002 package org.openstreetmap.josm.tools;
003
004 import static org.openstreetmap.josm.tools.I18n.tr;
005
006 import java.io.IOException;
007 import java.net.URL;
008
009 import javax.sound.sampled.AudioFormat;
010 import javax.sound.sampled.AudioInputStream;
011 import javax.sound.sampled.AudioSystem;
012 import javax.sound.sampled.DataLine;
013 import javax.sound.sampled.SourceDataLine;
014 import javax.swing.JOptionPane;
015
016 import org.openstreetmap.josm.Main;
017
018 /**
019 * Creates and controls a separate audio player thread.
020 *
021 * @author David Earl <david@frankieandshadow.com>
022 *
023 */
024 public class AudioPlayer extends Thread {
025
026 private static AudioPlayer audioPlayer = null;
027
028 private enum State { INITIALIZING, NOTPLAYING, PLAYING, PAUSED, INTERRUPTED }
029 private State state;
030 private enum Command { PLAY, PAUSE }
031 private enum Result { WAITING, OK, FAILED }
032 private URL playingUrl;
033 private double leadIn; // seconds
034 private double calibration; // ratio of purported duration of samples to true duration
035 private double position; // seconds
036 private double bytesPerSecond;
037 private static long chunk = 4000; /* bytes */
038 private double speed = 1.0;
039
040 /**
041 * Passes information from the control thread to the playing thread
042 */
043 private class Execute {
044 private Command command;
045 private Result result;
046 private Exception exception;
047 private URL url;
048 private double offset; // seconds
049 private double speed; // ratio
050
051 /*
052 * Called to execute the commands in the other thread
053 */
054 protected void play(URL url, double offset, double speed) throws Exception {
055 this.url = url;
056 this.offset = offset;
057 this.speed = speed;
058 command = Command.PLAY;
059 result = Result.WAITING;
060 send();
061 }
062 protected void pause() throws Exception {
063 command = Command.PAUSE;
064 send();
065 }
066 private void send() throws Exception {
067 result = Result.WAITING;
068 interrupt();
069 while (result == Result.WAITING) { sleep(10); /* yield(); */ }
070 if (result == Result.FAILED)
071 throw exception;
072 }
073 private void possiblyInterrupt() throws InterruptedException {
074 if (interrupted() || result == Result.WAITING)
075 throw new InterruptedException();
076 }
077 protected void failed (Exception e) {
078 exception = e;
079 result = Result.FAILED;
080 state = State.NOTPLAYING;
081 }
082 protected void ok (State newState) {
083 result = Result.OK;
084 state = newState;
085 }
086 protected double offset() {
087 return offset;
088 }
089 protected double speed() {
090 return speed;
091 }
092 protected URL url() {
093 return url;
094 }
095 protected Command command() {
096 return command;
097 }
098 }
099
100 private Execute command;
101
102 /**
103 * Plays a WAV audio file from the beginning. See also the variant which doesn't
104 * start at the beginning of the stream
105 * @param url The resource to play, which must be a WAV file or stream
106 * @throws audio fault exception, e.g. can't open stream, unhandleable audio format
107 */
108 public static void play(URL url) throws Exception {
109 AudioPlayer.get().command.play(url, 0.0, 1.0);
110 }
111
112 /**
113 * Plays a WAV audio file from a specified position.
114 * @param url The resource to play, which must be a WAV file or stream
115 * @param seconds The number of seconds into the audio to start playing
116 * @throws audio fault exception, e.g. can't open stream, unhandleable audio format
117 */
118 public static void play(URL url, double seconds) throws Exception {
119 AudioPlayer.get().command.play(url, seconds, 1.0);
120 }
121
122 /**
123 * Plays a WAV audio file from a specified position at variable speed.
124 * @param url The resource to play, which must be a WAV file or stream
125 * @param seconds The number of seconds into the audio to start playing
126 * @param speed Rate at which audio playes (1.0 = real time, > 1 is faster)
127 * @throws audio fault exception, e.g. can't open stream, unhandleable audio format
128 */
129 public static void play(URL url, double seconds, double speed) throws Exception {
130 AudioPlayer.get().command.play(url, seconds, speed);
131 }
132
133 /**
134 * Pauses the currently playing audio stream. Does nothing if nothing playing.
135 * @throws audio fault exception, e.g. can't open stream, unhandleable audio format
136 */
137 public static void pause() throws Exception {
138 AudioPlayer.get().command.pause();
139 }
140
141 /**
142 * To get the Url of the playing or recently played audio.
143 * @return url - could be null
144 */
145 public static URL url() {
146 return AudioPlayer.get().playingUrl;
147 }
148
149 /**
150 * Whether or not we are paused.
151 * @return boolean whether or not paused
152 */
153 public static boolean paused() {
154 return AudioPlayer.get().state == State.PAUSED;
155 }
156
157 /**
158 * Whether or not we are playing.
159 * @return boolean whether or not playing
160 */
161 public static boolean playing() {
162 return AudioPlayer.get().state == State.PLAYING;
163 }
164
165 /**
166 * How far we are through playing, in seconds.
167 * @return double seconds
168 */
169 public static double position() {
170 return AudioPlayer.get().position;
171 }
172
173 /**
174 * Speed at which we will play.
175 * @return double, speed multiplier
176 */
177 public static double speed() {
178 return AudioPlayer.get().speed;
179 }
180
181 /**
182 * gets the singleton object, and if this is the first time, creates it along with
183 * the thread to support audio
184 */
185 private static AudioPlayer get() {
186 if (audioPlayer != null)
187 return audioPlayer;
188 try {
189 audioPlayer = new AudioPlayer();
190 return audioPlayer;
191 } catch (Exception ex) {
192 return null;
193 }
194 }
195
196 public static void reset() {
197 if(audioPlayer != null)
198 {
199 try {
200 pause();
201 } catch(Exception e) {}
202 audioPlayer.playingUrl = null;
203 }
204 }
205
206 private AudioPlayer() {
207 state = State.INITIALIZING;
208 command = new Execute();
209 playingUrl = null;
210 leadIn = Main.pref.getDouble("audio.leadin", "1.0" /* default, seconds */);
211 calibration = Main.pref.getDouble("audio.calibration", "1.0" /* default, ratio */);
212 start();
213 while (state == State.INITIALIZING) { yield(); }
214 }
215
216 /**
217 * Starts the thread to actually play the audio, per Thread interface
218 * Not to be used as public, though Thread interface doesn't allow it to be made private
219 */
220 @Override public void run() {
221 /* code running in separate thread */
222
223 playingUrl = null;
224 AudioInputStream audioInputStream = null;
225 SourceDataLine audioOutputLine = null;
226 AudioFormat audioFormat = null;
227 byte[] abData = new byte[(int)chunk];
228
229 for (;;) {
230 try {
231 switch (state) {
232 case INITIALIZING:
233 // we're ready to take interrupts
234 state = State.NOTPLAYING;
235 break;
236 case NOTPLAYING:
237 case PAUSED:
238 sleep(200);
239 break;
240 case PLAYING:
241 command.possiblyInterrupt();
242 for(;;) {
243 int nBytesRead = 0;
244 nBytesRead = audioInputStream.read(abData, 0, abData.length);
245 position += nBytesRead / bytesPerSecond;
246 command.possiblyInterrupt();
247 if (nBytesRead < 0) { break; }
248 audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten
249 command.possiblyInterrupt();
250 }
251 // end of audio, clean up
252 audioOutputLine.drain();
253 audioOutputLine.close();
254 audioOutputLine = null;
255 audioInputStream.close();
256 audioInputStream = null;
257 playingUrl = null;
258 state = State.NOTPLAYING;
259 command.possiblyInterrupt();
260 break;
261 }
262 } catch (InterruptedException e) {
263 interrupted(); // just in case we get an interrupt
264 State stateChange = state;
265 state = State.INTERRUPTED;
266 try {
267 switch (command.command()) {
268 case PLAY:
269 double offset = command.offset();
270 speed = command.speed();
271 if (playingUrl != command.url() ||
272 stateChange != State.PAUSED ||
273 offset != 0.0)
274 {
275 if (audioInputStream != null) {
276 audioInputStream.close();
277 audioInputStream = null;
278 }
279 playingUrl = command.url();
280 audioInputStream = AudioSystem.getAudioInputStream(playingUrl);
281 audioFormat = audioInputStream.getFormat();
282 long nBytesRead = 0;
283 position = 0.0;
284 offset -= leadIn;
285 double calibratedOffset = offset * calibration;
286 bytesPerSecond = audioFormat.getFrameRate() /* frames per second */
287 * audioFormat.getFrameSize() /* bytes per frame */;
288 if (speed * bytesPerSecond > 256000.0) {
289 speed = 256000 / bytesPerSecond;
290 }
291 if (calibratedOffset > 0.0) {
292 long bytesToSkip = (long)(
293 calibratedOffset /* seconds (double) */ * bytesPerSecond);
294 /* skip doesn't seem to want to skip big chunks, so
295 * reduce it to smaller ones
296 */
297 // audioInputStream.skip(bytesToSkip);
298 while (bytesToSkip > chunk) {
299 nBytesRead = audioInputStream.skip(chunk);
300 if (nBytesRead <= 0)
301 throw new IOException(tr("This is after the end of the recording"));
302 bytesToSkip -= nBytesRead;
303 }
304 if (bytesToSkip > 0) {
305 audioInputStream.skip(bytesToSkip);
306 }
307 position = offset;
308 }
309 if (audioOutputLine != null) {
310 audioOutputLine.close();
311 }
312 audioFormat = new AudioFormat(audioFormat.getEncoding(),
313 audioFormat.getSampleRate() * (float) (speed * calibration),
314 audioFormat.getSampleSizeInBits(),
315 audioFormat.getChannels(),
316 audioFormat.getFrameSize(),
317 audioFormat.getFrameRate() * (float) (speed * calibration),
318 audioFormat.isBigEndian());
319 DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
320 audioOutputLine = (SourceDataLine) AudioSystem.getLine(info);
321 audioOutputLine.open(audioFormat);
322 audioOutputLine.start();
323 }
324 stateChange = State.PLAYING;
325 break;
326 case PAUSE:
327 stateChange = State.PAUSED;
328 break;
329 }
330 command.ok(stateChange);
331 } catch (Exception startPlayingException) {
332 command.failed(startPlayingException); // sets state
333 }
334 } catch (Exception e) {
335 state = State.NOTPLAYING;
336 }
337 }
338 }
339
340 public static void audioMalfunction(Exception ex) {
341 String msg = ex.getMessage();
342 if(msg == null)
343 msg = tr("unspecified reason");
344 else
345 msg = tr(msg);
346 JOptionPane.showMessageDialog(Main.parent,
347 "<html><p>" + msg + "</p></html>",
348 tr("Error playing sound"), JOptionPane.ERROR_MESSAGE);
349 }
350 }