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

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

fix problem where it wasn't jumping as far ahead in the audio as I had thought; and adjust buffer size to match common frequency

File size: 8.4 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 private static long chunk = 8000; /* bytes */
38
39 /**
40 * Passes information from the control thread to the playing thread
41 */
42 private class Execute {
43 private Command command;
44 private Result result;
45 private Exception exception;
46 private URL url;
47 private double offset; // seconds
48
49 /*
50 * Called to execute the commands in the other thread
51 */
52 protected void play(URL url, double offset) throws Exception {
53 this.url = url;
54 this.offset = offset;
55 command = Command.PLAY;
56 result = Result.WAITING;
57 send();
58 }
59 protected void pause() throws Exception {
60 command = Command.PAUSE;
61 send();
62 }
63 private void send() throws Exception {
64 result = Result.WAITING;
65 interrupt();
66 while (result == Result.WAITING) { sleep(10); /* yield(); */ }
67 if (result == Result.FAILED) { throw exception; }
68 }
69 private void possiblyInterrupt() throws InterruptedException {
70 if (interrupted() || result == Result.WAITING)
71 throw new InterruptedException();
72 }
73 protected void failed (Exception e) {
74 exception = e;
75 result = Result.FAILED;
76 state = State.NOTPLAYING;
77 }
78 protected void ok (State newState) {
79 result = Result.OK;
80 state = newState;
81 }
82 protected double offset() {
83 return offset;
84 }
85 protected URL url() {
86 return url;
87 }
88 protected Command command() {
89 return command;
90 }
91 }
92
93 private Execute command;
94
95 /**
96 * Plays a WAV audio file from the beginning. See also the variant which doesn't
97 * start at the beginning of the stream
98 * @param url The resource to play, which must be a WAV file or stream
99 * @throws audio fault exception, e.g. can't open stream, unhandleable audio format
100 */
101 public static void play(URL url) throws Exception {
102 AudioPlayer.play(url, 0.0);
103 }
104
105 /**
106 * Plays a WAV audio file from a specified position.
107 * @param url The resource to play, which must be a WAV file or stream
108 * @param seconds The number of seconds into the audio to start playing
109 * @throws audio fault exception, e.g. can't open stream, unhandleable audio format
110 */
111 public static void play(URL url, double seconds) throws Exception {
112 AudioPlayer.get().command.play(url, seconds);
113 }
114
115 /**
116 * Pauses the currently playing audio stream. Does nothing if nothing playing.
117 * @throws audio fault exception, e.g. can't open stream, unhandleable audio format
118 */
119 public static void pause() throws Exception {
120 AudioPlayer.get().command.pause();
121 }
122
123 /**
124 * To get the Url of the playing or recently played audio.
125 * @returns url - could be null
126 */
127 public static URL url() {
128 return AudioPlayer.get().playingUrl;
129 }
130
131 /**
132 * Whether or not we are paused.
133 * @returns boolean whether or not paused
134 */
135 public static boolean paused() {
136 return AudioPlayer.get().state == State.PAUSED;
137 }
138
139 /**
140 * Whether or not we are playing.
141 * @returns boolean whether or not playing
142 */
143 public static boolean playing() {
144 return AudioPlayer.get().state == State.PLAYING;
145 }
146
147 /**
148 * How far we are through playing, in seconds.
149 * @returns double seconds
150 */
151 public static double position() {
152 return AudioPlayer.get().position;
153 }
154
155 /**
156 * gets the singleton object, and if this is the first time, creates it along with
157 * the thread to support audio
158 */
159 private static AudioPlayer get() {
160 if (audioPlayer != null)
161 return audioPlayer;
162 try {
163 audioPlayer = new AudioPlayer();
164 return audioPlayer;
165 } catch (Exception ex) {
166 return null;
167 }
168 }
169
170 private AudioPlayer() {
171 state = State.INITIALIZING;
172 command = new Execute();
173 playingUrl = null;
174 try {
175 leadIn = Double.parseDouble(Main.pref.get("audio.leadin", "1.0" /* default, seconds */));
176 } catch (NumberFormatException e) {
177 leadIn = 1.0; // failed to parse
178 }
179 start();
180 while (state == State.INITIALIZING) { yield(); }
181 }
182
183 /**
184 * Starts the thread to actually play the audio, per Thread interface
185 * Not to be used as public, though Thread interface doesn't allow it to be made private
186 */
187 @Override public void run() {
188 /* code running in separate thread */
189
190 playingUrl = null;
191 AudioInputStream audioInputStream = null;
192 int nBytesRead = 0;
193 SourceDataLine audioOutputLine = null;
194 AudioFormat audioFormat = null;
195 byte[] abData = new byte[(int)chunk];
196
197 for (;;) {
198 try {
199 switch (state) {
200 case INITIALIZING:
201 // we're ready to take interrupts
202 state = State.NOTPLAYING;
203 break;
204 case NOTPLAYING:
205 case PAUSED:
206 sleep(200);
207 break;
208 case PLAYING:
209 for(;;) {
210 nBytesRead = audioInputStream.read(abData, 0, abData.length);
211 position += nBytesRead / bytesPerSecond;
212 command.possiblyInterrupt();
213 if (nBytesRead < 0) { break; }
214 audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten
215 command.possiblyInterrupt();
216 }
217 // end of audio, clean up
218 audioOutputLine.drain();
219 audioOutputLine.close();
220 audioOutputLine = null;
221 audioInputStream.close();
222 audioInputStream = null;
223 playingUrl = null;
224 state = State.NOTPLAYING;
225 command.possiblyInterrupt();
226 break;
227 }
228 } catch (InterruptedException e) {
229 interrupted(); // just in case we get an interrupt
230 State stateChange = state;
231 state = State.INTERRUPTED;
232 try {
233 switch (command.command()) {
234 case PLAY:
235 double offset = command.offset();
236 if (playingUrl != command.url() ||
237 stateChange != State.PAUSED ||
238 offset != 0.0)
239 {
240 if (audioInputStream != null) {
241 audioInputStream.close();
242 audioInputStream = null;
243 }
244 playingUrl = command.url();
245 audioInputStream = AudioSystem.getAudioInputStream(playingUrl);
246 audioFormat = audioInputStream.getFormat();
247 DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
248 nBytesRead = 0;
249 position = 0.0;
250 double adjustedOffset = offset - leadIn;
251 bytesPerSecond = audioFormat.getFrameRate() /* frames per second */
252 * audioFormat.getFrameSize() /* bytes per frame */;
253 if (offset != 0.0 && adjustedOffset > 0.0) {
254 long bytesToSkip = (long)(
255 adjustedOffset /* seconds (double) */ * bytesPerSecond);
256 /* skip doesn't seem to want to skip big chunks, so
257 * reduce it to smaller ones
258 */
259 // audioInputStream.skip(bytesToSkip);
260 while (bytesToSkip > chunk) {
261 bytesToSkip -= audioInputStream.skip(chunk);
262 }
263 if (bytesToSkip > 0)
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.