001    // License: GPL. Copyright 2008 by David Earl and others
002    package org.openstreetmap.josm.tools;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.io.IOException;
007    import java.net.URL;
008    
009    import javax.sound.sampled.AudioFormat;
010    import javax.sound.sampled.AudioInputStream;
011    import javax.sound.sampled.AudioSystem;
012    import javax.sound.sampled.DataLine;
013    import javax.sound.sampled.SourceDataLine;
014    import javax.swing.JOptionPane;
015    
016    import org.openstreetmap.josm.Main;
017    
018    /**
019     * Creates and controls a separate audio player thread.
020     *
021     * @author David Earl <david@frankieandshadow.com>
022     *
023     */
024    public 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 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    }