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