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; } } }