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}