001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030/** 031 * 032 */ 033package org.openimaj.audio; 034 035import java.util.ArrayList; 036import java.util.List; 037 038import javax.sound.sampled.LineUnavailableException; 039import javax.sound.sampled.SourceDataLine; 040 041import org.openimaj.audio.timecode.AudioTimecode; 042import org.openimaj.audio.util.AudioUtils; 043import org.openimaj.time.TimeKeeper; 044import org.openimaj.time.Timecode; 045 046/** 047 * Wraps the Java Sound APIs into the OpenIMAJ audio core for playing sounds. 048 * <p> 049 * The {@link AudioPlayer} supports the {@link TimeKeeper} interface so that 050 * other methods can synchronise to the audio timestamps. 051 * <p> 052 * The Audio Player as a {@link TimeKeeper} supports seeking but it may be 053 * possible that the underlying stream does not support seeking so the seek 054 * method may not affect the time keeper as expected. 055 * 056 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 057 * @created 8 Jun 2011 058 * 059 */ 060public class AudioPlayer implements Runnable, TimeKeeper<AudioTimecode> 061{ 062 /** The audio stream being played */ 063 private AudioStream stream = null; 064 065 /** The java audio output stream line */ 066 private SourceDataLine mLine = null; 067 068 /** The current timecode being played */ 069 private AudioTimecode currentTimecode = null; 070 071 /** The current audio timestamp */ 072 private long currentTimestamp = 0; 073 074 /** At what timestamp the current timecode was read at */ 075 private long timecodeReadAt = 0; 076 077 /** The device name on which to play */ 078 private String deviceName = null; 079 080 /** The mode of the player */ 081 private Mode mode = Mode.PLAY; 082 083 /** Listeners for events */ 084 private final List<AudioEventListener> listeners = new ArrayList<AudioEventListener>(); 085 086 /** Whether the system has been started */ 087 private boolean started = false; 088 089 /** 090 * Number of milliseconds in the sound line buffer. < 100ms is good 091 * for real-time whereas the bigger the better for smooth sound reproduction 092 */ 093 private double soundLineBufferSize = 100; 094 095 /** 096 * Enumerator for the current state of the audio player. 097 * 098 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 099 * 100 * @created 29 Nov 2011 101 */ 102 public enum Mode 103 { 104 /** The audio player is playing */ 105 PLAY, 106 107 /** The audio player is paused */ 108 PAUSE, 109 110 /** The audio player is stopped */ 111 STOP 112 } 113 114 /** 115 * Default constructor that takes an audio 116 * stream to play. 117 * 118 * @param a The audio stream to play 119 */ 120 public AudioPlayer( final AudioStream a ) 121 { 122 this( a, null ); 123 } 124 125 /** 126 * Play the given stream to a specific device. 127 * @param a The audio stream to play. 128 * @param deviceName The device to play the audio to. 129 */ 130 public AudioPlayer( final AudioStream a, final String deviceName ) 131 { 132 this.stream = a; 133 this.deviceName = deviceName; 134 this.setTimecodeObject( new AudioTimecode(0) ); 135 } 136 137 /** 138 * Set the length of the sound line's buffer in milliseconds. The longer 139 * the buffer the less likely the soundline will be to pop but the shorter 140 * the buffer the closer to real-time the sound output will be. This value 141 * must be set before the audio line is opened otherwise it will have no 142 * effect. 143 * @param ms The length of the sound line in milliseconds. 144 */ 145 public void setSoundLineBufferSize( final double ms ) 146 { 147 this.soundLineBufferSize = ms; 148 } 149 150 /** 151 * Add the given audio event listener to this player. 152 * @param l The listener to add. 153 */ 154 public void addAudioEventListener( final AudioEventListener l ) 155 { 156 this.listeners.add( l ); 157 } 158 159 /** 160 * Remove the given event from the listeners on this player. 161 * @param l The listener to remove. 162 */ 163 public void removeAudioEventListener( final AudioEventListener l ) 164 { 165 this.listeners.remove( l ); 166 } 167 168 /** 169 * Fires the audio ended event to the listeners. 170 * @param as The audio stream that ended 171 */ 172 protected void fireAudioEnded( final AudioStream as ) 173 { 174 for( final AudioEventListener ael : this.listeners ) 175 ael.audioEnded(); 176 } 177 178 /** 179 * Fires an event that says the samples will be played. 180 * @param sc The samples to play 181 */ 182 protected void fireBeforePlay( final SampleChunk sc ) 183 { 184 for( final AudioEventListener ael: this.listeners ) 185 ael.beforePlay( sc ); 186 } 187 188 /** 189 * Fires an event that says the samples have been played. 190 * @param sc The sampled have been played 191 */ 192 protected void fireAfterPlay( final SampleChunk sc ) 193 { 194 for( final AudioEventListener ael: this.listeners ) 195 ael.afterPlay( this, sc ); 196 } 197 198 /** 199 * Set the timecode object that is updated as the audio is played. 200 * @param t The timecode object. 201 */ 202 public void setTimecodeObject( final AudioTimecode t ) 203 { 204 this.currentTimecode = t; 205 } 206 207 /** 208 * Returns the current timecode. 209 * @return The timecode object. 210 */ 211 public Timecode getTimecodeObject() 212 { 213 return this.currentTimecode; 214 } 215 216 /** 217 * {@inheritDoc} 218 * @see java.lang.Runnable#run() 219 */ 220 @Override 221 public void run() 222 { 223 this.setMode( Mode.PLAY ); 224 this.timecodeReadAt = 0; 225 if( !this.started ) 226 { 227 this.started = true; 228 try 229 { 230 // Open the sound system. 231 this.openJavaSound(); 232 233 // Read samples until there are no more. 234 SampleChunk samples = null; 235 boolean ended = false; 236 while( !ended && this.mode != Mode.STOP ) 237 { 238 if( this.mode == Mode.PLAY ) 239 { 240 // Get the next sample chunk 241 samples = this.stream.nextSampleChunk(); 242 243 // Check if we've reached the end of the line 244 if( samples == null ) 245 { 246 ended = true; 247 continue; 248 } 249 250 // Fire the before event 251 this.fireBeforePlay( samples ); 252 253 // Play the samples 254 this.playJavaSound( samples ); 255 256 // Fire the after event 257 this.fireAfterPlay( samples ); 258 259 // If we have a timecode object to update, we'll update it here 260 if( this.currentTimecode != null ) 261 { 262 this.currentTimestamp = samples.getStartTimecode(). 263 getTimecodeInMilliseconds(); 264 this.timecodeReadAt = System.currentTimeMillis(); 265 this.currentTimecode.setTimecodeInMilliseconds( this.currentTimestamp ); 266 } 267 } 268 else 269 { 270 // Let's be nice and not loop madly if we're not playing 271 // (we must be in PAUSE mode) 272 try 273 { 274 Thread.sleep( 500 ); 275 } 276 catch( final InterruptedException ie ) 277 { 278 } 279 } 280 } 281 282 // Fire the audio ended event 283 this.fireAudioEnded( this.stream ); 284 this.setMode( Mode.STOP ); 285 this.reset(); 286 } 287 catch( final Exception e ) 288 { 289 e.printStackTrace(); 290 } 291 finally 292 { 293 // Close the sound system 294 this.closeJavaSound(); 295 } 296 } 297 else 298 { 299 // Already playing something, so we just start going again 300 this.setMode( Mode.PLAY ); 301 } 302 } 303 304 /** 305 * Create a new audio player in a separate thread for playing audio. 306 * 307 * @param as The audio stream to play. 308 * @return The audio player created. 309 */ 310 public static AudioPlayer createAudioPlayer( final AudioStream as ) 311 { 312 final AudioPlayer ap = new AudioPlayer( as ); 313 new Thread( ap ).start(); 314 return ap; 315 } 316 317 /** 318 * Create a new audio player in a separate thread for playing audio. 319 * To find out device names, use {@link AudioUtils#getDevices()}. 320 * 321 * @param as The audio stream to play. 322 * @param device The name of the device to use. 323 * @return The audio player created. 324 */ 325 public static AudioPlayer createAudioPlayer( final AudioStream as, final String device ) 326 { 327 final AudioPlayer ap = new AudioPlayer( as, device ); 328 new Thread( ap ).start(); 329 return ap; 330 } 331 332 /** 333 * Open a line to the Java Sound APIs. 334 * 335 * @throws Exception if the Java sound system could not be initialised. 336 */ 337 private void openJavaSound() throws Exception 338 { 339 try 340 { 341 // Get a line (either the one we ask for, or any one). 342 if( this.deviceName != null ) 343 this.mLine = AudioUtils.getJavaOutputLine( this.deviceName, this.stream.getFormat() ); 344 else this.mLine = AudioUtils.getAnyJavaOutputLine( this.stream.getFormat() ); 345 346 if( this.mLine == null ) 347 throw new Exception( "Cannot instantiate a sound line." ); 348 349 // If no exception has been thrown we open the line. 350 this.mLine.open( this.mLine.getFormat(), (int) 351 (this.stream.getFormat().getSampleRateKHz() * this.soundLineBufferSize) ); 352 353 // If we've opened the line, we start it running 354 this.mLine.start(); 355 356 System.out.println( "Opened Java Sound Line: "+this.mLine.getFormat() ); 357 } 358 catch( final LineUnavailableException e ) 359 { 360 throw new Exception( "Could not open Java Sound audio line for" + 361 " the audio format "+this.stream.getFormat() ); 362 } 363 } 364 365 /** 366 * Play the given sample chunk to the Java sound line. The line should be 367 * set up to accept the samples that we're going to give it, as we did 368 * that in the {@link #openJavaSound()} method. 369 * 370 * @param chunk The chunk to play. 371 */ 372 private void playJavaSound( final SampleChunk chunk ) 373 { 374 final byte[] rawBytes = chunk.getSamples(); 375 this.mLine.write( rawBytes, 0, rawBytes.length ); 376 } 377 378 /** 379 * Close down the Java sound APIs. 380 */ 381 private void closeJavaSound() 382 { 383 if( this.mLine != null ) 384 { 385 // Wait for the buffer to empty... 386 this.mLine.drain(); 387 388 // ...then close 389 this.mLine.close(); 390 this.mLine = null; 391 } 392 } 393 394 /** 395 * {@inheritDoc} 396 * @see org.openimaj.time.TimeKeeper#getTime() 397 */ 398 @Override 399 public AudioTimecode getTime() 400 { 401 // If we've not yet read any samples, just return the timecode 402 // object as it was first given to us. 403 if( this.timecodeReadAt == 0 ) 404 return this.currentTimecode; 405 406 // Update the timecode if we're playing (otherwise we'll return the 407 // latest timecode) 408 if( this.mode == Mode.PLAY ) 409 this.currentTimecode.setTimecodeInMilliseconds( this.currentTimestamp + 410 (System.currentTimeMillis() - this.timecodeReadAt) ); 411 412 return this.currentTimecode; 413 } 414 415 /** 416 * {@inheritDoc} 417 * @see org.openimaj.time.TimeKeeper#stop() 418 */ 419 @Override 420 public void stop() 421 { 422 this.setMode( Mode.STOP ); 423 } 424 425 /** 426 * Set the mode of the player. 427 * @param m 428 */ 429 public void setMode( final Mode m ) 430 { 431 this.mode = m; 432 } 433 434 /** 435 * {@inheritDoc} 436 * @see org.openimaj.time.TimeKeeper#supportsPause() 437 */ 438 @Override 439 public boolean supportsPause() 440 { 441 return true; 442 } 443 444 /** 445 * {@inheritDoc} 446 * @see org.openimaj.time.TimeKeeper#supportsSeek() 447 */ 448 @Override 449 public boolean supportsSeek() 450 { 451 return true; 452 } 453 454 /** 455 * {@inheritDoc} 456 * @see org.openimaj.time.TimeKeeper#seek(long) 457 */ 458 @Override 459 public void seek( final long timestamp ) 460 { 461 this.stream.seek( timestamp ); 462 } 463 464 /** 465 * {@inheritDoc} 466 * @see org.openimaj.time.TimeKeeper#reset() 467 */ 468 @Override 469 public void reset() 470 { 471 this.timecodeReadAt = 0; 472 this.currentTimestamp = 0; 473 this.started = false; 474 this.currentTimecode.setTimecodeInMilliseconds( 0 ); 475 this.stream.reset(); 476 } 477 478 /** 479 * {@inheritDoc} 480 * @see org.openimaj.time.TimeKeeper#pause() 481 */ 482 @Override 483 public void pause() 484 { 485 this.setMode( Mode.PAUSE ); 486 487 // Set the current timecode to the time at which we paused. 488 this.currentTimecode.setTimecodeInMilliseconds( this.currentTimestamp + 489 (System.currentTimeMillis() - this.timecodeReadAt) ); 490 } 491}