Rebase channel/alias model redesign branch on current master

(v0.2.0.beta5)
This commit is contained in:
denny
2016-02-14 11:45:43 -05:00
parent 1d7f255784
commit 1f654b5b9c
22 changed files with 1019 additions and 752 deletions

View File

@@ -19,14 +19,11 @@ package audio;
******************************************************************************/
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -37,10 +34,12 @@ import javax.sound.sampled.Mixer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import sample.Broadcaster;
import sample.Listener;
import source.mixer.MixerChannel;
import source.mixer.MixerChannelConfiguration;
import source.mixer.MixerManager;
import audio.AudioEvent.Type;
import audio.output.AudioOutput;
import audio.output.MonoAudioOutput;
@@ -53,7 +52,9 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
private static final Logger mLog = LoggerFactory.getLogger( AudioManager.class );
public static final int AUDIO_TIMEOUT = 2000; //2 seconds
public static final int NO_SOURCE = 0;
public static final String AUDIO_CHANNELS_PROPERTY = "audio.manager.channels";
public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer";
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null );
@@ -62,49 +63,90 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null );
private LinkedTransferQueue<AudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
private Map<Integer,AudioChannelAssignment> mChannelAssignments = new HashMap<>();
private int mLowestPrioritySource;
private Map<Integer,AudioOutputConnection> mChannelConnectionMap = new HashMap<>();
private List<AudioOutputConnection> mAudioOutputConnections = new ArrayList<>();
private AudioOutputConnection mLowestPriorityConnection;
private int mAvailableConnectionCount;
private List<AudioOutput> mAvailableAudioOutputs = new ArrayList<>();
private Map<String,AudioOutput> mAudioOutputMap = new HashMap<>();
private List<String> mChannelNames = new ArrayList<>();
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture<?> mProcessingTask;
private MixerManager mMixerManager;
private MixerChannelConfiguration mMixerChannelConfiguration;
private MixerChannelConfiguration mMixerChannel;
/**
* Processes all audio produced by the decoding channels and routes audio
* packets to any combination of outputs based on any alias audio routing
* options specified by the user.
*/
public AudioManager( ThreadPoolManager manager )
public AudioManager( ThreadPoolManager manager, MixerManager mixerManager )
{
mThreadPoolManager = manager;
mMixerManager = mixerManager;
//TODO: save audio system preferences to the settings manager
loadSettings();
}
private void loadSettings()
{
MixerChannelConfiguration configuration = null;
/* Use the system default mixer and mono channel as default startup */
Mixer mixer = AudioSystem.getMixer( null );
SystemProperties properties = SystemProperties.getInstance();
if( mixer != null )
Mixer defaultMixer = AudioSystem.getMixer( null );
String mixer = properties.get( AUDIO_MIXER_PROPERTY, defaultMixer.getMixerInfo().getName() );
String channels = properties.get( AUDIO_CHANNELS_PROPERTY, MixerChannel.MONO.name() );
MixerChannelConfiguration[] mixerConfigurations = mMixerManager.getOutputMixers();
for( MixerChannelConfiguration mixerConfig: mixerConfigurations )
{
mMixerChannel = new MixerChannelConfiguration( mixer, MixerChannel.MONO );
if( mixerConfig.matches( mixer, channels ) )
{
configuration = mixerConfig;
}
}
if( configuration == null )
{
configuration = getDefaultConfiguration();
}
try
{
setMixerChannelConfiguration( configuration );
}
catch( Exception e )
{
mLog.error( "Couldn't set stored audio mixer/channel configuration"
+ " - using default", e );
try
{
setMixerChannelConfiguration( mMixerChannel );
}
catch ( AudioException e )
setMixerChannelConfiguration( getDefaultConfiguration() );
}
catch( Exception e2 )
{
mLog.error( "Couldn't create an audio output device", e );
mLog.error( "Couldn't set default audio mixer/channel "
+ "configuration - no audio will be available", e2 );
}
}
}
private MixerChannelConfiguration getDefaultConfiguration()
{
/* Use the system default mixer and mono channel as default startup */
Mixer defaultMixer = AudioSystem.getMixer( null );
return new MixerChannelConfiguration( defaultMixer, MixerChannel.MONO );
}
public void dispose()
{
if( mThreadPoolManager != null && mProcessingTask != null )
@@ -116,6 +158,15 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mThreadPoolManager = null;
mProcessingTask = null;
mChannelConnectionMap.clear();
for( AudioOutputConnection connection: mAudioOutputConnections )
{
connection.dispose();
}
mAudioOutputConnections.clear();
}
@Override
@@ -124,207 +175,44 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mAudioPacketQueue.add( packet );
}
public class AudioPacketProcessor implements Runnable
{
@Override
public void run()
{
removeCompletedChannelAssignments();
if( mAudioPacketQueue != null )
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mAudioPacketQueue.drainTo( packets );
for( AudioPacket packet: packets )
{
/* Don't process any packet's marked as do not monitor */
if( !packet.getAudioMetadata().isDoNotMonitor() )
{
int source = packet.getAudioMetadata().getSource();
if( packet.getType() != AudioPacket.Type.END )
{
/* Existing channel assignment */
if( mChannelAssignments.containsKey( source ) )
{
mChannelAssignments.get( source ).getAudioOutput()
.receive( packet );
}
/* Start a new channel assignment */
else if( !mAvailableAudioOutputs.isEmpty() )
{
AudioOutput channel = mAvailableAudioOutputs.remove( 0 );
mChannelAssignments.put( packet.getAudioMetadata().getSource(),
new AudioChannelAssignment( channel, packet.getAudioMetadata().getPriority() ) );
updateLowestPriorityAssignment();
channel.receive( packet );
}
/* Override the lowest priority assignment */
else
{
if( mLowestPrioritySource > -1 )
{
int priority = packet.getAudioMetadata().getPriority();
if( packet.getAudioMetadata().isSelected() ||
priority < mChannelAssignments
.get( mLowestPrioritySource ).getPriority() )
{
AudioChannelAssignment assignment =
mChannelAssignments.remove( mLowestPrioritySource );
if( assignment != null )
{
assignment.setPriority( priority );
int overrideSource = packet.getAudioMetadata().getSource();
mChannelAssignments.put( overrideSource, assignment );
mLowestPrioritySource = overrideSource;
assignment.getAudioOutput().receive( packet );
}
else
{
mLowestPrioritySource = -1;
}
}
}
}
}
}
}
}
}
}
/**
* Checks each audio channel assignment and removes any completed channels.
* Checks each audio channel assignment and disconnects any inactive connections
*/
private void removeCompletedChannelAssignments()
private void disconnectInactiveChannelAssignments()
{
if( !mChannelAssignments.isEmpty() )
boolean changed = false;
for( AudioOutputConnection connection: mAudioOutputConnections )
{
Iterator<Entry<Integer,AudioChannelAssignment>> it =
mChannelAssignments.entrySet().iterator();
Entry<Integer,AudioChannelAssignment> entry;
boolean removed = false;
while( it.hasNext() )
if( connection.isInactive() &&
mChannelConnectionMap.containsKey( connection.getSource() ) )
{
entry = it.next();
if( entry.getValue().isComplete() )
{
mAvailableAudioOutputs.add( entry.getValue().getAudioOutput() );
entry.getValue().dispose();
it.remove();
removed = true;
}
}
if( removed )
{
updateLowestPriorityAssignment();
mChannelConnectionMap.remove( connection.getSource() );
connection.disconnect();
changed = true;
}
}
if( changed )
{
updateLowestPriorityAssignment();
}
}
/**
* Identifies the lowest priority channel assignment ... the one that can
* be overridden with a higher priority call
* Identifies the lowest priority channel connection
*/
private void updateLowestPriorityAssignment()
{
int lowestPriority = -1;
mLowestPriorityConnection = null;
mLowestPrioritySource = -1;
for( int source: mChannelAssignments.keySet() )
for( AudioOutputConnection connection: mAudioOutputConnections )
{
AudioChannelAssignment assignment = mChannelAssignments.get( source );
if( assignment.getPriority() > lowestPriority )
if( connection.isConnected() &&
( mLowestPriorityConnection == null ||
mLowestPriorityConnection.getPriority() > connection.getPriority() ) )
{
lowestPriority = assignment.getPriority();
mLowestPrioritySource = source;
}
}
}
public void endAllAudioAssignments()
{
for( AudioOutput output: mAudioOutputMap.values() )
{
output.receive( new AudioPacket( AudioPacket.Type.END, null ) );
}
}
public class AudioChannelAssignment implements Listener<AudioEvent>
{
private static final long CALL_END_DELAY = 2000; //2 seconds
private AudioOutput mAudioOutput;
private int mPriority;
private long mAudioStoppedTime = System.currentTimeMillis();
public AudioChannelAssignment( AudioOutput audioOutput, int priority )
{
mAudioOutput = audioOutput;
mPriority = priority;
mAudioOutput.addAudioEventListener( this );
}
public boolean isComplete()
{
return mAudioStoppedTime != Long.MAX_VALUE &&
mAudioStoppedTime + CALL_END_DELAY < System.currentTimeMillis();
}
public void dispose()
{
mAudioOutput.removeAudioEventListener( this );
mAudioOutput.reset();
mAudioOutput = null;
}
public AudioOutput getAudioOutput()
{
return mAudioOutput;
}
public int getPriority()
{
return mPriority;
}
public void setPriority( int priority )
{
mPriority = priority;
}
@Override
public void receive( AudioEvent event )
{
switch( event.getType() )
{
case AUDIO_STARTED:
case AUDIO_STOPPED:
case AUDIO_CONTINUATION:
mAudioStoppedTime = System.currentTimeMillis();
break;
default:
break;
mLowestPriorityConnection = connection;
}
}
}
@@ -342,41 +230,29 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
{
mThreadPoolManager.cancel( mProcessingTask );
}
endAllAudioAssignments();
for( Entry<Integer,AudioChannelAssignment> e: mChannelAssignments.entrySet() )
{
e.getValue().getAudioOutput().dispose();
}
mChannelAssignments.clear();
disposeCurrentConfiguration();
for( AudioOutput output: mAvailableAudioOutputs )
{
output.dispose();
}
mAvailableAudioOutputs.clear();
mChannelNames.clear();
mAudioOutputMap.clear();
switch( entry.getMixerChannel() )
{
case MONO:
AudioOutput mono = new MonoAudioOutput( entry.getMixer() );
mAvailableAudioOutputs.add( mono );
mChannelNames.add( mono.getChannelName() );
AudioOutput mono = new MonoAudioOutput( mThreadPoolManager,
entry.getMixer() );
mAudioOutputConnections.add( new AudioOutputConnection( mono ) );
mAvailableConnectionCount++;
mAudioOutputMap.put( mono.getChannelName(), mono );
break;
case STEREO:
AudioOutput left = new StereoAudioOutput( entry.getMixer(), MixerChannel.LEFT );
mAvailableAudioOutputs.add( left );
mChannelNames.add( left.getChannelName() );
AudioOutput left = new StereoAudioOutput( mThreadPoolManager,
entry.getMixer(), MixerChannel.LEFT );
mAudioOutputConnections.add( new AudioOutputConnection( left ) );
mAvailableConnectionCount++;
mAudioOutputMap.put( left.getChannelName(), left );
AudioOutput right = new StereoAudioOutput( entry.getMixer(), MixerChannel.RIGHT );
mAvailableAudioOutputs.add( right );
mChannelNames.add( right.getChannelName() );
AudioOutput right = new StereoAudioOutput( mThreadPoolManager,
entry.getMixer(), MixerChannel.RIGHT );
mAudioOutputConnections.add( new AudioOutputConnection( right ) );
mAvailableConnectionCount++;
mAudioOutputMap.put( right.getChannelName(), right );
break;
default:
@@ -389,6 +265,12 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
TimeUnit.MILLISECONDS );
mControllerBroadcaster.broadcast( CONFIGURATION_CHANGE_COMPLETE );
SystemProperties properties = SystemProperties.getInstance();
properties.set( AUDIO_MIXER_PROPERTY, entry.getMixer().getMixerInfo().getName() );
properties.set( AUDIO_CHANNELS_PROPERTY, entry.getMixerChannel().name() );
properties.save();
}
}
@@ -418,7 +300,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
@Override
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException
{
return mMixerChannel;
return mMixerChannelConfiguration;
}
@Override
@@ -449,4 +331,244 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
{
mControllerBroadcaster.removeListener( listener );
}
}
/**
* Returns an audio output connection for the packet if one is
* available, or overrides an existing lower priority connection.
* Returns null if no connection is available for the audio packet.
*
* @param packet from a source
* @return an audio output connection or null
*/
private AudioOutputConnection getConnection( AudioPacket packet )
{
int source = packet.getAudioMetadata().getSource();
//Use an existing connection
if( mChannelConnectionMap.containsKey( source ) )
{
return mChannelConnectionMap.get( source );
}
//Connect to and use an available connection
if( mAvailableConnectionCount > 0 )
{
for( AudioOutputConnection connection: mAudioOutputConnections )
{
if( connection.isDisconnected() )
{
connection.connect( source );
mChannelConnectionMap.put( source, connection );
return connection;
}
}
}
//Preempt an existing connection and connect it to this higher priority source
else //Check for a channel priority override
{
int priority = packet.getAudioMetadata().getPriority();
if( mLowestPriorityConnection != null &&
priority < mLowestPriorityConnection.getPriority() )
{
AudioOutputConnection connection = mLowestPriorityConnection;
mChannelConnectionMap.remove( connection.getSource() );
connection.connect( source );
mChannelConnectionMap.put( source, connection );
updateLowestPriorityAssignment();
return connection;
}
}
return null;
}
public class AudioPacketProcessor implements Runnable
{
@Override
public void run()
{
try
{
disconnectInactiveChannelAssignments();
if( mAudioPacketQueue != null )
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mAudioPacketQueue.drainTo( packets );
for( AudioPacket packet: packets )
{
/* Don't process any packet's marked as do not monitor */
if( !packet.getAudioMetadata().isDoNotMonitor() &&
packet.getType() == AudioPacket.Type.AUDIO )
{
AudioOutputConnection connection = getConnection( packet );
if( connection != null )
{
connection.receive( packet );
}
}
}
}
}
catch( Exception e )
{
mLog.error( "Encountered error while processing audio packets", e );
}
}
}
/**
* Audio output connection manages a connection between a source and an audio
* output and maintains current state information about the audio activity
* received form the source.
*/
public class AudioOutputConnection implements Listener<AudioEvent>
{
private static final long CALL_END_DELAY = 2000; //2 seconds
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 )
{
if( packet.hasAudioMetadata() &&
packet.getAudioMetadata().getSource() == mSource )
{
int priority = packet.getAudioMetadata().getPriority();
if( mPriority != priority )
{
mPriority = priority;
updateLowestPriorityAssignment();
}
updateTimestamp();
if( mAudioOutput != null )
{
mAudioOutput.receive( packet );
}
}
else
{
if( packet.hasAudioMetadata() )
{
mLog.error( "Received audio packet from channel source [" +
packet.getAudioMetadata().getSource() +
"] however this assignment is currently connected "
+ "to channel source [" + mSource + "]" );
}
else
{
mLog.error( "Received audio packet with no metadata - "
+ "cannot route audio packet" );
}
}
}
private void updateTimestamp()
{
mLastActivity = System.currentTimeMillis();
}
/**
* Terminates the audio output and prepares this connection for disposal
*/
public void dispose()
{
mAudioOutput.dispose();
mAudioOutput = null;
}
/**
* Indicates if this assignment is currently disconnected from a channel source
*/
public boolean isDisconnected()
{
return mSource == DISCONNECTED;
}
/**
* Indicates if this assignment is currently connected to a channel source
*/
public boolean isConnected()
{
return !isDisconnected();
}
/**
* Connects this assignment to the indicated source so that audio
* packets from this source can be sent to the audio output
*/
public void connect( int source )
{
mSource = source;
updateTimestamp();
}
/**
* Currently connected source or -1 if disconnected
*/
public int getSource()
{
return mSource;
}
/**
* Disconnects this assignment from the source and prevents any audio
* from being routed to the audio output until another source is assigned
*/
public void disconnect()
{
mSource = DISCONNECTED;
}
/**
* Indicates if audio output is current inactive, meaning that the
* audio output hasn't recently processed any audio packets.
*/
public boolean isInactive()
{
return mLastActivity != Long.MAX_VALUE &&
mLastActivity + CALL_END_DELAY < 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

@@ -1,10 +1,9 @@
package audio.output;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -12,23 +11,26 @@ import org.slf4j.LoggerFactory;
import sample.Broadcaster;
import sample.Listener;
import audio.AudioEvent;
import audio.AudioEvent.Type;
import audio.AudioPacket;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
public abstract class AudioOutput implements Listener<AudioPacket>
public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
{
private final static Logger mLog = LoggerFactory.getLogger( AudioOutput.class );
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 ThreadPoolManager mThreadPoolManager;
protected boolean mCanProcessAudio;
protected long mLastActivity;
public AudioOutput( ThreadPoolManager threadPoolManager )
{
mThreadPoolManager = threadPoolManager;
}
public void reset()
{
broadcast( new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
@@ -41,14 +43,11 @@ public abstract class AudioOutput implements Listener<AudioPacket>
mBuffer.clear();
if( mExecutorService != null )
{
mExecutorService.shutdown();
}
mAudioEventBroadcaster.dispose();
mAudioEventBroadcaster = null;
mAudioMetadataListener = null;
mThreadPoolManager = null;
}
public abstract String getChannelName();
@@ -72,19 +71,6 @@ public abstract class AudioOutput implements Listener<AudioPacket>
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()
{
return mSquelched.get();
}
/**
* Registers a single listener to receive audio start and audio stop events
*/
@@ -130,4 +116,4 @@ public abstract class AudioOutput implements Listener<AudioPacket>
mBuffer.add( packet );
}
}
}
}

