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