mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
Initial commit of redesigned internal processing and audio management.
Note: Several of the decoders are currently non-functioning in this commit. If you want the latest functioning codebase, please checkout the commit prior to this one. All decoders will eventually be updated with this new architecture. Currently broken: - Logging and recording - Aux decoders (Fleetsync/MDC/LoJack/Tait) - LTR/Passport decoders Changes in this commit: - Module architecture for decoders and components with central provisioning by the DecoderFactory - Transition from Complex sample stream processing to float buffer processing for improved memory management - Redesigned filter structures - to support buffers and streams - Resolved several memory leak issues - Universal sharing of the thread pool manager - Added centralized audio management with call/user prioritization and new gui with mute/volume controls - Embedded audio metadata for eventual streaming support - Redesigned ProcessingChain to support independent re-use - Added new TrafficChannelManager to handle traffic channel allocations - Traffic Channel pool size and Call timeout available in P25 and MPT1327 configs - Redesigned channel and decoder gui components
This commit is contained in:
379
src/audio/AudioManager.java
Normal file
379
src/audio/AudioManager.java
Normal file
@@ -0,0 +1,379 @@
|
||||
package audio;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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;
|
||||
|
||||
import javax.sound.sampled.BooleanControl;
|
||||
import javax.sound.sampled.FloatControl;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sample.Listener;
|
||||
import audio.metadata.AudioMetadata;
|
||||
import audio.output.AudioOutput;
|
||||
import audio.output.MonoAudioOutput;
|
||||
import controller.ThreadPoolManager;
|
||||
import controller.ThreadPoolManager.ThreadType;
|
||||
|
||||
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;
|
||||
|
||||
private LinkedTransferQueue<AudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
|
||||
private Map<Integer,AudioChannelAssignment> mChannelAssignments = new HashMap<>();
|
||||
private int mLowestPrioritySource;
|
||||
|
||||
private List<AudioOutput> mAvailableAudioOutputs = new ArrayList<>();
|
||||
private Map<String,AudioOutput> mChannelMap = new HashMap<>();
|
||||
private List<String> mChannelNames = new ArrayList<>();
|
||||
private AudioOutput mAudioOutput;
|
||||
private boolean mMuted = false;
|
||||
|
||||
private Listener<AudioEvent> mConfigurationChangeListener;
|
||||
|
||||
private ThreadPoolManager mThreadPoolManager;
|
||||
private ScheduledFuture<?> mProcessingTask;
|
||||
|
||||
/**
|
||||
* Handles 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 )
|
||||
{
|
||||
mThreadPoolManager = manager;
|
||||
|
||||
mAudioOutput = new MonoAudioOutput();
|
||||
mAvailableAudioOutputs.add( mAudioOutput );
|
||||
mChannelNames.add( mAudioOutput.getChannelName() );
|
||||
mChannelMap.put( mAudioOutput.getChannelName(), mAudioOutput );
|
||||
|
||||
mProcessingTask = mThreadPoolManager.scheduleFixedRate(
|
||||
ThreadType.AUDIO_PROCESSING, new AudioPacketProcessor(), 15,
|
||||
TimeUnit.MILLISECONDS );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
if( mThreadPoolManager != null && mProcessingTask != null )
|
||||
{
|
||||
mThreadPoolManager.cancel( mProcessingTask );
|
||||
}
|
||||
|
||||
mAudioPacketQueue.clear();
|
||||
|
||||
mThreadPoolManager = null;
|
||||
mProcessingTask = null;
|
||||
|
||||
mAudioOutput.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void receive( AudioPacket packet )
|
||||
{
|
||||
mAudioPacketQueue.add( packet );
|
||||
}
|
||||
|
||||
public class AudioPacketProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if( !mMuted )
|
||||
{
|
||||
|
||||
}
|
||||
removeCompletedChannelAssignments();
|
||||
|
||||
if( mAudioPacketQueue != null )
|
||||
{
|
||||
List<AudioPacket> packets = new ArrayList<AudioPacket>();
|
||||
|
||||
mAudioPacketQueue.drainTo( packets );
|
||||
|
||||
for( AudioPacket packet: packets )
|
||||
{
|
||||
int source = packet.getMetadata().getSource();
|
||||
|
||||
/* 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.getMetadata().getSource(),
|
||||
new AudioChannelAssignment( channel, packet.getMetadata().getPriority() ) );
|
||||
|
||||
updateLowestPriorityAssignment();
|
||||
|
||||
channel.receive( packet );
|
||||
}
|
||||
/* Override the lowest priority assignment */
|
||||
else
|
||||
{
|
||||
if( mLowestPrioritySource > -1 )
|
||||
{
|
||||
int priority = packet.getMetadata().getPriority();
|
||||
|
||||
if( packet.getMetadata().isSelected() ||
|
||||
priority < mChannelAssignments
|
||||
.get( mLowestPrioritySource ).getPriority() )
|
||||
{
|
||||
AudioChannelAssignment assignment =
|
||||
mChannelAssignments.remove( mLowestPrioritySource );
|
||||
|
||||
if( assignment != null )
|
||||
{
|
||||
assignment.setPriority( priority );
|
||||
|
||||
int overrideSource = packet.getMetadata().getSource();
|
||||
|
||||
mChannelAssignments.put( overrideSource, assignment );
|
||||
|
||||
mLowestPrioritySource = overrideSource;
|
||||
|
||||
assignment.getAudioOutput().receive( packet );
|
||||
}
|
||||
else
|
||||
{
|
||||
mLowestPrioritySource = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks each audio channel assignment and removes any completed channels.
|
||||
*/
|
||||
private void removeCompletedChannelAssignments()
|
||||
{
|
||||
if( !mChannelAssignments.isEmpty() )
|
||||
{
|
||||
Iterator<Entry<Integer,AudioChannelAssignment>> it =
|
||||
mChannelAssignments.entrySet().iterator();
|
||||
|
||||
Entry<Integer,AudioChannelAssignment> entry;
|
||||
|
||||
boolean removed = false;
|
||||
|
||||
while( it.hasNext() )
|
||||
{
|
||||
entry = it.next();
|
||||
|
||||
if( entry.getValue().isComplete() )
|
||||
{
|
||||
mAvailableAudioOutputs.add( entry.getValue().getAudioOutput() );
|
||||
entry.getValue().dispose();
|
||||
it.remove();
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if( removed )
|
||||
{
|
||||
updateLowestPriorityAssignment();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the lowest priority channel assignment ... the one that can
|
||||
* be overridden with a higher priority call
|
||||
*/
|
||||
private void updateLowestPriorityAssignment()
|
||||
{
|
||||
int lowestPriority = -1;
|
||||
|
||||
mLowestPrioritySource = -1;
|
||||
|
||||
for( int source: mChannelAssignments.keySet() )
|
||||
{
|
||||
AudioChannelAssignment assignment = mChannelAssignments.get( source );
|
||||
|
||||
if( assignment.getPriority() > lowestPriority )
|
||||
{
|
||||
lowestPriority = assignment.getPriority();
|
||||
|
||||
mLowestPrioritySource = source;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = Long.MAX_VALUE;
|
||||
|
||||
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 = 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:
|
||||
mAudioStoppedTime = Long.MAX_VALUE;
|
||||
break;
|
||||
case AUDIO_STOPPED:
|
||||
mAudioStoppedTime = System.currentTimeMillis();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAudioChannels()
|
||||
{
|
||||
return mChannelNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BooleanControl getMuteControl( String channel )
|
||||
throws AudioException
|
||||
{
|
||||
if( mChannelMap.containsKey( channel ) )
|
||||
{
|
||||
return mChannelMap.get( channel ).getMuteControl();
|
||||
}
|
||||
|
||||
throw new AudioException( "Channel [" + channel + "] is not a valid channel" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMuteControl( String channel )
|
||||
{
|
||||
return mChannelMap.containsKey( channel ) &&
|
||||
mChannelMap.get( channel ) != null &&
|
||||
mChannelMap.get( channel ).getMuteControl() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FloatControl getGainControl( String channel ) throws AudioException
|
||||
{
|
||||
if( mChannelMap.containsKey( channel ) )
|
||||
{
|
||||
return mChannelMap.get( channel ).getGainControl();
|
||||
}
|
||||
|
||||
throw new AudioException( "Channel [" + channel + "] is not a valid channel" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasGainControl( String channel )
|
||||
{
|
||||
return mChannelMap.containsKey( channel ) &&
|
||||
mChannelMap.get( channel ) != null &&
|
||||
mChannelMap.get( channel ).getGainControl() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener to receive audio events, specifically, audio
|
||||
* configuration change events
|
||||
*/
|
||||
@Override
|
||||
public void setConfigurationChangeListener( Listener<AudioEvent> listener )
|
||||
{
|
||||
mConfigurationChangeListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAudioEventListener( String channel, Listener<AudioEvent> listener )
|
||||
throws AudioException
|
||||
{
|
||||
if( mChannelMap.containsKey( channel ) )
|
||||
{
|
||||
mChannelMap.get( channel ).addAudioEventListener( listener );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new AudioException( "Channel [" + channel + "] is not a valid "
|
||||
+ "channel. Audio event listener was not registered" );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAudioEventListener( String channel, Listener<AudioEvent> listener )
|
||||
{
|
||||
if( mChannelMap.containsKey( channel ) )
|
||||
{
|
||||
mChannelMap.get( channel ).removeAudioEventListener( listener );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAudioMetadataListener( String channel,
|
||||
Listener<AudioMetadata> listener )
|
||||
{
|
||||
if( mChannelMap.containsKey( channel ) )
|
||||
{
|
||||
mChannelMap.get( channel ).setAudioMetadataListener( listener );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAudioMetadataListener( String channel )
|
||||
{
|
||||
if( mChannelMap.containsKey( channel ) )
|
||||
{
|
||||
mChannelMap.get( channel ).removeAudioMetadataListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user