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.getInstance().command.play(url, 0.0, 1.0); 127 } 128 129 /** 130 * Plays a WAV audio file from a specified position. 131 * @param url The resource to play, which must be a WAV file or stream 132 * @param seconds The number of seconds into the audio to start playing 133 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format 134 */ 135 public static void play(URL url, double seconds) throws Exception { 136 AudioPlayer.getInstance().command.play(url, seconds, 1.0); 137 } 138 139 /** 140 * Plays a WAV audio file from a specified position at variable speed. 141 * @param url The resource to play, which must be a WAV file or stream 142 * @param seconds The number of seconds into the audio to start playing 143 * @param speed Rate at which audio playes (1.0 = real time, > 1 is faster) 144 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format 145 */ 146 public static void play(URL url, double seconds, double speed) throws Exception { 147 AudioPlayer.getInstance().command.play(url, seconds, speed); 148 } 149 150 /** 151 * Pauses the currently playing audio stream. Does nothing if nothing playing. 152 * @throws Exception audio fault exception, e.g. can't open stream, unhandleable audio format 153 */ 154 public static void pause() throws Exception { 155 AudioPlayer.getInstance().command.pause(); 156 } 157 158 /** 159 * To get the Url of the playing or recently played audio. 160 * @return url - could be null 161 */ 162 public static URL url() { 163 return AudioPlayer.getInstance().playingUrl; 164 } 165 166 /** 167 * Whether or not we are paused. 168 * @return boolean whether or not paused 169 */ 170 public static boolean paused() { 171 return AudioPlayer.getInstance().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 return AudioPlayer.getInstance().state == State.PLAYING; 180 } 181 182 /** 183 * How far we are through playing, in seconds. 184 * @return double seconds 185 */ 186 public static double position() { 187 return AudioPlayer.getInstance().position; 188 } 189 190 /** 191 * Speed at which we will play. 192 * @return double, speed multiplier 193 */ 194 public static double speed() { 195 return AudioPlayer.getInstance().speed; 196 } 197 198 /** 199 * Returns the singleton object, and if this is the first time, creates it along with 200 * the thread to support audio 201 * @return the unique instance 202 */ 203 private static AudioPlayer getInstance() { 204 if (audioPlayer != null) 205 return audioPlayer; 206 try { 207 audioPlayer = new AudioPlayer(); 208 return audioPlayer; 209 } catch (RuntimeException ex) { 210 Main.error(ex); 211 return null; 212 } 213 } 214 215 /** 216 * Resets the audio player. 217 */ 218 public static void reset() { 219 if (audioPlayer != null) { 220 try { 221 pause(); 222 } catch (Exception e) { 223 Main.warn(e); 224 } 225 audioPlayer.playingUrl = null; 226 } 227 } 228 229 private AudioPlayer() { 230 state = State.INITIALIZING; 231 command = new Execute(); 232 playingUrl = null; 233 leadIn = Main.pref.getDouble("audio.leadin", 1.0 /* default, seconds */); 234 calibration = Main.pref.getDouble("audio.calibration", 1.0 /* default, ratio */); 235 start(); 236 while (state == State.INITIALIZING) { 237 yield(); 238 } 239 } 240 241 /** 242 * Starts the thread to actually play the audio, per Thread interface 243 * Not to be used as public, though Thread interface doesn't allow it to be made private 244 */ 245 @Override public void run() { 246 /* code running in separate thread */ 247 248 playingUrl = null; 249 AudioInputStream audioInputStream = null; 250 SourceDataLine audioOutputLine = null; 251 AudioFormat audioFormat; 252 byte[] abData = new byte[(int) chunk]; 253 254 for (;;) { 255 try { 256 switch (state) { 257 case INITIALIZING: 258 // we're ready to take interrupts 259 state = State.NOTPLAYING; 260 break; 261 case NOTPLAYING: 262 case PAUSED: 263 sleep(200); 264 break; 265 case PLAYING: 266 command.possiblyInterrupt(); 267 for (;;) { 268 int nBytesRead; 269 nBytesRead = audioInputStream.read(abData, 0, abData.length); 270 position += nBytesRead / bytesPerSecond; 271 command.possiblyInterrupt(); 272 if (nBytesRead < 0) { 273 break; 274 } 275 audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten 276 command.possiblyInterrupt(); 277 } 278 // end of audio, clean up 279 audioOutputLine.drain(); 280 audioOutputLine.close(); 281 audioOutputLine = null; 282 Utils.close(audioInputStream); 283 audioInputStream = null; 284 playingUrl = null; 285 state = State.NOTPLAYING; 286 command.possiblyInterrupt(); 287 break; 288 default: // Do nothing 289 } 290 } catch (InterruptedException e) { 291 interrupted(); // just in case we get an interrupt 292 State stateChange = state; 293 state = State.INTERRUPTED; 294 try { 295 switch (command.command()) { 296 case PLAY: 297 double offset = command.offset(); 298 speed = command.speed(); 299 if (playingUrl != command.url() || 300 stateChange != State.PAUSED || 301 offset != 0) { 302 if (audioInputStream != null) { 303 Utils.close(audioInputStream); 304 } 305 playingUrl = command.url(); 306 audioInputStream = AudioSystem.getAudioInputStream(playingUrl); 307 audioFormat = audioInputStream.getFormat(); 308 long nBytesRead; 309 position = 0.0; 310 offset -= leadIn; 311 double calibratedOffset = offset * calibration; 312 bytesPerSecond = audioFormat.getFrameRate() /* frames per second */ 313 * audioFormat.getFrameSize() /* bytes per frame */; 314 if (speed * bytesPerSecond > 256_000.0) { 315 speed = 256_000 / bytesPerSecond; 316 } 317 if (calibratedOffset > 0.0) { 318 long bytesToSkip = (long) (calibratedOffset /* seconds (double) */ * bytesPerSecond); 319 // skip doesn't seem to want to skip big chunks, so reduce it to smaller ones 320 while (bytesToSkip > chunk) { 321 nBytesRead = audioInputStream.skip(chunk); 322 if (nBytesRead <= 0) 323 throw new IOException(tr("This is after the end of the recording")); 324 bytesToSkip -= nBytesRead; 325 } 326 while (bytesToSkip > 0) { 327 long skippedBytes = audioInputStream.skip(bytesToSkip); 328 bytesToSkip -= skippedBytes; 329 if (skippedBytes == 0) { 330 // Avoid inifinite loop 331 Main.warn("Unable to skip bytes from audio input stream"); 332 bytesToSkip = 0; 333 } 334 } 335 position = offset; 336 } 337 if (audioOutputLine != null) { 338 audioOutputLine.close(); 339 } 340 audioFormat = new AudioFormat(audioFormat.getEncoding(), 341 audioFormat.getSampleRate() * (float) (speed * calibration), 342 audioFormat.getSampleSizeInBits(), 343 audioFormat.getChannels(), 344 audioFormat.getFrameSize(), 345 audioFormat.getFrameRate() * (float) (speed * calibration), 346 audioFormat.isBigEndian()); 347 DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); 348 audioOutputLine = (SourceDataLine) AudioSystem.getLine(info); 349 audioOutputLine.open(audioFormat); 350 audioOutputLine.start(); 351 } 352 stateChange = State.PLAYING; 353 break; 354 case PAUSE: 355 stateChange = State.PAUSED; 356 break; 357 default: // Do nothing 358 } 359 command.ok(stateChange); 360 } catch (LineUnavailableException | IOException | UnsupportedAudioFileException | 361 SecurityException | IllegalArgumentException startPlayingException) { 362 Main.error(startPlayingException); 363 command.failed(startPlayingException); // sets state 364 } 365 } catch (IOException e) { 366 state = State.NOTPLAYING; 367 Main.error(e); 368 } 369 } 370 } 371 372 /** 373 * Shows a popup audio error message for the given exception. 374 * @param ex The exception used as error reason. Cannot be {@code null}. 375 */ 376 public static void audioMalfunction(Exception ex) { 377 String msg = ex.getMessage(); 378 if (msg == null) 379 msg = tr("unspecified reason"); 380 else 381 msg = tr(msg); 382 Main.error(msg); 383 if (!GraphicsEnvironment.isHeadless()) { 384 JOptionPane.showMessageDialog(Main.parent, 385 "<html><p>" + msg + "</p></html>", 386 tr("Error playing sound"), JOptionPane.ERROR_MESSAGE); 387 } 388 } 389}