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

/*
 * Copyright (c) 1999, 2000 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.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;


import java.io.File;
import java.io.IOException;

import java.net.URL;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.sound.sampled.FloatControl;




public class BaseAudioStream
	implements	Runnable
{
	/**	Flag for debugging messages.
	 *	If true, some messages are dumped to the console
	 *	during operation.	
	 */
	private static boolean	DEBUG = true;

	/**
	 *	means that the stream has reached EOF or was not started.
	 *	This value is returned in property change callbacks that
	 *	report the current media position.
	 */
	public static final long	MEDIA_POSITION_EOF = -1L;
	public static final String	MEDIA_POSITION_PROPERTY = "BaseAudioStream_media_position";

	// TODO: better size
	private static final int	EXTERNAL_BUFFER_SIZE = 4000 * 4;

	private Thread			m_thread = null;
	private Object			m_dataSource;
	private AudioInputStream	m_audioInputStream;
	private SourceDataLine		m_line;
	private FloatControl		m_gainControl;
	private FloatControl		m_panControl;


	/**
	 *	This variable is used to distinguish stopped state from
	 *	paused state. In case of paused state, m_bRunning is still
	 *	true. In case of stopped state, it is set to false. Doing so
	 *	will terminate the thread.
	 */
	private boolean			m_bRunning;


	protected BaseAudioStream()
	{
		m_dataSource = null;
		m_audioInputStream = null;
		m_line = null;
		m_gainControl = null;
		m_panControl = null;
	}



	protected void setDataSource(File file)
		throws	UnsupportedAudioFileException, LineUnavailableException, IOException
	{
		m_dataSource = file;
		initAudioInputStream();
	}



	protected void setDataSource(URL url)
		throws	UnsupportedAudioFileException, LineUnavailableException, IOException
	{
		m_dataSource = url;
		initAudioInputStream();
	}		



	private void initAudioInputStream()
		throws	UnsupportedAudioFileException, LineUnavailableException, IOException
	{
		if (m_dataSource instanceof URL)
		{
			initAudioInputStream((URL) m_dataSource);
		}
		else if (m_dataSource instanceof File)
		{
			initAudioInputStream((File) m_dataSource);
		}
	}



	private void initAudioInputStream(File file)
		throws	UnsupportedAudioFileException, IOException
	{
/*
		try
		{
*/
			m_audioInputStream = AudioSystem.getAudioInputStream(file);
/*
		}
		catch (IOException e)
		{
			throw new IllegalArgumentException("cannot create AudioInputStream for " + file);
		}
		if (m_audioInputStream == null)
		{
			throw new IllegalArgumentException("cannot create AudioInputStream for " + file);
		}
*/
	}



	private void initAudioInputStream(URL url)
		throws	UnsupportedAudioFileException, IOException
	{
/*
		try
		{
*/
			m_audioInputStream = AudioSystem.getAudioInputStream(url);
/*
		}
		catch (IOException e)
		{
			throw new IllegalArgumentException("cannot create AudioInputStream for " + url);
		}
		if (m_audioInputStream == null)
		{
			throw new IllegalArgumentException("cannot create AudioInputStream for " + url);
		}
*/
	}



	// from AudioPlayer.java
		/*
		 *	Compressed audio data cannot be fed directely to
		 *	Java Sound. It has to be converted explicitely.
		 *	To do this, we create a new AudioFormat that
		 *	says to which format we want to convert to. Then,
		 *	we try to get a converted AudioInputStream.
		 *	Furthermore, we use the new format and the converted
		 *	stream.
		 *
		 *	Note that the technique shown here is partly non-
		 *	portable. It is used here to keep the example
		 *	simple. A more advanced, more portable technique
		 *	will (hopefully) show up in BaseAudioStream.java soon.
		 *
		 *	Thanks to Christoph Hecker for finding out that this
		 *	was missing.
		 */
