/*
* MpegFormatConversionProvider.java
*
* This file is part of Tritonus: http://www.tritonus.org/
*/
/*
* Copyright (c) 1999 - 2004 by Matthias Pfisterer
* Copyright (c) 2008 by Florian Bomers
*
* 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.
*
*/
/*
|<--- this code is formatted to fit into 80 columns --->|
*/
package org.tritonus.sampled.convert.javalayer;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.util.Arrays;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import org.tritonus.share.TDebug;
import org.tritonus.share.sampled.AudioUtils;
import org.tritonus.share.sampled.TConversionTool;
import org.tritonus.share.sampled.convert.TEncodingFormatConversionProvider;
import org.tritonus.share.sampled.convert.TAsynchronousFilteredAudioInputStream;
import javazoom.jl.decoder.Bitstream;
import javazoom.jl.decoder.BitstreamException;
import javazoom.jl.decoder.Decoder;
import javazoom.jl.decoder.DecoderException;
import javazoom.jl.decoder.Header;
import javazoom.jl.decoder.Obuffer;
/**
* ConversionProvider for decoding mp3 files.
*
* @author Matthias Pfisterer
* @author Florian Bomers
*/
public class MpegFormatConversionProvider
extends TEncodingFormatConversionProvider
{
public static final AudioFormat.Encoding MPEG1L1 = new AudioFormat.Encoding("MPEG1L1");
public static final AudioFormat.Encoding MPEG1L2 = new AudioFormat.Encoding("MPEG1L2");
public static final AudioFormat.Encoding MPEG1L3 = new AudioFormat.Encoding("MPEG1L3");
public static final AudioFormat.Encoding MP3 = new AudioFormat.Encoding("MP3"); // alias for MPEG1L3
public static final AudioFormat.Encoding MPEG2L1 = new AudioFormat.Encoding("MPEG2L1");
public static final AudioFormat.Encoding MPEG2L2 = new AudioFormat.Encoding("MPEG2L2");
public static final AudioFormat.Encoding MPEG2L3 = new AudioFormat.Encoding("MPEG2L3");
public static final AudioFormat.Encoding MPEG2DOT5L1 = new AudioFormat.Encoding("MPEG2DOT5L1");
public static final AudioFormat.Encoding MPEG2DOT5L2 = new AudioFormat.Encoding("MPEG2DOT5L2");
public static final AudioFormat.Encoding MPEG2DOT5L3 = new AudioFormat.Encoding("MPEG2DOT5L3");
private static final AudioFormat.Encoding PCM_SIGNED = AudioFormat.Encoding.PCM_SIGNED;
/* TODO: mechanism to make the double specification with
different endianess obsolete. */
private static final AudioFormat[] INPUT_FORMATS =
{
// mono
new AudioFormat(MPEG1L1, -1.0F, -1, 1, -1, -1.0F, false),
new AudioFormat(MPEG1L1, -1.0F, -1, 1, -1, -1.0F, true),
// stereo
new AudioFormat(MPEG1L1, -1.0F, -1, 2, -1, -1.0F, false),
new AudioFormat(MPEG1L1, -1.0F, -1, 2, -1, -1.0F, true),
// mono
new AudioFormat(MPEG1L2, -1.0F, -1, 1, -1, -1.0F, false),
new AudioFormat(MPEG1L2, -1.0F, -1, 1, -1, -1.0F, true),
// stereo
new AudioFormat(MPEG1L2, -1.0F, -1, 2, -1, -1.0F, false),
new AudioFormat(MPEG1L2, -1.0F, -1, 2, -1, -1.0F, true),
// mono
new AudioFormat(MPEG1L3, -1.0F, -1, 1, -1, -1.0F, false),
new AudioFormat(MPEG1L3, -1.0F, -1, 1, -1, -1.0F, true),
// stereo
new AudioFormat(MPEG1L3, -1.0F, -1, 2, -1, -1.0F, false),
new AudioFormat(MPEG1L3, -1.0F, -1, 2, -1, -1.0F, true),
// mono
new AudioFormat(MP3, -1.0F, -1, 1, -1, -1.0F, false),
new AudioFormat(MP3, -1.0F, -1, 1, -1, -1.0F, true),
// stereo
new AudioFormat(MP3, -1.0F, -1, 2, -1, -1.0F, false),
new AudioFormat(MP3, -1.0F, -1, 2, -1, -1.0F, true),
// mono
new AudioFormat(MPEG2L1, -1.0F, -1, 1, -1, -1.0F, false),
new AudioFormat(MPEG2L1, -1.0F, -1, 1, -1, -1.0F, true),
// stereo
new AudioFormat(MPEG2L1, -1.0F, -1, 2, -1, -1.0F, false),
new AudioFormat(MPEG2L1, -1.0F, -1, 2, -1, -1.0F, true),
// mono
new AudioFormat(MPEG2L2, -1.0F, -1, 1, -1, -1.0F, false),
new AudioFormat(MPEG2L2, -1.0F, -1, 1, -1, -1.0F, true),
// stereo
new AudioFormat(MPEG2L2, -1.0F, -1, 2, -1, -1.0F, false),
new AudioFormat(MPEG2L2, -1.0F, -1, 2, -1, -1.0F, true),
// mono
new AudioFormat(MPEG2L3, -1.0F, -1, 1, -1, -1.0F, false),
new AudioFormat(MPEG2L3, -1.0F, -1, 1, -1, -1.0F, true),
// stereo
new AudioFormat(MPEG2L3, -1.0F, -1, 2, -1, -1.0F, false),
new AudioFormat(MPEG2L3, -1.0F, -1, 2, -1, -1.0F, true),
// mono
new AudioFormat(MPEG2DOT5L1, -1.0F, -1, 1, -1, -1.0F, false),
new AudioFormat(MPEG2DOT5L1, -1.0F, -1, 1, -1, -1.0F, true),
// stereo
new AudioFormat(MPEG2DOT5L1, -1.0F, -1, 2, -1, -1.0F, false),
new AudioFormat(MPEG2DOT5L1, -1.0F, -1, 2, -1, -1.0F, true),
// mono
new AudioFormat(MPEG2DOT5L2, -1.0F, -1, 1, -1, -1.0F, false),
new AudioFormat(MPEG2DOT5L2, -1.0F, -1, 1, -1, -1.0F, true),
// stereo
new AudioFormat(MPEG2DOT5L2, -1.0F, -1, 2, -1, -1.0F, false),
new AudioFormat(MPEG2DOT5L2, -1.0F, -1, 2, -1, -1.0F, true),
// mono
new AudioFormat(MPEG2DOT5L3, -1.0F, -1, 1, -1, -1.0F, false),
new AudioFormat(MPEG2DOT5L3, -1.0F, -1, 1, -1, -1.0F, true),
// stereo
new AudioFormat(MPEG2DOT5L3, -1.0F, -1, 2, -1, -1.0F, false),
new AudioFormat(MPEG2DOT5L3, -1.0F, -1, 2, -1, -1.0F, true),
};
private static final AudioFormat[] OUTPUT_FORMATS =
{
// mono, 16 bit signed
new AudioFormat(PCM_SIGNED, -1.0F, 16, 1, 2, -1.0F, false),
new AudioFormat(PCM_SIGNED, -1.0F, 16, 1, 2, -1.0F, true),
// stereo, 16 bit signed
new AudioFormat(PCM_SIGNED, -1.0F, 16, 2, 4, -1.0F, false),
new AudioFormat(PCM_SIGNED, -1.0F, 16, 2, 4, -1.0F, true),
/* 24 and 32 bit not yet possible
// mono, 24 bit signed
new AudioFormat(PCM_SIGNED, -1.0F, 24, 1, 3, -1.0F, false),
new AudioFormat(PCM_SIGNED, -1.0F, 24, 1, 3, -1.0F, true),
// stereo, 24 bit signed
new AudioFormat(PCM_SIGNED, -1.0F, 24, 2, 6, -1.0F, false),
new AudioFormat(PCM_SIGNED, -1.0F, 24, 2, 6, -1.0F, true),
// mono, 32 bit signed
new AudioFormat(PCM_SIGNED, -1.0F, 32, 1, 4, -1.0F, false),
new AudioFormat(PCM_SIGNED, -1.0F, 32, 1, 4, -1.0F, true),
// stereo, 32 bit signed
new AudioFormat(PCM_SIGNED, -1.0F, 32, 2, 8, -1.0F, false),
new AudioFormat(PCM_SIGNED, -1.0F, 32, 2, 8, -1.0F, true),
*/
};
/**
* Constructor.
*/
public MpegFormatConversionProvider()
{
super(Arrays.asList(INPUT_FORMATS), Arrays.asList(OUTPUT_FORMATS));
if (TDebug.TraceAudioConverter)
{
TDebug.out("MpegFormatConversionProvider()");
}
}
@Override
public AudioInputStream getAudioInputStream(AudioFormat targetFormat, AudioInputStream audioInputStream)
{
AudioFormat sourceFormat = audioInputStream.getFormat();
if (TDebug.TraceAudioConverter)
{
TDebug.out(">MpegFormatConversionProvider.getAudioInputStream(AudioFormat, AudioInputStream):");
TDebug.out("trying to convert");
TDebug.out("\tfrom: " + sourceFormat);
TDebug.out("\tto: " + targetFormat);
}
targetFormat = getFullyQualifiedTargetFormat(targetFormat, sourceFormat, false);
if (targetFormat != null)
{
if (TDebug.TraceAudioConverter)
{
TDebug.out("< OK");
}
return new DecodedMpegAudioInputStream(
targetFormat,
audioInputStream);
}
if (TDebug.TraceAudioConverter)
{
TDebug.out("< not supported");
}
throw new IllegalArgumentException("conversion not supported");
}
private AudioFormat getFullyQualifiedTargetFormat(AudioFormat targetFormat, AudioFormat sourceFormat, boolean allowUnspecified) {
// check that sourceFormat and targetFormat are in list of supported formats
if (!super.isConversionSupported(targetFormat.getEncoding(), sourceFormat)) {
if (TDebug.TraceAudioConverter) TDebug.out("cannot convert: super.isConversionSupported()==false");
return null;
}
// make it simple: we can only convert to PCM_SIGNED,
// therefore, just fill in the missing fields
if (!targetFormat.getEncoding().equals(PCM_SIGNED)) {
if (TDebug.TraceAudioConverter) TDebug.out("cannot convert: target is not PCM_SIGNED");
return null;
}
// some values are never allowed
if (sourceFormat.getChannels() > 2
|| targetFormat.getChannels() > 2
|| sourceFormat.getChannels() == 0
|| targetFormat.getChannels() == 0
|| sourceFormat.getSampleRate() == 0
|| targetFormat.getSampleRate() == 0) {
if (TDebug.TraceAudioConverter) TDebug.out("cannot convert: channels or sample rate out of bounds");
return null;
}
// check channels
if (sourceFormat.getChannels() < 0) {
if (allowUnspecified) {
// both channel fields must be -1
if (targetFormat.getChannels() >= 0) {
// cannot convert a non-specified channel number to a different specified channel
if (TDebug.TraceAudioConverter) TDebug.out("cannot convert: cannot any to specific channels");
return null;
}
} else {
// do not allow source channels = -1
if (TDebug.TraceAudioConverter) TDebug.out("cannot convert: channels cannot be AudioSystem.NOT_SPECIFIED");
return null;
}
} else {
// if target channels are given, they must equal source channels
if (targetFormat.getChannels() > 0 && targetFormat.getChannels() != sourceFormat.getChannels()) {
// cannot convert a specified channel number to a different specified channel
if (TDebug.TraceAudioConverter) TDebug.out("cannot convert: specified channel number must be the same");
return null;
}
}
// check sample rate
if (sourceFormat.getSampleRate() < 0) {
if (allowUnspecified) {
// both SampleRate fields must be -1
if (targetFormat.getSampleRate() >= 0) {
// cannot convert a non-specified SampleRate to a different specified SampleRate
if (TDebug.TraceAudioConverter) TDebug.out("cannot convert any to specific sample rate");
return null;
}
} else {
// do not allow SampleRate = -1
if (TDebug.TraceAudioConverter) TDebug.out("cannot convert: source sample rate is NOT_SPECIFIED");
return null;
}
} else {
// if target SampleRate is given, must equal source SampleRate
if (targetFormat.getSampleRate() > 0 && targetFormat.getSampleRate() != sourceFormat.getSampleRate()) {
// cannot convert a specified SampleRate to a different specified SampleRate
if (TDebug.TraceAudioConverter) TDebug.out("cannot convert sample rate");
return null;
}
}
// check sample size
if (targetFormat.getSampleSizeInBits() != 16) {
if (TDebug.TraceAudioConverter) TDebug.out("cannot convert: source sample width is not 16");
return null;
}
return new AudioFormat(
PCM_SIGNED,
sourceFormat.getSampleRate(),
targetFormat.getSampleSizeInBits(),
sourceFormat.getChannels(),
AudioUtils.getFrameSize(sourceFormat.getChannels(), targetFormat.getSampleSizeInBits()),
sourceFormat.getSampleRate(),
targetFormat.isBigEndian(),
targetFormat.properties());
}
@Override
public boolean isConversionSupported(AudioFormat targetFormat, AudioFormat sourceFormat)
{
if (TDebug.TraceAudioConverter)
{
TDebug.out(">MpegFormatConversionProvider.isConversionSupported(AudioFormat targetFormat, AudioFormat sourceFormat):");
TDebug.out("checking if conversion possible");
TDebug.out("from: " + sourceFormat);
TDebug.out("to: " + targetFormat);
}
AudioFormat format = getFullyQualifiedTargetFormat(targetFormat, sourceFormat, true);
boolean supported = (format != null);
if (TDebug.TraceAudioConverter)
{
TDebug.out("<MpegFormatConversionProvider.isConversionSupported(AudioFormat targetFormat, AudioFormat sourceFormat), result=" + supported);
}
return supported;
}
public static class DecodedMpegAudioInputStream
extends TAsynchronousFilteredAudioInputStream
{
private InputStream m_encodedStream;
private Bitstream m_bitstream;
private Decoder m_decoder;
private DMAISObuffer m_oBuffer;
public DecodedMpegAudioInputStream(AudioFormat outputFormat, AudioInputStream inputStream)
{
// TODO: try to find out length (possible?)
super(outputFormat, AudioSystem.NOT_SPECIFIED);
m_encodedStream = inputStream;
m_bitstream = new Bitstream(inputStream);
m_decoder = new Decoder(null);
m_oBuffer = new DMAISObuffer(outputFormat.getChannels());
m_decoder.setOutputBuffer(m_oBuffer);
}
public void execute()
{
try
{
Header header = m_bitstream.readFrame();
if (header == null)
{
if (TDebug.TraceAudioConverter)
{
TDebug.out("header is null (end of mpeg stream)");
}
getCircularBuffer().close();
return;
}
m_decoder.decodeFrame(header, m_bitstream);
m_bitstream.closeFrame();
getCircularBuffer().write(m_oBuffer.getBuffer(), 0, m_oBuffer.getCurrentBufferSize());
m_oBuffer.reset();
}
catch (BitstreamException e)
{
if (TDebug.TraceAudioConverter || TDebug.TraceAllExceptions)
{
TDebug.out(e);
}
}
catch (DecoderException e)
{
if (TDebug.TraceAudioConverter || TDebug.TraceAllExceptions)
{
TDebug.out(e);
}
}
}
protected boolean isBigEndian()
{
return getFormat().isBigEndian();
}
@Override
public void close()
throws IOException
{
super.close();
m_encodedStream.close();
}
private class DMAISObuffer
extends Obuffer
{
private int m_nChannels;
private byte[] m_abBuffer;
private int[] m_anBufferPointers;
private boolean m_bIsBigEndian;
public DMAISObuffer(int nChannels)
{
m_nChannels = nChannels;
m_abBuffer = new byte[OBUFFERSIZE * nChannels];
m_anBufferPointers = new int[nChannels];
reset();
m_bIsBigEndian = DecodedMpegAudioInputStream.this.isBigEndian();
}
@Override
public void append(int nChannel, short sValue)
{
// TODO: replace by TConversionTool methods
/*
byte bFirstByte;
byte bSecondByte;
if (m_bIsBigEndian)
{
bFirstByte = (byte) ((sValue >>> 8) & 0xFF);
bSecondByte = (byte) (sValue & 0xFF);
}
else // little endian
{
bFirstByte = (byte) (sValue & 0xFF);
bSecondByte = (byte) ((sValue >>> 8) & 0xFF);
}
m_abBuffer[m_anBufferPointers[nChannel]] = bFirstByte;
m_abBuffer[m_anBufferPointers[nChannel] + 1] = bSecondByte;
*/
TConversionTool.shortToBytes16(sValue, m_abBuffer, m_anBufferPointers[nChannel], m_bIsBigEndian);
m_anBufferPointers[nChannel] += m_nChannels * 2;
}
@Override
public void set_stop_flag()
{
}
@Override
public void close()
{
}
@Override
public void write_buffer(int nValue)
{
}
@Override
public void clear_buffer()
{
}
public byte[] getBuffer()
{
return m_abBuffer;
}
public int getCurrentBufferSize()
{
return m_anBufferPointers[0];
}
public void reset()
{
for (int i = 0; i < m_nChannels; i++)
{
/* Points to byte location,
* implicitely assuming 16 bit
* samples.
*/
m_anBufferPointers[i] = i * 2;
}
}
}
}
private static int test(AudioFormat target, AudioFormat source, boolean failSupported, boolean failAIS, int testNum) {
boolean verbose = false;
AudioInputStream ais = new AudioInputStream(new ByteArrayInputStream(new byte[8]), source, 8);
MpegFormatConversionProvider provider = new MpegFormatConversionProvider();
boolean isConversionSupported = provider.isConversionSupported(target, source);
AudioInputStream convertedAIS = null;
try {
convertedAIS = provider.getAudioInputStream(target, ais);
} catch (Exception e) {
// ignore
}
boolean failed = (failSupported == isConversionSupported)
|| (failAIS != (convertedAIS == null));
if (failed || verbose) {
if (failed) {
System.out.println(""+(testNum)+".ERROR:");
} else {
System.out.println(""+(testNum)+".PASSED:");
}
System.out.println(" source: "+source);
System.out.println(" target: "+target);
if (failSupported == isConversionSupported) {
System.out.println(" isConversionSupported() erronously returned "+isConversionSupported);
} else {
System.out.println(" isConversionSupported() correctly returned "+isConversionSupported);
}
if (convertedAIS != null) {
if (failAIS) {
System.out.println(" converted stream was erronously returned with format:");
} else {
System.out.println(" converted stream was correctly returned with format:");
}
System.out.println(" converted format: "+convertedAIS.getFormat());
} else {
if (failAIS) {
System.out.println(" converted stream was correctly not returned.");
} else {
System.out.println(" converted stream was erronously not returned.");
}
}
} else if (!failed) {
System.out.println(""+(testNum)+".OK");
}
return failed?0:1;
}
/** unit test */
public static void main(String[] args) {
int testNum = 0;
int passed = 0;
// negative tests: should not be able to convert mp3 to mp3
AudioFormat source = new AudioFormat(MPEG1L3, 44100, -1, 2, -1, -1, false);
AudioFormat target = new AudioFormat(MPEG1L3, 44100, 16, 2, 4, 44100, false);
passed += test(target, source, true, true, testNum++);
source = new AudioFormat(MPEG1L3, 44100, -1, 2, -1, -1, false);
target = new AudioFormat(MPEG1L3, -1, 16, -1, -1, -1, false);
passed += test(target, source, true, true, testNum++);
source = new AudioFormat(MPEG1L3, 44100, -1, 2, -1, -1, false);
target = new AudioFormat(MPEG1L3, -1, 32, 2, 8, -1, false);
passed += test(target, source, true, true, testNum++);
// negative test: should not claim to convert channels
source = new AudioFormat(MPEG1L3, 44100, -1, 1, -1, -1, false);
target = new AudioFormat(PCM_SIGNED, -1, 16, 2, 4, -1, false);
passed += test(target, source, true, true, testNum++);
source = new AudioFormat(MPEG1L3, 44100, -1, 2, -1, -1, false);
target = new AudioFormat(PCM_SIGNED, -1, 16, 1, 2, -1, false);
passed += test(target, source, true, true, testNum++);
// negative test: should not claim to convert sample rate
source = new AudioFormat(MPEG1L3, 44100, -1, 2, -1, -1, false);
target = new AudioFormat(PCM_SIGNED, 8000, 16, 2, 4, 8000, false);
passed += test(target, source, true, true, testNum++);
// positive test: should convert MP3 to PCM
source = new AudioFormat(MPEG1L3, 44100, -1, 2, -1, -1, false);
target = new AudioFormat(PCM_SIGNED, 44100, 16, 2, 4, 44100, false);
passed += test(target, source, false, false, testNum++);
// positive test: should convert MP3 to PCM
source = new AudioFormat(MPEG1L3, 44100, -1, 1, -1, -1, false);
target = new AudioFormat(PCM_SIGNED, 44100, 16, 1, 2, 44100, false);
passed += test(target, source, false, false, testNum++);
// special case: can check isSupported with -1 for both fields, but should not return an AIS
source = new AudioFormat(MPEG1L3, -1, -1, 1, -1, -1, false);
target = new AudioFormat(PCM_SIGNED, -1, 16, 1, 2, -1, false);
passed += test(target, source, false, true, testNum++);
source = new AudioFormat(MPEG1L3, 8000, -1, -1, -1, -1, false);
target = new AudioFormat(PCM_SIGNED, 8000, 16, -1, -1, 8000, false);
passed += test(target, source, false, true, testNum++);
System.out.println("Passed "+passed+" tests of "+testNum);
}
}
/*** MpegFormatConversionProvider.java ***/