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