/*
* 11/19/04 1.0 moved to LGPL.
* 12/12/99 Original verion. mdm@techie.com.
*-----------------------------------------------------------------------
* 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 javazoom.jl.converter;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import javazoom.jl.decoder.Bitstream;
import javazoom.jl.decoder.Decoder;
import javazoom.jl.decoder.Header;
import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.decoder.Obuffer;
/**
* The <code>Converter</code> class implements the conversion of
* an MPEG audio file to a .WAV file. To convert an MPEG audio stream,
* just create an instance of this class and call the convert()
* method, passing in the names of the input and output files. You can
* pass in optional <code>ProgressListener</code> and
* <code>Decoder.Params</code> objects also to customize the conversion.
*
* @author MDM 12/12/99
* @since 0.0.7
*/
public class Converter
{
/**
* Creates a new converter instance.
*/
public Converter()
{
}
public synchronized void convert(String sourceName, String destName)
throws JavaLayerException
{
convert(sourceName, destName, null, null);
}
public synchronized void convert(String sourceName, String destName,
ProgressListener progressListener)
throws JavaLayerException
{
convert(sourceName, destName, progressListener, null);
}
public void convert(String sourceName, String destName,
ProgressListener progressListener, Decoder.Params decoderParams)
throws JavaLayerException
{
if (destName.length()==0)
destName = null;
try {
InputStream in = openInput(sourceName);
convert(in, destName, progressListener, decoderParams);
in.close();
} catch(IOException ioe) {
throw new JavaLayerException(ioe.getLocalizedMessage(), ioe);
}
}
public synchronized void convert(InputStream sourceStream, String destName,
ProgressListener progressListener, Decoder.Params decoderParams)
throws JavaLayerException
{
if (progressListener==null)
progressListener = PrintWriterProgressListener.newStdOut(
PrintWriterProgressListener.NO_DETAIL);
try {
if (!(sourceStream instanceof BufferedInputStream))
sourceStream = new BufferedInputStream(sourceStream);
int frameCount = -1;
if (sourceStream.markSupported()) {
sourceStream.mark(-1);
frameCount = countFrames(sourceStream);
sourceStream.reset();
}
progressListener.converterUpdate(ProgressListener.UPDATE_FRAME_COUNT, frameCount, 0);
Obuffer output = null;
Decoder decoder = new Decoder(decoderParams);
Bitstream stream = new Bitstream(sourceStream);
if (frameCount==-1)
frameCount = Integer.MAX_VALUE;
int frame = 0;
long startTime = System.currentTimeMillis();
try
{
for (; frame<frameCount; frame++)
{
try
{
Header header = stream.readFrame();
if (header==null)
break;
progressListener.readFrame(frame, header);
if (output==null)
{
// REVIEW: Incorrect functionality.
// the decoder should provide decoded
// frequency and channels output as it may differ from
// the source (e.g. when downmixing stereo to mono.)
int channels = (header.mode()==Header.SINGLE_CHANNEL) ? 1 : 2;
int freq = header.frequency();
output = new WaveFileObuffer(channels, freq, destName);
decoder.setOutputBuffer(output);
}
Obuffer decoderOutput = decoder.decodeFrame(header, stream);
// REVIEW: the way the output buffer is set
// on the decoder is a bit dodgy. Even though
// this exception should never happen, we test to be sure.
if (decoderOutput!=output)
throw new InternalError("Output buffers are different.");
progressListener.decodedFrame(frame, header, output);
stream.closeFrame();
}
catch (Exception ex)
{
boolean stop = !progressListener.converterException(ex);
if (stop)
{
throw new JavaLayerException(ex.getLocalizedMessage(), ex);
}
}
}
}
finally
{
if (output!=null)
output.close();
}
int time = (int)(System.currentTimeMillis()-startTime);
progressListener.converterUpdate(ProgressListener.UPDATE_CONVERT_COMPLETE,
time, frame);
}
catch (IOException ex)
{
throw new JavaLayerException(ex.getLocalizedMessage(), ex);
}
}
protected int countFrames(InputStream in)
{
return -1;
}
protected InputStream openInput(String fileName)
throws IOException
{
// ensure name is abstract path name
File file = new File(fileName);
InputStream fileIn = new FileInputStream(file);
BufferedInputStream bufIn = new BufferedInputStream(fileIn);
return bufIn;
}
/**
* This interface is used by the Converter to provide
* notification of tasks being carried out by the converter,
* and to provide new information as it becomes available.
*/
static public interface ProgressListener
{
public static final int UPDATE_FRAME_COUNT = 1;
/**
* Conversion is complete. Param1 contains the time
* to convert in milliseconds. Param2 contains the number
* of MPEG audio frames converted.
*/
public static final int UPDATE_CONVERT_COMPLETE = 2;
/**
* Notifies the listener that new information is available.
*
* @param updateID Code indicating the information that has been
* updated.
*
* @param param1 Parameter whose value depends upon the update code.
* @param param2 Parameter whose value depends upon the update code.
*
* The <code>updateID</code> parameter can take these values:
*
* UPDATE_FRAME_COUNT: param1 is the frame count, or -1 if not known.
* UPDATE_CONVERT_COMPLETE: param1 is the conversion time, param2
* is the number of frames converted.
*/
public void converterUpdate(int updateID, int param1, int param2);
/**
* If the converter wishes to make a first pass over the
* audio frames, this is called as each frame is parsed.
*/
public void parsedFrame(int frameNo, Header header);
/**
* This method is called after each frame has been read,
* but before it has been decoded.
*
* @param frameNo The 0-based sequence number of the frame.
* @param header The Header rerpesenting the frame just read.
*/
public void readFrame(int frameNo, Header header);
/**
* This method is called after a frame has been decoded.
*
* @param frameNo The 0-based sequence number of the frame.
* @param header The Header rerpesenting the frame just read.
* @param o The Obuffer the deocded data was written to.
*/
public void decodedFrame(int frameNo, Header header, Obuffer o);
/**
* Called when an exception is thrown during while converting
* a frame.
*
* @param t The <code>Throwable</code> instance that
* was thrown.
*
* @return <code>true</code> to continue processing, or false
* to abort conversion.
*
* If this method returns <code>false</code>, the exception
* is propagated to the caller of the convert() method. If
* <code>true</code> is returned, the exception is silently
* ignored and the converter moves onto the next frame.
*/
public boolean converterException(Throwable t);
}
/**
* Implementation of <code>ProgressListener</code> that writes
* notification text to a <code>PrintWriter</code>.
*/
// REVIEW: i18n of text and order required.
static public class PrintWriterProgressListener implements ProgressListener
{
static public final int NO_DETAIL = 0;
/**
* Level of detail typically expected of expert
* users.
*/
static public final int EXPERT_DETAIL = 1;
/**
* Verbose detail.
*/
static public final int VERBOSE_DETAIL = 2;
/**
* Debug detail. All frame read notifications are shown.
*/
static public final int DEBUG_DETAIL = 7;
static public final int MAX_DETAIL = 10;
private PrintWriter pw;
private int detailLevel;
static public PrintWriterProgressListener newStdOut(int detail)
{
return new PrintWriterProgressListener(
new PrintWriter(System.out, true), detail);
}
public PrintWriterProgressListener(PrintWriter writer, int detailLevel)
{
this.pw = writer;
this.detailLevel = detailLevel;
}
public boolean isDetail(int detail)
{
return (this.detailLevel >= detail);
}
public void converterUpdate(int updateID, int param1, int param2)
{
if (isDetail(VERBOSE_DETAIL))
{
switch (updateID)
{
case UPDATE_CONVERT_COMPLETE:
// catch divide by zero errors.
if (param2==0)
param2 = 1;
pw.println();
pw.println("Converted "+param2+" frames in "+param1+" ms ("+
(param1/param2)+" ms per frame.)");
}
}
}
public void parsedFrame(int frameNo, Header header)
{
if ((frameNo==0) && isDetail(VERBOSE_DETAIL))
{
String headerString = header.toString();
pw.println("File is a "+headerString);
}
else if (isDetail(MAX_DETAIL))
{
String headerString = header.toString();
pw.println("Prased frame "+frameNo+": "+headerString);
}
}
public void readFrame(int frameNo, Header header)
{
if ((frameNo==0) && isDetail(VERBOSE_DETAIL))
{
String headerString = header.toString();
pw.println("File is a "+headerString);
}
else if (isDetail(MAX_DETAIL))
{
String headerString = header.toString();
pw.println("Read frame "+frameNo+": "+headerString);
}
}
public void decodedFrame(int frameNo, Header header, Obuffer o)
{
if (isDetail(MAX_DETAIL))
{
String headerString = header.toString();
pw.println("Decoded frame "+frameNo+": "+headerString);
pw.println("Output: "+o);
}
else if (isDetail(VERBOSE_DETAIL))
{
if (frameNo==0)
{
pw.print("Converting.");
pw.flush();
}
if ((frameNo % 10)==0)
{
pw.print('.');
pw.flush();
}
}
}
public boolean converterException(Throwable t)
{
if (this.detailLevel>NO_DETAIL)
{
t.printStackTrace(pw);
pw.flush();
}
return false;
}
}
}