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

Revision 3264, 13.3 KB checked in by stoecker, 2 years ago (diff)

see #5052 - fix exception, thought there should be a useful error message nevertheless

  • Property svn:eol-style set to native
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.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.SourceDataLine;
14import javax.swing.JOptionPane;
15
16import org.openstreetmap.josm.Main;
17
18/**
19 * Creates and controls a separate audio player thread.
20 *
21 * @author David Earl <david@frankieandshadow.com>
22 *
23 */
24public class AudioPlayer extends Thread {
25
26    private static AudioPlayer audioPlayer = null;
27
28    private enum State { INITIALIZING, NOTPLAYING, PLAYING, PAUSED, INTERRUPTED }
29    private State state;
30    private enum Command { PLAY, PAUSE }
31    private enum Result { WAITING, OK, FAILED }
32    private URL playingUrl;
33    private double leadIn; // seconds
34    private double calibration; // ratio of purported duration of samples to true duration
35    private double position; // seconds
36    private double bytesPerSecond;
37    private static long chunk = 4000; /* bytes */
38    private double speed = 1.0;
39
40    /**
41     * Passes information from the control thread to the playing thread
42     */
43    private class Execute {
44        private Command command;
45        private Result result;
46        private Exception exception;
47        private URL url;
48        private double offset; // seconds
49        private double speed; // ratio
50
51        /*
52         * Called to execute the commands in the other thread
53         */
54        protected void play(URL url, double offset, double speed) throws Exception {
55            this.url = url;
56            this.offset = offset;
57            this.speed = speed;
58            command = Command.PLAY;
59            result = Result.WAITING;
60            send();
61        }
62        protected void pause() throws Exception {
63            command = Command.PAUSE;
64            send();
65        }
66        private void send() throws Exception {
67            result = Result.WAITING;
68            interrupt();
69            while (result == Result.WAITING) { sleep(10); /* yield(); */ }
70            if (result == Result.FAILED)
71                throw exception;
72        }
73        private void possiblyInterrupt() throws InterruptedException {
74            if (interrupted() || result == Result.WAITING)
75                throw new InterruptedException();
76        }
77        protected void failed (Exception e) {
78            exception = e;
79            result = Result.FAILED;
80            state = State.NOTPLAYING;
81        }
82        protected void ok (State newState) {
83            result = Result.OK;
84            state = newState;
85        }
86        protected double offset() {
87            return offset;
88        }
89        protected double speed() {
90            return speed;
91        }
92        protected URL url() {
93            return url;
94        }
95        protected Command command() {
96            return command;
97        }
98    }
99
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}
Note: See TracBrowser for help on using the repository browser.