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

Last change on this file since 553 was 553, checked in by david, 16 years ago

(a) add preference dialog, (b) add audio tracing

File size: 8.3 KB
Line 
1// License: GPL. Copyright 2008 by David Earl and others
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.lang.Thread;
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.SourceDataLine;
14import javax.swing.JOptionPane;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.data.Preferences;
18
19/**
20 * Creates and controls a separate audio player thread.
21 *
22 * @author David Earl <david@frankieandshadow.com>
23 *
24 */
25public class AudioPlayer extends Thread {
26
27 private static AudioPlayer audioPlayer = null;
28
29 private enum State { INITIALIZING, NOTPLAYING, PLAYING, PAUSED, INTERRUPTED }
30 private State state;
31 private enum Command { PLAY, PAUSE }
32 private enum Result { WAITING, OK, FAILED }
33 private URL playingUrl;
34 private double leadIn; // seconds
35 private double position; // seconds
36 private double bytesPerSecond;
37
38 /**
39 * Passes information from the control thread to the playing thread
40 */
41 private class Execute {
42 private Command command;
43 private Result result;
44 private Exception exception;
45 private URL url;
46 private double offset; // seconds
47
48 /*
49 * Called to execute the commands in the other thread
50 */
51 protected void play(URL url, double offset) throws Exception {
52 this.url = url;
53 this.offset = offset;
54 command = Command.PLAY;
55 result = Result.WAITING;
56 send();
57 }
58 protected void pause() throws Exception {
59 command = Command.PAUSE;
60 send();
61 }
62 private void send() throws Exception {
63 result = Result.WAITING;
64 interrupt();
65 while (result == Result.WAITING) { sleep(10); /* yield(); */ }
66 if (result == Result.FAILED) { throw exception; }
67 }
68 private void possiblyInterrupt() throws InterruptedException {
69 if (interrupted() || result == Result.WAITING)
70 throw new InterruptedException();
71 }
72 protected void failed (Exception e) {
73 exception = e;
74 result = Result.FAILED;
75 state = State.NOTPLAYING;
76 }
77 protected void ok (State newState) {
78 result = Result.OK;
79 state = newState;
80 }
81 protected double offset() {
82 return offset;
83 }
84 protected URL url() {
85 return url;
86 }
87 protected Command command() {
88 return command;
89 }
90 }
91
92 private Execute command;
93
94 /**
95 * Plays a WAV audio file from the beginning. See also the variant which doesn't
96 * start at the beginning of the stream
97 * @param url The resource to play, which must be a WAV file or stream
98 * @throws audio fault exception, e.g. can't open stream, unhandleable audio format
99 */
100 public static void play(URL url) throws Exception {
101 AudioPlayer.play(url, 0.0);
102 }
103
104 /**
105 * Plays a WAV audio file from a specified position.
106 * @param url The resource to play, which must be a WAV file or stream
107 * @param seconds The number of seconds into the audio to start playing
108 * @throws audio fault exception, e.g. can't open stream, unhandleable audio format
109 */
110 public static void play(URL url, double seconds) throws Exception {
111 AudioPlayer.get().command.play(url, seconds);
112 }
113
114 /**
115 * Pauses the currently playing audio stream. Does nothing if nothing playing.
116 * @throws audio fault exception, e.g. can't open stream, unhandleable audio format
117 */
118 public static void pause() throws Exception {
119 AudioPlayer.get().command.pause();
120 }
121
122 /**
123 * To get the Url of the playing or recently played audio.
124 * @returns url - could be null
125 */
126 public static URL url() {
127 return AudioPlayer.get().playingUrl;
128 }
129
130 /**
131 * Whether or not we are paused.
132 * @returns boolean whether or not paused
133 */
134 public static boolean paused() {
135 return AudioPlayer.get().state == State.PAUSED;
136 }
137
138 /**
139 * Whether or not we are playing.
140 * @returns boolean whether or not playing
141 */
142 public static boolean playing() {
143 return AudioPlayer.get().state == State.PLAYING;
144 }
145
146 /**
147 * How far we are through playing, in seconds.
148 * @returns double seconds
149 */
150 public static double position() {
151 return AudioPlayer.get().position;
152 }
153
154 /**
155 * gets the singleton object, and if this is the first time, creates it along with
156 * the thread to support audio
157 */
158 private static AudioPlayer get() {
159 if (audioPlayer != null)
160 return audioPlayer;
161 try {
162 audioPlayer = new AudioPlayer();
163 return audioPlayer;
164 } catch (Exception ex) {
165 return null;
166 }
167 }
168
169 private AudioPlayer() {
170 state = State.INITIALIZING;
171 command = new Execute();
172 playingUrl = null;
173 try {
174 leadIn = Double.parseDouble(Main.pref.get("audio.leadin", "1.0" /* default, seconds */));
175 } catch (NumberFormatException e) {
176 leadIn = 1.0; // failed to parse
177 }
178 start();
179 while (state == State.INITIALIZING) { yield(); }
180 }
181
182 /**
183 * Starts the thread to actually play the audio, per Thread interface
184 * Not to be used as public, though Thread interface doesn't allow it to be made private
185 */
186 @Override public void run() {
187 /* code running in separate thread */
188
189 playingUrl = null;
190 AudioInputStream audioInputStream = null;
191 int nBytesRead = 0;
192 SourceDataLine audioOutputLine = null;
193 AudioFormat audioFormat = null;
194 byte[] abData = new byte[8192];
195
196 for (;;) {
197 try {
198 switch (state) {
199 case INITIALIZING:
200 // we're ready to take interrupts
201 state = State.NOTPLAYING;
202 break;
203 case NOTPLAYING:
204 case PAUSED:
205 sleep(200);
206 break;
207 case PLAYING:
208 for(;;) {
209 nBytesRead = audioInputStream.read(abData, 0, abData.length);
210 position += nBytesRead / bytesPerSecond;
211 command.possiblyInterrupt();
212 if (nBytesRead < 0) { break; }
213 audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten
214 command.possiblyInterrupt();
215 }
216 // end of audio, clean up
217 audioOutputLine.drain();
218 audioOutputLine.close();
219 audioOutputLine = null;
220 audioInputStream.close();
221 audioInputStream = null;
222 playingUrl = null;
223 state = State.NOTPLAYING;
224 command.possiblyInterrupt();
225 break;
226 }
227 } catch (InterruptedException e) {
228 interrupted(); // just in case we get an interrupt
229 State stateChange = state;
230 state = State.INTERRUPTED;
231 try {
232 switch (command.command()) {
233 case PLAY:
234 double offset = command.offset();
235 if (playingUrl != command.url() ||
236 stateChange != State.PAUSED ||
237 offset != 0.0)
238 {
239 if (audioInputStream != null) {
240 audioInputStream.close();
241 audioInputStream = null;
242 }
243 playingUrl = command.url();
244 audioInputStream = AudioSystem.getAudioInputStream(playingUrl);
245 audioFormat = audioInputStream.getFormat();
246 DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
247 nBytesRead = 0;
248 position = 0.0;
249 double adjustedOffset = offset - leadIn;
250 bytesPerSecond = audioFormat.getFrameRate() /* frames per second */
251 * audioFormat.getFrameSize() /* bytes per frame */;
252 if (offset != 0.0 && adjustedOffset > 0.0) {
253 long bytesToSkip = (long)(
254 adjustedOffset /* seconds (double) */ * bytesPerSecond);
255 /* skip doesn't seem to want to skip big chunks, so
256 * reduce it to smaller ones
257 */
258 // audioInputStream.skip(bytesToSkip);
259 int skipsize = 8192;
260 while (bytesToSkip > skipsize) {
261 audioInputStream.skip(skipsize);
262 bytesToSkip -= skipsize;
263 }
264 audioInputStream.skip(bytesToSkip);
265 position = adjustedOffset;
266 }
267 if (audioOutputLine == null) {
268 audioOutputLine = (SourceDataLine) AudioSystem.getLine(info);
269 audioOutputLine.open(audioFormat);
270 audioOutputLine.start();
271 }
272 }
273 stateChange = State.PLAYING;
274 break;
275 case PAUSE:
276 stateChange = state.PAUSED;
277 break;
278 }
279 command.ok(stateChange);
280 } catch (Exception startPlayingException) {
281 command.failed(startPlayingException); // sets state
282 }
283 } catch (Exception e) {
284 state = State.NOTPLAYING;
285 }
286 }
287 }
288
289 public static void audioMalfunction(Exception ex) {
290 JOptionPane.showMessageDialog(Main.parent,
291 "<html><b>" +
292 tr("There was an error while trying to play the sound file for this marker.") +
293 "</b><br>" + ex.getClass().getName() + ":<br><i>" + ex.getMessage() + "</i></html>",
294 tr("Error playing sound"), JOptionPane.ERROR_MESSAGE);
295 }
296}
Note: See TracBrowser for help on using the repository browser.