/*
* 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 ***/