001 // License: GPL. Copyright 2008 by David Earl and others 002 package org.openstreetmap.josm.tools; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 006 import java.io.IOException; 007 import java.net.URL; 008 009 import javax.sound.sampled.AudioFormat; 010 import javax.sound.sampled.AudioInputStream; 011 import javax.sound.sampled.AudioSystem; 012 import javax.sound.sampled.DataLine; 013 import javax.sound.sampled.SourceDataLine; 014 import javax.swing.JOptionPane; 015 016 import org.openstreetmap.josm.Main; 017 018 /** 019 * Creates and controls a separate audio player thread. 020 * 021 * @author David Earl <david@frankieandshadow.com> 022 * 023 */ 024 public 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 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 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 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 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 public static void reset() { 197 if(audioPlayer != null) 198 { 199 try { 200 pause(); 201 } catch(Exception e) {} 202 audioPlayer.playingUrl = null; 203 } 204 } 205 206 private AudioPlayer() { 207 state = State.INITIALIZING; 208 command = new Execute(); 209 playingUrl = null; 210 leadIn = Main.pref.getDouble("audio.leadin", "1.0" /* default, seconds */); 211 calibration = Main.pref.getDouble("audio.calibration", "1.0" /* default, ratio */); 212 start(); 213 while (state == State.INITIALIZING) { yield(); } 214 } 215 216 /** 217 * Starts the thread to actually play the audio, per Thread interface 218 * Not to be used as public, though Thread interface doesn't allow it to be made private 219 */ 220 @Override public void run() { 221 /* code running in separate thread */ 222 223 playingUrl = null; 224 AudioInputStream audioInputStream = null; 225 SourceDataLine audioOutputLine = null; 226 AudioFormat audioFormat = null; 227 byte[] abData = new byte[(int)chunk]; 228 229 for (;;) { 230 try { 231 switch (state) { 232 case INITIALIZING: 233 // we're ready to take interrupts 234 state = State.NOTPLAYING; 235 break; 236 case NOTPLAYING: 237 case PAUSED: 238 sleep(200); 239 break; 240 case PLAYING: 241 command.possiblyInterrupt(); 242 for(;;) { 243 int nBytesRead = 0; 244 nBytesRead = audioInputStream.read(abData, 0, abData.length); 245 position += nBytesRead / bytesPerSecond; 246 command.possiblyInterrupt(); 247 if (nBytesRead < 0) { break; } 248 audioOutputLine.write(abData, 0, nBytesRead); // => int nBytesWritten 249 command.possiblyInterrupt(); 250 } 251 // end of audio, clean up 252 audioOutputLine.drain(); 253 audioOutputLine.close(); 254 audioOutputLine = null; 255 audioInputStream.close(); 256 audioInputStream = null; 257 playingUrl = null; 258 state = State.NOTPLAYING; 259 command.possiblyInterrupt(); 260 break; 261 } 262 } catch (InterruptedException e) { 263 interrupted(); // just in case we get an interrupt 264 State stateChange = state; 265 state = State.INTERRUPTED; 266 try { 267 switch (command.command()) { 268 case PLAY: 269 double offset = command.offset(); 270 speed = command.speed(); 271 if (playingUrl != command.url() || 272 stateChange != State.PAUSED || 273 offset != 0.0) 274 { 275 if (audioInputStream != null) { 276 audioInputStream.close(); 277 audioInputStream = null; 278 } 279 playingUrl = command.url(); 280 audioInputStream = AudioSystem.getAudioInputStream(playingUrl); 281 audioFormat = audioInputStream.getFormat(); 282 long nBytesRead = 0; 283 position = 0.0; 284 offset -= leadIn; 285 double calibratedOffset = offset * calibration; 286 bytesPerSecond = audioFormat.getFrameRate() /* frames per second */ 287 * audioFormat.getFrameSize() /* bytes per frame */; 288 if (speed * bytesPerSecond > 256000.0) { 289 speed = 256000 / bytesPerSecond; 290 } 291 if (calibratedOffset > 0.0) { 292 long bytesToSkip = (long)( 293 calibratedOffset /* seconds (double) */ * bytesPerSecond); 294 /* skip doesn't seem to want to skip big chunks, so 295 * reduce it to smaller ones 296 */ 297 // audioInputStream.skip(bytesToSkip); 298 while (bytesToSkip > chunk) { 299 nBytesRead = audioInputStream.skip(chunk); 300 if (nBytesRead <= 0) 301 throw new IOException(tr("This is after the end of the recording")); 302 bytesToSkip -= nBytesRead; 303 } 304 if (bytesToSkip > 0) { 305 audioInputStream.skip(bytesToSkip); 306 } 307 position = offset; 308 } 309 if (audioOutputLine != null) { 310 audioOutputLine.close(); 311 } 312 audioFormat = new AudioFormat(audioFormat.getEncoding(), 313 audioFormat.getSampleRate() * (float) (speed * calibration), 314 audioFormat.getSampleSizeInBits(), 315 audioFormat.getChannels(), 316 audioFormat.getFrameSize(), 317 audioFormat.getFrameRate() * (float) (speed * calibration), 318 audioFormat.isBigEndian()); 319 DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); 320 audioOutputLine = (SourceDataLine) AudioSystem.getLine(info); 321 audioOutputLine.open(audioFormat); 322 audioOutputLine.start(); 323 } 324 stateChange = State.PLAYING; 325 break; 326 case PAUSE: 327 stateChange = State.PAUSED; 328 break; 329 } 330 command.ok(stateChange); 331 } catch (Exception startPlayingException) { 332 command.failed(startPlayingException); // sets state 333 } 334 } catch (Exception e) { 335 state = State.NOTPLAYING; 336 } 337 } 338 } 339 340 public static void audioMalfunction(Exception ex) { 341 String msg = ex.getMessage(); 342 if(msg == null) 343 msg = tr("unspecified reason"); 344 else 345 msg = tr(msg); 346 JOptionPane.showMessageDialog(Main.parent, 347 "<html><p>" + msg + "</p></html>", 348 tr("Error playing sound"), JOptionPane.ERROR_MESSAGE); 349 } 350 }