Resolves issue with audio playback where audio playback stops working.

Potential existed for the audio output class to become unsynchronized
with the underlying source data line and attempt to perform a blocking
write to the data line when the data line doesn't have the capacity to
accept the full buffer, and the data line is not started.  Thus,
playback could potentially stay in a persitent blocked state.

Refactored Mono and Stereo audio output implementations to push the
audio buffer queue processing into the parent abstract class.

AudioManager was updated to remove the last time active timestamp out 
of the audio manager and down into the audio output to remove duplicate
timestamp accounting.
This commit is contained in:
denny
2016-02-20 08:53:09 -05:00
parent 894ebeb040
commit a7da60cd27
5 changed files with 416 additions and 583 deletions

View File

@@ -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 <http://www.gnu.org/licenses/>
******************************************************************************/
package audio;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -429,19 +429,17 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
* output and maintains current state information about the audio activity
* received form the source.
*/
public class AudioOutputConnection implements Listener<AudioEvent>
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<AudioPacket>, IAudioController
updateLowestPriorityAssignment();
}
updateTimestamp();
if( mAudioOutput != null )
{
mAudioOutput.receive( packet );
@@ -481,11 +477,6 @@ public class AudioManager implements Listener<AudioPacket>, 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<AudioPacket>, IAudioController
public void connect( int source )
{
mSource = source;
updateTimestamp();
mAudioOutput.updateTimestamp();
}
/**
@@ -536,6 +528,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
public void disconnect()
{
mSource = DISCONNECTED;
mPriority = 0;
}
/**
@@ -544,28 +537,12 @@ public class AudioManager implements Listener<AudioPacket>, 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;
}
}
}
}

View File

@@ -64,6 +64,11 @@ public class AudioPacket
return mAudioData;
}
public boolean hasAudioBuffer()
{
return mAudioData != null;
}
public enum Type
{
AUDIO,

View File

@@ -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<AudioPacket>, LineListener
{
private final static Logger mLog = LoggerFactory.getLogger( AudioOutput.class );
private Listener<AudioMetadata> mAudioMetadataListener;
protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
protected LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
protected ThreadPoolManager mThreadPoolManager;
protected boolean mCanProcessAudio;
protected long mLastActivity;
private LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
private int mBufferStartThreshold;
private int mBufferStopThreshold;
public AudioOutput( ThreadPoolManager threadPoolManager )
private Listener<AudioMetadata> mAudioMetadataListener;
private Broadcaster<AudioEvent> 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<AudioPacket>, 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<AudioPacket>, 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<AudioPacket>, 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<AudioPacket> packets = new ArrayList<AudioPacket>();
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 );
}
}
}

View File

@@ -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<AudioPacket> packets = new ArrayList<AudioPacket>();
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;
}
}

View File

@@ -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<AudioPacket> packets = new ArrayList<AudioPacket>();
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;
}
}