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