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