001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.GraphicsEnvironment; 007import java.io.IOException; 008import java.net.URL; 009 010import javax.sound.sampled.AudioFormat; 011import javax.sound.sampled.AudioInputStream; 012import javax.sound.sampled.AudioSystem; 013import javax.sound.sampled.DataLine; 014import javax.sound.sampled.LineUnavailableException; 015import javax.sound.sampled.SourceDataLine; 016import javax.sound.sampled.UnsupportedAudioFileException; 017import javax.swing.JOptionPane; 018 019import org.openstreetmap.josm.Main; 020 021/** 022 * Creates and controls a separate audio player thread. 023 * 024 * @author David Earl <david@frankieandshadow.com> 025 * @since 547 026 */ 027public final class AudioPlayer extends Thread { 028 029 private static volatile AudioPlayer audioPlayer; 030 031 private enum State { INITIALIZING, NOTPLAYING, PLAYING, PAUSED, INTERRUPTED } 032 033 private enum Command { PLAY, PAUSE } 034 035 private enum Result { WAITING, OK, FAILED } 036 037 private State state; 038 private URL playingUrl; 039 private final double leadIn; // seconds 040 private final double calibration; // ratio of purported duration of samples to true duration 041 private double position; // seconds 042 private double bytesPerSecond; 043 private static long chunk = 4000; /* bytes */ 044 private double speed = 1.0; 045 046 /** 047 * Passes information from the control thread to the playing thread 048 */ 049 private class Execute { 050 private Command command; 051 private Result result; 052 private Exception exception; 053 private URL url; 054 private double offset; // seconds 055 private double speed; // ratio 056 057 /* 058 * Called to execute the commands in the other thread 059 */ 060 protected void play(URL url, double offset, double speed) throws Exception { 061 this.url = url; 062 this.offset = offset; 063 this.speed = speed; 064 command = Command.PLAY; 065 result = Result.WAITING; 066 send(); 067 } 068 069 protected void pause() throws Exception { 070 command = Command.PAUSE; 071 send(); 072 } 073 074 private void send() throws Exception { 075 result = Result.WAITING; 076 interrupt(); 077 while (result == Result.WAITING) { 078 sleep(10); 079 } 080 if (result == Result.FAILED) 081 throw exception; 082 } 083 084 private void possiblyInterrupt() throws InterruptedException { 085 if (interrupted() || result == Result.WAITING) 086 throw new InterruptedException(); 087 } 088 089 protected void failed(Exception e) { 090 exception = e; 091 result = Result.FAILED; 092 state = State.NOTPLAYING; 093 } 094 095 protected void ok(State newState) { 096 result = Result.OK; 097 state = newState; 098 } 099 100 protected double offset() { 101 return offset; 102 } 103 104 protected double speed() { 105 return speed; 106 } 107 108 protected URL url() { 109 return url; 110 } 111 112 protected Command command() { 113 return command; 114 } 115 } 116 117 private final Execute command; 118 119 /** 120 * Plays a WAV audio file from the beginning. See also the variant which doesn't 121 * start at the beginning of the stream 122 * @param url The resource to play, which must be a WAV file or stream 123 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format 124 */ 125 public static void play(URL url) throws Exception { 126 AudioPlayer instance = AudioPlayer.getInstance(); 127 if (instance != null) 128 instance.command.play(url, 0.0, 1.0); 129 } 130 131 /** 132 * Plays a WAV audio file from a specified position. 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 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format 136 */ 137 public static void play(URL url, double seconds) throws Exception { 138 AudioPlayer instance = AudioPlayer.getInstance(); 139 if (instance != null) 140 instance.command.play(url, seconds, 1.0); 141 } 142 143 /** 144 * Plays a WAV audio file from a specified position at variable speed. 145 * @param url The resource to play, which must be a WAV file or stream 146 * @param seconds The number of seconds into the audio to start playing 147 * @param speed Rate at which audio playes (1.0 = real time, > 1 is faster) 148 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format 149 */ 150 public static void play(URL url, double seconds, double speed) throws Exception { 151 AudioPlayer instance = AudioPlayer.getInstance(); 152 if (instance != null) 153 instance.command.play(url, seconds, speed); 154 } 155 156 /** 157 * Pauses the currently playing audio stream. Does nothing if nothing playing. 158 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format 159 */ 160 public static void pause() throws Exception { 161 AudioPlayer instance = AudioPlayer.getInstance(); 162 if (instance != null) 163 instance.command.pause(); 164 } 165 166 /** 167 * To get the Url of the playing or recently played audio. 168 * @return url - could be null 169 */ 170 public static URL url() { 171 AudioPlayer instance = AudioPlayer.getInstance(); 172 return instance == null ? null : instance.playingUrl; 173 } 174 175 /** 176 * Whether or not we are paused. 177 * @return boolean whether or not paused 178 */ 179 public static boolean paused() { 180 AudioPlayer instance = AudioPlayer.getInstance(); 181 return instance == null ? false : (instance.state == State.PAUSED); 182 } 183 184 /** 185 * Whether or not we are playing. 186 * @return boolean whether or not playing 187 */ 188 public static boolean playing() { 189 AudioPlayer instance = AudioPlayer.getInstance(); 190 return instance == null ? false : (instance.state == State.PLAYING); 191 } 192 193 /** 194 * How far we are through playing, in seconds. 195 * @return double seconds 196 */ 197 public static double position() { 198 AudioPlayer instance = AudioPlayer.getInstance(); 199 return instance == null ? -1 : instance.position; 200 } 201 202 /** 203 * Speed at which we will play. 204 * @return double, speed multiplier 205 */ 206 public static double speed() { 207 AudioPlayer instance = AudioPlayer.getInstance(); 208 return instance == null ? -1 : instance.speed; 209 } 210 211 /** 212 * Returns the singleton object, and if this is the first time, creates it along with 213 * the thread to support audio 214 * @return the unique instance 215 */ 216 private static AudioPlayer getInstance() { 217 if (audioPlayer != null) 218 return audioPlayer; 219 try { 220 audioPlayer = new AudioPlayer(); 221 return audioPlayer; 222 } catch (RuntimeException ex) { 223 Main.error(ex); 224 return null; 225 } 226 } 227 228 /** 229 * Resets the audio player. 230 */ 231 public static void reset() { 232 if (audioPlayer != null) { 233 try { 234 pause(); 235 } catch (Exception e) { 236 Main.warn(e); 237 } 238 audioPlayer.playingUrl = null; 239 } 240 } 241 242 private AudioPlayer() { 243 state = State.INITIALIZING; 244 command = new Execute(); 245 playingUrl = null; 246 leadIn = Main.pref.getDouble("audio.leadin", 1.0 /* default, seconds */); 247 calibration = Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */); 248 start(); 249 while (state == State.INITIALIZING) { 250 yield(); 251 } 252 } 253 254 /** 255 * Starts the thread to actually play the audio, per Thread interface 256 * Not to be used as public, though Thread interface doesn't allow it to be made private 257 */ 258 @Override public void run() { 259 /* code running in separate thread */ 260 261 playingUrl = null; 262 AudioInputStream audioInputStream = null; 263 SourceDataLine audioOutputLine = null; 264 AudioFormat audioFormat; 265 byte[] abData = new byte[(int) chunk]; 266 267 for (;;) { 268 try { 269 switch (state) { 270 case INITIALIZING: 271 // we're ready to take interrupts 272 state = State.NOTPLAYING; 273 break; 274 case NOTPLAYING: 275 case PAUSED: 276 sleep(200); 277 break; 278 case PLAYING: 279 command.possiblyInterrupt(); 280 for (;;) { 281 int nBytesRead; 282 nBytesRead = audioInputStream.read(abData, 0, abData.length); 283 position += nBytesRead / bytesPerSecond; 284 command.possiblyInterrupt(); 285 if (nBytesRead < 0) { 286 break; 287 } 288 audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten 289 command.possiblyInterrupt(); 290 } 291 // end of audio, clean up 292 audioOutputLine.drain(); 293 audioOutputLine.close(); 294 audioOutputLine = null; 295 Utils.close(audioInputStream); 296 audioInputStream = null; 297 playingUrl = null; 298 state = State.NOTPLAYING; 299 command.possiblyInterrupt(); 300 break; 301 default: // Do nothing 302 } 303 } catch (InterruptedException e) { 304 interrupted(); // just in case we get an interrupt 305 State stateChange = state; 306 state = State.INTERRUPTED; 307 try { 308 switch (command.command()) { 309 case PLAY: 310 double offset = command.offset(); 311 speed = command.speed(); 312 if (playingUrl != command.url() || 313 stateChange != State.PAUSED || 314 offset != 0) { 315 if (audioInputStream != null) { 316 Utils.close(audioInputStream); 317 } 318 playingUrl = command.url(); 319 audioInputStream = AudioSystem.getAudioInputStream(playingUrl); 320 audioFormat = audioInputStream.getFormat(); 321 long nBytesRead; 322 position = 0.0; 323 offset -= leadIn; 324 double calibratedOffset = offset * calibration; 325 bytesPerSecond = audioFormat.getFrameRate() /* frames per second */ 326 * audioFormat.getFrameSize() /* bytes per frame */; 327 if (speed * bytesPerSecond > 256_000.0) { 328 speed = 256_000 / bytesPerSecond; 329 } 330 if (calibratedOffset > 0.0) { 331 long bytesToSkip = (long) (calibratedOffset /* seconds (double) */ * bytesPerSecond); 332 // skip doesn't seem to want to skip big chunks, so reduce it to smaller ones 333 while (bytesToSkip > chunk) { 334 nBytesRead = audioInputStream.skip(chunk); 335 if (nBytesRead <= 0) 336 throw new IOException(tr("This is after the end of the recording")); 337 bytesToSkip -= nBytesRead; 338 } 339 while (bytesToSkip > 0) { 340 long skippedBytes = audioInputStream.skip(bytesToSkip); 341 bytesToSkip -= skippedBytes; 342 if (skippedBytes == 0) { 343 // Avoid inifinite loop 344 Main.warn("Unable to skip bytes from audio input stream"); 345 bytesToSkip = 0; 346 } 347 } 348 position = offset; 349 } 350 if (audioOutputLine != null) { 351 audioOutputLine.close(); 352 } 353 audioFormat = new AudioFormat(audioFormat.getEncoding(), 354 audioFormat.getSampleRate() * (float) (speed * calibration), 355 audioFormat.getSampleSizeInBits(), 356 audioFormat.getChannels(), 357 audioFormat.getFrameSize(), 358 audioFormat.getFrameRate() * (float) (speed * calibration), 359 audioFormat.isBigEndian()); 360 DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); 361 audioOutputLine = (SourceDataLine) AudioSystem.getLine(info); 362 audioOutputLine.open(audioFormat); 363 audioOutputLine.start(); 364 } 365 stateChange = State.PLAYING; 366 break; 367 case PAUSE: 368 stateChange = State.PAUSED; 369 break; 370 default: // Do nothing 371 } 372 command.ok(stateChange); 373 } catch (LineUnavailableException | IOException | UnsupportedAudioFileException | 374 SecurityException | IllegalArgumentException startPlayingException) { 375 Main.error(startPlayingException); 376 command.failed(startPlayingException); // sets state 377 } 378 } catch (IOException e) { 379 state = State.NOTPLAYING; 380 Main.error(e); 381 } 382 } 383 } 384 385 /** 386 * Shows a popup audio error message for the given exception. 387 * @param ex The exception used as error reason. Cannot be {@code null}. 388 */ 389 public static void audioMalfunction(Exception ex) { 390 String msg = ex.getMessage(); 391 if (msg == null) 392 msg = tr("unspecified reason"); 393 else 394 msg = tr(msg); 395 Main.error(msg); 396 if (!GraphicsEnvironment.isHeadless()) { 397 JOptionPane.showMessageDialog(Main.parent, 398 "<html><p>" + msg + "</p></html>", 399 tr("Error playing sound"), JOptionPane.ERROR_MESSAGE); 400 } 401 } 402}