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 for 091 * 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 stream to play. 116 * 117 * @param a 118 * 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 * 128 * @param a 129 * The audio stream to play. 130 * @param deviceName 131 * The device to play the audio to. 132 */ 133 public AudioPlayer(final AudioStream a, final String deviceName) 134 { 135 this.stream = a; 136 this.deviceName = deviceName; 137 this.setTimecodeObject(new AudioTimecode(0)); 138 } 139 140 /** 141 * Set the length of the sound line's buffer in milliseconds. The longer the 142 * buffer the less likely the soundline will be to pop but the shorter the 143 * buffer the closer to real-time the sound output will be. This value must 144 * be set before the audio line is opened otherwise it will have no effect. 145 * 146 * @param ms 147 * The length of the sound line in milliseconds. 148 */ 149 public void setSoundLineBufferSize(final double ms) 150 { 151 this.soundLineBufferSize = ms; 152 } 153 154 /** 155 * Add the given audio event listener to this player. 156 * 157 * @param l 158 * The listener to add. 159 */ 160 public void addAudioEventListener(final AudioEventListener l) 161 { 162 this.listeners.add(l); 163 } 164 165 /** 166 * Remove the given event from the listeners on this player. 167 * 168 * @param l 169 * The listener to remove. 170 */ 171 public void removeAudioEventListener(final AudioEventListener l) 172 { 173 this.listeners.remove(l); 174 } 175 176 /** 177 * Fires the audio ended event to the listeners. 178 * 179 * @param as 180 * The audio stream that ended 181 */ 182 protected void fireAudioEnded(final AudioStream as) 183 { 184 for (final AudioEventListener ael : this.listeners) 185 ael.audioEnded(); 186 } 187 188 /** 189 * Fires an event that says the samples will be played. 190 * 191 * @param sc 192 * The samples to play 193 */ 194 protected void fireBeforePlay(final SampleChunk sc) 195 { 196 for (final AudioEventListener ael : this.listeners) 197 ael.beforePlay(sc); 198 } 199 200 /** 201 * Fires an event that says the samples have been played. 202 * 203 * @param sc 204 * The sampled have been played 205 */ 206 protected void fireAfterPlay(final SampleChunk sc) 207 { 208 for (final AudioEventListener ael : this.listeners) 209 ael.afterPlay(this, sc); 210 } 211 212 /** 213 * Set the timecode object that is updated as the audio is played. 214 * 215 * @param t 216 * The timecode object. 217 */ 218 public void setTimecodeObject(final AudioTimecode t) 219 { 220 this.currentTimecode = t; 221 } 222 223 /** 224 * Returns the current timecode. 225 * 226 * @return The timecode object. 227 */ 228 public Timecode getTimecodeObject() 229 { 230 return this.currentTimecode; 231 } 232 233 /** 234 * {@inheritDoc} 235 * 236 * @see java.lang.Runnable#run() 237 */ 238 @Override 239 public void run() 240 { 241 this.setMode(Mode.PLAY); 242 this.timecodeReadAt = 0; 243 if (!this.started) 244 { 245 this.started = true; 246 try 247 { 248 // Open the sound system. 249 this.openJavaSound(); 250 251 // Read samples until there are no more. 252 SampleChunk samples = null; 253 boolean ended = false; 254 while (!ended && this.mode != Mode.STOP) 255 { 256 if (this.mode == Mode.PLAY) 257 { 258 // System.out.println("loop"); 259 // Get the next sample chunk 260 samples = this.stream.nextSampleChunk(); 261 262 // Check if we've reached the end of the line 263 if (samples == null) 264 { 265 ended = true; 266 continue; 267 } 268 269 // Fire the before event 270 this.fireBeforePlay(samples); 271 272 // Play the samples 273 this.playJavaSound(samples); 274 275 // Fire the after event 276 this.fireAfterPlay(samples); 277 278 // If we have a timecode object to update, we'll update 279 // it here 280 if (this.currentTimecode != null) 281 { 282 this.currentTimestamp = samples.getStartTimecode(). 283 getTimecodeInMilliseconds(); 284 this.timecodeReadAt = System.currentTimeMillis(); 285 this.currentTimecode.setTimecodeInMilliseconds(this.currentTimestamp); 286 } 287 } 288 else 289 { 290 // Let's be nice and not loop madly if we're not playing 291 // (we must be in PAUSE mode) 292 try 293 { 294 Thread.sleep(500); 295 } catch (final InterruptedException ie) 296 { 297 } 298 } 299 } 300 301 // Fire the audio ended event 302 this.fireAudioEnded(this.stream); 303 this.setMode(Mode.STOP); 304 this.reset(); 305 } catch (final Exception e) 306 { 307 e.printStackTrace(); 308 } finally 309 { 310 // Close the sound system 311 this.closeJavaSound(); 312 } 313 } 314 else 315 { 316 // Already playing something, so we just start going again 317 this.setMode(Mode.PLAY); 318 } 319 } 320 321 /** 322 * Create a new audio player in a separate thread for playing audio. 323 * 324 * @param as 325 * The audio stream to play. 326 * @return The audio player created. 327 */ 328 public static AudioPlayer createAudioPlayer(final AudioStream as) 329 { 330 final AudioPlayer ap = new AudioPlayer(as); 331 new Thread(ap).start(); 332 return ap; 333 } 334 335 /** 336 * Create a new audio player in a separate thread for playing audio. To find 337 * out device names, use {@link AudioUtils#getDevices()}. 338 * 339 * @param as 340 * The audio stream to play. 341 * @param device 342 * The name of the device to use. 343 * @return The audio player created. 344 */ 345 public static AudioPlayer createAudioPlayer(final AudioStream as, final String device) 346 { 347 final AudioPlayer ap = new AudioPlayer(as, device); 348 new Thread(ap).start(); 349 return ap; 350 } 351 352 /** 353 * Open a line to the Java Sound APIs. 354 * 355 * @throws Exception 356 * if the Java sound system could not be initialised. 357 */ 358 private void openJavaSound() throws Exception 359 { 360 try 361 { 362 // Get a line (either the one we ask for, or any one). 363 if (this.deviceName != null) 364 this.mLine = AudioUtils.getJavaOutputLine(this.deviceName, this.stream.getFormat()); 365 else 366 this.mLine = AudioUtils.getAnyJavaOutputLine(this.stream.getFormat()); 367 368 if (this.mLine == null) 369 throw new Exception("Cannot instantiate a sound line."); 370 371 // If no exception has been thrown we open the line. 372 this.mLine.open(this.mLine.getFormat(), (int) 373 (this.stream.getFormat().getSampleRateKHz() * this.soundLineBufferSize)); 374 375 // If we've opened the line, we start it running 376 this.mLine.start(); 377 378 System.out.println("Opened Java Sound Line: " + this.mLine.getFormat()); 379 } catch (final LineUnavailableException e) 380 { 381 throw new Exception("Could not open Java Sound audio line for" + 382 " the audio format " + this.stream.getFormat()); 383 } 384 } 385 386 /** 387 * Play the given sample chunk to the Java sound line. The line should be 388 * set up to accept the samples that we're going to give it, as we did that 389 * in the {@link #openJavaSound()} method. 390 * 391 * @param chunk 392 * The chunk to play. 393 */ 394 private void playJavaSound(final SampleChunk chunk) 395 { 396 final byte[] rawBytes = chunk.getSamples(); 397 this.mLine.write(rawBytes, 0, rawBytes.length); 398 } 399 400 /** 401 * Close down the Java sound APIs. 402 */ 403 private void closeJavaSound() 404 { 405 if (this.mLine != null) 406 { 407 // Wait for the buffer to empty... 408 this.mLine.drain(); 409 410 // ...then close 411 this.mLine.close(); 412 this.mLine = null; 413 } 414 } 415 416 /** 417 * {@inheritDoc} 418 * 419 * @see org.openimaj.time.TimeKeeper#getTime() 420 */ 421 @Override 422 public AudioTimecode getTime() 423 { 424 // If we've not yet read any samples, just return the timecode 425 // object as it was first given to us. 426 if (this.timecodeReadAt == 0) 427 return this.currentTimecode; 428 429 // Update the timecode if we're playing (otherwise we'll return the 430 // latest timecode) 431 if (this.mode == Mode.PLAY) 432 this.currentTimecode.setTimecodeInMilliseconds(this.currentTimestamp + 433 (System.currentTimeMillis() - this.timecodeReadAt)); 434 435 return this.currentTimecode; 436 } 437 438 /** 439 * {@inheritDoc} 440 * 441 * @see org.openimaj.time.TimeKeeper#stop() 442 */ 443 @Override 444 public void stop() 445 { 446 this.setMode(Mode.STOP); 447 } 448 449 /** 450 * Set the mode of the player. 451 * 452 * @param m 453 */ 454 public void setMode(final Mode m) 455 { 456 this.mode = m; 457 } 458 459 /** 460 * {@inheritDoc} 461 * 462 * @see org.openimaj.time.TimeKeeper#supportsPause() 463 */ 464 @Override 465 public boolean supportsPause() 466 { 467 return true; 468 } 469 470 /** 471 * {@inheritDoc} 472 * 473 * @see org.openimaj.time.TimeKeeper#supportsSeek() 474 */ 475 @Override 476 public boolean supportsSeek() 477 { 478 return true; 479 } 480 481 /** 482 * {@inheritDoc} 483 * 484 * @see org.openimaj.time.TimeKeeper#seek(long) 485 */ 486 @Override 487 public void seek(final long timestamp) 488 { 489 this.stream.seek(timestamp); 490 } 491 492 /** 493 * {@inheritDoc} 494 * 495 * @see org.openimaj.time.TimeKeeper#reset() 496 */ 497 @Override 498 public void reset() 499 { 500 this.timecodeReadAt = 0; 501 this.currentTimestamp = 0; 502 this.started = false; 503 this.currentTimecode.setTimecodeInMilliseconds(0); 504 this.stream.reset(); 505 } 506 507 /** 508 * {@inheritDoc} 509 * 510 * @see org.openimaj.time.TimeKeeper#pause() 511 */ 512 @Override 513 public void pause() 514 { 515 this.setMode(Mode.PAUSE); 516 517 // Set the current timecode to the time at which we paused. 518 this.currentTimecode.setTimecodeInMilliseconds(this.currentTimestamp + 519 (System.currentTimeMillis() - this.timecodeReadAt)); 520 } 521}