package paulscode.sound.libraries;
import java.util.LinkedList;
import java.util.List;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import paulscode.sound.Channel;
import paulscode.sound.SoundBuffer;
import paulscode.sound.SoundSystemConfig;
/**
* The ChannelJavaSound class is used to reserve a sound-card voice using
* JavaSound. Channels can be either normal or streaming channels.
*
* For more information about the JavaSound API, visit
* http://java.sun.com/products/java-media/sound/
*<br><br>
*<b><i> SoundSystem LibraryJavaSound License:</b></i><br><b><br>
* You are free to use this library for any purpose, commercial or otherwise.
* You may modify this library or source code, and distribute it any way you
* like, provided the following conditions are met:
*<br>
* 1) You may not falsely claim to be the author of this library or any
* unmodified portion of it.
*<br>
* 2) You may not copyright this library or a modified version of it and then
* sue me for copyright infringement.
*<br>
* 3) If you modify the source code, you must clearly document the changes
* made before redistributing the modified source code, so other users know
* it is not the original code.
*<br>
* 4) You are not required to give me credit for this library in any derived
* work, but if you do, you must also mention my website:
* http://www.paulscode.com
*<br>
* 5) I the author will not be responsible for any damages (physical,
* financial, or otherwise) caused by the use if this library or any part
* of it.
*<br>
* 6) I the author do not guarantee, warrant, or make any representations,
* either expressed or implied, regarding the use of this library or any
* part of it.
* <br><br>
* Author: Paul Lamb
* <br>
* http://www.paulscode.com
* </b>
*/
public class ChannelJavaSound extends Channel
{
// NORMAL SOURCE VARRIABLES:
/**
* Used to play back a normal source.
*/
public Clip clip = null;
/**
* The paulscode.sound.LibraryJavaSound.SoundBuffer containing the sound data
* to play for a normal source.
*/
SoundBuffer soundBuffer;
// END NORMAL SOURCE VARRIABLES
// STREAMING SOURCE VARRIABLES:
/**
* Used to play back a streaming source.
*/
public SourceDataLine sourceDataLine = null;
/**
* List of paulscode.sound.LibraryJavaSound.SoundBuffer, used to queue chunks
* of sound data to be streamed.
*/
private List<SoundBuffer> streamBuffers;
/**
* Number of queued stream-buffers that have finished being processed.
*/
private int processed = 0;
// END STREAMING SOURCE VARRIABLES:
/**
* Handle to the Mixer, which is used to mix output from all channels.
*/
private Mixer myMixer = null;
/**
* Format to use when playing back the assigned source.
*/
private AudioFormat myFormat = null;
/**
* Control for changing the gain.
*/
private FloatControl gainControl = null;
/**
* Control for changing the pan.
*/
private FloatControl panControl = null;
/**
* Control for changing the sample rate.
*/
private FloatControl sampleRateControl = null;
/**
* The initial decible change (start at normal volume).
*/
private float initialGain = 0.0f;
/**
* The initial sample rate for this channel.
*/
private float initialSampleRate = 0.0f;
/**
* When toLoop is true, the assigned source is immediately replayed when the
* end is reached.
*/
private boolean toLoop = false;
/**
* Constructor: takes channelType identifier and a handle to the Mixer as
* paramaters. Possible values for channel type can be found in the
* {@link paulscode.sound.SoundSystemConfig SoundSystemConfig} class.
* @param type Type of channel (normal or streaming).
* @param mixer Handle to the JavaSound Mixer.
*/
public ChannelJavaSound( int type, Mixer mixer )
{
super( type );
libraryType = LibraryJavaSound.class;
myMixer = mixer;
clip = null;
sourceDataLine = null;
streamBuffers = new LinkedList<SoundBuffer>();
}
/**
* Empties the streamBuffers list, shuts the channel down and removes
* references to all instantiated objects.
*/
@Override
public void cleanup()
{
if( streamBuffers != null )
{
SoundBuffer buf = null;
while( !streamBuffers.isEmpty() )
{
buf = streamBuffers.remove( 0 );
buf.cleanup();
buf = null;
}
streamBuffers.clear();
}
clip = null;
soundBuffer = null;
sourceDataLine = null;
streamBuffers.clear();
myMixer = null;
myFormat = null;
streamBuffers = null;
super.cleanup();
}
/**
* Changes the current mixer
* @param m New mixer to use.
*/
public void newMixer( Mixer m )
{
if( myMixer != m )
{
try
{
if( clip != null )
clip.close();
else if( sourceDataLine != null )
sourceDataLine.close();
}
catch( SecurityException e )
{}
myMixer = m;
if( attachedSource != null )
{
if( channelType == SoundSystemConfig.TYPE_NORMAL
&& soundBuffer != null )
attachBuffer( soundBuffer );
else if( myFormat != null )
resetStream( myFormat );
}
}
}
/**
* Attaches the SoundBuffer to be played back for a normal source.
* @param buffer SoundBuffer containing the wave data and format to attach
* @return False if an error occurred.
*/
public boolean attachBuffer( SoundBuffer buffer )
{
// Can only attach a buffer to a normal source:
if( errorCheck( channelType != SoundSystemConfig.TYPE_NORMAL,
"Buffers may only be attached to non-streaming " +
"sources" ) )
return false;
// make sure the Mixer exists:
if( errorCheck( myMixer == null,
"Mixer null in method 'attachBuffer'" ) )
return false;
// make sure the buffer exists:
if( errorCheck( buffer == null,
"Buffer null in method 'attachBuffer'" ) )
return false;
// make sure the buffer exists:
if( errorCheck( buffer.audioData == null,
"Buffer missing audio data in method " +
"'attachBuffer'" ) )
return false;
// make sure there is format information about this sound buffer:
if( errorCheck( buffer.audioFormat == null,
"Buffer missing format information in method " +
"'attachBuffer'" ) )
return false;
DataLine.Info lineInfo;
lineInfo = new DataLine.Info( Clip.class, buffer.audioFormat );
if( errorCheck( !AudioSystem.isLineSupported( lineInfo ),
"Line not supported in method 'attachBuffer'" ) )
return false;
Clip newClip = null;
try
{
newClip = (Clip) myMixer.getLine( lineInfo );
}
catch( Exception e )
{
errorMessage( "Unable to create clip in method 'attachBuffer'" );
printStackTrace( e );
return false;
}
if( errorCheck( newClip == null,
"New clip null in method 'attachBuffer'" ) )
return false;
// if there was already a clip playing on this channel, remove it now:
if( clip != null )
{
clip.stop();
clip.flush();
clip.close();
}
// Update the clip and format varriables:
clip = newClip;
soundBuffer = buffer;
myFormat = buffer.audioFormat;
newClip = null;
try
{
clip.open( myFormat, buffer.audioData, 0, buffer.audioData.length );
}
catch( Exception e )
{
errorMessage( "Unable to attach buffer to clip in method " +
"'attachBuffer'" );
printStackTrace( e );
return false;
}
resetControls();
// Success:
return true;
}
/**
* Sets the channel up to receive the specified audio format.
* @param audioFormat Format to use when playing the stream data.
*/
@Override
public void setAudioFormat( AudioFormat audioFormat )
{
resetStream( audioFormat );
if( attachedSource != null && attachedSource.rawDataStream &&
attachedSource.active() && sourceDataLine != null )
sourceDataLine.start();
}
/**
* Sets the channel up to be streamed using the specified AudioFormat.
* @param format Format to use when playing the stream data.
* @return False if an error occurred.
*/
public boolean resetStream( AudioFormat format )
{
// make sure the Mixer exists:
if( errorCheck( myMixer == null,
"Mixer null in method 'resetStream'" ) )
return false;
// make sure a format was specified:
if( errorCheck( format == null,
"AudioFormat null in method 'resetStream'" ) )
return false;
DataLine.Info lineInfo;
lineInfo = new DataLine.Info( SourceDataLine.class, format );
if( errorCheck( !AudioSystem.isLineSupported( lineInfo ),
"Line not supported in method 'resetStream'" ) )
return false;
SourceDataLine newSourceDataLine = null;
try
{
newSourceDataLine = (SourceDataLine) myMixer.getLine( lineInfo );
}
catch( Exception e )
{
errorMessage( "Unable to create a SourceDataLine " +
"in method 'resetStream'" );
printStackTrace( e );
return false;
}
if( errorCheck( newSourceDataLine == null,
"New SourceDataLine null in method 'resetStream'" ) )
return false;
streamBuffers.clear();
processed = 0;
// if there was already something playing on this channel, remove it:
if( sourceDataLine != null )
{
sourceDataLine.stop();
sourceDataLine.flush();
sourceDataLine.close();
}
// Update the clip and format varriables:
sourceDataLine = newSourceDataLine;
myFormat = format;
newSourceDataLine = null;
try
{
sourceDataLine.open( myFormat );
}
catch( Exception e )
{
errorMessage( "Unable to open the new SourceDataLine in method " +
"'resetStream'" );
printStackTrace( e );
return false;
}
resetControls();
// Success:
return true;
}
/**
* (Re)Creates the pan and gain controls for this channel.
*/
private void resetControls()
{
switch( channelType )
{
case SoundSystemConfig.TYPE_NORMAL:
// Check if panning is supported:
try
{
if( !clip.isControlSupported( FloatControl.Type.PAN ) )
panControl = null;
else
// Create a new pan Control:
panControl = (FloatControl) clip.getControl(
FloatControl.Type.PAN );
}
catch( IllegalArgumentException iae )
{
panControl = null;
}
// Check if changing the volume is supported:
try
{
if( !clip.isControlSupported( FloatControl.Type.MASTER_GAIN ) )
{
gainControl = null;
initialGain = 0;
}
else
{
// Create a new gain control:
gainControl = (FloatControl) clip.getControl(
FloatControl.Type.MASTER_GAIN );
// Store it's initial gain to use as "maximum volume" later:
initialGain = gainControl.getValue();
}
}
catch( IllegalArgumentException iae )
{
gainControl = null;
initialGain = 0;
}
// Check if changing the sample rate is supported:
try
{
if( !clip.isControlSupported( FloatControl.Type.SAMPLE_RATE ) )
{
sampleRateControl = null;
initialSampleRate = 0;
}
else
{
// Create a new sample rate control:
sampleRateControl = (FloatControl) clip.getControl(
FloatControl.Type.SAMPLE_RATE );
// Store it's initial value to use later:
initialSampleRate = sampleRateControl.getValue();
}
}
catch( IllegalArgumentException iae )
{
sampleRateControl = null;
initialSampleRate = 0;
}
break;
case SoundSystemConfig.TYPE_STREAMING:
// Check if panning is supported:
try
{
if( !sourceDataLine.isControlSupported(
FloatControl.Type.PAN ) )
panControl = null;
else
// Create a new pan Control:
panControl = (FloatControl) sourceDataLine.getControl(
FloatControl.Type.PAN );
}
catch( IllegalArgumentException iae )
{
panControl = null;
}
// Check if changing the volume is supported:
try
{
if( !sourceDataLine.isControlSupported(
FloatControl.Type.MASTER_GAIN ) )
{
gainControl = null;
initialGain = 0;
}
else
{
// Create a new gain control:
gainControl = (FloatControl) sourceDataLine.getControl(
FloatControl.Type.MASTER_GAIN );
// Store it's initial gain to use as "maximum volume" later:
initialGain = gainControl.getValue();
}
}
catch( IllegalArgumentException iae )
{
gainControl = null;
initialGain = 0;
}
// Check if changing the sample rate is supported:
try
{
if( !sourceDataLine.isControlSupported(
FloatControl.Type.SAMPLE_RATE ) )
{
sampleRateControl = null;
initialSampleRate = 0;
}
else
{
// Create a new sample rate control:
sampleRateControl = (FloatControl) sourceDataLine
.getControl( FloatControl.Type.SAMPLE_RATE );
// Store it's initial value to use later:
initialSampleRate = sampleRateControl.getValue();
}
}
catch( IllegalArgumentException iae )
{
sampleRateControl = null;
initialSampleRate = 0;
}
break;
default:
errorMessage( "Unrecognized channel type in method " +
"'resetControls'" );
panControl = null;
gainControl = null;
sampleRateControl = null;
break;
}
}
/**
* Defines whether playback should loop or just play once.
* @param value Loop or not.
*/
public void setLooping( boolean value )
{
toLoop = value;
}
/**
* Changes the pan between left and right speaker to the specified value.
* -1 = left speaker only. 0 = middle, both speakers. 1 = right speaker only.
* @param p Pan value to use.
*/
public void setPan( float p )
{
// Make sure there is a pan control
if( panControl == null )
return;
float pan = p;
// make sure the value is valid (between -1 and 1)
if( pan < -1.0f )
pan = -1.0f;
if( pan > 1.0f )
pan = 1.0f;
// Update the pan:
panControl.setValue( pan );
}
/**
* Changes the volume.
* 0 = no volume. 1 = maximum volume (initial gain)
* @param g Gain value to use.
*/
public void setGain( float g )
{
// Make sure there is a gain control
if( gainControl == null )
return;
// make sure the value is valid (between 0 and 1)
float gain = g;
if( gain < 0.0f )
gain = 0.0f;
if( gain > 1.0f )
gain = 1.0f;
double minimumDB = gainControl.getMinimum();
double maximumDB = initialGain;
// convert the supplied linear gain into a "decible change" value
// minimumDB is no volume
// maximumDB is maximum volume
// (Number of decibles is a logrithmic function of linear gain)
double ampGainDB = ( (10.0f / 20.0f) * maximumDB ) - minimumDB;
double cste = Math.log(10.0) / 20;
float valueDB = (float) ( minimumDB + (1 / cste) * Math.log( 1 +
(Math.exp(cste * ampGainDB) - 1)
* gain ) );
// Update the gain:
gainControl.setValue( valueDB );
}
/**
* Changes the pitch to the specified value.
* @param p Float value between 0.5f and 2.0f.
*/
public void setPitch( float p )
{
// Make sure there is a pan control
if( sampleRateControl == null )
{
return;
}
float sampleRate = p;
// make sure the value is valid (between 0.5f and 2.0f)
if( sampleRate < 0.5f )
sampleRate = 0.5f;
if( sampleRate > 2.0f )
sampleRate = 2.0f;
sampleRate = sampleRate * initialSampleRate;
// Update the pan:
sampleRateControl.setValue( sampleRate );
}
/**
* Queues up the initial byte[] buffers of data to be streamed.
* @param bufferList List of the first buffers to be played for a streaming source.
* @return False if problem occurred or end of stream was reached.
*/
@Override
public boolean preLoadBuffers( LinkedList<byte[]> bufferList )
{
// Stream buffers can only be queued for streaming sources:
if( errorCheck( channelType != SoundSystemConfig.TYPE_STREAMING,
"Buffers may only be queued for streaming sources." ) )
return false;
// Make sure we have a SourceDataLine:
if( errorCheck( sourceDataLine == null,
"SourceDataLine null in method 'preLoadBuffers'." ) )
return false;
sourceDataLine.start();
if( bufferList.isEmpty() )
return true;
// preload one stream buffer worth of data:
byte[] preLoad = bufferList.remove( 0 );
// Make sure we have some data:
if( errorCheck( preLoad == null,
"Missing sound-bytes in method 'preLoadBuffers'." ) )
return false;
// If we are using more than one stream buffer, pre-load the
// remaining ones now:
while( !bufferList.isEmpty() )
{
streamBuffers.add( new SoundBuffer( bufferList.remove( 0 ),
myFormat ) );
}
// Pre-load the first stream buffer into the dataline:
sourceDataLine.write( preLoad, 0, preLoad.length );
processed = 0;
return true;
}
/**
* Queues up a byte[] buffer of data to be streamed.
* @param buffer The next buffer to be played for a streaming source.
* @return False if an error occurred or if the channel is shutting down.
*/
@Override
public boolean queueBuffer( byte[] buffer )
{
// Stream buffers can only be queued for streaming sources:
if( errorCheck( channelType != SoundSystemConfig.TYPE_STREAMING,
"Buffers may only be queued for streaming sources." ) )
return false;
// Make sure we have a SourceDataLine:
if( errorCheck( sourceDataLine == null,
"SourceDataLine null in method 'queueBuffer'." ) )
return false;
// make sure a format was specified:
if( errorCheck( myFormat == null,
"AudioFormat null in method 'queueBuffer'" ) )
return false;
// Queue a new buffer:
streamBuffers.add( new SoundBuffer( buffer, myFormat ) );
// Dequeue a buffer and process it:
processBuffer();
processed = 0;
return true;
}
/**
* Plays the next queued byte[] buffer. This method is run from the seperate
* {@link paulscode.sound.StreamThread StreamThread}.
* @return False when no more buffers are left to process.
*/
@Override
public boolean processBuffer()
{
// Stream buffers can only be queued for streaming sources:
if( errorCheck( channelType != SoundSystemConfig.TYPE_STREAMING,
"Buffers are only processed for streaming sources." ) )
return false;
// Make sure we have a SourceDataLine:
if( errorCheck( sourceDataLine == null,
"SourceDataLine null in method 'processBuffer'." ) )
return false;
if( streamBuffers == null || streamBuffers.isEmpty() )
return false;
// Dequeue a buffer and feed it to the SourceDataLine:
SoundBuffer nextBuffer = streamBuffers.remove( 0 );
sourceDataLine.write( nextBuffer.audioData, 0,
nextBuffer.audioData.length );
if( !sourceDataLine.isActive() )
sourceDataLine.start();
nextBuffer.cleanup();
nextBuffer = null;
return true;
}
/**
* Feeds raw data to the stream.
* @param buffer Buffer containing raw audio data to stream.
* @return Number of prior buffers that have been processed, or -1 if error.
*/
@Override
public int feedRawAudioData( byte[] buffer )
{
// Stream buffers can only be queued for streaming sources:
if( errorCheck( channelType != SoundSystemConfig.TYPE_STREAMING,
"Raw audio data can only be processed by streaming sources." ) )
return -1;
if( errorCheck( streamBuffers == null,
"StreamBuffers queue null in method 'feedRawAudioData'." ) )
return -1;
streamBuffers.add( new SoundBuffer( buffer, myFormat ) );
return buffersProcessed();
}
/**
* Returns the number of queued byte[] buffers that have finished playing.
* @return Number of buffers processed.
*/
@Override
public int buffersProcessed()
{
processed = 0;
// Stream buffers can only be queued for streaming sources:
if( errorCheck( channelType != SoundSystemConfig.TYPE_STREAMING,
"Buffers may only be queued for streaming sources." ) )
{
if( streamBuffers != null )
streamBuffers.clear();
return 0;
}
// Make sure we have a SourceDataLine:
if( sourceDataLine == null )
{
if( streamBuffers != null )
streamBuffers.clear();
return 0;
}
if( sourceDataLine.available() > 0 )
{
processed = 1;
}
return processed;
}
/**
* Dequeues all previously queued data.
*/
@Override
public void flush()
{
// only a streaming source can be flushed:
// Only streaming sources process buffers:
if( channelType != SoundSystemConfig.TYPE_STREAMING )
return;
// Make sure we have a SourceDataLine:
if( errorCheck( sourceDataLine == null,
"SourceDataLine null in method 'flush'." ) )
return;
sourceDataLine.stop();
sourceDataLine.flush();
sourceDataLine.drain();
streamBuffers.clear();
processed = 0;
}
/**
* Stops the channel, dequeues any queued data, and closes the channel.
*/
@Override
public void close()
{
switch( channelType )
{
case SoundSystemConfig.TYPE_NORMAL:
if( clip != null )
{
clip.stop();
clip.flush();
clip.close();
}
break;
case SoundSystemConfig.TYPE_STREAMING:
if( sourceDataLine != null )
{
flush();
sourceDataLine.close();
}
break;
default:
break;
}
}
/**
* Plays the currently attached normal source, opens this channel up for
* streaming, or resumes playback if this channel was paused.
*/
@Override
public void play()
{
switch( channelType )
{
case SoundSystemConfig.TYPE_NORMAL:
if( clip != null )
{
if( toLoop )
{
clip.stop();
clip.loop( Clip.LOOP_CONTINUOUSLY );
}
else
{
clip.stop();
clip.start();
}
}
break;
case SoundSystemConfig.TYPE_STREAMING:
if( sourceDataLine != null )
{
sourceDataLine.start();
}
break;
default:
break;
}
}
/**
* Temporarily stops playback for this channel.
*/
@Override
public void pause()
{
switch( channelType )
{
case SoundSystemConfig.TYPE_NORMAL:
if( clip != null )
clip.stop();
break;
case SoundSystemConfig.TYPE_STREAMING:
if( sourceDataLine != null )
sourceDataLine.stop();
break;
default:
break;
}
}
/**
* Stops playback for this channel and rewinds the attached source to the
* beginning.
*/
@Override
public void stop()
{
switch( channelType )
{
case SoundSystemConfig.TYPE_NORMAL:
if( clip != null )
{
clip.stop();
clip.setFramePosition(0);
}
break;
case SoundSystemConfig.TYPE_STREAMING:
if( sourceDataLine != null )
sourceDataLine.stop();
break;
default:
break;
}
}
/**
* Rewinds the attached source to the beginning. Stops the source if it was
* paused.
*/
@Override
public void rewind()
{
switch( channelType )
{
case SoundSystemConfig.TYPE_NORMAL:
if( clip != null )
{
boolean rePlay = clip.isRunning();
clip.stop();
clip.setFramePosition(0);
if( rePlay )
{
if( toLoop )
clip.loop( Clip.LOOP_CONTINUOUSLY );
else
clip.start();
}
}
break;
case SoundSystemConfig.TYPE_STREAMING:
// rewinding for streaming sources is handled elsewhere
break;
default:
break;
}
}
/**
* Calculates the number of milliseconds since the channel began playing.
* @return Milliseconds, or -1 if unable to calculate.
*/
@Override
public float millisecondsPlayed()
{
switch( channelType )
{
case SoundSystemConfig.TYPE_NORMAL:
if( clip == null )
return -1;
return clip.getMicrosecondPosition() / 1000f;
case SoundSystemConfig.TYPE_STREAMING:
if( sourceDataLine == null )
return -1;
return sourceDataLine.getMicrosecondPosition() / 1000f;
default:
return -1;
}
}
/**
* Used to determine if a channel is actively playing a source. This method
* will return false if the channel is paused or stopped and when no data is
* queued to be streamed.
* @return True if this channel is playing a source.
*/
@Override
public boolean playing()
{
switch( channelType )
{
case SoundSystemConfig.TYPE_NORMAL:
if( clip == null )
return false;
return clip.isActive();
case SoundSystemConfig.TYPE_STREAMING:
if( sourceDataLine == null )
return false;
return sourceDataLine.isActive();
default:
return false;
}
}
}