package net.sf.fmj.media.multiplexer.audio;
import java.io.IOException;
import java.io.OutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.Buffer;
import javax.media.Format;
import javax.media.ResourceUnavailableException;
import javax.media.format.AudioFormat;
import javax.media.protocol.ContentDescriptor;
import net.sf.fmj.media.multiplexer.AbstractInputStreamMux;
import net.sf.fmj.utility.FormatArgUtils;
import net.sf.fmj.utility.LoggerSingleton;
import com.lti.utils.UnsignedUtils;
/**
* Experimental mux for dumping out audio to a delimited text file. Useful for debugging endian/sign/conversion problems.
* @author Ken Larson
*
*/
public class CsvAudioMux extends AbstractInputStreamMux
{
private static final Logger logger = LoggerSingleton.logger;
public CsvAudioMux()
{
super(new ContentDescriptor("audio.csv"));
}
@Override
public Format[] getSupportedInputFormats()
{
// TODO: we accept anything, really, as long as it is in a byte array
return new Format[] {
new AudioFormat(AudioFormat.LINEAR, -1.0, -1, -1, -1, -1, -1, -1.0, Format.byteArray),
};
}
@Override
public Format setInputFormat(Format format, int trackID)
{
logger.finer("setInputFormat " + format + " " + trackID);
boolean match = false;
for (Format supported : getSupportedInputFormats())
{
if (format.matches(supported))
{ match = true;
break;
}
}
if (!match)
{
logger.warning("Input format does not match any supported input format: " + format);
return null;
}
if (inputFormats != null) // TODO: should we save this somewhere and apply once inputFormats is not null?
inputFormats[trackID] = format;
return format;
}
private void outputHeader(OutputStream os) throws IOException
{
// TODO: this violates the CSV format
os.write(FormatArgUtils.toString(inputFormats[0]).getBytes());
os.write("\n".getBytes());
}
private void outputTrailer(OutputStream os) throws IOException
{
}
private boolean headerWritten = false;
private boolean trailerWritten = false;
public void open() throws ResourceUnavailableException
{
super.open();
if (!headerWritten)
{ try
{
outputHeader(getOutputStream());
} catch (IOException e)
{
logger.log(Level.WARNING, "" + e, e);
throw new ResourceUnavailableException("" + e);
}
headerWritten = true;
}
}
@Override
public void close()
{
if (!trailerWritten)
{ try
{
outputTrailer(getOutputStream());
} catch (IOException e)
{
logger.log(Level.WARNING, "" + e, e);
throw new RuntimeException(e);
}
trailerWritten = true;
}
super.close();
}
@Override
protected void doProcess(Buffer buffer, int trackID, OutputStream os) throws IOException
{
if (!headerWritten)
{ outputHeader(os);
headerWritten = true;
}
if (buffer.isEOM())
{
if (!trailerWritten)
{ outputTrailer(os);
trailerWritten = true;
}
os.close();
return; // TODO: what if there is data in buffer?
}
if (buffer.isDiscard())
return;
audioBufferToStream((AudioFormat) inputFormats[0], buffer, os);
}
public static void audioBufferToStream(AudioFormat f, Buffer buffer, OutputStream os) throws IOException
{
final byte[] data = (byte[]) buffer.getData();
final int sampleSizeInBytes = f.getSampleSizeInBits() / 8;
if (sampleSizeInBytes * 8 != f.getSampleSizeInBits())
throw new RuntimeException("Sample size in bytes must be divisible by 8");
final int frameSizeInBytes = sampleSizeInBytes * f.getChannels(); // TODO: check f.getFrameSizeInBits();
final int framesInBuffer = buffer.getLength() / frameSizeInBytes;
if (buffer.getLength() != framesInBuffer * frameSizeInBytes)
throw new RuntimeException("Length of buffer not an integral number of samples");
// if getSampleSizeInBits is 8, this value is 255:
final long inputUnsignedMax = (1L << f.getSampleSizeInBits()) - 1;
// if getSampleSizeInBits is 8, this value is 127:
final long inputSignedMax = (1L << (f.getSampleSizeInBits() - 1)) - 1;
for (int frame = 0; frame < framesInBuffer; ++frame)
{
for (int channel = 0; channel < f.getChannels(); ++channel)
{
final int offset = buffer.getOffset() + frame * frameSizeInBytes + channel * sampleSizeInBytes;
final int inputSampleLiteral = getSample(data, offset, sampleSizeInBytes, f.getEndian());
final long inputSampleLongWithoutSign = UnsignedUtils.uIntToLong(inputSampleLiteral);
final long inputSampleLongWithSign;
if (f.getSigned() == AudioFormat.UNSIGNED)
inputSampleLongWithSign = inputSampleLongWithoutSign;
else if (f.getSigned() == AudioFormat.SIGNED)
{
if (inputSampleLongWithoutSign > inputSignedMax)
inputSampleLongWithSign = inputSampleLongWithoutSign - inputUnsignedMax - 1;
else
inputSampleLongWithSign = inputSampleLongWithoutSign;
}
else
throw new RuntimeException("input format signed not specified");
if (channel > 0)
os.write(",".getBytes());
os.write(("" + inputSampleLongWithSign).getBytes());
}
os.write("\n".getBytes());
}
}
// TODO: copied from RateConverter
/** bit-wise literal. Is in general unsigned, but may be signed if all 32 bits are used. */
private static int getSample(byte[] inputBufferData, int byteOffsetOfSample, int inputSampleSizeInBytes, int inputEndian)
{
int sample = 0;
for (int j = 0; j < inputSampleSizeInBytes; ++j)
{
// offset within sample handles endian-ness:
final int offsetWithinSample = inputEndian == AudioFormat.BIG_ENDIAN ? j : (inputSampleSizeInBytes - 1 - j);
final byte b = inputBufferData[byteOffsetOfSample + offsetWithinSample];
sample <<= 8;
sample |= b & 0xff;
}
// handle signed-ness.
return sample;
}
}