Resolves issue #23.

Adds audio output display and management options supporting mono and 
stereo output options.

Provides audio output supporting prioritization by talkgroup and/or 
channel selection.

Supports muting all channels, or muting individual channels and
individual audio gain/volume controls.
This commit is contained in:
denny
2015-10-03 17:50:03 -04:00
parent 20f5d06104
commit 3e05a9eb03
27 changed files with 1174 additions and 458 deletions

View File

@@ -1,5 +1,7 @@
package audio.output;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.BooleanControl;
@@ -11,6 +13,7 @@ import org.slf4j.LoggerFactory;
import sample.Broadcaster;
import sample.Listener;
import audio.AudioEvent;
import audio.AudioEvent.Type;
import audio.AudioPacket;
import audio.metadata.AudioMetadata;
@@ -20,10 +23,24 @@ public abstract class AudioOutput implements Listener<AudioPacket>
private AtomicBoolean mSquelched = new AtomicBoolean( false );
private Listener<AudioMetadata> mAudioMetadataListener;
protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
protected LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
protected ScheduledExecutorService mExecutorService;
protected boolean mCanProcessAudio;
protected long mLastBufferReceived;
public void dispose()
{
mCanProcessAudio = false;
mBuffer.clear();
if( mExecutorService != null )
{
mExecutorService.shutdown();
}
mAudioEventBroadcaster.dispose();
mAudioEventBroadcaster = null;
mAudioMetadataListener = null;
@@ -31,8 +48,8 @@ public abstract class AudioOutput implements Listener<AudioPacket>
public abstract String getChannelName();
public abstract BooleanControl getMuteControl();
public abstract boolean hasMuteControl();
public abstract void setMuted( boolean muted );
public abstract boolean isMuted();
public abstract FloatControl getGainControl();
public abstract boolean hasGainControl();
@@ -45,9 +62,17 @@ public abstract class AudioOutput implements Listener<AudioPacket>
}
}
protected void broadcast( AudioEvent audioEvent )
{
mAudioEventBroadcaster.broadcast( audioEvent );
}
public void setSquelched( boolean squelched )
{
mSquelched.set( squelched );
broadcast( new AudioEvent( squelched ? Type.AUDIO_SQUELCHED :
Type.AUDIO_UNSQUELCHED, getChannelName() ) );
}
public boolean isSquelched()
@@ -81,4 +106,23 @@ public abstract class AudioOutput implements Listener<AudioPacket>
{
mAudioMetadataListener = null;
}
/**
* Timestamp when most recent buffer was received
*/
public long getLastBufferReceived()
{
return mLastBufferReceived;
}
@Override
public void receive( AudioPacket packet )
{
if( mCanProcessAudio )
{
mLastBufferReceived = System.currentTimeMillis();
mBuffer.add( packet );
}
}
}

View File

@@ -6,20 +6,19 @@ import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.FloatControl;
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;
@@ -27,14 +26,14 @@ import audio.AudioPacket.Type;
import controller.NamingThreadFactory;
/**
* Audio output with automatic flow control based on the availability of
* Mono Audio output with automatic flow control based on the availability of
* audio data packets.
*/
public class MonoAudioOutput extends AudioOutput
{
private final static Logger mLog = LoggerFactory.getLogger( MonoAudioOutput.class );
/* Output line buffer size is set at one second of audio at 48 kHz and
/* 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;
@@ -42,40 +41,34 @@ public class MonoAudioOutput extends AudioOutput
* 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 = 48000 / 25;
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 LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
private ScheduledExecutorService mExecutorService;
private SourceDataLine mOutput;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private boolean mCanProcessAudio;
private long mLastBufferReceived;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
public MonoAudioOutput()
public MonoAudioOutput( Mixer mixer )
{
super();
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED, getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED, getChannelName() );
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
getChannelName() );
try
{
mOutput = AudioSystem.getSourceDataLine(
AudioFormats.PCM_SIGNED_48KHZ_16BITS );
mOutput = (SourceDataLine)mixer.getLine(
AudioFormats.MONO_SOURCE_DATALINE_INFO );
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS, BUFFER_SIZE );
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE );
mCanProcessAudio = true;
@@ -103,47 +96,18 @@ public class MonoAudioOutput extends AudioOutput
@Override
public String getChannelName()
{
return "Mono";
}
/**
* Timestamp when most recent buffer was received
*/
public long getLastBufferReceived()
{
return mLastBufferReceived;
return MixerChannel.MONO.getLabel();
}
public void dispose()
{
super.dispose();
mCanProcessAudio = false;
mBuffer.clear();
if( mExecutorService != null )
{
mExecutorService.shutdown();
}
if( mOutput != null )
{
mOutput.close();
}
}
@Override
public void receive( AudioPacket packet )
{
if( mCanProcessAudio )
{
mLastBufferReceived = System.currentTimeMillis();
mBuffer.add( packet );
}
}
public class QueueProcessor implements Runnable
{
@@ -233,17 +197,27 @@ public class MonoAudioOutput extends AudioOutput
}
}
@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 BooleanControl getMuteControl()
public boolean isMuted()
{
return mMuteControl;
}
@Override
public boolean hasMuteControl()
{
return mMuteControl != null;
if( mMuteControl != null )
{
return mMuteControl.getValue();
}
return false;
}
@Override

View File

@@ -0,0 +1,259 @@
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.Executors;
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.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.NamingThreadFactory;
/**
* Mono Audio output in stereo format with automatic flow control based on the
* availability of audio data packets.
*/
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 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 MixerChannel mMixerChannel;
public StereoAudioOutput( Mixer mixer, MixerChannel channel )
{
super();
assert( channel == MixerChannel.LEFT || channel == MixerChannel.RIGHT );
mMixerChannel = channel;
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
getChannelName() );
try
{
mOutput = (SourceDataLine)mixer.getLine(
AudioFormats.STEREO_SOURCE_DATALINE_INFO );
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, BUFFER_SIZE );
mCanProcessAudio = true;
mGainControl = (FloatControl)mOutput.getControl(
FloatControl.Type.MASTER_GAIN );
mMuteControl = (BooleanControl)mOutput.getControl(
BooleanControl.Type.MUTE );
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (stereo) output" ) );
/* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(),
0, 40, TimeUnit.MILLISECONDS );
}
}
catch ( LineUnavailableException e )
{
mLog.error( "Couldn't obtain source data line for 48kHz PCM stereo "
+ "audio output" );
}
}
@Override
public String getChannelName()
{
return mMixerChannel.getLabel();
}
public void dispose()
{
super.dispose();
if( mOutput != null )
{
mOutput.close();
}
}
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 )
{
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()
{
/* 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 )
{
if( mCanProcessAudio )
{
if( isSquelched() )
{
checkStop();
}
else if( packet.getType() == Type.AUDIO )
{
ByteBuffer buffer = convert(
packet.getAudioBuffer().getSamples() );
mOutput.write( buffer.array(), 0,
buffer.array().length );
checkStart();
broadcast( packet.getAudioMetadata() );
}
}
}
}
else
{
checkStop();
}
mProcessing.set( false );
}
}
private void checkStart()
{
if( mCanProcessAudio && !isSquelched() &&
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
{
mOutput.start();
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
}
private void checkStop()
{
if( mCanProcessAudio && mOutput.isRunning() &&
( mOutput.available() >= STOP_THRESHOLD || isSquelched() ) )
{
mOutput.drain();
mOutput.stop();
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}
@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;
}
}