/*
 *	GSMFormatConversionProvider.java
 *
 *	This file is part of Tritonus: http://www.tritonus.org/
 */

/*
 *  Copyright (c) 1999 - 2001 by Matthias Pfisterer
 *  Copyright (c) 2001 by Florian Bomers <http://www.bomers.de>
 *
 *   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.gsm;

import java.io.DataInputStream;
import java.io.InputStream;
import java.io.IOException;

import java.util.Arrays;
import java.util.Iterator;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;

import org.tritonus.share.TDebug;
import org.tritonus.lowlevel.gsm.InvalidGSMFrameException;
import org.tritonus.lowlevel.gsm.GSMDecoder;
import org.tritonus.lowlevel.gsm.Encoder;
import org.tritonus.share.sampled.TConversionTool;
import org.tritonus.share.sampled.convert.TAsynchronousFilteredAudioInputStream;
import org.tritonus.share.sampled.convert.TEncodingFormatConversionProvider;
import org.tritonus.share.sampled.convert.TSimpleFormatConversionProvider;
import org.tritonus.share.sampled.AudioFormats;



/**	ConversionProvider for GSM files.
	@author Matthias Pfisterer
*/
public class GSMFormatConversionProvider
extends TSimpleFormatConversionProvider
// extends TEncodingFormatConversionProvider
{
	/*	Debugging (profiling) hack.
	 */
	private static final boolean	MEASURE_DECODING_TIME = false;



	private static final AudioFormat[]	FORMATS1 =
	{
		new AudioFormat(new AudioFormat.Encoding("GSM0610"), 8000.0F, -1, 1, 33, 50.0F, false),
		new AudioFormat(new AudioFormat.Encoding("GSM0610"), 8000.0F, -1, 1, 33, 50.0F, true),
		// temporary only
		new AudioFormat(new AudioFormat.Encoding("PCM_SIGNED"), 8000.0F, 16, 1, 2, 8000.0F, false),
		new AudioFormat(new AudioFormat.Encoding("PCM_SIGNED"), 8000.0F, 16, 1, 2, 8000.0F, true),
	};

/*
  private static final AudioFormat[]	FORMATS2 =
  {
  new AudioFormat(8000.0F, 16, 1, true, false),
  new AudioFormat(8000.0F, 16, 1, true, true),
  };
*/
	private static final AudioFormat[]	FORMATS2 = FORMATS1;



	/**	This is the size of the circular buffer.
	 *	This value is in bytes. It is
	 *	chosen so that one (decoded) GSM frame fits into the buffer.
	 *	GSM frames contain 160 samples.
	 */
	private static final int	BUFFER_SIZE = 320;

	private static final int	ENCODED_GSM_FRAME_SIZE = 33;



	/**	Constructor.
	 */
	public GSMFormatConversionProvider()
	{
		super(Arrays.asList(FORMATS1),
		      Arrays.asList(FORMATS2));
		if (TDebug.TraceAudioConverter) { TDebug.out("GSMFormatConversionProvider.<init>(): begin"); }
		if (TDebug.TraceAudioConverter) { TDebug.out("GSMFormatConversionProvider.<init>(): end"); }
	}



	public AudioInputStream getAudioInputStream(AudioFormat targetFormat, AudioInputStream audioInputStream)
	{
		if (TDebug.TraceAudioConverter)
		{
			TDebug.out("GSMFormatConversionProvider.getAudioInputStream(): begin");
			TDebug.out("GSMFormatConversionProvider.getAudioInputStream():");
			TDebug.out("checking if conversion supported");
			TDebug.out("from: " + audioInputStream.getFormat());
			TDebug.out("to: " + targetFormat);
		}

		targetFormat=getDefaultTargetFormat(targetFormat, audioInputStream.getFormat());
		if (isConversionSupported(targetFormat,
					  audioInputStream.getFormat()))
		{
			if (targetFormat.getEncoding().equals(new AudioFormat.Encoding("PCM_SIGNED")))
			{
				if (TDebug.TraceAudioConverter)
				{
					TDebug.out("GSMFormatConversionProvider.getAudioInputStream():");
					TDebug.out("conversion supported; trying to create DecodedGSMAudioInputStream");
				}
				return new DecodedGSMAudioInputStream(
					targetFormat,
					audioInputStream);
			}
			else
			{
				if (TDebug.TraceAudioConverter)
				{
					TDebug.out("GSMFormatConversionProvider.getAudioInputStream():");
					TDebug.out("conversion supported; trying to create EncodedGSMAudioInputStream");
				}
				return new EncodedGSMAudioInputStream(
					targetFormat,
					audioInputStream);
			}
		}
		else
		{
			if (TDebug.TraceAudioConverter)
			{
				TDebug.out("GSMFormatConversionProvider.getAudioInputStream():");
				TDebug.out("conversion not supported; throwing IllegalArgumentException");
			}
			throw new IllegalArgumentException("conversion not supported");
		}
		// TODO: this is unreachable
		// if (TDebug.TraceAudioConverter) { TDebug.out("GSMFormatConversionProvider.getAudioInputStream(): end"); }
	}



	protected AudioFormat getDefaultTargetFormat(AudioFormat targetFormat, AudioFormat sourceFormat) {
		// return first of the matching formats
		// pre-condition: the predefined target formats (FORMATS2) must be well-defined !
		Iterator iterator=getCollectionTargetFormats().iterator();
		while (iterator.hasNext()) {
			AudioFormat format=(AudioFormat) iterator.next();
			if (AudioFormats.matches(targetFormat, format)) {
				return format;
			}
		}
		throw new IllegalArgumentException("conversion not supported");
	}



	/**	AudioInputStream returned on decoding of GSM.
		An instance of this class is returned if you call
		AudioSystem.getAudioInputStream(AudioFormat, AudioInputStream)
		to decode a GSM stream. This class contains the logic
		of maintaining buffers and calling the decoder.
	 */
	public static class DecodedGSMAudioInputStream
	extends TAsynchronousFilteredAudioInputStream
	{
		/*
		  Seems like DataInputStream (opposite to InputStream) is only needed for
		  readFully(). readFully-behavious should perhaps be implemented in
		  AudioInputStream anyway (so this construct may become obsolete).
		 */
		private DataInputStream		m_encodedStream;
		private GSMDecoder		m_decoder;

		/*
		 *	Holds one encoded GSM frame.
		 */
		private byte[]			m_abFrameBuffer;
		private byte[]			m_abBuffer;


		public DecodedGSMAudioInputStream(AudioFormat outputFormat, AudioInputStream inputStream)
		{
			super(outputFormat,
			      inputStream.getFrameLength() == AudioSystem.NOT_SPECIFIED ? AudioSystem.NOT_SPECIFIED : inputStream.getFrameLength() * 160);
			if (TDebug.TraceAudioConverter) { TDebug.out("DecodedGSMAudioInputStream.<init>(): begin"); }
			m_encodedStream = new DataInputStream(inputStream);
			m_decoder = new GSMDecoder();
			m_abFrameBuffer = new byte[ENCODED_GSM_FRAME_SIZE];
			m_abBuffer = new byte[BUFFER_SIZE];
			if (TDebug.TraceAudioConverter) { TDebug.out("DecodedGSMAudioInputStream.<init>(): end"); }
		}



		public void execute()
		{
			if (TDebug.TraceAudioConverter) { TDebug.out("DecodedGSMAudioInputStream.execute(): begin"); }
			try
			{
				m_encodedStream.readFully(m_abFrameBuffer);
			}
			catch (IOException e)
			{
				/*
				  Not only errors, but also EOF is caught here.
				 */
				if (TDebug.TraceAllExceptions) { TDebug.out(e); }
				getCircularBuffer().close();
				return;
			}

			try
			{
				long	lTimestamp1;
				long	lTimestamp2;
				if (MEASURE_DECODING_TIME)
				{
					lTimestamp1 = System.currentTimeMillis();
				}
				m_decoder.decode(m_abFrameBuffer, 0,
						 m_abBuffer, 0, isBigEndian());
				// testing test hack
				// m_abBuffer[0] = 0;
				if (MEASURE_DECODING_TIME)
				{
					lTimestamp2 = System.currentTimeMillis();
					System.out.println("GSM decode (ms): " + (lTimestamp2 - lTimestamp1));
				}
			}
			catch (InvalidGSMFrameException e)
			{
				if (TDebug.TraceAllExceptions) { TDebug.out(e); }
				getCircularBuffer().close();
				return;
			}

/// end new version

			getCircularBuffer().write(m_abBuffer);
			if (TDebug.TraceAudioConverter) { TDebug.out("DecodedGSMAudioInputStream.execute(): decoded GSM frame written"); }
			if (TDebug.TraceAudioConverter) { TDebug.out("DecodedGSMAudioInputStream.execute(): end"); }
		}



		private boolean isBigEndian()
		{
			return getFormat().isBigEndian();
		}



		public void close()
			throws IOException
		{
			super.close();
			m_encodedStream.close();
		}
	}



	/**	AudioInputStream returned on encoding of GSM.
		An instance of this class is returned if you call
		AudioSystem.getAudioInputStream(AudioFormat, AudioInputStream)
		to encode data to GSM. This class contains the logic
		of maintaining buffers and calling the encoder.
	 */
	public static class EncodedGSMAudioInputStream
	extends TAsynchronousFilteredAudioInputStream
	{
		private AudioInputStream	m_decodedStream;
		private Encoder			m_encoder;

		/*
		 *	Holds one block of decoded data.
		 */
		private byte[]			m_abBuffer;

		/*
		 *	Holds one block of decoded data.
		 */
		private short[]			m_asBuffer;

		/*
		 *	Holds one encoded GSM frame.
		 */
		private byte[]			m_abFrameBuffer;



		public EncodedGSMAudioInputStream(AudioFormat outputFormat, AudioInputStream inputStream)
		{
			super(outputFormat,
			      //$$fb 2001-04-16: FrameLength gives the number of 33-byte blocks !
			      //inputStream.getFrameLength() == AudioSystem.NOT_SPECIFIED ? AudioSystem.NOT_SPECIFIED : inputStream.getFrameLength() / 160 * 33);
			      inputStream.getFrameLength() == AudioSystem.NOT_SPECIFIED ? AudioSystem.NOT_SPECIFIED : inputStream.getFrameLength() / 160);
			if (TDebug.TraceAudioConverter) { TDebug.out("EncodedGSMAudioInputStream.<init>(): begin"); }
			m_decodedStream = inputStream;
			m_encoder = new Encoder();
			m_abBuffer = new byte[BUFFER_SIZE];
			m_asBuffer = new short[160];
			m_abFrameBuffer = new byte[ENCODED_GSM_FRAME_SIZE];
			if (TDebug.TraceAudioConverter) { TDebug.out("EncodedGSMAudioInputStream.<init>(): end"); }
		}



		public void execute()
		{
			if (TDebug.TraceAudioConverter) { TDebug.out(">EncodedGSMAudioInputStream.execute(): begin"); }
			try
			{
				int	nRead = m_decodedStream.read(m_abBuffer);
				/*
				 *	Currently, we take all kinds of errors
				 *	as end of stream.
				 */
				if (nRead != m_abBuffer.length)
				{
					if (TDebug.TraceAudioConverter) { TDebug.out("<EncodedGSMAudioInputStream.execute(): not read whole 160 sample block (" + nRead + ")"); }
					getCircularBuffer().close();
					return;
				}
			}
			catch (IOException e)
			{
				if (TDebug.TraceAllExceptions) { TDebug.out(e); }
				getCircularBuffer().close();
				if (TDebug.TraceAudioConverter) { TDebug.out("<"); }
				return;
			}
			for (int i = 0; i < 160; i++)
			{
			        m_asBuffer[i] = TConversionTool.bytesToShort16(m_abBuffer, i * 2, isBigEndian());
			}
			m_encoder.encode(m_asBuffer, m_abFrameBuffer);
			getCircularBuffer().write(m_abFrameBuffer);
			if (TDebug.TraceAudioConverter) { TDebug.out("<EncodedGSMAudioInputStream.execute(): encoded GSM frame written"); }
			if (TDebug.TraceAudioConverter) { TDebug.out(">EncodedGSMAudioInputStream.execute(): end"); }
		}



		private boolean isBigEndian()
		{
			return m_decodedStream.getFormat().isBigEndian();
		}



		public void close()
			throws IOException
		{
			super.close();
			m_decodedStream.close();
		}
	}
}



/*** GSMFormatConversionProvider.java ***/
