001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.IOException;
007import java.net.URL;
008
009import javax.sound.sampled.AudioFormat;
010import javax.sound.sampled.AudioInputStream;
011import javax.sound.sampled.AudioSystem;
012import javax.sound.sampled.DataLine;
013import javax.sound.sampled.SourceDataLine;
014import javax.swing.JOptionPane;
015
016import org.openstreetmap.josm.Main;
017
018/**
019 * Creates and controls a separate audio player thread.
020 *
021 * @author David Earl <david@frankieandshadow.com>
022 * @since 547
023 */
024public final 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 Exception 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 Exception 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 Exception 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 Exception 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    /**
197     * Resets the audio player.
198     */
199    public static void reset() {
200        if(audioPlayer != null) {
201            try {
202                pause();
203            } catch(Exception e) {
204                Main.warn(e);
205            }
206            audioPlayer.playingUrl = null;
207        }
208    }
209
210    private AudioPlayer() {
211        state = State.INITIALIZING;
212        command = new Execute();
213        playingUrl = null;
214        leadIn = Main.pref.getDouble("audio.leadin", 1.0 /* default, seconds */);
215        calibration = Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */);
216        start();
217        while (state == State.INITIALIZING) { yield(); }
218    }
219
220    /**
221     * Starts the thread to actually play the audio, per Thread interface
222     * Not to be used as public, though Thread interface doesn't allow it to be made private
223     */
224    @Override public void run() {
225        /* code running in separate thread */
226
227        playingUrl = null;
228        AudioInputStream audioInputStream = null;
229        SourceDataLine audioOutputLine = null;
230        AudioFormat audioFormat = null;
231        byte[] abData = new byte[(int)chunk];
232
233        for (;;) {
234            try {
235                switch (state) {
236                    case INITIALIZING:
237                        // we're ready to take interrupts
238                        state = State.NOTPLAYING;
239                        break;
240                    case NOTPLAYING:
241                    case PAUSED:
242                        sleep(200);
243                        break;
244                    case PLAYING:
245                        command.possiblyInterrupt();
246                        for(;;) {
247                            int nBytesRead = 0;
248                            nBytesRead = audioInputStream.read(abData, 0, abData.length);
249                            position += nBytesRead / bytesPerSecond;
250                            command.possiblyInterrupt();
251                            if (nBytesRead < 0) { break; }
252                            audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten
253                            command.possiblyInterrupt();
254                        }
255                        // end of audio, clean up
256                        audioOutputLine.drain();
257                        audioOutputLine.close();
258                        audioOutputLine = null;
259                        Utils.close(audioInputStream);
260                        audioInputStream = null;
261                        playingUrl = null;
262                        state = State.NOTPLAYING;
263                        command.possiblyInterrupt();
264                        break;
265                }
266            } catch (InterruptedException e) {
267                interrupted(); // just in case we get an interrupt
268                State stateChange = state;
269                state = State.INTERRUPTED;
270                try {
271                    switch (command.command()) {
272                        case PLAY:
273                            double offset = command.offset();
274                            speed = command.speed();
275                            if (playingUrl != command.url() ||
276                                    stateChange != State.PAUSED ||
277                                    offset != 0.0)
278                            {
279                                if (audioInputStream != null) {
280                                    Utils.close(audioInputStream);
281                                    audioInputStream = null;
282                                }
283                                playingUrl = command.url();
284                                audioInputStream = AudioSystem.getAudioInputStream(playingUrl);
285                                audioFormat = audioInputStream.getFormat();
286                                long nBytesRead = 0;
287                                position = 0.0;
288                                offset -= leadIn;
289                                double calibratedOffset = offset * calibration;
290                                bytesPerSecond = audioFormat.getFrameRate() /* frames per second */
291                                * audioFormat.getFrameSize() /* bytes per frame */;
292                                if (speed * bytesPerSecond > 256000.0) {
293                                    speed = 256000 / bytesPerSecond;
294                                }
295                                if (calibratedOffset > 0.0) {
296                                    long bytesToSkip = (long)(
297                                            calibratedOffset /* seconds (double) */ * bytesPerSecond);
298                                    /* skip doesn't seem to want to skip big chunks, so
299                                     * reduce it to smaller ones
300                                     */
301                                    // audioInputStream.skip(bytesToSkip);
302                                    while (bytesToSkip > chunk) {
303                                        nBytesRead = audioInputStream.skip(chunk);
304                                        if (nBytesRead <= 0)
305                                            throw new IOException(tr("This is after the end of the recording"));
306                                        bytesToSkip -= nBytesRead;
307                                    }
308                                    while (bytesToSkip > 0) {
309                                        long skippedBytes = audioInputStream.skip(bytesToSkip);
310                                        bytesToSkip -= skippedBytes;
311                                        if (skippedBytes == 0) {
312                                            // Avoid inifinite loop
313                                            Main.warn("Unable to skip bytes from audio input stream");
314                                            bytesToSkip = 0;
315                                        }
316                                    }
317                                    position = offset;
318                                }
319                                if (audioOutputLine != null) {
320                                    audioOutputLine.close();
321                                }
322                                audioFormat = new AudioFormat(audioFormat.getEncoding(),
323                                        audioFormat.getSampleRate() * (float) (speed * calibration),
324                                        audioFormat.getSampleSizeInBits(),
325                                        audioFormat.getChannels(),
326                                        audioFormat.getFrameSize(),
327                                        audioFormat.getFrameRate() * (float) (speed * calibration),
328                                        audioFormat.isBigEndian());
329                                DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
330                                audioOutputLine = (SourceDataLine) AudioSystem.getLine(info);
331                                audioOutputLine.open(audioFormat);
332                                audioOutputLine.start();
333                            }
334                            stateChange = State.PLAYING;
335                            break;
336                        case PAUSE:
337                            stateChange = State.PAUSED;
338                            break;
339                    }
340                    command.ok(stateChange);
341                } catch (Exception startPlayingException) {
342                    command.failed(startPlayingException); // sets state
343                }
344            } catch (Exception e) {
345                state = State.NOTPLAYING;
346            }
347        }
348    }
349
350    /**
351     * Shows a popup audio error message for the given exception.
352     * @param ex The exception used as error reason. Cannot be {@code null}.
353     */
354    public static void audioMalfunction(Exception ex) {
355        String msg = ex.getMessage();
356        if(msg == null)
357            msg = tr("unspecified reason");
358        else
359            msg = tr(msg);
360        JOptionPane.showMessageDialog(Main.parent,
361                "<html><p>" + msg + "</p></html>",
362                tr("Error playing sound"), JOptionPane.ERROR_MESSAGE);
363    }
364}