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