diff --git a/src/audio/AudioManager.java b/src/audio/AudioManager.java index 3a314901..f35499fb 100644 --- a/src/audio/AudioManager.java +++ b/src/audio/AudioManager.java @@ -1,5 +1,3 @@ -package audio; - /******************************************************************************* * SDR Trunk * Copyright (C) 2014-2016 Dennis Sheirer @@ -17,6 +15,8 @@ package audio; * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ +package audio; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -429,19 +429,17 @@ public class AudioManager implements Listener, IAudioController * output and maintains current state information about the audio activity * received form the source. */ - public class AudioOutputConnection implements Listener + public class AudioOutputConnection { private static final int DISCONNECTED = -1; private AudioOutput mAudioOutput; private int mPriority = 0; - private long mLastActivity = System.currentTimeMillis(); private int mSource = DISCONNECTED; public AudioOutputConnection( AudioOutput audioOutput ) { mAudioOutput = audioOutput; - mAudioOutput.addAudioEventListener( this ); } public void receive( AudioPacket packet ) @@ -457,8 +455,6 @@ public class AudioManager implements Listener, IAudioController updateLowestPriorityAssignment(); } - updateTimestamp(); - if( mAudioOutput != null ) { mAudioOutput.receive( packet ); @@ -481,11 +477,6 @@ public class AudioManager implements Listener, IAudioController } } - private void updateTimestamp() - { - mLastActivity = System.currentTimeMillis(); - } - /** * Terminates the audio output and prepares this connection for disposal */ @@ -518,7 +509,8 @@ public class AudioManager implements Listener, IAudioController public void connect( int source ) { mSource = source; - updateTimestamp(); + + mAudioOutput.updateTimestamp(); } /** @@ -536,6 +528,7 @@ public class AudioManager implements Listener, IAudioController public void disconnect() { mSource = DISCONNECTED; + mPriority = 0; } /** @@ -544,28 +537,12 @@ public class AudioManager implements Listener, IAudioController */ public boolean isInactive() { - return mLastActivity != Long.MAX_VALUE && - mLastActivity + AUDIO_TIMEOUT < System.currentTimeMillis(); + return ( mAudioOutput.getLastActivityTimestamp() + AUDIO_TIMEOUT ) < System.currentTimeMillis(); } public int getPriority() { return mPriority; } - - @Override - public void receive( AudioEvent event ) - { - switch( event.getType() ) - { - case AUDIO_STARTED: - case AUDIO_STOPPED: - case AUDIO_CONTINUATION: - updateTimestamp(); - break; - default: - break; - } - } } } diff --git a/src/audio/AudioPacket.java b/src/audio/AudioPacket.java index c14754e1..12bbf22b 100644 --- a/src/audio/AudioPacket.java +++ b/src/audio/AudioPacket.java @@ -64,6 +64,11 @@ public class AudioPacket return mAudioData; } + public boolean hasAudioBuffer() + { + return mAudioData != null; + } + public enum Type { AUDIO, diff --git a/src/audio/output/AudioOutput.java b/src/audio/output/AudioOutput.java index 74b2e822..caa9396f 100644 --- a/src/audio/output/AudioOutput.java +++ b/src/audio/output/AudioOutput.java @@ -1,34 +1,148 @@ package audio.output; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.BooleanControl; +import javax.sound.sampled.Control; import javax.sound.sampled.FloatControl; +import javax.sound.sampled.Line; +import javax.sound.sampled.LineEvent; import javax.sound.sampled.LineListener; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.Mixer; +import javax.sound.sampled.SourceDataLine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Broadcaster; import sample.Listener; +import source.mixer.MixerChannel; import audio.AudioEvent; import audio.AudioPacket; +import audio.AudioPacket.Type; import audio.metadata.AudioMetadata; import controller.ThreadPoolManager; +import controller.ThreadPoolManager.ThreadType; public abstract class AudioOutput implements Listener, LineListener { private final static Logger mLog = LoggerFactory.getLogger( AudioOutput.class ); - private Listener mAudioMetadataListener; - protected Broadcaster mAudioEventBroadcaster = new Broadcaster<>(); - protected LinkedTransferQueue mBuffer = new LinkedTransferQueue<>(); - protected ThreadPoolManager mThreadPoolManager; - protected boolean mCanProcessAudio; - protected long mLastActivity; + private LinkedTransferQueue mBuffer = new LinkedTransferQueue<>(); + private int mBufferStartThreshold; + private int mBufferStopThreshold; - public AudioOutput( ThreadPoolManager threadPoolManager ) + private Listener mAudioMetadataListener; + private Broadcaster mAudioEventBroadcaster = new Broadcaster<>(); + + private ThreadPoolManager mThreadPoolManager; + private ScheduledFuture mProcessorTask; + + private SourceDataLine mOutput; + private Mixer mMixer; + private MixerChannel mMixerChannel; + private FloatControl mGainControl; + private BooleanControl mMuteControl; + + private AudioEvent mAudioStartEvent; + private AudioEvent mAudioStopEvent; + + private boolean mCanProcessAudio = false; + private long mLastActivity = System.currentTimeMillis(); + + /** + * Single audio channel playback with automatic starting and stopping of the + * underlying sourcedataline specified by the mixer and mixer channel + * arguments. + * + * Maintains an internal non-blocking audio packet queue and processes this + * queue 25 times a second (every 40 ms). + * + * @param threadPoolManager for assigning buffer processing schedule task + * @param mixer to obtain source data line + * @param mixerChannel either mono or left/right stereo + * @param audioFormat to use during playback + * @param lineInfo to use when obtaining the source data line + * @param requestedBufferSize of approximately 1 second of audio + */ + public AudioOutput( ThreadPoolManager threadPoolManager, + Mixer mixer, + MixerChannel mixerChannel, + AudioFormat audioFormat, + Line.Info lineInfo, + int requestedBufferSize ) { mThreadPoolManager = threadPoolManager; + mMixer = mixer; + mMixerChannel = mixerChannel; + + try + { + mOutput = (SourceDataLine)mMixer.getLine( lineInfo ); + + if( mOutput != null ) + { + mOutput.open( audioFormat, requestedBufferSize ); + + //Start threshold: buffer is full with 10% or less of capacity remaining + mBufferStartThreshold = (int)( mOutput.getBufferSize() * 0.10 ); + + //Stop threshold: buffer is empty with 90% or more capacity available + mBufferStopThreshold = (int)( mOutput.getBufferSize() * 0.90 ); + + mOutput.addLineListener( this ); + + if( mOutput != null ) + { + try + { + Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN ); + mGainControl = (FloatControl)gain; + } + catch( IllegalArgumentException iae ) + { + mLog.warn( "Couldn't obtain MASTER GAIN control for stereo line [" + + mixer.getMixerInfo().getName() + " | " + getChannelName() + "]" ); + } + + try + { + Control mute = mOutput.getControl( BooleanControl.Type.MUTE ); + mMuteControl = (BooleanControl)mute; + } + catch( IllegalArgumentException iae ) + { + mLog.warn( "Couldn't obtain MUTE control for stereo line [" + + mixer.getMixerInfo().getName() + " | " + getChannelName() + "]" ); + } + + /* Run the queue processor task every 40 milliseconds or 25 times a second */ + mProcessorTask = mThreadPoolManager.scheduleFixedRate( + ThreadType.AUDIO_PROCESSING, new BufferProcessor(), + 40, TimeUnit.MILLISECONDS ); + } + + mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED, + getChannelName() ); + mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED, + getChannelName() ); + + mCanProcessAudio = true; + } + } + catch ( LineUnavailableException e ) + { + mLog.error( "Couldn't obtain audio source data line for " + + "audio output - mixer [" + mMixer.getMixerInfo().getName() + "]" ); + } } public void reset() @@ -41,36 +155,52 @@ public abstract class AudioOutput implements Listener, LineListener { mCanProcessAudio = false; + if( mProcessorTask != null && mThreadPoolManager != null ) + { + mThreadPoolManager.cancel( mProcessorTask ); + } + + mThreadPoolManager = null; + mProcessorTask = null; + mBuffer.clear(); mAudioEventBroadcaster.dispose(); mAudioEventBroadcaster = null; mAudioMetadataListener = null; - mThreadPoolManager = null; - } - - public abstract String getChannelName(); - - public abstract void setMuted( boolean muted ); - public abstract boolean isMuted(); - - public abstract FloatControl getGainControl(); - public abstract boolean hasGainControl(); - - protected void broadcast( AudioMetadata metadata ) - { - if( mAudioMetadataListener != null ) + if( mOutput != null ) { - mAudioMetadataListener.receive( metadata ); + mOutput.close(); } + + mOutput = null; + mGainControl = null; + mMuteControl = null; } - - protected void broadcast( AudioEvent audioEvent ) + + /** + * Converts the audio packet data into a byte buffer format appropriate for + * the underlying source data line. + */ + protected abstract ByteBuffer convert( AudioPacket packet ); + + /** + * Audio output channel name + */ + public String getChannelName() { - mAudioEventBroadcaster.broadcast( audioEvent ); + return mMixerChannel.getLabel(); } + /** + * Mixer Channel for this audio output + */ + protected MixerChannel getMixerChannel() + { + return mMixerChannel; + } + /** * Registers a single listener to receive audio start and audio stop events */ @@ -84,6 +214,14 @@ public abstract class AudioOutput implements Listener, LineListener mAudioEventBroadcaster.removeListener( listener ); } + /** + * Broadcasts an audio event to the registered listener + */ + private void broadcast( AudioEvent audioEvent ) + { + mAudioEventBroadcaster.broadcast( audioEvent ); + } + /** * Registers a single listener to receive the audio metadata from each * audio packet @@ -99,21 +237,200 @@ public abstract class AudioOutput implements Listener, LineListener } /** - * Timestamp when most recent buffer was received + * Broadcasts audio metadata to the registered listener */ - public long getLastBufferReceived() + private void broadcast( AudioMetadata metadata ) + { + if( mAudioMetadataListener != null ) + { + mAudioMetadataListener.receive( metadata ); + } + } + + /** + * Timestamp of either last buffer received or last buffer processed + */ + public long getLastActivityTimestamp() { return mLastActivity; } + /** + * Updates the last activity timestamp to current system time + */ + public void updateTimestamp() + { + mLastActivity = System.currentTimeMillis(); + } + @Override public void receive( AudioPacket packet ) { if( mCanProcessAudio ) { - mLastActivity = System.currentTimeMillis(); + //Update the activity timestamp so that this audio output doesn't + //get disconnected before it starts processing the audio stream + updateTimestamp(); mBuffer.add( packet ); } } + + public class BufferProcessor implements Runnable + { + private AtomicBoolean mProcessing = new AtomicBoolean(); + + public BufferProcessor() + { + } + + @Override + public void run() + { + try + { + /* The processing flag ensures that only one instance of the + * processor can run at any given time */ + if( mProcessing.compareAndSet( false, true ) ) + { + List packets = new ArrayList(); + + mBuffer.drainTo( packets ); + + for( AudioPacket packet: packets ) + { + if( packet.getType() == Type.AUDIO ) + { + broadcast( packet.getAudioMetadata() ); + + ByteBuffer buffer = convert( packet ); + + int wrote = 0; + + if( !mOutput.isRunning() ) + { + int toWrite = mOutput.available(); + + if( toWrite > buffer.array().length ) + { + toWrite = buffer.array().length; + } + + //Top off the buffer and check if we can start it + wrote += mOutput.write( buffer.array(), 0, toWrite ); + + checkStart(); + } + + if( mOutput.isRunning() && wrote < buffer.array().length ) + { + //Blocking write + wrote += mOutput.write( buffer.array(), wrote, + buffer.array().length - wrote ); + } + + updateTimestamp(); + } + } + + checkStop(); + + mProcessing.set( false ); + } + } + catch( Exception e ) + { + mLog.error( "Error while processing audio buffers", e ); + } + } + + /** + * Starts audio playback once audio buffer is almost full and remaining + * capacity falls below the start threshold. + */ + private void checkStart() + { + if( mCanProcessAudio && + !mOutput.isRunning() && + mOutput.available() <= mBufferStartThreshold ) + { + mOutput.start(); + } + } + + /** + * Stops audio playback and drains the audio buffer to empty when the + * audio buffer is mostly empty and the available buffer capacity + * exceeds the stop threshold + */ + private void checkStop() + { + if( mCanProcessAudio && + mOutput.isRunning() && + mOutput.available() >= mBufferStopThreshold ) + { + mOutput.drain(); + mOutput.stop(); + } + } + } + + /** + * Sets the mute state for this audio output channel + */ + public void setMuted( boolean muted ) + { + if( mMuteControl != null ) + { + mMuteControl.setValue( muted ); + + broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED : + AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) ); + } + } + + /** + * Current mute state for this audio output channel + */ + public boolean isMuted() + { + if( mMuteControl != null ) + { + return mMuteControl.getValue(); + } + + return false; + } + + /** + * Gain/volume control for this audio output channel, if one is available. + */ + public FloatControl getGainControl() + { + return mGainControl; + } + + public boolean hasGainControl() + { + return mGainControl != null; + } + + /** + * Monitors the source data line playback state and broadcasts audio events + * to the registered listener as the state changes + */ + @Override + public void update( LineEvent event ) + { + LineEvent.Type type = event.getType(); + + if( type == LineEvent.Type.START ) + { + mAudioEventBroadcaster.broadcast( mAudioStartEvent ); + } + else if( type == LineEvent.Type.STOP ) + { + mAudioEventBroadcaster.broadcast( mAudioStopEvent ); + } + } } diff --git a/src/audio/output/MonoAudioOutput.java b/src/audio/output/MonoAudioOutput.java index ec1f7816..33ecfe92 100644 --- a/src/audio/output/MonoAudioOutput.java +++ b/src/audio/output/MonoAudioOutput.java @@ -3,280 +3,55 @@ package audio.output; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.ShortBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import javax.sound.sampled.BooleanControl; -import javax.sound.sampled.Control; -import javax.sound.sampled.FloatControl; -import javax.sound.sampled.LineEvent; -import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.Mixer; -import javax.sound.sampled.SourceDataLine; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import source.mixer.MixerChannel; -import audio.AudioEvent; import audio.AudioFormats; import audio.AudioPacket; -import audio.AudioPacket.Type; import controller.ThreadPoolManager; -import controller.ThreadPoolManager.ThreadType; /** - * Mono Audio output with automatic flow control based on the availability of - * audio data packets. + * Mono Audio output implementation */ public class MonoAudioOutput extends AudioOutput { - private final static Logger mLog = LoggerFactory.getLogger( MonoAudioOutput.class ); - - /* Output line buffer size is set at one half second of audio at 48 kHz and - * two bytes per sample */ private final static int BUFFER_SIZE = 48000; - /* The queue processor will run every 20 milliseconds checking for inbound - * audio and automatically starting or stopping audio playback. We set the - * data line available space threshold to start when we're 20 ms away from - * being full, and the stop threshold when 20 ms away from being empty */ - private final static int SAMPLE_SIZE_40_MS = BUFFER_SIZE / 25; - private final static int START_THRESHOLD = SAMPLE_SIZE_40_MS; - private final static int STOP_THRESHOLD = BUFFER_SIZE - SAMPLE_SIZE_40_MS; - - private SourceDataLine mOutput; - private FloatControl mGainControl; - private BooleanControl mMuteControl; - private AudioEvent mAudioStartEvent; - private AudioEvent mAudioStopEvent; - private AudioEvent mAudioContinuationEvent; - private ScheduledFuture mProcessorTask; - public MonoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer ) { - super( threadPoolManager ); + super( threadPoolManager, + mixer, + MixerChannel.MONO, + AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, + AudioFormats.MONO_SOURCE_DATALINE_INFO, + BUFFER_SIZE ); + } - mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED, - getChannelName() ); - mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED, - getChannelName() ); - mAudioContinuationEvent = new AudioEvent( AudioEvent.Type.AUDIO_CONTINUATION, - getChannelName() ); - - try + /** + * Converts the audio packet data into mono audio frames. + */ + protected ByteBuffer convert( AudioPacket packet ) + { + if( packet.hasAudioBuffer() ) { - mOutput = (SourceDataLine)mixer.getLine( - AudioFormats.MONO_SOURCE_DATALINE_INFO ); + float[] samples = packet.getAudioBuffer().getSamples(); - if( mOutput != null ) + /* Little-endian byte buffer */ + ByteBuffer buffer = + ByteBuffer.allocate( samples.length * 2 ) + .order( ByteOrder.LITTLE_ENDIAN ); + + ShortBuffer shortBuffer = buffer.asShortBuffer(); + + for( float sample: samples ) { - mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE ); - mOutput.addLineListener( this ); - mCanProcessAudio = true; - } - } - catch ( LineUnavailableException e ) - { - mLog.error( "Couldn't obtain source data line for 48kHz PCM mono " - + "audio output" ); - } - - if( mOutput != null ) - { - try - { - Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN ); - mGainControl = (FloatControl)gain; - } - catch( IllegalArgumentException iae ) - { - mLog.warn( "Couldn't obtain MASTER GAIN control for mono line [" + - mixer.getMixerInfo().getName() + "]" ); + shortBuffer.put( (short)( sample * Short.MAX_VALUE ) ); } - try - { - Control mute = mOutput.getControl( BooleanControl.Type.MUTE ); - mMuteControl = (BooleanControl)mute; - } - catch( IllegalArgumentException iae ) - { - mLog.warn( "Couldn't obtain MUTE control for mono line [" + - mixer.getMixerInfo().getName() + "]" ); - } - - /* Run the queue processor task every 40 milliseconds */ - mProcessorTask = mThreadPoolManager.scheduleFixedRate( - ThreadType.AUDIO_PROCESSING, new QueueProcessor(), - 40, TimeUnit.MILLISECONDS ); - } - } - - @Override - public String getChannelName() - { - return MixerChannel.MONO.getLabel(); - } - - public void dispose() - { - if( mProcessorTask != null && mThreadPoolManager != null ) - { - mThreadPoolManager.cancel( mProcessorTask ); + return buffer; } - mProcessorTask = null; - - super.dispose(); - - if( mOutput != null ) - { - mOutput.close(); - } - - mOutput = null; - mGainControl = null; - mMuteControl = null; - } - - public class QueueProcessor implements Runnable - { - private AtomicBoolean mProcessing = new AtomicBoolean(); - - @Override - public void run() - { - try - { - /* The mProcessing flag ensures that only one instance of the - * processor can run at any given time */ - if( mProcessing.compareAndSet( false, true ) ) - { - List packets = new ArrayList(); - - mBuffer.drainTo( packets ); - - if( packets.size() > 0 ) - { - for( AudioPacket packet: packets ) - { - broadcast( mAudioContinuationEvent ); - - if( mCanProcessAudio ) - { - if( packet.getType() == Type.AUDIO ) - { - float[] samples = packet.getAudioBuffer().getSamples(); - - /* Little-endian byte buffer */ - ByteBuffer buffer = - ByteBuffer.allocate( samples.length * 2 ) - .order( ByteOrder.LITTLE_ENDIAN ); - - ShortBuffer shortBuffer = buffer.asShortBuffer(); - - for( float sample: samples ) - { - shortBuffer.put( (short)( sample * Short.MAX_VALUE ) ); - } - - //This is a blocking write - mOutput.write( buffer.array(), 0, buffer.array().length ); - - checkStart(); - - broadcast( packet.getAudioMetadata() ); - - mLastActivity = System.currentTimeMillis(); - } - } - } - } - else - { - checkStop(); - } - - mProcessing.set( false ); - } - } - catch( Exception e ) - { - mLog.error( "Error while processing audio buffers", e ); - } - } - - private void checkStart() - { - if( mCanProcessAudio && - !mOutput.isRunning() && mOutput.available() <= START_THRESHOLD ) - { - mOutput.start(); - } - } - - private void checkStop() - { - if( mCanProcessAudio && mOutput.isRunning() && - mOutput.available() >= STOP_THRESHOLD ) - { - mOutput.drain(); - mOutput.stop(); - } - } - } - - @Override - public void setMuted( boolean muted ) - { - if( mMuteControl != null ) - { - mMuteControl.setValue( muted ); - - broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED : - AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) ); - } - } - - @Override - public boolean isMuted() - { - if( mMuteControl != null ) - { - return mMuteControl.getValue(); - } - - return false; - } - - @Override - public FloatControl getGainControl() - { - return mGainControl; - } - - @Override - public boolean hasGainControl() - { - return mGainControl != null; - } - - @Override - public void update( LineEvent event ) - { - LineEvent.Type type = event.getType(); - - if( type == LineEvent.Type.START ) - { - mAudioEventBroadcaster.broadcast( mAudioStartEvent ); - } - else if( type == LineEvent.Type.STOP ) - { - mAudioEventBroadcaster.broadcast( mAudioStopEvent ); - } + return null; } } diff --git a/src/audio/output/StereoAudioOutput.java b/src/audio/output/StereoAudioOutput.java index 1c3d1683..1ef7ae69 100644 --- a/src/audio/output/StereoAudioOutput.java +++ b/src/audio/output/StereoAudioOutput.java @@ -3,310 +3,69 @@ package audio.output; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.ShortBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import javax.sound.sampled.BooleanControl; -import javax.sound.sampled.Control; -import javax.sound.sampled.FloatControl; -import javax.sound.sampled.LineEvent; -import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.Mixer; -import javax.sound.sampled.SourceDataLine; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import source.mixer.MixerChannel; -import audio.AudioEvent; import audio.AudioFormats; import audio.AudioPacket; -import audio.AudioPacket.Type; import controller.ThreadPoolManager; -import controller.ThreadPoolManager.ThreadType; /** - * Mono Audio output in stereo format with automatic flow control based on the - * availability of audio data packets. + * Stereo audio output implementation. */ public class StereoAudioOutput extends AudioOutput { - private final static Logger mLog = LoggerFactory.getLogger( StereoAudioOutput.class ); - - /* Output line buffer size is set at one half second of audio at 48 kHz and - * two bytes per sample for two channels */ private final static int BUFFER_SIZE = 96000; - /* The queue processor will run every 20 milliseconds checking for inbound - * audio and automatically start or stop audio playback. We set the - * data line available space threshold to start when we're 20 ms away from - * being full and the stop threshold when 20 ms away from being empty */ - private final static int SAMPLE_SIZE_40_MS = BUFFER_SIZE / 25; - private final static int START_THRESHOLD = SAMPLE_SIZE_40_MS; - private final static int STOP_THRESHOLD = BUFFER_SIZE - SAMPLE_SIZE_40_MS; - - private SourceDataLine mOutput; - private FloatControl mGainControl; - private BooleanControl mMuteControl; - private AudioEvent mAudioStartEvent; - private AudioEvent mAudioStopEvent; - private AudioEvent mAudioContinuationEvent; - private MixerChannel mMixerChannel; - private ScheduledFuture mProcessorTask; - public StereoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer, MixerChannel channel ) { - super( threadPoolManager ); - - assert( channel == MixerChannel.LEFT || channel == MixerChannel.RIGHT ); - - mMixerChannel = channel; + super( threadPoolManager, + mixer, + channel, + AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, + AudioFormats.STEREO_SOURCE_DATALINE_INFO, + BUFFER_SIZE ); + } - mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED, - getChannelName() ); - mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED, - getChannelName() ); - mAudioContinuationEvent = new AudioEvent( AudioEvent.Type.AUDIO_CONTINUATION, - getChannelName() ); - - try + /** + * Converts the audio packet data into stereo audio frames with the mixer + * channel containing the audio and the other channel containing zero + * valued (silent) samples. + */ + protected ByteBuffer convert( AudioPacket packet ) + { + if( packet.hasAudioBuffer() ) { - mOutput = (SourceDataLine)mixer.getLine( - AudioFormats.STEREO_SOURCE_DATALINE_INFO ); + float[] samples = packet.getAudioBuffer().getSamples(); - if( mOutput != null ) - { - mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, BUFFER_SIZE ); - mOutput.addLineListener( this ); - mCanProcessAudio = true; - } - } - catch ( LineUnavailableException e ) - { - mLog.error( "Couldn't obtain source data line for 48kHz PCM stereo " - + "audio output - mixer [" + mixer.getMixerInfo().getName() + - "] channel [" + mMixerChannel.name() + "]" ); - } - - if( mOutput != null ) - { - try - { - Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN ); - mGainControl = (FloatControl)gain; - } - catch( IllegalArgumentException iae ) - { - mLog.warn( "Couldn't obtain MASTER GAIN control for stereo line [" + - mixer.getMixerInfo().getName() + " | " + channel.name() + "]" ); - } + /* Little-endian byte buffer */ + ByteBuffer buffer = ByteBuffer.allocate( samples.length * 4 ) + .order( ByteOrder.LITTLE_ENDIAN ); - try - { - Control mute = mOutput.getControl( BooleanControl.Type.MUTE ); - mMuteControl = (BooleanControl)mute; - } - catch( IllegalArgumentException iae ) - { - mLog.warn( "Couldn't obtain MUTE control for stereo line [" + - mixer.getMixerInfo().getName() + " | " + channel.name() + "]" ); - } - - /* Run the queue processor task every 40 milliseconds */ - mProcessorTask = mThreadPoolManager.scheduleFixedRate( - ThreadType.AUDIO_PROCESSING, new QueueProcessor(), - 40, TimeUnit.MILLISECONDS ); - } - } + ShortBuffer shortBuffer = buffer.asShortBuffer(); - @Override - public String getChannelName() - { - return mMixerChannel.getLabel(); - } - - public void dispose() - { - if( mProcessorTask != null && mThreadPoolManager != null ) - { - mThreadPoolManager.cancel( mProcessorTask ); - } - - mProcessorTask = null; - - super.dispose(); - - if( mOutput != null ) - { - mOutput.close(); - } - - mOutput = null; - mGainControl = null; - mMuteControl = null; - } - - private ByteBuffer convert( float[] samples ) - { - /* Little-endian byte buffer */ - ByteBuffer buffer = ByteBuffer.allocate( samples.length * 4 ) - .order( ByteOrder.LITTLE_ENDIAN ); - - ShortBuffer shortBuffer = buffer.asShortBuffer(); - - if( mMixerChannel == MixerChannel.LEFT ) - { - for( float sample: samples ) + if( getMixerChannel() == MixerChannel.LEFT ) { - shortBuffer.put( (short)( sample * Short.MAX_VALUE ) ); - shortBuffer.put( (short)0 ); - } - } - else - { - for( float sample: samples ) - { - shortBuffer.put( (short)0 ); - shortBuffer.put( (short)( sample * Short.MAX_VALUE ) ); - } - } - - return buffer; - } - - public class QueueProcessor implements Runnable - { - private AtomicBoolean mProcessing = new AtomicBoolean(); - - public QueueProcessor() - { - } - - @Override - public void run() - { - try - { - /* The mProcessing flag ensures that only one instance of the - * processor can run at any given time */ - if( mProcessing.compareAndSet( false, true ) ) + for( float sample: samples ) { - List packets = new ArrayList(); - - mBuffer.drainTo( packets ); - - if( packets.size() > 0 ) - { - - for( AudioPacket packet: packets ) - { - broadcast( mAudioContinuationEvent ); - - if( mCanProcessAudio ) - { - if( packet.getType() == Type.AUDIO ) - { - ByteBuffer buffer = convert( - packet.getAudioBuffer().getSamples() ); - - mOutput.write( buffer.array(), 0, - buffer.array().length ); - - checkStart(); - - broadcast( packet.getAudioMetadata() ); - - mLastActivity = System.currentTimeMillis(); - } - } - } - } - else - { - checkStop(); - } - - mProcessing.set( false ); + shortBuffer.put( (short)( sample * Short.MAX_VALUE ) ); + shortBuffer.put( (short)0 ); } } - catch( Exception e ) + else { - mLog.error( "Error while processing audio buffers", e ); + for( float sample: samples ) + { + shortBuffer.put( (short)0 ); + shortBuffer.put( (short)( sample * Short.MAX_VALUE ) ); + } } - } - - private void checkStart() - { - if( mCanProcessAudio && - !mOutput.isRunning() && mOutput.available() <= START_THRESHOLD ) - { - mOutput.start(); - } - } - - private void checkStop() - { - if( mCanProcessAudio && mOutput.isRunning() && - mOutput.available() >= STOP_THRESHOLD ) - { - mOutput.drain(); - mOutput.stop(); - } - } - } - - @Override - public void setMuted( boolean muted ) - { - if( mMuteControl != null ) - { - mMuteControl.setValue( muted ); - broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED : - AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) ); - } - } - - @Override - public boolean isMuted() - { - if( mMuteControl != null ) - { - return mMuteControl.getValue(); + return buffer; } - return false; - } - - @Override - public FloatControl getGainControl() - { - return mGainControl; - } - - @Override - public boolean hasGainControl() - { - return mGainControl != null; - } - - - @Override - public void update( LineEvent event ) - { - LineEvent.Type type = event.getType(); - - if( type == LineEvent.Type.START ) - { - mAudioEventBroadcaster.broadcast( mAudioStartEvent ); - } - else if( type == LineEvent.Type.STOP ) - { - mAudioEventBroadcaster.broadcast( mAudioStopEvent ); - } + return null; } }