Resolves Audio issues (underwater/overlapping audio buffers).

Redesigned audio manager handling of audio outputs to ensure that only
one channel's audio can be routed to an audio output at a time.

Changed audio outputs to use the shared thread pool manager for
scheduled tasks, instead of creating a thread pool for each audio
output.
This commit is contained in:
denny
2016-01-23 10:19:47 -05:00
parent 1db6779dec
commit 94d85f636d
5 changed files with 372 additions and 356 deletions

View File

@@ -2,7 +2,7 @@ package audio;
/******************************************************************************* /*******************************************************************************
* SDR Trunk * SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer * Copyright (C) 2014-2016 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -19,14 +19,11 @@ package audio;
******************************************************************************/ ******************************************************************************/
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -53,7 +50,6 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
private static final Logger mLog = LoggerFactory.getLogger( AudioManager.class ); private static final Logger mLog = LoggerFactory.getLogger( AudioManager.class );
public static final int AUDIO_TIMEOUT = 2000; //2 seconds public static final int AUDIO_TIMEOUT = 2000; //2 seconds
public static final int NO_SOURCE = 0;
public static final AudioEvent CONFIGURATION_CHANGE_STARTED = public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null ); new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null );
@@ -62,19 +58,19 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null ); new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null );
private LinkedTransferQueue<AudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>(); private LinkedTransferQueue<AudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
private Map<Integer,AudioChannelAssignment> mChannelAssignments = new HashMap<>(); private Map<Integer,AudioOutputConnection> mChannelConnectionMap = new HashMap<>();
private int mLowestPrioritySource; 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 Map<String,AudioOutput> mAudioOutputMap = new HashMap<>();
private List<String> mChannelNames = new ArrayList<>();
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>(); private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
private ThreadPoolManager mThreadPoolManager; private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture<?> mProcessingTask; private ScheduledFuture<?> mProcessingTask;
private MixerChannelConfiguration mMixerChannel; private MixerChannelConfiguration mMixerChannelConfiguration;
/** /**
* Processes all audio produced by the decoding channels and routes audio * Processes all audio produced by the decoding channels and routes audio
@@ -92,11 +88,11 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
if( mixer != null ) if( mixer != null )
{ {
mMixerChannel = new MixerChannelConfiguration( mixer, MixerChannel.MONO ); mMixerChannelConfiguration = new MixerChannelConfiguration( mixer, MixerChannel.MONO );
try try
{ {
setMixerChannelConfiguration( mMixerChannel ); setMixerChannelConfiguration( mMixerChannelConfiguration );
} }
catch ( AudioException e ) catch ( AudioException e )
{ {
@@ -116,6 +112,15 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mThreadPoolManager = null; mThreadPoolManager = null;
mProcessingTask = null; mProcessingTask = null;
mChannelConnectionMap.clear();
for( AudioOutputConnection connection: mAudioOutputConnections )
{
connection.dispose();
}
mAudioOutputConnections.clear();
} }
@Override @Override
@@ -124,210 +129,47 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mAudioPacketQueue.add( packet ); 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 = if( connection.isConnected() && connection.isInactive() )
mChannelAssignments.entrySet().iterator();
Entry<Integer,AudioChannelAssignment> entry;
boolean removed = false;
while( it.hasNext() )
{ {
entry = it.next(); mChannelConnectionMap.remove( connection.getSource() );
connection.disconnect();
if( entry.getValue().isComplete() ) changed = true;
{
mAvailableAudioOutputs.add( entry.getValue().getAudioOutput() );
entry.getValue().dispose();
it.remove();
removed = true;
}
} }
}
if( removed ) if( changed )
{ {
updateLowestPriorityAssignment(); updateLowestPriorityAssignment();
}
} }
} }
/** /**
* Identifies the lowest priority channel assignment ... the one that can * Identifies the lowest priority channel connection
* be overridden with a higher priority call
*/ */
private void updateLowestPriorityAssignment() private void updateLowestPriorityAssignment()
{ {
int lowestPriority = -1; mLowestPriorityConnection = null;
mLowestPrioritySource = -1; for( AudioOutputConnection connection: mAudioOutputConnections )
for( int source: mChannelAssignments.keySet() )
{ {
AudioChannelAssignment assignment = mChannelAssignments.get( source ); if( connection.isConnected() &&
( mLowestPriorityConnection == null ||
if( assignment.getPriority() > lowestPriority ) mLowestPriorityConnection.getPriority() > connection.getPriority() ) )
{ {
lowestPriority = assignment.getPriority(); mLowestPriorityConnection = connection;
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;
}
}
}
@Override @Override
public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException
@@ -343,40 +185,28 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mThreadPoolManager.cancel( mProcessingTask ); mThreadPoolManager.cancel( mProcessingTask );
} }
endAllAudioAssignments(); disposeCurrentConfiguration();
for( Entry<Integer,AudioChannelAssignment> e: mChannelAssignments.entrySet() )
{
e.getValue().getAudioOutput().dispose();
}
mChannelAssignments.clear();
for( AudioOutput output: mAvailableAudioOutputs )
{
output.dispose();
}
mAvailableAudioOutputs.clear();
mChannelNames.clear();
mAudioOutputMap.clear();
switch( entry.getMixerChannel() ) switch( entry.getMixerChannel() )
{ {
case MONO: case MONO:
AudioOutput mono = new MonoAudioOutput( entry.getMixer() ); AudioOutput mono = new MonoAudioOutput( mThreadPoolManager,
mAvailableAudioOutputs.add( mono ); entry.getMixer() );
mChannelNames.add( mono.getChannelName() ); mAudioOutputConnections.add( new AudioOutputConnection( mono ) );
mAvailableConnectionCount++;
mAudioOutputMap.put( mono.getChannelName(), mono ); mAudioOutputMap.put( mono.getChannelName(), mono );
break; break;
case STEREO: case STEREO:
AudioOutput left = new StereoAudioOutput( entry.getMixer(), MixerChannel.LEFT ); AudioOutput left = new StereoAudioOutput( mThreadPoolManager,
mAvailableAudioOutputs.add( left ); entry.getMixer(), MixerChannel.LEFT );
mChannelNames.add( left.getChannelName() ); mAudioOutputConnections.add( new AudioOutputConnection( left ) );
mAvailableConnectionCount++;
mAudioOutputMap.put( left.getChannelName(), left ); mAudioOutputMap.put( left.getChannelName(), left );
AudioOutput right = new StereoAudioOutput( entry.getMixer(), MixerChannel.RIGHT ); AudioOutput right = new StereoAudioOutput( mThreadPoolManager,
mAvailableAudioOutputs.add( right ); entry.getMixer(), MixerChannel.RIGHT );
mChannelNames.add( right.getChannelName() ); mAudioOutputConnections.add( new AudioOutputConnection( right ) );
mAvailableConnectionCount++;
mAudioOutputMap.put( right.getChannelName(), right ); mAudioOutputMap.put( right.getChannelName(), right );
break; break;
default: default:
@@ -392,10 +222,33 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
} }
} }
/**
* Clears all channel assignments and terminates all audio outputs in
* preparation for complete shutdown or change to another mixer/channel
* configuration
*/
private void disposeCurrentConfiguration()
{
mChannelConnectionMap.clear();
for( AudioOutputConnection connection : mAudioOutputConnections )
{
connection.dispose();
}
mAvailableConnectionCount = 0;
mAudioOutputConnections.clear();
mAudioOutputMap.clear();
mLowestPriorityConnection = null;
}
@Override @Override
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException
{ {
return mMixerChannel; return mMixerChannelConfiguration;
} }
@Override @Override
@@ -426,4 +279,237 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
{ {
mControllerBroadcaster.removeListener( listener ); 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()
{
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 );
}
}
}
}
}
}
/**
* Audio output connection manages a connection between a soure 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,7 +1,6 @@
package audio.output; package audio.output;
import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.FloatControl; import javax.sound.sampled.FloatControl;
@@ -15,6 +14,7 @@ import audio.AudioEvent;
import audio.AudioEvent.Type; import audio.AudioEvent.Type;
import audio.AudioPacket; import audio.AudioPacket;
import audio.metadata.AudioMetadata; import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
public abstract class AudioOutput implements Listener<AudioPacket> public abstract class AudioOutput implements Listener<AudioPacket>
{ {
@@ -25,10 +25,15 @@ public abstract class AudioOutput implements Listener<AudioPacket>
protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>(); protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
protected LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>(); protected LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
protected ScheduledExecutorService mExecutorService; protected ThreadPoolManager mThreadPoolManager;
protected boolean mCanProcessAudio; protected boolean mCanProcessAudio;
protected long mLastActivity; protected long mLastActivity;
public AudioOutput( ThreadPoolManager threadPoolManager )
{
mThreadPoolManager = threadPoolManager;
}
public void reset() public void reset()
{ {
broadcast( new AudioEvent( AudioEvent.Type.AUDIO_STOPPED, broadcast( new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
@@ -41,14 +46,11 @@ public abstract class AudioOutput implements Listener<AudioPacket>
mBuffer.clear(); mBuffer.clear();
if( mExecutorService != null )
{
mExecutorService.shutdown();
}
mAudioEventBroadcaster.dispose(); mAudioEventBroadcaster.dispose();
mAudioEventBroadcaster = null; mAudioEventBroadcaster = null;
mAudioMetadataListener = null; mAudioMetadataListener = null;
mThreadPoolManager = null;
} }
public abstract String getChannelName(); public abstract String getChannelName();

View File

@@ -5,7 +5,7 @@ import java.nio.ByteOrder;
import java.nio.ShortBuffer; import java.nio.ShortBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -24,7 +24,8 @@ import audio.AudioEvent;
import audio.AudioFormats; import audio.AudioFormats;
import audio.AudioPacket; import audio.AudioPacket;
import audio.AudioPacket.Type; import audio.AudioPacket.Type;
import controller.NamingThreadFactory; import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
/** /**
* Mono Audio output with automatic flow control based on the availability of * Mono Audio output with automatic flow control based on the availability of
@@ -54,9 +55,11 @@ public class MonoAudioOutput extends AudioOutput
private AudioEvent mAudioStopEvent; private AudioEvent mAudioStopEvent;
private AudioEvent mAudioContinuationEvent; private AudioEvent mAudioContinuationEvent;
public MonoAudioOutput( Mixer mixer ) private ScheduledFuture<?> mProcessorTask;
public MonoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer )
{ {
super(); super( threadPoolManager );
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED, mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() ); getChannelName() );
@@ -73,15 +76,7 @@ public class MonoAudioOutput extends AudioOutput
if( mOutput != null ) if( mOutput != null )
{ {
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE ); mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE );
mCanProcessAudio = true; 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 ) catch ( LineUnavailableException e )
@@ -114,12 +109,10 @@ public class MonoAudioOutput extends AudioOutput
mixer.getMixerInfo().getName() + "]" ); mixer.getMixerInfo().getName() + "]" );
} }
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (mono) output" ) );
/* Run the queue processor task every 40 milliseconds */ /* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(), mProcessorTask = mThreadPoolManager.scheduleFixedRate(
0, 40, TimeUnit.MILLISECONDS ); ThreadType.AUDIO_PROCESSING, new QueueProcessor(),
40, TimeUnit.MILLISECONDS );
} }
} }
@@ -131,12 +124,23 @@ public class MonoAudioOutput extends AudioOutput
public void dispose() public void dispose()
{ {
if( mProcessorTask != null && mThreadPoolManager != null )
{
mThreadPoolManager.cancel( mProcessorTask );
}
mProcessorTask = null;
super.dispose(); super.dispose();
if( mOutput != null ) if( mOutput != null )
{ {
mOutput.close(); mOutput.close();
} }
mOutput = null;
mGainControl = null;
mMuteControl = null;
} }
public class QueueProcessor implements Runnable public class QueueProcessor implements Runnable
@@ -184,55 +188,10 @@ public class MonoAudioOutput extends AudioOutput
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) ); shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
} }
if( mOutput.available() >= buffer.array().length ) //This is a blocking write
{ mOutput.write( buffer.array(), 0, buffer.array().length );
mOutput.write( buffer.array(), 0,
buffer.array().length );
checkStart(); checkStart();
}
else
{
int wrote = 0;
int toWrite = buffer.array().length;
while( toWrite > 0 )
{
int available = mOutput.available();
if( available < toWrite )
{
if( available > 0 )
{
wrote += mOutput.write( buffer.array(),
wrote, available );
checkStart();
}
else
{
try
{
Thread.sleep( 100 );
}
catch ( InterruptedException e )
{
mLog.debug( "Mono Audio Output - interrupted while sleeping awaiting buffer drain" );
}
}
}
else
{
wrote += mOutput.write( buffer.array(), wrote,
toWrite );
checkStart();
}
toWrite = buffer.array().length - wrote;
}
}
broadcast( packet.getAudioMetadata() ); broadcast( packet.getAudioMetadata() );

View File

@@ -5,7 +5,7 @@ import java.nio.ByteOrder;
import java.nio.ShortBuffer; import java.nio.ShortBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -24,7 +24,8 @@ import audio.AudioEvent;
import audio.AudioFormats; import audio.AudioFormats;
import audio.AudioPacket; import audio.AudioPacket;
import audio.AudioPacket.Type; 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 * Mono Audio output in stereo format with automatic flow control based on the
@@ -56,9 +57,12 @@ public class StereoAudioOutput extends AudioOutput
private MixerChannel mMixerChannel; private MixerChannel mMixerChannel;
public StereoAudioOutput( Mixer mixer, MixerChannel channel ) private ScheduledFuture<?> mProcessorTask;
public StereoAudioOutput( ThreadPoolManager threadPoolManager,
Mixer mixer, MixerChannel channel )
{ {
super(); super( threadPoolManager );
assert( channel == MixerChannel.LEFT || channel == MixerChannel.RIGHT ); assert( channel == MixerChannel.LEFT || channel == MixerChannel.RIGHT );
@@ -113,12 +117,10 @@ public class StereoAudioOutput extends AudioOutput
mixer.getMixerInfo().getName() + " | " + channel.name() + "]" ); mixer.getMixerInfo().getName() + " | " + channel.name() + "]" );
} }
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (stereo) output" ) );
/* Run the queue processor task every 40 milliseconds */ /* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(), mProcessorTask = mThreadPoolManager.scheduleFixedRate(
0, 40, TimeUnit.MILLISECONDS ); ThreadType.AUDIO_PROCESSING, new QueueProcessor(),
40, TimeUnit.MILLISECONDS );
} }
} }
@@ -130,12 +132,23 @@ public class StereoAudioOutput extends AudioOutput
public void dispose() public void dispose()
{ {
if( mProcessorTask != null && mThreadPoolManager != null )
{
mThreadPoolManager.cancel( mProcessorTask );
}
mProcessorTask = null;
super.dispose(); super.dispose();
if( mOutput != null ) if( mOutput != null )
{ {
mOutput.close(); mOutput.close();
} }
mOutput = null;
mGainControl = null;
mMuteControl = null;
} }
private ByteBuffer convert( float[] samples ) private ByteBuffer convert( float[] samples )
@@ -204,44 +217,10 @@ public class StereoAudioOutput extends AudioOutput
ByteBuffer buffer = convert( ByteBuffer buffer = convert(
packet.getAudioBuffer().getSamples() ); packet.getAudioBuffer().getSamples() );
if( mOutput.available() >= buffer.array().length ) mOutput.write( buffer.array(), 0,
{ buffer.array().length );
mOutput.write( buffer.array(), 0,
buffer.array().length );
checkStart(); checkStart();
}
else
{
int wrote = 0;
int toWrite = buffer.array().length;
while( toWrite > 0 )
{
int available = mOutput.available();
if( available < toWrite )
{
if( available > 0 )
{
wrote += mOutput.write( buffer.array(),
wrote, available );
checkStart();
}
}
else
{
wrote += mOutput.write( buffer.array(), wrote,
toWrite );
checkStart();
}
toWrite = buffer.array().length - wrote;
}
}
broadcast( packet.getAudioMetadata() ); broadcast( packet.getAudioMetadata() );

View File

@@ -36,7 +36,9 @@ public class ThreadPoolManager
private HashMap<ScheduledFuture<?>,ThreadType> mTasks = private HashMap<ScheduledFuture<?>,ThreadType> mTasks =
new HashMap<ScheduledFuture<?>,ThreadType>(); new HashMap<ScheduledFuture<?>,ThreadType>();
private ScheduledExecutorService mExecutor; private ScheduledExecutorService mExecutor =
Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
new NamingThreadFactory( "sdrtrunk" ) );
public ThreadPoolManager() public ThreadPoolManager()
{ {
@@ -48,12 +50,6 @@ public class ThreadPoolManager
TimeUnit unit ) TimeUnit unit )
throws RejectedExecutionException throws RejectedExecutionException
{ {
if( mExecutor == null )
{
mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
new NamingThreadFactory( "sdrtrunk" ) );
}
ScheduledFuture<?> task = ScheduledFuture<?> task =
mExecutor.scheduleAtFixedRate( command, 0, period, unit ); mExecutor.scheduleAtFixedRate( command, 0, period, unit );
@@ -65,12 +61,6 @@ public class ThreadPoolManager
public void scheduleOnce( Runnable command, long delay, TimeUnit unit ) public void scheduleOnce( Runnable command, long delay, TimeUnit unit )
throws RejectedExecutionException throws RejectedExecutionException
{ {
if( mExecutor == null )
{
mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
new NamingThreadFactory( "sdrtrunk" ) );
}
mExecutor.schedule( command, delay, unit ); mExecutor.schedule( command, delay, unit );
} }