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