package ddf.minim.ugens;
import java.util.Arrays;
import ddf.minim.AudioMetaData;
import ddf.minim.Minim;
import ddf.minim.MultiChannelBuffer;
import ddf.minim.Playable;
import ddf.minim.UGen;
import ddf.minim.spi.AudioRecordingStream;
/**
* The FilePlayer UGen provides a way for you to play audio files in the same
* way that AudioPlayer does, allowing you to patch them into a UGen graph any way you choose.
* The constructor for FilePlayer takes an AudioRecordingStream,
* which you can get from a Minim object by calling the loadFileStream method.
*
* @example Synthesis/filePlayerExample
*
* @author Damien Di Fede
*
* @related Minim
* @related AudioPlayer
* @related UGen
*
*/
public class FilePlayer extends UGen implements Playable
{
private AudioRecordingStream mFileStream;
private boolean isPaused;
// buffer we use to read from the stream
private MultiChannelBuffer buffer;
// where in the buffer we should read the next sample from
private int bufferOutIndex;
/**
* Construct a FilePlayer that will read from iFileStream.
*
* @param iFileStream
* AudioRecordingStream: the stream this should read from
*
* @example Synthesis/filePlayerExample
*/
public FilePlayer( AudioRecordingStream iFileStream )
{
mFileStream = iFileStream;
buffer = new MultiChannelBuffer(1024, mFileStream.getFormat().getChannels());
bufferOutIndex = 0;
// we'll need to do this eventually, I think.
// but for now we don't need this because it starts the iothread,
// which is not what we want.
// mFileStream.open();
// mFileStream.play();
}
/**
* Returns the underlying AudioRecordingStream.
*
* @return AudioRecordingStream: the underlying stream
*
* @related Minim
* @related AudioRecordingStream
* @related FilePlayer
*/
public AudioRecordingStream getStream()
{
return mFileStream;
}
/**
* Starts playback from the current position.
* If this was previously set to loop, looping will be disabled.
*
* @example Synthesis/filePlayerExample
*
* @related FilePlayer
*
*/
public void play()
{
mFileStream.play();
isPaused = false;
}
/**
* Starts playback <code>millis</code> from the beginning.
* If this was previously set to loop, looping will be disabled.
*
* @param millis
* int: where to start playing the file, in milliseconds
*
* @related FilePlayer
*/
public void play(int millis)
{
cue(millis);
play();
}
/**
* Pauses playback.
*
* @example Synthesis/filePlayerExample
*
* @related FilePlayer
*/
public void pause()
{
mFileStream.pause();
isPaused = true;
}
/**
* Rewinds to the beginning. This <i>does not</i> stop playback.
*
* @related FilePlayer
*/
public void rewind()
{
cue(0);
}
/**
* Sets looping to continuous. If this is already playing, the position
* <i>will not</i> be reset to the beginning. If this is not playing,
* it will start playing.
*
* @shortdesc Start looping playback of the file.
*
* @example Synthesis/filePlayerExample
*
* @related loopCount ( )
* @related setLoopPoints ( )
* @related isLooping ( )
* @related FilePlayer
*/
public void loop()
{
loop(Minim.LOOP_CONTINUOUSLY);
}
/**
* Sets this to loop <code>loopCount</code> times.
* If this is already playing,
* the position <i>will not</i> be reset to the beginning.
* If this is not playing, it will start playing.
*
* @shortdesc Sets this to loop <code>loopCount</code> times.
*
* @param loopCount
* int: the number of times to loop
*
* @related loopCount ( )
* @related setLoopPoints ( )
* @related isLooping ( )
* @related FilePlayer
*/
public void loop(int loopCount)
{
if ( isPaused )
{
int pos = mFileStream.getMillisecondPosition();
mFileStream.loop( loopCount );
cue( pos );
}
else
{
mFileStream.loop(loopCount);
}
isPaused = false;
}
/**
* Returns the number of loops left to do.
*
* @return int: the number of loops left
*
* @related loop ( )
* @related FilePlayer
*/
public int loopCount()
{
return mFileStream.getLoopCount();
}
/**
* Returns the length of the sound in milliseconds. If for any reason the
* length could not be determined, this will return -1. However, an unknown
* length should not impact playback.
*
* @shortdesc Returns the length of the sound in milliseconds.
*
* @return int: the length of the sound in milliseconds
*
* @related FilePlayer
*/
public int length()
{
return mFileStream.getMillisecondLength();
}
/**
* Returns the current position of the "playhead" (ie how much of
* the sound has already been played)
*
* @return int: the current position of the "playhead", in milliseconds
*
* @related FilePlayer
*/
public int position()
{
return mFileStream.getMillisecondPosition();
}
/**
* Sets the position to <code>millis</code> milliseconds from
* the beginning. This will not change the play state. If an error
* occurs while trying to cue, the position will not change.
* If you try to cue to a negative position or try to a position
* that is greater than <code>length()</code>, the amount will be clamped
* to zero or <code>length()</code>.
*
* @shortdesc Sets the position to <code>millis</code> milliseconds from
* the beginning.
*
* @param millis int: the position to place the "playhead", in milliseconds
*
* @related FilePlayer
*/
public void cue(int millis)
{
if (millis < 0)
{
millis = 0;
}
else if (millis > length())
{
millis = length();
}
mFileStream.setMillisecondPosition(millis);
// change the position in the stream invalidates our buffer, so we read a new buffer
fillBuffer();
}
/**
* Skips <code>millis</code> from the current position. <code>millis</code>
* can be negative, which will make this skip backwards. If the skip amount
* would result in a negative position or a position that is greater than
* <code>length()</code>, the new position will be clamped to zero or
* <code>length()</code>.
*
* @shortdesc Skips <code>millis</code> from the current position.
*
* @param millis
* int: how many milliseconds to skip, sign indicates direction
*
* @related FilePlayer
*/
public void skip(int millis)
{
int pos = position() + millis;
if (pos < 0)
{
pos = 0;
}
else if (pos > length())
{
pos = length();
}
//Minim.debug("AudioPlayer.skip: skipping " + millis + " milliseconds, new position is " + pos);
cue( pos );
}
/**
* Returns true if this is currently playing and has more than one loop
* left to play.
*
* @return boolean: true if this is looping
*
* @related loop ( )
* @related FilePlayer
*/
public boolean isLooping()
{
return mFileStream.getLoopCount() != 0;
}
/**
* Returns true if this currently playing.
*
* @return boolean: the current play state
*
* @example Synthesis/filePlayerExample
*
* @related play ( )
* @related pause ( )
* @related FilePlayer
*/
public boolean isPlaying()
{
return mFileStream.isPlaying();
}
/**
* Returns the meta data for the recording being played by this player.
*
* @return
* AudioMetaData: the meta data for this player's recording
*
* @related AudioMetaData
* @related FilePlayer
*/
public AudioMetaData getMetaData()
{
return mFileStream.getMetaData();
}
/**
* Sets the loop points used when looping.
*
* @param start
* int: the start of the loop in milliseconds
* @param stop
* int: the end of the loop in milliseconds
*
* @related loop ( )
* @related FilePlayer
*/
public void setLoopPoints(int start, int stop)
{
mFileStream.setLoopPoints(start, stop);
}
/**
* Calling close will close the AudioRecordingStream that this wraps,
* which is proper cleanup for using the stream.
*
* @related FilePlayer
*/
public void close()
{
mFileStream.close();
}
private void fillBuffer()
{
mFileStream.read(buffer);
bufferOutIndex = 0;
}
@Override
protected void uGenerate(float[] channels)
{
if ( mFileStream.isPlaying() )
{
// special case: mono expands out to all channels.
if ( buffer.getChannelCount() == 1 )
{
Arrays.fill( channels, buffer.getSample( 0, bufferOutIndex ) );
}
// we have more than one channel, don't try to fill larger channel requests
if ( buffer.getChannelCount() <= channels.length )
{
for(int i = 0 ; i < channels.length; ++i)
{
channels[i] = buffer.getSample( i, bufferOutIndex );
}
}
// special case: we are stereo, output is mono.
else if ( channels.length == 1 && buffer.getChannelCount() == 2 )
{
channels[0] = (buffer.getSample( 0, bufferOutIndex ) + buffer.getSample( 1, bufferOutIndex ))/2.0f;
}
++bufferOutIndex;
if ( bufferOutIndex == buffer.getBufferSize() )
{
fillBuffer();
}
}
else
{
Arrays.fill( channels, 0 );
}
}
}