Merges master 894ebeb - patch to audio playback where audio output

channels were locking up after a period of time
This commit is contained in:
denny
2016-02-20 17:54:35 -05:00
parent 7da541ed06
commit 7ff9e9a7ce
5 changed files with 465 additions and 581 deletions

View File

@@ -430,19 +430,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 )
@@ -458,8 +456,6 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
updateLowestPriorityAssignment();
}
updateTimestamp();
if( mAudioOutput != null )
{
mAudioOutput.receive( packet );
@@ -482,11 +478,6 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
}
}
private void updateTimestamp()
{
mLastActivity = System.currentTimeMillis();
}
/**
* Terminates the audio output and prepares this connection for disposal
*/
@@ -519,7 +510,8 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
public void connect( int source )
{
mSource = source;
updateTimestamp();
mAudioOutput.updateTimestamp();
}
/**
@@ -537,6 +529,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
public void disconnect()
{
mSource = DISCONNECTED;
mPriority = 0;
}
/**
@@ -545,28 +538,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,165 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* 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.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 +172,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 +231,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 +254,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

@@ -1,282 +1,74 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* 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.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

@@ -1,312 +1,88 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* 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.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;
/**
* Stereo 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;
}
}