View File

@@ -5,13 +5,14 @@ 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.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;
@@ -24,8 +25,8 @@ import audio.AudioEvent;
import audio.AudioFormats;
import audio.AudioPacket;
import audio.AudioPacket.Type;
import audio.output.StereoAudioOutput.QueueProcessor;
import controller.NamingThreadFactory;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
/**
* Mono Audio output with automatic flow control based on the availability of
@@ -50,14 +51,14 @@ public class MonoAudioOutput extends AudioOutput
private SourceDataLine mOutput;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private AudioEvent mAudioContinuationEvent;
private ScheduledFuture<?> mProcessorTask;
public MonoAudioOutput( Mixer mixer )
public MonoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer )
{
super();
super( threadPoolManager );
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() );
@@ -74,15 +75,8 @@ public class MonoAudioOutput extends AudioOutput
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE );
mOutput.addLineListener( this );
mCanProcessAudio = true;
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (mono) output" ) );
/* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(),
0, 40, TimeUnit.MILLISECONDS );
}
}
catch ( LineUnavailableException e )
@@ -115,12 +109,10 @@ public class MonoAudioOutput extends AudioOutput
mixer.getMixerInfo().getName() + "]" );
}
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (mono) output" ) );
/* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(),
0, 40, TimeUnit.MILLISECONDS );
mProcessorTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_PROCESSING, new QueueProcessor(),
40, TimeUnit.MILLISECONDS );
}
}
@@ -132,102 +124,108 @@ public class MonoAudioOutput extends AudioOutput
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;
}
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 ) )
try
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo( packets );
if( packets.size() > 0 )
/* The mProcessing flag ensures that only one instance of the
* processor can run at any given time */
if( mProcessing.compareAndSet( false, true ) )
{
broadcast( mAudioContinuationEvent );
List<AudioPacket> packets = new ArrayList<AudioPacket>();
for( AudioPacket packet: packets )
mBuffer.drainTo( packets );
if( packets.size() > 0 )
{
if( mCanProcessAudio )
for( AudioPacket packet: packets )
{
if( isSquelched() )
broadcast( mAudioContinuationEvent );
if( mCanProcessAudio )
{
checkStop();
}
else 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 )
if( packet.getType() == Type.AUDIO )
{
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
}
mOutput.write( buffer.array(), 0,
buffer.array().length );
checkStart();
broadcast( packet.getAudioMetadata() );
float[] samples = packet.getAudioBuffer().getSamples();
/* Little-endian byte buffer */
ByteBuffer buffer =
ByteBuffer.allocate( samples.length * 2 )
.order( ByteOrder.LITTLE_ENDIAN );
ShortBuffer shortBuffer = buffer.asShortBuffer();
mLastActivity = System.currentTimeMillis();
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 );
}
else
{
checkStop();
}
mProcessing.set( false );
}
catch( Exception e )
{
mLog.error( "Error while processing audio buffers", e );
}
}
private void checkStart()
{
if( mCanProcessAudio && !isSquelched() &&
if( mCanProcessAudio &&
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
{
mOutput.start();
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
}
private void checkStop()
{
if( mCanProcessAudio && mOutput.isRunning() &&
( mOutput.available() >= STOP_THRESHOLD || isSquelched() ) )
mOutput.available() >= STOP_THRESHOLD )
{
mOutput.drain();
mOutput.stop();
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}
@@ -266,4 +264,19 @@ public class MonoAudioOutput extends AudioOutput
{
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 );
}
}
}

