/*
* Copyright (c) 2007 - 2008 by Damien Di Fede <ddf@compartmental.net>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as published
* by the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package ddf.minim.javasound;
import java.io.IOException;
import java.util.Arrays;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.Control;
import javax.sound.sampled.SourceDataLine;
import org.tritonus.share.sampled.AudioUtils;
import ddf.minim.AudioEffect;
import ddf.minim.AudioListener;
import ddf.minim.AudioMetaData;
import ddf.minim.Minim;
import ddf.minim.MultiChannelBuffer;
import ddf.minim.spi.AudioRecordingStream;
abstract class JSBaseAudioRecordingStream implements Runnable,
AudioRecordingStream
{
private Thread iothread;
private AudioListener listener;
private AudioEffect effect;
private AudioMetaData meta;
// reading stuff
private boolean play;
private boolean loop;
private int numLoops;
// loop begin is in milliseconds
private int loopBegin;
// loop end is in bytes
private int loopEnd;
protected AudioInputStream ais;
// byte array we use in readBytes
private byte[] rawBytes;
// byte array we use in skip
private byte[] skipBytes;
// whether or not we should read from the file
// this is different from whether we should play or not.
// this will always be true, unless we've got
// bytes left in rawBytes that need to be written
// to the output line. when that happens this will be
// set to false. i use a boolean instead of inferring the
// the state from the value of bytesWritten so that
// if the implementation changes, it can.
private boolean shouldRead;
// accumulates the total number of bytes that have been
// written out to the output line so that we can
// report how far into the stream we are.
private int totalBytesRead;
// how many bytes have we written to the output line
// we keep track of this so that if a line is stopped
// in the middle of a write, which can happen if
// the stream is paused, we can pick up where we left
// off. this means we don't have to sit and spin
// in writeBytes waiting to be able to write the rest
// of the bytes, we can just exit and allow silence
// to be broadcasted out to the listener.
private int bytesWritten;
// writing stuff
protected AudioFormat format;
private SourceDataLine line;
private FloatSampleBuffer buffer;
private int bufferSize;
private boolean finished;
private float[] silence;
protected JSMinim system;
JSBaseAudioRecordingStream(JSMinim sys, AudioMetaData metaData,
AudioInputStream stream, SourceDataLine sdl, int inBufferSize, int msLen)
{
system = sys;
meta = metaData;
format = sdl.getFormat();
bufferSize = inBufferSize;
// allocate reading data
buffer = new FloatSampleBuffer( format.getChannels(), bufferSize, format.getSampleRate() );
system.debug( "JSBaseAudioRecordingStream :: FloatSampleBuffer has " + buffer.getSampleCount() + " samples." );
rawBytes = new byte[buffer.getByteArrayBufferSize( format )];
system.debug( "JSBaseAudioRecordingStream :: rawBytes has length " + rawBytes.length );
skipBytes = new byte[ (int)AudioUtils.millis2BytesFrameAligned( 10000, format ) ];
system.debug( "JSBaseAudioRecordingStream :: skipBytes has length " + skipBytes.length );
finished = false;
line = sdl;
ais = stream;
loop = false;
play = false;
numLoops = 0;
loopBegin = 0;
loopEnd = (int)AudioUtils.millis2BytesFrameAligned( msLen, format );
silence = new float[bufferSize];
iothread = null;
totalBytesRead = 0;
bytesWritten = 0;
shouldRead = true;
}
public AudioMetaData getMetaData()
{
return meta;
}
public int getMillisecondLength()
{
return meta.length();
}
public void run()
{
while ( !finished )
{
if ( play )
{
if ( shouldRead )
{
// read in a full buffer of bytes from the file
if ( loop )
{
readBytesLoop();
}
else
{
readBytes();
}
// convert them to floating point
// hand those arrays to our effect
// and convert back to bytes
process();
}
// write to the line.
writeBytes();
// send samples to the listener
// these will be what we just put into the line
// which means they should be pretty well sync'd
// with the audible result
broadcast();
// take a nap
Thread.yield();
}
else
{
// if we're not playing, we can just chill out until we're told
// to play again.
// no reason to sit and spin doing nothing.
system.debug( "Gonna wait..." );
// but first set out an empty buffer, to represent our silenced
// state.
broadcast();
// go to sleep for a really long time. we'll be interrupted if
// we need to start up again.
sleep( 30000 );
system.debug( "Done waiting!" );
}
} // while ( !finished )
// flush the line before we close it. because it's polite.
line.flush();
line.close();
line = null;
}
private void sleep(int millis)
{
try
{
Thread.sleep( millis );
}
catch ( InterruptedException e )
{
}
}
private int readBytes()
{
int bytesRead = 0;
int toRead = rawBytes.length;
try
{
while ( bytesRead < toRead )
{
int actualRead = 0;
synchronized ( ais )
{
actualRead = ais.read( rawBytes, bytesRead, toRead - bytesRead );
// JSMinim.debug("Wanted to read " + (toRead-bytesRead) + ",
// actually read " + actualRead);
}
if ( actualRead == -1 )
{
system.debug( "Actual read was -1, pausing..." );
pause();
break;
}
else
{
bytesRead += actualRead;
}
}
}
catch ( IOException e )
{
system.error( "Error reading from the file - " + e.getMessage() );
}
totalBytesRead += bytesRead;
return bytesRead;
}
private void readBytesLoop()
{
int toLoopEnd = loopEnd - totalBytesRead;
if ( toLoopEnd <= 0 )
{
//System.out.println("Returning to loopBegin because toLoopEnd <= 0");
if ( loop && numLoops != Minim.LOOP_CONTINUOUSLY )
{
numLoops--;
}
if ( numLoops != 0 )
{
// whoops, our loop end point got switched up
setMillisecondPosition( loopBegin );
readBytesLoop();
}
else
{
Arrays.fill(rawBytes, (byte)0);
}
return;
}
if ( toLoopEnd < rawBytes.length )
{
readBytesWrap( toLoopEnd, 0 );
if ( loop && numLoops == 0 )
{
loop = false;
pause();
}
else if ( loop )
{
//System.out.println("Returning to loopBegin because else if loop");
if ( numLoops != Minim.LOOP_CONTINUOUSLY )
{
numLoops--;
}
setMillisecondPosition( loopBegin );
readBytesWrap( rawBytes.length - toLoopEnd, toLoopEnd );
}
}
else
{
readBytesWrap( rawBytes.length, 0 );
}
}
// read toRead bytes from ais into rawBytes.
// we assume here that if we get to the end of the file
// that we should wrap around to the beginning
private void readBytesWrap(int toRead, int offset)
{
int bytesRead = 0;
try
{
while ( bytesRead < toRead )
{
int actualRead = 0;
synchronized ( ais )
{
actualRead = ais.read( rawBytes, bytesRead + offset, toRead - bytesRead );
}
if ( -1 == actualRead )
{
//System.out.println("!!!!!!! Looping with numLoops " + numLoops);
setMillisecondPosition( 0 );
if ( numLoops != Minim.LOOP_CONTINUOUSLY )
{
numLoops--;
}
}
else if ( actualRead == 0 )
{
// we want to prevent an infinite loop
// but this will hopefully never happen because
// we set the loop end point with a frame aligned byte
// number
break;
}
else
{
bytesRead += actualRead;
totalBytesRead += actualRead;
}
}
}
catch ( IOException ioe )
{
system.error( "Error reading from the file - " + ioe.getMessage() );
}
}
private void writeBytes()
{
// the write call will block until the requested amount of bytes
// is written, however the user might stop the line in the
// middle of writing and then we get told how much was actually written.
// because of that, we might not need to write the entire array when we
// get here.
int needToWrite = rawBytes.length - bytesWritten;
int actualWrit = line.write( rawBytes, bytesWritten, needToWrite );
// if the total written is not equal to how much we needed to write
// then we need to remember where we were so that we don't read more
// until we finished writing our entire rawBytes array.
if ( actualWrit != needToWrite )
{
system.debug( "writeBytes: wrote " + actualWrit + " of " + needToWrite );
shouldRead = false;
bytesWritten += actualWrit;
}
else
{
// if it all got written, we should continue reading
// and we reset our bytesWritten value.
shouldRead = true;
bytesWritten = 0;
}
}
private void broadcast()
{
synchronized ( buffer )
{
if ( buffer.getChannelCount() == Minim.MONO )
{
if ( play )
{
listener.samples( buffer.getChannel( 0 ) );
}
else
{
listener.samples( silence );
}
}
else if ( buffer.getChannelCount() == Minim.STEREO )
{
if ( play )
{
listener.samples( buffer.getChannel( 0 ), buffer.getChannel( 1 ) );
}
else
{
listener.samples( silence, silence );
}
}
}
}
private synchronized void process()
{
synchronized ( buffer )
{
int frameCount = rawBytes.length / format.getFrameSize();
buffer.setSamplesFromBytes( rawBytes, 0, format, 0, frameCount );
// process the samples
if ( buffer.getChannelCount() == Minim.MONO )
{
effect.process( buffer.getChannel( 0 ) );
}
else if ( buffer.getChannelCount() == Minim.STEREO )
{
effect.process( buffer.getChannel( 0 ), buffer.getChannel( 1 ) );
}
// finally convert them back to bytes
buffer.convertToByteArray( rawBytes, 0, format );
}
}
public void play()
{
line.start();
loop = false;
numLoops = 0;
play = true;
// will wake up our data processing thread.
// iothread.interrupt();
}
public boolean isPlaying()
{
return play;
}
public void pause()
{
line.stop();
play = false;
}
public void loop(int n)
{
// let's get it cued before we muck with any of our state vars.
setMillisecondPosition( loopBegin );
loop = true;
numLoops = n;
play = true;
line.start();
// will wake up our data processing thread.
// iothread.interrupt();
}
public void open()
{
finished = false;
iothread = new Thread( this );
iothread.start();
}
public void close()
{
finished = true;
// try
// {
// iothread.join(10);
// }
// catch (InterruptedException e)
// {
// e.printStackTrace();
// }
iothread = null;
try
{
ais.close();
}
catch ( IOException e )
{
}
line.flush();
line.close();
}
public int bufferSize()
{
return bufferSize;
}
public AudioFormat getFormat()
{
return format;
}
public int getLoopCount()
{
return numLoops;
}
// TODO: consider using mark for marking the starting loop point
// in cases where the section being looped is not really huge.
// doing so will make it possible loop sections of large files
// without having to make a new AudioInputStream
public void setLoopPoints(int start, int stop)
{
if ( start <= 0 || start > stop )
{
loopBegin = 0;
}
else
{
loopBegin = start;
}
if ( stop <= getMillisecondLength() && stop > start )
{
loopEnd = (int)AudioUtils.millis2BytesFrameAligned( stop, format );
}
else
{
loopEnd = (int)AudioUtils.millis2BytesFrameAligned(
getMillisecondLength(), format );
}
}
public int getMillisecondPosition()
{
int pos = (int)AudioUtils.bytes2Millis( totalBytesRead, format );
// never report a position that is greater than the length of the stream
return Math.min( pos, getMillisecondLength() );
}
public void setMillisecondPosition(int millis)
{
// millis is guaranteed by methods that call this one to be
// in the interval [0, getMillisecondLength()], so we don't do bounds
// checking
boolean wasPlaying = play;
play = false;
if ( millis < getMillisecondPosition() )
{
rewind();
totalBytesRead = skip( millis );
}
else
{
totalBytesRead += skip( millis - getMillisecondPosition() );
}
play = wasPlaying;
// if we're supposed to be playing we need to
// poke the iothread, because it's possible it
// will have dropped into it's long sleep while we
// were doing our thing. this is especially
// likely if we are setting to a previous position.
// if ( play )
// {
// iothread.interrupt();
// }
}
public long getSampleFrameLength()
{
return ais.getFrameLength();
}
public Control[] getControls()
{
return line.getControls();
}
public void setAudioEffect(AudioEffect effect)
{
this.effect = effect;
}
public void setAudioListener(AudioListener listener)
{
this.listener = listener;
}
synchronized protected void rewind()
{
// close and reload
// because marking the thing such that you can play the
// entire file without the mark being invalidated,
// essentially means you are loading the file into memory
// as it is played. which can mean out-of-memory for large files.
try
{
ais.close();
}
catch ( IOException e )
{
system.error( "JSPCMAudioRecordingStream::rewind - Error closing the stream before reload: "
+ e.getMessage() );
}
ais = system.getAudioInputStream( meta.fileName() );
}
protected int skip(int millis)
{
long toSkip = AudioUtils.millis2BytesFrameAligned(millis, format);
if ( toSkip <= 0 )
{
if ( toSkip < 0 )
{
system.error( "JSBaseAudioRecordingStream.skip :: Tried to skip negative milleseconds!" );
}
return 0;
}
system.debug("Skipping forward by " + millis + " milliseconds, which is " + toSkip + " bytes.");
long totalSkipped = 0;
try
{
while (toSkip > 0)
{
long read;
synchronized ( ais )
{
// we don't use skip here because it sometimes has problems where
// it's "unable to skip an integer number of frames",
// which sometimes means it doesn't skip at all and other times
// means that you wind up with noise because it lands at half
// a sample off from where it should be.
// read seems to be rock solid.
int myBytesToRead = skipBytes.length;
if(toSkip < myBytesToRead)
{
myBytesToRead = (int)toSkip;
}
read = ais.read(skipBytes, 0, myBytesToRead);
}
if (read == -1)
{
// EOF!
system.debug( "JSBaseAudioRecordingStream.skip :: EOF reached!" );
break;
}
toSkip -= read;
totalSkipped += read;
}
}
catch (IOException e)
{
system.error("Unable to skip due to read error: " + e.getMessage());
}
system.debug("Total actually skipped was " + totalSkipped + ", which is "
+ AudioUtils.bytes2Millis(totalSkipped, ais.getFormat())
+ " milliseconds.");
return (int)totalSkipped;
}
// TODO: this implementation of float[] read is way temporary
public float[] read()
{
if ( buffer.getSampleCount() != 1 )
{
buffer.changeSampleCount( 1, true );
rawBytes = new byte[buffer.getByteArrayBufferSize( format )];
}
float[] samples = new float[buffer.getChannelCount()];
if ( play )
{
mRead();
for ( int i = 0; i < buffer.getChannelCount(); i++ )
{
samples[i] = buffer.getChannel( i )[0];
}
}
return samples;
}
// FIXME: temporary implementation of read
public int read(MultiChannelBuffer outBuffer)
{
if ( buffer.getSampleCount() != outBuffer.getBufferSize() )
{
buffer.changeSampleCount( outBuffer.getBufferSize(), true );
rawBytes = new byte[buffer.getByteArrayBufferSize( format )];
}
int framesRead = 0;
if ( play )
{
framesRead = mRead();
}
else
{
buffer.makeSilence();
}
for ( int i = 0; i < buffer.getChannelCount(); i++ )
{
outBuffer.setChannel( i, buffer.getChannel(i) );
}
return framesRead;
}
// returns number of samples read, not bytes
private int mRead()
{
// read in a full buffer of bytes from the file
int bytesRead = rawBytes.length;
if ( loop )
{
readBytesLoop();
}
else
{
bytesRead = readBytes();
}
// convert them to floating point
int frameCount = bytesRead / format.getFrameSize();
synchronized ( buffer )
{
buffer.setSamplesFromBytes( rawBytes, 0, format, 0, frameCount );
}
return frameCount;
}
}