/*
 *	MixingAudioInputStream.java
 *
 *	This file is part of jsresources.org
 *
 *	This code follows an idea of Paul Sorenson.
 */

/*
 * Copyright (c) 1999 - 2001 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.IOException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

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

/*
 * This is a class of Tritonus. It's not one of the best ideas to use it here.
 * However, we really don't want to reimplement its functionality here.
 * You need to have tritonus_share.jar in the classpath. 
 * Get it from http://www.tritonus.org .  
 */
import org.tritonus.share.sampled.TConversionTool;



/**
 * Mixing of multiple AudioInputStreams to one AudioInputStream. This class
 * takes a collection of AudioInputStreams and mixes them together. Being a
 * subclass of AudioInputStream itself, reading from instances of this class
 * behaves as if the mixdown result of the input streams is read.
 * 
 * @author Matthias Pfisterer
 */
public class MixingAudioInputStream
	extends		AudioInputStream
{
	private static final boolean	DEBUG = false;

	private List			m_audioInputStreamList;



	public MixingAudioInputStream(AudioFormat audioFormat, Collection audioInputStreams)
	{
		super(new ByteArrayInputStream(new byte[0]),
		      audioFormat,
		      AudioSystem.NOT_SPECIFIED);
		if (DEBUG) { out("MixingAudioInputStream.<init>(): begin"); }
		m_audioInputStreamList = new ArrayList(audioInputStreams);
		if (DEBUG)
		{
			out("MixingAudioInputStream.<init>(): stream list:");
			for (int i = 0; i < m_audioInputStreamList.size(); i++)
			{
				out("  " + m_audioInputStreamList.get(i));
			}
		}
		if (DEBUG) { out("MixingAudioInputStream.<init>(): end"); }
	}



	/**
	   The maximum of the frame length of the input stream is calculated and returned.
	   If at least one of the input streams has length
	   <code>AudioInputStream.NOT_SPECIFIED</code>, this value is returned.
	*/
	public long getFrameLength()
	{
		long	lLengthInFrames = 0;
		Iterator	streamIterator = m_audioInputStreamList.iterator();
		while (streamIterator.hasNext())
		{
			AudioInputStream	stream = (AudioInputStream) streamIterator.next();
			long	lLength = stream.getFrameLength();
			if (lLength == AudioSystem.NOT_SPECIFIED)
			{
				return AudioSystem.NOT_SPECIFIED;
			}
			else
			{
				lLengthInFrames = Math.max(lLengthInFrames, lLength);
			}
		}
		return lLengthInFrames;
	}



	public int read()
		throws	IOException
	{
		if (DEBUG) { out("MixingAudioInputStream.read(): begin"); }
		int	nSample = 0;
		Iterator	streamIterator = m_audioInputStreamList.iterator();
		while (streamIterator.hasNext())
		{
			AudioInputStream	stream = (AudioInputStream) streamIterator.next();
			int	nByte = stream.read();
			if (nByte == -1)
			{
				/*
				  The end of this stream has been signaled.
				  We remove the stream from our list.
				*/
				streamIterator.remove();
				continue;
			}
			else
			{
				/*
				  what about signed/unsigned?
				*/
				nSample += nByte;
			}
		}
		if (DEBUG) { out("MixingAudioInputStream.read(): end"); }
		return (byte) (nSample & 0xFF);
	}



	public int read(byte[] abData, int nOffset, int nLength)
		throws	IOException
	{
		if (DEBUG)
		{
			out("MixingAudioInputStream.read(byte[], int, int): begin");
			out("MixingAudioInputStream.read(byte[], int, int): requested length: " + nLength);
		}
		int	nChannels = getFormat().getChannels();
		int	nFrameSize = getFormat().getFrameSize();
		/*
		  This value is in bytes. Note that it is the storage size.
		  It may be four bytes for 24 bit samples.
		*/
		int	nSampleSize = nFrameSize / nChannels;
		boolean	bBigEndian = getFormat().isBigEndian();
		AudioFormat.Encoding	encoding = getFormat().getEncoding();
		if (DEBUG)
		{
			out("MixingAudioInputStream.read(byte[], int, int): channels: " + nChannels);
			out("MixingAudioInputStream.read(byte[], int, int): frame size: " + nFrameSize);
			out("MixingAudioInputStream.read(byte[], int, int): sample size (bytes, storage size): " + nSampleSize);
			out("MixingAudioInputStream.read(byte[], int, int): big endian: " + bBigEndian);
			out("MixingAudioInputStream.read(byte[], int, int): encoding: " + encoding);
		}
		byte[]	abBuffer = new byte[nFrameSize];
		int[]	anMixedSamples = new int[nChannels];
		for (int nFrameBoundry = 0; nFrameBoundry < nLength; nFrameBoundry += nFrameSize)
		{
			if (DEBUG) { out("MixingAudioInputStream.read(byte[], int, int): frame boundry: " + nFrameBoundry); }
			for (int i = 0; i < nChannels; i++)
			{
				anMixedSamples[i] = 0;
			}
			Iterator	streamIterator = m_audioInputStreamList.iterator();
			while (streamIterator.hasNext())
			{
				AudioInputStream	stream = (AudioInputStream) streamIterator.next();
				if (DEBUG)
				{
					out("MixingAudioInputStream.read(byte[], int, int): AudioInputStream: " + stream);
				}
				int	nBytesRead = stream.read(abBuffer, 0, nFrameSize);
				if (DEBUG)
				{
					out("MixingAudioInputStream.read(byte[], int, int): bytes read: " + nBytesRead);
				}
				/*
				  TODO: we have to handle incomplete reads.
				*/
				if (nBytesRead == -1)
				{
					/*
					  The end of the current stream has been signaled.
					  We remove it from the list of streams.
					*/
					streamIterator.remove();
					continue;
				}
				for (int nChannel = 0; nChannel < nChannels; nChannel++)
				{
					int	nBufferOffset = nChannel * nSampleSize;
					int	nSampleToAdd = 0;
					if (encoding.equals(AudioFormat.Encoding.PCM_SIGNED))
					{
						switch (nSampleSize)
						{
						case 1:
							nSampleToAdd = abBuffer[nBufferOffset];
							break;
						case 2:
							nSampleToAdd = TConversionTool.bytesToInt16(abBuffer, nBufferOffset, bBigEndian);
							break;
						case 3:
							nSampleToAdd = TConversionTool.bytesToInt24(abBuffer, nBufferOffset, bBigEndian);
							break;
						case 4:
							nSampleToAdd = TConversionTool.bytesToInt32(abBuffer, nBufferOffset, bBigEndian);
							break;
						}
					}
					// TODO: pcm unsigned
					else if (encoding.equals(AudioFormat.Encoding.ALAW))
					{
						nSampleToAdd = TConversionTool.alaw2linear(abBuffer[nBufferOffset]);
					}
					else if (encoding.equals(AudioFormat.Encoding.ULAW))
					{
						nSampleToAdd = TConversionTool.ulaw2linear(abBuffer[nBufferOffset]);
					}
					anMixedSamples[nChannel] += nSampleToAdd;
				} // loop over channels
			} // loop over streams
			if (DEBUG)
			{
				out("MixingAudioInputStream.read(byte[], int, int): starting to write to buffer passed by caller");
			}
			for (int nChannel = 0; nChannel < nChannels; nChannel++)
			{
				if (DEBUG)
				{
					out("MixingAudioInputStream.read(byte[], int, int): channel: " + nChannel);
				}
				int	nBufferOffset = nOffset + nFrameBoundry /* * nFrameSize*/ + nChannel * nSampleSize;
				if (DEBUG)
				{
					out("MixingAudioInputStream.read(byte[], int, int): buffer offset: " + nBufferOffset);
				}
				if (encoding.equals(AudioFormat.Encoding.PCM_SIGNED))
				{
					switch (nSampleSize)
					{
					case 1:
						abData[nBufferOffset] = (byte) anMixedSamples[nChannel];
						break;
					case 2:
						TConversionTool.intToBytes16(anMixedSamples[nChannel], abData, nBufferOffset, bBigEndian);
						break;
					case 3:
						TConversionTool.intToBytes24(anMixedSamples[nChannel], abData, nBufferOffset, bBigEndian);
						break;
					case 4:
						TConversionTool.intToBytes32(anMixedSamples[nChannel], abData, nBufferOffset, bBigEndian);
						break;
					}
				}
				// TODO: pcm unsigned
				else if (encoding.equals(AudioFormat.Encoding.ALAW))
				{
					abData[nBufferOffset] = TConversionTool.linear2alaw((short) anMixedSamples[nChannel]);
				}
				else if (encoding.equals(AudioFormat.Encoding.ULAW))
				{
					abData[nBufferOffset] = TConversionTool.linear2ulaw(anMixedSamples[nChannel]);
				}
			} // (final) loop over channels
		} // loop over frames
		if (DEBUG)
		{
			out("MixingAudioInputStream.read(byte[], int, int): end");
		}
		// TODO: return a useful value
		return nLength;
	}



	/**
	   calls skip() on all input streams. There is no way to assure that the number of
	   bytes really skipped is the same for all input streams. Due to that, this
	   method always returns the passed value. In other words: the return value
	   is useless (better ideas appreciated).
	*/
	public long skip(long lLength)
		throws	IOException
	{
		Iterator	streamIterator = m_audioInputStreamList.iterator();
		while (streamIterator.hasNext())
		{
			AudioInputStream	stream = (AudioInputStream) streamIterator.next();
			stream.skip(lLength);
		}
		return lLength;
	}



	/**
	   The minimum of available() of all input stream is calculated and returned.
	*/
	public int available()
		throws	IOException
	{
		int	nAvailable = 0;
		Iterator	streamIterator = m_audioInputStreamList.iterator();
		while (streamIterator.hasNext())
		{
			AudioInputStream	stream = (AudioInputStream) streamIterator.next();
			nAvailable = Math.min(nAvailable, stream.available());
		}
		return nAvailable;
	}



	public void close()
		throws	IOException
	{
		// TODO: should we close all streams in the list?
	}



	/**
	   Calls mark() on all input streams.
	*/
	public void mark(int nReadLimit)
	{
		Iterator	streamIterator = m_audioInputStreamList.iterator();
		while (streamIterator.hasNext())
		{
			AudioInputStream	stream = (AudioInputStream) streamIterator.next();
			stream.mark(nReadLimit);
		}
	}


	/**
	   Calls reset() on all input streams.
	*/
	public void reset()
		throws	IOException
	{
		Iterator	streamIterator = m_audioInputStreamList.iterator();
		while (streamIterator.hasNext())
		{
			AudioInputStream	stream = (AudioInputStream) streamIterator.next();
			stream.reset();
		}
	}



	/**
	   returns true if all input stream return true for markSupported().
	*/
	public boolean markSupported()
	{
		Iterator	streamIterator = m_audioInputStreamList.iterator();
		while (streamIterator.hasNext())
		{
			AudioInputStream	stream = (AudioInputStream) streamIterator.next();
			if (! stream.markSupported())
			{
				return false;
			}
		}
		return true;
	}



	private static void out(String strMessage)
	{
		System.out.println(strMessage);
	}
}



/*** MixingAudioInputStream.java ***/