/*
		if ((audioFormat.getEncoding() == AudioFormat.Encoding.ULAW) ||
		    (audioFormat.getEncoding() == AudioFormat.Encoding.ALAW)) 
		{
			if (DEBUG)
			{
				out("AudioPlayer.main(): converting");
			}
			AudioFormat newFormat = new AudioFormat(
				AudioFormat.Encoding.PCM_SIGNED, 
				audioFormat.getSampleRate(),
				audioFormat.getSampleSizeInBits() * 2,
				audioFormat.getChannels(),
				audioFormat.getFrameSize() * 2,
				audioFormat.getFrameRate(),
				true);
			AudioInputStream	newStream = AudioSystem.getAudioInputStream(newFormat, audioInputStream);
			audioFormat = newFormat;
			audioInputStream = newStream;
                }
*/



	protected void initLine()
		throws	LineUnavailableException
	{
		if (m_line == null)
		{
			createLine();
			openLine();
		}
		else
		{
			AudioFormat	lineAudioFormat = m_line.getFormat();
			AudioFormat	audioInputStreamFormat = m_audioInputStream == null ? null : m_audioInputStream.getFormat();
			if (!lineAudioFormat.equals(audioInputStreamFormat))
			{
				m_line.close();
				openLine();
			}
		}
	}



	private void createLine()
		throws	LineUnavailableException
	{
		if (m_line != null)
		{
			return;
		}
		/*
		 *	From the AudioInputStream, i.e. from the sound file, we
		 *	fetch information about the format of the audio data. These
		 *	information include the sampling frequency, the number of
		 *	channels and the size of the samples. There information
		 *	are needed to ask Java Sound for a suitable output line
		 *	for this audio file.
		 */
		AudioFormat	audioFormat = m_audioInputStream.getFormat();
		if (DEBUG)
		{
			out("BaseAudioStream.initLine(): audio format: " + audioFormat);
		}

		/*
		 *	Asking for a line is a rather tricky thing.
		 *	...
		 *	Furthermore, we have to give Java Sound a hint about how
		 *	big the internal buffer for the line should be. Here,
		 *	we say AudioSystem.NOT_SPECIFIED, signaling that we don't
		 *	care about the exact size. Java Sound will use some default
		 *	value for the buffer size.
		 */
		DataLine.Info	info = new DataLine.Info(SourceDataLine.class, audioFormat, AudioSystem.NOT_SPECIFIED);
		m_line = (SourceDataLine) AudioSystem.getLine(info);

		if (m_line.isControlSupported(FloatControl.Type.MASTER_GAIN/*VOLUME*/))
		{
			m_gainControl = (FloatControl) m_line.getControl(FloatControl.Type.MASTER_GAIN);
			if (DEBUG)
			{
				out("max gain: " + m_gainControl.getMaximum());
				out("min gain: " + m_gainControl.getMinimum());
				out("gain precision: " + m_gainControl.getPrecision());
			}
		}
		else
		{
			if (DEBUG)
			{
				out("FloatControl.Type.MASTER_GAIN is not supported");
			}
		}
		if (m_line.isControlSupported(FloatControl.Type.PAN/*BALANCE*/))
		{
			m_panControl = (FloatControl) m_line.getControl(FloatControl.Type.PAN);
			if (DEBUG)
			{
				out("max balance: " + m_panControl.getMaximum());
				out("min balance: " + m_panControl.getMinimum());
				out("balance precision: " + m_panControl.getPrecision());
			}
		}
		else
		{
			if (DEBUG)
			{
				out("FloatControl.Type.PAN is not supported");
			}
		}
	}



	private void openLine()
		throws	LineUnavailableException
	{
		if (m_line == null)
		{
			return;
		}
		AudioFormat	audioFormat = m_audioInputStream.getFormat();
		m_line.open(audioFormat, m_line.getBufferSize());
	}



	// TODO: if class can be instatiated without file or url, m_audioInputStream may
	// be null
	protected AudioFormat getFormat()
	{
		return m_audioInputStream.getFormat();
	}



	public void start()
	{
		if (DEBUG)
		{
			out("start() called");
		}
		if (!(m_thread == null || !m_thread.isAlive()))
		{
			if (DEBUG)
			{
				out("WARNING: old thread still running!!");
			}
		}
		if (DEBUG)
		{
			out("creating new thread");
		}
		m_thread = new Thread(this);
		m_thread.start();
		if (DEBUG)
		{
			out("additional thread started");
		}
		if (DEBUG)
		{
			out("starting line");
		}
		m_line.start();
	}



	protected void stop()
	{
		if (m_bRunning)
		{
			if (m_line != null)
			{
				m_line.stop();
				m_line.flush();
			}
			m_bRunning = false;
			/*
			 *	We re-initialize the AudioInputStream. Since doing
			 *	a stop on the stream implies that there has been
			 *	a successful creation of an AudioInputStream before,
			 *	we can almost safely ignore this exception.
			 *	The LineUnavailableException can be ignored because
			 *	in case of reinitializing the same AudioInputStream,
			 *	no new line is created or opened.
			 */
			try
			{
				initAudioInputStream();
			}
			catch (UnsupportedAudioFileException e)
			{
			}
			catch (LineUnavailableException e)
			{
			}
			catch (IOException e)
			{
			}
		}
	}



	public void pause()
	{
		m_line.stop();
	}



	public void resume()
	{
		m_line.start();
	}



	public void run()
	{
		if (DEBUG)
		{
			out("thread start");
		}
		int	nBytesRead = 0;
		m_bRunning = true;
		byte[]	abData = new byte[EXTERNAL_BUFFER_SIZE];
		// int	nFrameSize = m_line.getFormat().getFrameSize();
		while (nBytesRead != -1 && m_bRunning)
		{
			try
			{
				nBytesRead = m_audioInputStream.read(abData, 0, abData.length);
			}
			catch (IOException e)
			{
				e.printStackTrace();
			}
			if (nBytesRead >= 0)
			{
				//int	nFramesToWrite = nBytesRead / nFrameSize;
				if (DEBUG)
				{
					out("Trying to write: " + nBytesRead);
				}
				int	nBytesWritten = m_line.write(abData, 0, nBytesRead);
				if (DEBUG)
				{
					out("Written: " + nBytesWritten);
				}
			}
		}

		/*
		 *	Wait until all data are played.
		 *	This is only necessary because of the bug noted below.
		 *	(If we do not wait, we would interrupt the playback by
		 *	prematurely closing the line and exiting the VM.)
		 */
		// TODO: check how this interferes with stop()
		m_line.drain();
		if (DEBUG)
		{
			out("after drain()");
		}

		/*
		 *	Stop the line and reinitialize the AudioInputStream.
		 *	This should be done before reporting end-of-media to be
		 *	prepared if the EOM message triggers a new start().
		 */
		stop();
		if (DEBUG)
		{
			out("after this.stop()");
		}
	}


	public boolean hasGainControl()
	{
		return m_gainControl != null;
	}

