org.openimaj.audio.AudioPlayer Maven / Gradle / Ivy
/**
* Copyright (c) 2011, The University of Southampton and the individual contributors.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the University of Southampton nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
*
*/
package org.openimaj.audio;
import java.util.ArrayList;
import java.util.List;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import org.openimaj.audio.timecode.AudioTimecode;
import org.openimaj.audio.util.AudioUtils;
import org.openimaj.time.TimeKeeper;
import org.openimaj.time.Timecode;
/**
* Wraps the Java Sound APIs into the OpenIMAJ audio core for playing sounds.
*
* The {@link AudioPlayer} supports the {@link TimeKeeper} interface so that
* other methods can synchronise to the audio timestamps.
*
* The Audio Player as a {@link TimeKeeper} supports seeking but it may be
* possible that the underlying stream does not support seeking so the seek
* method may not affect the time keeper as expected.
*
* @author David Dupplaw ([email protected])
* @created 8 Jun 2011
*
*/
public class AudioPlayer implements Runnable, TimeKeeper
{
/** The audio stream being played */
private AudioStream stream = null;
/** The java audio output stream line */
private SourceDataLine mLine = null;
/** The current timecode being played */
private AudioTimecode currentTimecode = null;
/** The current audio timestamp */
private long currentTimestamp = 0;
/** At what timestamp the current timecode was read at */
private long timecodeReadAt = 0;
/** The device name on which to play */
private String deviceName = null;
/** The mode of the player */
private Mode mode = Mode.PLAY;
/** Listeners for events */
private final List listeners = new ArrayList();
/** Whether the system has been started */
private boolean started = false;
/**
* Number of milliseconds in the sound line buffer. < 100ms is good
* for real-time whereas the bigger the better for smooth sound reproduction
*/
private double soundLineBufferSize = 100;
/**
* Enumerator for the current state of the audio player.
*
* @author David Dupplaw ([email protected])
*
* @created 29 Nov 2011
*/
public enum Mode
{
/** The audio player is playing */
PLAY,
/** The audio player is paused */
PAUSE,
/** The audio player is stopped */
STOP
}
/**
* Default constructor that takes an audio
* stream to play.
*
* @param a The audio stream to play
*/
public AudioPlayer( final AudioStream a )
{
this( a, null );
}
/**
* Play the given stream to a specific device.
* @param a The audio stream to play.
* @param deviceName The device to play the audio to.
*/
public AudioPlayer( final AudioStream a, final String deviceName )
{
this.stream = a;
this.deviceName = deviceName;
this.setTimecodeObject( new AudioTimecode(0) );
}
/**
* Set the length of the sound line's buffer in milliseconds. The longer
* the buffer the less likely the soundline will be to pop but the shorter
* the buffer the closer to real-time the sound output will be. This value
* must be set before the audio line is opened otherwise it will have no
* effect.
* @param ms The length of the sound line in milliseconds.
*/
public void setSoundLineBufferSize( final double ms )
{
this.soundLineBufferSize = ms;
}
/**
* Add the given audio event listener to this player.
* @param l The listener to add.
*/
public void addAudioEventListener( final AudioEventListener l )
{
this.listeners.add( l );
}
/**
* Remove the given event from the listeners on this player.
* @param l The listener to remove.
*/
public void removeAudioEventListener( final AudioEventListener l )
{
this.listeners.remove( l );
}
/**
* Fires the audio ended event to the listeners.
* @param as The audio stream that ended
*/
protected void fireAudioEnded( final AudioStream as )
{
for( final AudioEventListener ael : this.listeners )
ael.audioEnded();
}
/**
* Fires an event that says the samples will be played.
* @param sc The samples to play
*/
protected void fireBeforePlay( final SampleChunk sc )
{
for( final AudioEventListener ael: this.listeners )
ael.beforePlay( sc );
}
/**
* Fires an event that says the samples have been played.
* @param sc The sampled have been played
*/
protected void fireAfterPlay( final SampleChunk sc )
{
for( final AudioEventListener ael: this.listeners )
ael.afterPlay( this, sc );
}
/**
* Set the timecode object that is updated as the audio is played.
* @param t The timecode object.
*/
public void setTimecodeObject( final AudioTimecode t )
{
this.currentTimecode = t;
}
/**
* Returns the current timecode.
* @return The timecode object.
*/
public Timecode getTimecodeObject()
{
return this.currentTimecode;
}
/**
* {@inheritDoc}
* @see java.lang.Runnable#run()
*/
@Override
public void run()
{
this.setMode( Mode.PLAY );
this.timecodeReadAt = 0;
if( !this.started )
{
this.started = true;
try
{
// Open the sound system.
this.openJavaSound();
// Read samples until there are no more.
SampleChunk samples = null;
boolean ended = false;
while( !ended && this.mode != Mode.STOP )
{
if( this.mode == Mode.PLAY )
{
// Get the next sample chunk
samples = this.stream.nextSampleChunk();
// Check if we've reached the end of the line
if( samples == null )
{
ended = true;
continue;
}
// Fire the before event
this.fireBeforePlay( samples );
// Play the samples
this.playJavaSound( samples );
// Fire the after event
this.fireAfterPlay( samples );
// If we have a timecode object to update, we'll update it here
if( this.currentTimecode != null )
{
this.currentTimestamp = samples.getStartTimecode().
getTimecodeInMilliseconds();
this.timecodeReadAt = System.currentTimeMillis();
this.currentTimecode.setTimecodeInMilliseconds( this.currentTimestamp );
}
}
else
{
// Let's be nice and not loop madly if we're not playing
// (we must be in PAUSE mode)
try
{
Thread.sleep( 500 );
}
catch( final InterruptedException ie )
{
}
}
}
// Fire the audio ended event
this.fireAudioEnded( this.stream );
this.setMode( Mode.STOP );
this.reset();
}
catch( final Exception e )
{
e.printStackTrace();
}
finally
{
// Close the sound system
this.closeJavaSound();
}
}
else
{
// Already playing something, so we just start going again
this.setMode( Mode.PLAY );
}
}
/**
* Create a new audio player in a separate thread for playing audio.
*
* @param as The audio stream to play.
* @return The audio player created.
*/
public static AudioPlayer createAudioPlayer( final AudioStream as )
{
final AudioPlayer ap = new AudioPlayer( as );
new Thread( ap ).start();
return ap;
}
/**
* Create a new audio player in a separate thread for playing audio.
* To find out device names, use {@link AudioUtils#getDevices()}.
*
* @param as The audio stream to play.
* @param device The name of the device to use.
* @return The audio player created.
*/
public static AudioPlayer createAudioPlayer( final AudioStream as, final String device )
{
final AudioPlayer ap = new AudioPlayer( as, device );
new Thread( ap ).start();
return ap;
}
/**
* Open a line to the Java Sound APIs.
*
* @throws Exception if the Java sound system could not be initialised.
*/
private void openJavaSound() throws Exception
{
try
{
// Get a line (either the one we ask for, or any one).
if( this.deviceName != null )
this.mLine = AudioUtils.getJavaOutputLine( this.deviceName, this.stream.getFormat() );
else this.mLine = AudioUtils.getAnyJavaOutputLine( this.stream.getFormat() );
if( this.mLine == null )
throw new Exception( "Cannot instantiate a sound line." );
// If no exception has been thrown we open the line.
this.mLine.open( this.mLine.getFormat(), (int)
(this.stream.getFormat().getSampleRateKHz() * this.soundLineBufferSize) );
// If we've opened the line, we start it running
this.mLine.start();
System.out.println( "Opened Java Sound Line: "+this.mLine.getFormat() );
}
catch( final LineUnavailableException e )
{
throw new Exception( "Could not open Java Sound audio line for" +
" the audio format "+this.stream.getFormat() );
}
}
/**
* Play the given sample chunk to the Java sound line. The line should be
* set up to accept the samples that we're going to give it, as we did
* that in the {@link #openJavaSound()} method.
*
* @param chunk The chunk to play.
*/
private void playJavaSound( final SampleChunk chunk )
{
final byte[] rawBytes = chunk.getSamples();
this.mLine.write( rawBytes, 0, rawBytes.length );
}
/**
* Close down the Java sound APIs.
*/
private void closeJavaSound()
{
if( this.mLine != null )
{
// Wait for the buffer to empty...
this.mLine.drain();
// ...then close
this.mLine.close();
this.mLine = null;
}
}
/**
* {@inheritDoc}
* @see org.openimaj.time.TimeKeeper#getTime()
*/
@Override
public AudioTimecode getTime()
{
// If we've not yet read any samples, just return the timecode
// object as it was first given to us.
if( this.timecodeReadAt == 0 )
return this.currentTimecode;
// Update the timecode if we're playing (otherwise we'll return the
// latest timecode)
if( this.mode == Mode.PLAY )
this.currentTimecode.setTimecodeInMilliseconds( this.currentTimestamp +
(System.currentTimeMillis() - this.timecodeReadAt) );
return this.currentTimecode;
}
/**
* {@inheritDoc}
* @see org.openimaj.time.TimeKeeper#stop()
*/
@Override
public void stop()
{
this.setMode( Mode.STOP );
}
/**
* Set the mode of the player.
* @param m
*/
public void setMode( final Mode m )
{
this.mode = m;
}
/**
* {@inheritDoc}
* @see org.openimaj.time.TimeKeeper#supportsPause()
*/
@Override
public boolean supportsPause()
{
return true;
}
/**
* {@inheritDoc}
* @see org.openimaj.time.TimeKeeper#supportsSeek()
*/
@Override
public boolean supportsSeek()
{
return true;
}
/**
* {@inheritDoc}
* @see org.openimaj.time.TimeKeeper#seek(long)
*/
@Override
public void seek( final long timestamp )
{
this.stream.seek( timestamp );
}
/**
* {@inheritDoc}
* @see org.openimaj.time.TimeKeeper#reset()
*/
@Override
public void reset()
{
this.timecodeReadAt = 0;
this.currentTimestamp = 0;
this.started = false;
this.currentTimecode.setTimecodeInMilliseconds( 0 );
this.stream.reset();
}
/**
* {@inheritDoc}
* @see org.openimaj.time.TimeKeeper#pause()
*/
@Override
public void pause()
{
this.setMode( Mode.PAUSE );
// Set the current timecode to the time at which we paused.
this.currentTimecode.setTimecodeInMilliseconds( this.currentTimestamp +
(System.currentTimeMillis() - this.timecodeReadAt) );
}
}