/*
 *	SingleChannelStereoAudioInputStream.java
 *
 *	This file is part of jsresources.org
 */

/*
 * Copyright (c) 2004 by Matthias Pfisterer
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
|<---            this code is formatted to fit into 80 columns             --->|
*/

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.IOException;

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



/**	Converting a mono stream to single-sided a stereo stream.


	Note: skip(), available() and mark()/reset() are not tested!

	@author Matthias Pfisterer
 */
public class SingleChannelStereoAudioInputStream
extends AudioInputStream
{
	private static final boolean	DEBUG = false;

	/** Flag for writing silence samples.  If this flag is true, zero
		bytes for silence samples are not written to the array passed
		to read(). This assumes that the array has been filled with
		0's outside of this class, which is often the case. Non-zero
		bytes (e.g. 128 for unsigned 8 bit) are always written.
	*/
	private static boolean sm_bOptimizeSilenceWriting = true;

	/** The AudioInputStream for this one, already converted to mono.
	 */
	private AudioInputStream	m_sourceStream;

	/** Stream should appear left or right?  If true, the signal is
		put on the left channel and silence on the right, otherwise
		vice versa.
	*/
	private boolean m_bSignalOnLeftChannel;

	/* A silence sample (mono) in byte representation of this stream.
	 */
	private byte[] m_abSilenceSample;

	/** Intermediate buffer for read().  This reference to this buffer
		is an instance variable so that there is no need to allocate
		the buffer on each invocation of read(). We initialize it here
		with an array of length 0 because this saves the handling of
		null references.
	*/
	private byte[] m_abSourceBuffer = new byte[0];

	/** Constructor.

	@param sourceStream the stream this one should be based on. The
	stream has to be in a PCM encoding. It may be stereo or mono if
	Tritonus' PCM2PCM converter is available in the system. If not,
	only mono is allowed.

	@param bSignalOnLeftChannel determines on which of the stereo
	channels (left or right) the sourceStream should appear. Passing
	true puts the stream on the left side and silence on the right
	side, passing false does it the other way round.

	@throws IllegalArgumentException if the encoding of sourceStream
	is neither PCM_SIGNED nor PCM_UNSIGNED, or if the encoding is
	PCM_UNSIGNED and the sample size in bits is different from 8.
	*/
	public SingleChannelStereoAudioInputStream(AudioInputStream sourceStream,
											   boolean bSignalOnLeftChannel)
	{
		super(new ByteArrayInputStream(new byte[0]),
			  new AudioFormat(sourceStream.getFormat().getSampleRate(),
							  sourceStream.getFormat().getSampleSizeInBits(),
							  2, // always stereo
							  sourceStream.getFormat().getEncoding().equals(AudioFormat.Encoding.PCM_SIGNED),
							  sourceStream.getFormat().isBigEndian()),
		      sourceStream.getFrameLength());
		if (DEBUG) { out("SingleChannelStereoAudioInputStream.<init>(): begin"); }
		AudioFormat sourceFormat = sourceStream.getFormat();
		if (! AudioCommon.isPcm(sourceFormat.getEncoding()))
		{
			throw new IllegalArgumentException("source stream has to be PCM");
		}

		/* Convert to mono, if necessary. For this to work cleanly,
		 * the PCM2PCM converter of Tritonus is needed.
		 */
		if (sourceFormat.getChannels() != 1)
		{
			AudioFormat monoFormat = new AudioFormat(
				sourceFormat.getEncoding(),
				sourceFormat.getSampleRate(),
				sourceFormat.getSampleSizeInBits(),
				1,
				(sourceFormat.getSampleSizeInBits() + 7) / 8,
				sourceFormat.getFrameRate(),
				sourceFormat.isBigEndian());
			sourceStream = AudioSystem.getAudioInputStream(monoFormat, sourceStream);
		}
		m_sourceStream = sourceStream;
		m_bSignalOnLeftChannel = bSignalOnLeftChannel;

		/* This value is in bytes. Note that it is the storage size.
		   It may be four bytes for 24 bit samples.
		   The channel number is always 2 (stereo).
		*/
		int	nSampleSizeInBytes = getFormat().getFrameSize() / 2;
		m_abSilenceSample = new byte[nSampleSizeInBytes];
		/* For signed PCM representation, the values of the array
		   m_abSilenceSample are left with the initial value 0 (this
		   represents silence). For unsigned 8 bit, the value 128
		   represents silence. Therefore, the (single) byte is
		   initialized with this value. Unsigned formats with more
		   than 8 bits are not supported.
		*/
		if (getFormat().getEncoding().equals(AudioFormat.Encoding.PCM_UNSIGNED))
		{
			if (getFormat().getSampleSizeInBits() == 8)
				m_abSilenceSample[0] = (byte) 128;
			else
				throw new IllegalArgumentException("unsigned formats are only supported for 8 bit");
		}
		if (DEBUG) { out("SingleChannelStereoAudioInputStream.<init>(): end"); }
	}



	public int read()
		throws	IOException
	{
		if (DEBUG) { out("SingleChannelStereoAudioInputStream.read(): begin"); }
		/* This stream is stereo, so the frame size is at least 2
		 * bytes.  Therefore, reading a single byte is always an
		 * error.
		 */
		throw new IOException("cannot read fraction of a frame");
	}



	public int read(byte[] abData, int nOffset, int nLength)
		throws	IOException
	{
		if (DEBUG)
		{
			out("SingleChannelStereoAudioInputStream.read(byte[], int, int): begin");
			out("SingleChannelStereoAudioInputStream.read(byte[], int, int): requested length: " + nLength);
		}
		// TODO: check if cacheing in an instance variable makes a difference in performance
		int	nThisFrameSize = getFormat().getFrameSize();
		/* This value is in bytes. Note that it is the storage size.
		   It may be four bytes for 24 bit samples.
		*/
		int nThisSampleSizeInBytes = nThisFrameSize / 2;
		int	nSourceFrameSize = m_sourceStream.getFormat().getFrameSize();
		if ((nLength % nThisFrameSize) != 0)
		{
			throw new IOException("cannot read fraction of a frame");
		}
		AudioFormat.Encoding	encoding = getFormat().getEncoding();
		if (DEBUG)
		{
			out("SingleChannelStereoAudioInputStream.read(byte[], int, int): frame size: " + nThisFrameSize);
			out("SingleChannelStereoAudioInputStream.read(byte[], int, int): encoding: " + encoding);
		}
		int nFrames = nLength / nThisFrameSize;
		int nUsedSourceBufferLength = nFrames * nSourceFrameSize;
		if (m_abSourceBuffer.length < nUsedSourceBufferLength)
		{
			m_abSourceBuffer = new byte[nUsedSourceBufferLength];
		}
		// using a local variable for performance
		byte[] abSourceBuffer = m_abSourceBuffer;
		// out("nUsedSourceBufferLength: " + nUsedSourceBufferLength);
		int	nBytesRead = m_sourceStream.read(abSourceBuffer, 0, nUsedSourceBufferLength);
		if (DEBUG) { out("SingleChannelStereoAudioInputStream.read(byte[], int, int): bytes read: " + nBytesRead); }
		if (nBytesRead == -1)
		{
			/*
			  The end of the source stream has been reached.
			  Nothing more to do here.
			*/
			return -1;
		}
		nFrames = nBytesRead / m_sourceStream.getFormat().getFrameSize();
		int nThisFrameBoundry = nOffset;
		int nSourceFrameBoundry = 0;
		int n;
		if (nThisSampleSizeInBytes == 2)
		{
			for (int i = 0; i < nFrames; i++)
			{
				int nSignalDestIndex;
				int nSilenceDestIndex;
				if (m_bSignalOnLeftChannel)
				{
					nSignalDestIndex = nThisFrameBoundry;
					nSilenceDestIndex = nThisFrameBoundry + nThisSampleSizeInBytes;
				}
				else
				{
					nSilenceDestIndex = nThisFrameBoundry;
					nSignalDestIndex = nThisFrameBoundry + nThisSampleSizeInBytes;
				}
				// signal
				abData[nSignalDestIndex] = abSourceBuffer[nSourceFrameBoundry];
				abData[nSignalDestIndex + 1] = abSourceBuffer[nSourceFrameBoundry + 1];
				// silence
				if (! sm_bOptimizeSilenceWriting)
				{
					abData[nSilenceDestIndex] = 0;
					abData[nSilenceDestIndex + 1] = 0;
				}
				nThisFrameBoundry += nThisFrameSize;
				nSourceFrameBoundry += nSourceFrameSize;
			}
		}
		else
		{
			for (int i = 0; i < nFrames; i++)
			{
				int nSignalDestIndex;
				int nSilenceDestIndex;
				if (m_bSignalOnLeftChannel)
				{
					nSignalDestIndex = nThisFrameBoundry;
					nSilenceDestIndex = nThisFrameBoundry + nThisSampleSizeInBytes;
				}
				else
				{
					nSilenceDestIndex = nThisFrameBoundry;
					nSignalDestIndex = nThisFrameBoundry + nThisSampleSizeInBytes;
				}
				// signal
				n = 0;
				while (n < nThisSampleSizeInBytes)
				{
					abData[nSignalDestIndex + n] = abSourceBuffer[nSourceFrameBoundry + n];
					n++;
				}
				// silence
				if (! sm_bOptimizeSilenceWriting)
				{
					n = 0;
					while (n < nThisSampleSizeInBytes)
					{
						abData[nSilenceDestIndex + n] = 0;
						n++;
					}
				}
				nThisFrameBoundry += nThisFrameSize;
				nSourceFrameBoundry += nSourceFrameSize;
			}
		}
		if (DEBUG) { out("SingleChannelStereoAudioInputStream.read(byte[], int, int): end"); }
		return nFrames * nThisFrameSize;
	}



	/**
	   Calls skip() on the source stream. Since the source stream may
	   have a different frame size, The number of bytes is
	   recalculated, so that the number of skipped frames is the same
	   as requested.
	*/
	public long skip(long lLength)
		throws	IOException
	{
		// frame size of this stream
		int nThisFrameSize = getFormat().getFrameSize();
		// frame size of source stream
		int nSourceFrameSize = m_sourceStream.getFormat().getFrameSize();

		long lBytesInSource = (lLength / nThisFrameSize) * nSourceFrameSize;
		long lBytesSkippedInSource = m_sourceStream.skip(lBytesInSource);
		return (lBytesSkippedInSource / nSourceFrameSize) * nThisFrameSize;
	}



	/**
	   The minimum of available() of all input stream is calculated and returned.
	*/
	public int available()
		throws	IOException
	{
		// frame size of this stream
		int nThisFrameSize = getFormat().getFrameSize();
		// frame size of source stream
		int nSourceFrameSize = m_sourceStream.getFormat().getFrameSize();

		int	nAvailableInSource = m_sourceStream.available();
		return (nAvailableInSource / nSourceFrameSize) * nThisFrameSize;
	}



	public void close()
		throws	IOException
	{
		m_sourceStream.close();
	}



	/**
	   Recalculates nReadLimit to an equivalent number (same number of
	   frames) for the source stream and calls mark() on it.
	*/
	public void mark(int nReadLimit)
	{
		// frame size of this stream
		int nThisFrameSize = getFormat().getFrameSize();
		// frame size of source stream
		int nSourceFrameSize = m_sourceStream.getFormat().getFrameSize();

		int nSourceReadLimit = (nReadLimit / nThisFrameSize) * nSourceFrameSize;
		m_sourceStream.mark(nSourceReadLimit);
	}


	/**
	   Calls reset() on the source stream.
	*/
	public void reset()
		throws	IOException
	{
		m_sourceStream.reset();
	}



	/**
	   returns true if the source stream return true for
	   markSupported().
	*/
	public boolean markSupported()
	{
		return m_sourceStream.markSupported();
	}



	/** Print a message to standard output.
		@param strMessage the string that should be printed
	*/
	private static void out(String strMessage)
	{
		System.out.println(strMessage);
	}
}



/*** SingleChannelStereoAudioInputStream.java ***/