001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.audio;
003
004import java.io.IOException;
005import java.net.URL;
006
007import org.openstreetmap.josm.spi.preferences.Config;
008import org.openstreetmap.josm.tools.JosmRuntimeException;
009import org.openstreetmap.josm.tools.Logging;
010
011/**
012 * Creates and controls a separate audio player thread.
013 *
014 * @author David Earl <david@frankieandshadow.com>
015 * @since 12326 (move to new package)
016 * @since 547
017 */
018public final class AudioPlayer extends Thread implements AudioListener {
019
020    private static volatile AudioPlayer audioPlayer;
021
022    /**
023     * Audio player state.
024     */
025    public enum State {
026        /** Initializing */
027        INITIALIZING,
028        /** Not playing */
029        NOTPLAYING,
030        /** Playing */
031        PLAYING,
032        /** Paused */
033        PAUSED,
034        /** Interrupted */
035        INTERRUPTED
036    }
037
038    /**
039     * Audio player command.
040     */
041    public enum Command { /** Audio play */ PLAY, /** Audio pause */ PAUSE }
042
043    /**
044     * Audio player result.
045     */
046    public enum Result { /** In progress */ WAITING, /** Success */ OK, /** Failure */ FAILED }
047
048    private State state;
049    private SoundPlayer soundPlayer;
050    private URL playingUrl;
051
052    /**
053     * Passes information from the control thread to the playing thread
054     */
055    public class Execute {
056        private Command command;
057        private Result result;
058        private Exception exception;
059        private URL url;
060        private double offset; // seconds
061        private double speed; // ratio
062
063        /*
064         * Called to execute the commands in the other thread
065         */
066        protected void play(URL url, double offset, double speed) throws InterruptedException, IOException {
067            this.url = url;
068            this.offset = offset;
069            this.speed = speed;
070            command = Command.PLAY;
071            result = Result.WAITING;
072            send();
073        }
074
075        protected void pause() throws InterruptedException, IOException {
076            command = Command.PAUSE;
077            send();
078        }
079
080        private void send() throws InterruptedException, IOException {
081            result = Result.WAITING;
082            interrupt();
083            while (result == Result.WAITING) {
084                sleep(10);
085            }
086            if (result == Result.FAILED)
087                throw new IOException(exception);
088        }
089
090        protected void possiblyInterrupt() throws InterruptedException {
091            if (interrupted() || result == Result.WAITING)
092                throw new InterruptedException();
093        }
094
095        protected void failed(Exception e) {
096            exception = e;
097            result = Result.FAILED;
098            state = State.NOTPLAYING;
099        }
100
101        protected void ok(State newState) {
102            result = Result.OK;
103            state = newState;
104        }
105
106        /**
107         * Returns the offset.
108         * @return the offset, in seconds
109         */
110        public double offset() {
111            return offset;
112        }
113
114        /**
115         * Returns the speed.
116         * @return the speed (ratio)
117         */
118        public double speed() {
119            return speed;
120        }
121
122        /**
123         * Returns the URL.
124         * @return The resource to play, which must be a WAV file or stream
125         */
126        public URL url() {
127            return url;
128        }
129
130        /**
131         * Returns the command.
132         * @return the command
133         */
134        public Command command() {
135            return command;
136        }
137    }
138
139    private final Execute command;
140
141    /**
142     * Plays a WAV audio file from the beginning. See also the variant which doesn't
143     * start at the beginning of the stream
144     * @param url The resource to play, which must be a WAV file or stream
145     * @throws InterruptedException thread interrupted
146     * @throws IOException audio fault exception, e.g. can't open stream, unhandleable audio format
147     */
148    public static void play(URL url) throws InterruptedException, IOException {
149        AudioPlayer instance = AudioPlayer.getInstance();
150        if (instance != null)
151            instance.command.play(url, 0.0, 1.0);
152    }
153
154    /**
155     * Plays a WAV audio file from a specified position.
156     * @param url The resource to play, which must be a WAV file or stream
157     * @param seconds The number of seconds into the audio to start playing
158     * @throws InterruptedException thread interrupted
159     * @throws IOException audio fault exception, e.g. can't open stream, unhandleable audio format
160     */
161    public static void play(URL url, double seconds) throws InterruptedException, IOException {
162        AudioPlayer instance = AudioPlayer.getInstance();
163        if (instance != null)
164            instance.command.play(url, seconds, 1.0);
165    }
166
167    /**
168     * Plays a WAV audio file from a specified position at variable speed.
169     * @param url The resource to play, which must be a WAV file or stream
170     * @param seconds The number of seconds into the audio to start playing
171     * @param speed Rate at which audio playes (1.0 = real time, > 1 is faster)
172     * @throws InterruptedException thread interrupted
173     * @throws IOException audio fault exception, e.g. can't open stream,  unhandleable audio format
174     */
175    public static void play(URL url, double seconds, double speed) throws InterruptedException, IOException {
176        AudioPlayer instance = AudioPlayer.getInstance();
177        if (instance != null)
178            instance.command.play(url, seconds, speed);
179    }
180
181    /**
182     * Pauses the currently playing audio stream. Does nothing if nothing playing.
183     * @throws InterruptedException thread interrupted
184     * @throws IOException audio fault exception, e.g. can't open stream,  unhandleable audio format
185     */
186    public static void pause() throws InterruptedException, IOException {
187        AudioPlayer instance = AudioPlayer.getInstance();
188        if (instance != null)
189            instance.command.pause();
190    }
191
192    /**
193     * To get the Url of the playing or recently played audio.
194     * @return url - could be null
195     */
196    public static URL url() {
197        AudioPlayer instance = AudioPlayer.getInstance();
198        return instance == null ? null : instance.playingUrl;
199    }
200
201    /**
202     * Whether or not we are paused.
203     * @return boolean whether or not paused
204     */
205    public static boolean paused() {
206        AudioPlayer instance = AudioPlayer.getInstance();
207        return instance != null && instance.state == State.PAUSED;
208    }
209
210    /**
211     * Whether or not we are playing.
212     * @return boolean whether or not playing
213     */
214    public static boolean playing() {
215        AudioPlayer instance = AudioPlayer.getInstance();
216        return instance != null && instance.state == State.PLAYING;
217    }
218
219    /**
220     * How far we are through playing, in seconds.
221     * @return double seconds
222     */
223    public static double position() {
224        AudioPlayer instance = AudioPlayer.getInstance();
225        return instance == null ? -1 : instance.soundPlayer.position();
226    }
227
228    /**
229     * Speed at which we will play.
230     * @return double, speed multiplier
231     */
232    public static double speed() {
233        AudioPlayer instance = AudioPlayer.getInstance();
234        return instance == null ? -1 : instance.soundPlayer.speed();
235    }
236
237    /**
238     * Returns the singleton object, and if this is the first time, creates it along with
239     * the thread to support audio
240     * @return the unique instance
241     */
242    private static AudioPlayer getInstance() {
243        if (audioPlayer != null)
244            return audioPlayer;
245        try {
246            audioPlayer = new AudioPlayer();
247            return audioPlayer;
248        } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException ex) {
249            Logging.error(ex);
250            return null;
251        }
252    }
253
254    /**
255     * Resets the audio player.
256     */
257    public static void reset() {
258        if (audioPlayer != null) {
259            try {
260                pause();
261            } catch (InterruptedException | IOException e) {
262                Logging.warn(e);
263            }
264            audioPlayer.playingUrl = null;
265        }
266    }
267
268    private AudioPlayer() {
269        state = State.INITIALIZING;
270        command = new Execute();
271        playingUrl = null;
272        double leadIn = Config.getPref().getDouble("audio.leadin", 1.0 /* default, seconds */);
273        double calibration = Config.getPref().getDouble("audio.calibration", 1.0 /* default, ratio */);
274        try {
275            soundPlayer = (SoundPlayer) Class.forName("org.openstreetmap.josm.io.audio.fx.JavaFxMediaPlayer")
276                    .getDeclaredConstructor().newInstance();
277        } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) {
278            Logging.debug(e);
279            Logging.warn("JOSM compiled without Java FX support. Falling back to Java Sound API");
280        } catch (NoClassDefFoundError | JosmRuntimeException e) {
281            Logging.debug(e);
282            Logging.warn("Java FX is unavailable. Falling back to Java Sound API");
283        }
284        if (soundPlayer == null) {
285            soundPlayer = new JavaSoundPlayer(leadIn, calibration);
286        }
287        soundPlayer.addAudioListener(this);
288        start();
289        while (state == State.INITIALIZING) {
290            yield();
291        }
292    }
293
294    /**
295     * Starts the thread to actually play the audio, per Thread interface
296     * Not to be used as public, though Thread interface doesn't allow it to be made private
297     */
298    @Override
299    public void run() {
300        /* code running in separate thread */
301
302        playingUrl = null;
303
304        for (;;) {
305            try {
306                switch (state) {
307                    case INITIALIZING:
308                        // we're ready to take interrupts
309                        state = State.NOTPLAYING;
310                        break;
311                    case NOTPLAYING:
312                    case PAUSED:
313                        sleep(200);
314                        break;
315                    case PLAYING:
316                        command.possiblyInterrupt();
317                        if (soundPlayer.playing(command)) {
318                            playingUrl = null;
319                            state = State.NOTPLAYING;
320                        }
321                        command.possiblyInterrupt();
322                        break;
323                    default: // Do nothing
324                }
325            } catch (InterruptedException e) {
326                interrupted(); // just in case we get an interrupt
327                State stateChange = state;
328                state = State.INTERRUPTED;
329                try {
330                    switch (command.command()) {
331                        case PLAY:
332                            soundPlayer.play(command, stateChange, playingUrl);
333                            stateChange = State.PLAYING;
334                            break;
335                        case PAUSE:
336                            soundPlayer.pause(command, stateChange, playingUrl);
337                            stateChange = State.PAUSED;
338                            break;
339                        default: // Do nothing
340                    }
341                    command.ok(stateChange);
342                } catch (AudioException | IOException | SecurityException | IllegalArgumentException startPlayingException) {
343                    Logging.error(startPlayingException);
344                    command.failed(startPlayingException); // sets state
345                }
346            } catch (AudioException | IOException e) {
347                state = State.NOTPLAYING;
348                Logging.error(e);
349            }
350        }
351    }
352
353    @Override
354    public void playing(URL playingURL) {
355        this.playingUrl = playingURL;
356    }
357}