/*
	public void setMute(boolean bMute)
	{
		if (hasGainControl())
		{
			m_gainControl.setMute(bMute);
		}
	}



	public boolean getMute()
	{
		if (hasGainControl())
		{
			return m_gainControl.getMute();
		}
		else
		{
			return false;
		}
	}
*/



	public void setGain(float fGain)
	{
		if (hasGainControl())
		{
			m_gainControl.setValue(fGain);
		}
	}



	public float getGain()
	{
		if (hasGainControl())
		{
			return m_gainControl.getValue();
		}
		else
		{
			return 0.0F;
		}
	}


	public float getMaximum()
	{
		if (hasGainControl())
		{
			return m_gainControl.getMaximum();
		}
		else
		{
			return 0.0F;
		}
	}


	public float getMinimum()
	{
		if (hasGainControl())
		{
			return m_gainControl.getMinimum();
		}
		else
		{
			return 0.0F;
		}
	}


	public boolean hasPanControl()
	{
		return m_panControl != null;
	}



	public float getPrecision()
	{
		if (hasPanControl())
		{
			return m_panControl.getPrecision();
		}
		else
		{
			return 0.0F;
		}
	}



	public float getPan()
	{
		if (hasPanControl())
		{
			return m_panControl.getValue();
		}
		else
		{
			return 0.0F;
		}
	}



	public void setPan(float fPan)
	{
		if (hasPanControl())
		{
			m_panControl.setValue(fPan);
		}
	}



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



/*** BaseAudioStream.java ***/