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}