mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
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:
@@ -2,7 +2,7 @@ package audio;
|
||||
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
* 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.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;
|
||||
@@ -53,7 +50,6 @@ 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 AudioEvent CONFIGURATION_CHANGE_STARTED =
|
||||
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 );
|
||||
|
||||
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 MixerChannelConfiguration mMixerChannel;
|
||||
private MixerChannelConfiguration mMixerChannelConfiguration;
|
||||
|
||||
/**
|
||||
* 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 )
|
||||
{
|
||||
mMixerChannel = new MixerChannelConfiguration( mixer, MixerChannel.MONO );
|
||||
mMixerChannelConfiguration = new MixerChannelConfiguration( mixer, MixerChannel.MONO );
|
||||
|
||||
try
|
||||
{
|
||||
setMixerChannelConfiguration( mMixerChannel );
|
||||
setMixerChannelConfiguration( mMixerChannelConfiguration );
|
||||
}
|
||||
catch ( AudioException e )
|
||||
{
|
||||
@@ -116,6 +112,15 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
|
||||
mThreadPoolManager = null;
|
||||
mProcessingTask = null;
|
||||
|
||||
mChannelConnectionMap.clear();
|
||||
|
||||
for( AudioOutputConnection connection: mAudioOutputConnections )
|
||||
{
|
||||
connection.dispose();
|
||||
}
|
||||
|
||||
mAudioOutputConnections.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -124,210 +129,47 @@ 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.isConnected() && connection.isInactive() )
|
||||
{
|
||||
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;
|
||||
mLowestPriorityConnection = connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException
|
||||
@@ -342,41 +184,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:
|
||||
@@ -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
|
||||
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException
|
||||
{
|
||||
return mMixerChannel;
|
||||
return mMixerChannelConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -426,4 +279,237 @@ 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()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package audio.output;
|
||||
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import javax.sound.sampled.FloatControl;
|
||||
@@ -15,6 +14,7 @@ import audio.AudioEvent;
|
||||
import audio.AudioEvent.Type;
|
||||
import audio.AudioPacket;
|
||||
import audio.metadata.AudioMetadata;
|
||||
import controller.ThreadPoolManager;
|
||||
|
||||
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 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 +46,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();
|
||||
|
||||
@@ -5,7 +5,7 @@ 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;
|
||||
|
||||
@@ -24,7 +24,8 @@ 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 with automatic flow control based on the availability of
|
||||
@@ -54,9 +55,11 @@ public class MonoAudioOutput extends AudioOutput
|
||||
private AudioEvent mAudioStopEvent;
|
||||
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,
|
||||
getChannelName() );
|
||||
@@ -73,15 +76,7 @@ public class MonoAudioOutput extends AudioOutput
|
||||
if( mOutput != null )
|
||||
{
|
||||
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE );
|
||||
|
||||
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 )
|
||||
@@ -114,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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,12 +124,23 @@ 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
|
||||
@@ -183,57 +187,12 @@ public class MonoAudioOutput extends AudioOutput
|
||||
{
|
||||
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
|
||||
}
|
||||
|
||||
if( mOutput.available() >= buffer.array().length )
|
||||
{
|
||||
mOutput.write( buffer.array(), 0,
|
||||
buffer.array().length );
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
//This is a blocking write
|
||||
mOutput.write( buffer.array(), 0, buffer.array().length );
|
||||
|
||||
checkStart();
|
||||
|
||||
broadcast( packet.getAudioMetadata() );
|
||||
|
||||
mLastActivity = System.currentTimeMillis();
|
||||
|
||||
@@ -5,7 +5,7 @@ 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;
|
||||
|
||||
@@ -24,7 +24,8 @@ 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
|
||||
@@ -56,9 +57,12 @@ public class StereoAudioOutput extends AudioOutput
|
||||
|
||||
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 );
|
||||
|
||||
@@ -113,12 +117,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,12 +132,23 @@ 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 )
|
||||
@@ -204,44 +217,10 @@ public class StereoAudioOutput extends AudioOutput
|
||||
ByteBuffer buffer = convert(
|
||||
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();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
checkStart();
|
||||
|
||||
broadcast( packet.getAudioMetadata() );
|
||||
|
||||
|
||||
@@ -36,24 +36,20 @@ public class ThreadPoolManager
|
||||
private HashMap<ScheduledFuture<?>,ThreadType> mTasks =
|
||||
new HashMap<ScheduledFuture<?>,ThreadType>();
|
||||
|
||||
private ScheduledExecutorService mExecutor;
|
||||
private ScheduledExecutorService mExecutor =
|
||||
Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
|
||||
new NamingThreadFactory( "sdrtrunk" ) );
|
||||
|
||||
public ThreadPoolManager()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public ScheduledFuture<?> scheduleFixedRate( ThreadType type,
|
||||
Runnable command,
|
||||
long period,
|
||||
TimeUnit unit )
|
||||
throws RejectedExecutionException
|
||||
{
|
||||
if( mExecutor == null )
|
||||
{
|
||||
mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
|
||||
new NamingThreadFactory( "sdrtrunk" ) );
|
||||
}
|
||||
|
||||
ScheduledFuture<?> task =
|
||||
mExecutor.scheduleAtFixedRate( command, 0, period, unit );
|
||||
|
||||
@@ -65,12 +61,6 @@ public class ThreadPoolManager
|
||||
public void scheduleOnce( Runnable command, long delay, TimeUnit unit )
|
||||
throws RejectedExecutionException
|
||||
{
|
||||
if( mExecutor == null )
|
||||
{
|
||||
mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
|
||||
new NamingThreadFactory( "sdrtrunk" ) );
|
||||
}
|
||||
|
||||
mExecutor.schedule( command, delay, unit );
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user