View File

@@ -5,13 +5,14 @@ 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.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;
@@ -24,10 +25,11 @@ import audio.AudioEvent;
import audio.AudioFormats;
import audio.AudioPacket;
import audio.AudioPacket.Type;
import controller.NamingThreadFactory;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
/**
* Mono Audio output in stereo format with automatic flow control based on the
* Stereo Audio output in stereo format with automatic flow control based on the
* availability of audio data packets.
*/
public class StereoAudioOutput extends AudioOutput
@@ -39,9 +41,9 @@ public class StereoAudioOutput extends AudioOutput
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
* 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 */
* 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;
@@ -49,16 +51,16 @@ public class StereoAudioOutput extends AudioOutput
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( Mixer mixer, MixerChannel channel )
public StereoAudioOutput( ThreadPoolManager threadPoolManager,
Mixer mixer, MixerChannel channel )
{
super();
super( threadPoolManager );
assert( channel == MixerChannel.LEFT || channel == MixerChannel.RIGHT );
@@ -79,6 +81,7 @@ public class StereoAudioOutput extends AudioOutput
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, BUFFER_SIZE );
mOutput.addLineListener( this );
mCanProcessAudio = true;
}
}
@@ -113,12 +116,10 @@ public class StereoAudioOutput extends AudioOutput
mixer.getMixerInfo().getName() + " | " + channel.name() + "]" );
}
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (stereo) output" ) );
/* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(),
0, 40, TimeUnit.MILLISECONDS );
mProcessorTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_PROCESSING, new QueueProcessor(),
40, TimeUnit.MILLISECONDS );
}
}
@@ -130,20 +131,30 @@ public class StereoAudioOutput extends AudioOutput
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 );
ByteBuffer buffer = ByteBuffer.allocate( samples.length * 4 )
.order( ByteOrder.LITTLE_ENDIAN );
ShortBuffer shortBuffer = buffer.asShortBuffer();
@@ -178,72 +189,72 @@ public class StereoAudioOutput extends AudioOutput
@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 ) )
try
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo( packets );
if( packets.size() > 0 )
/* The mProcessing flag ensures that only one instance of the
* processor can run at any given time */
if( mProcessing.compareAndSet( false, true ) )
{
broadcast( mAudioContinuationEvent );
List<AudioPacket> packets = new ArrayList<AudioPacket>();
for( AudioPacket packet: packets )
mBuffer.drainTo( packets );
if( packets.size() > 0 )
{
if( mCanProcessAudio )
for( AudioPacket packet: packets )
{
if( isSquelched() )
broadcast( mAudioContinuationEvent );
if( mCanProcessAudio )
{
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() );
mLastActivity = System.currentTimeMillis();
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 );
}
else
{
checkStop();
}
mProcessing.set( false );
}
catch( Exception e )
{
mLog.error( "Error while processing audio buffers", e );
}
}
private void checkStart()
{
if( mCanProcessAudio && !isSquelched() &&
if( mCanProcessAudio &&
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
{
mOutput.start();
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
}
private void checkStop()
{
if( mCanProcessAudio && mOutput.isRunning() &&
( mOutput.available() >= STOP_THRESHOLD || isSquelched() ) )
mOutput.available() >= STOP_THRESHOLD )
{
mOutput.drain();
mOutput.stop();
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}
@@ -282,4 +293,20 @@ public class StereoAudioOutput extends AudioOutput
{
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 );
}
}
}