mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
Rebase channel/alias model redesign branch on current master
(v0.2.0.beta5)
This commit is contained in:
@@ -5,9 +5,9 @@
|
||||
|
||||
<!-- version properties -->
|
||||
<property name="major" value="0"/>
|
||||
<property name="minor" value="2"/>
|
||||
<property name="minor" value="3"/>
|
||||
<property name="patch" value="0"/>
|
||||
<property name="release" value="beta.2"/>
|
||||
<property name="release" value="alpha.1"/>
|
||||
|
||||
<!-- Build properties -->
|
||||
<property name="classes" location="../classes"/>
|
||||
|
||||
@@ -34,7 +34,7 @@ public class Alias
|
||||
private String mName;
|
||||
private int mColor;
|
||||
private String mIconName;
|
||||
private ArrayList<AliasID> mAliasIDs = new ArrayList<AliasID>();
|
||||
private List<AliasID> mAliasIDs = new ArrayList<AliasID>();
|
||||
private List<AliasAction> mAliasActions = new ArrayList<AliasAction>();
|
||||
|
||||
public Alias()
|
||||
@@ -121,7 +121,7 @@ public class Alias
|
||||
mIconName = iconName;
|
||||
}
|
||||
|
||||
public ArrayList<AliasID> getId()
|
||||
public List<AliasID> getId()
|
||||
{
|
||||
return mAliasIDs;
|
||||
}
|
||||
|
||||
@@ -36,10 +36,6 @@ public class TalkgroupIDEditor extends JPanel implements ActionListener
|
||||
private TalkgroupIDNode mTalkgroupIDNode;
|
||||
|
||||
private JTextField mTextTalkgroupID;
|
||||
<<<<<<< Upstream, based on origin/master
|
||||
=======
|
||||
private JLabel mLabelAudio;
|
||||
>>>>>>> 92e63f7 Adds AliasModel and AliasEvent and support for conversion from legacy playlist format.
|
||||
|
||||
private String mHelpText = "Enter a formatted talkgroup identifier.\n\n"
|
||||
+ "P25 Talkgroup Format: hexadecimal value, 4 characters for "
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,6 @@ public class ConfigurationControllerModel extends DefaultTreeModel
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private SettingsManager mSettingsManager;
|
||||
private SourceManager mSourceManager;
|
||||
private ChannelModel mChannelModel;
|
||||
|
||||
/**
|
||||
* Implements a merged system controller and (tree) model. The user will
|
||||
@@ -80,8 +79,6 @@ public class ConfigurationControllerModel extends DefaultTreeModel
|
||||
{
|
||||
super( new BaseNode( null ) );
|
||||
|
||||
mChannelModel = channelModel;
|
||||
|
||||
mChannelModel = channelModel;
|
||||
mChannelProcessingManager = channelProcessingManager;
|
||||
mPlaylistManager = playlistManager;
|
||||
|
||||
@@ -74,11 +74,13 @@ public class ThreadPoolManager
|
||||
mExecutor.schedule( command, delay, unit );
|
||||
}
|
||||
|
||||
public void cancel( ScheduledFuture<?> task )
|
||||
public boolean cancel( ScheduledFuture<?> task )
|
||||
{
|
||||
task.cancel( true );
|
||||
boolean success = task.cancel( true );
|
||||
|
||||
mTasks.remove( task );
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
public int getTaskCount( ThreadType type )
|
||||
|
||||
@@ -78,14 +78,13 @@ public class Channel extends Configuration implements IFrequencyChangeProcessor
|
||||
|
||||
private boolean mEnabled;
|
||||
private boolean mSelected;
|
||||
|
||||
private TunerChannel mTunerChannel = null;
|
||||
|
||||
private ChannelType mChannelType = ChannelType.STANDARD;
|
||||
|
||||
private int mChannelID;
|
||||
private int mChannelFrequencyCorrection = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Channel represents a complete set of configurations needed to setup and
|
||||
* manage a processing chain and/or manage as a set of business objects that
|
||||
@@ -112,8 +111,6 @@ public class Channel extends Configuration implements IFrequencyChangeProcessor
|
||||
/**
|
||||
* Constructs a new standard channel with a default name of "Channel"
|
||||
*/
|
||||
|
||||
//TODO: remove this
|
||||
public Channel()
|
||||
{
|
||||
mChannelID = UNIQUE_ID++;
|
||||
@@ -163,7 +160,6 @@ public class Channel extends Configuration implements IFrequencyChangeProcessor
|
||||
return channel;
|
||||
}
|
||||
|
||||
//TODO: remove this
|
||||
/**
|
||||
* Unique identifier for this channel. Value is transient (not persisted).
|
||||
*/
|
||||
@@ -442,4 +438,25 @@ public class Channel extends Configuration implements IFrequencyChangeProcessor
|
||||
return tunerChannel != null &&
|
||||
tunerChannel.overlaps( minimum, maximum );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to make the current channel frequency correction value
|
||||
* available for use outside of a constructed processing chain while maintaining
|
||||
* linkage to the source channel. This hack allows the correction value to
|
||||
* be used to visually show in the spectral display.
|
||||
*/
|
||||
@Override
|
||||
public void frequencyChanged( FrequencyChangeEvent event )
|
||||
{
|
||||
if( event.getEvent() == Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE )
|
||||
{
|
||||
mChannelFrequencyCorrection = event.getValue().intValue();
|
||||
}
|
||||
}
|
||||
|
||||
@XmlTransient
|
||||
public int getChannelFrequencyCorrection()
|
||||
{
|
||||
return mChannelFrequencyCorrection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ public class SDRTrunk
|
||||
AliasActionManager aliasActionManager = new AliasActionManager( threadPoolManager );
|
||||
channelProcessingManager.addMessageListener( aliasActionManager );
|
||||
|
||||
AudioManager audioManager = new AudioManager( threadPoolManager );
|
||||
AudioManager audioManager = new AudioManager( threadPoolManager, sourceManager.getMixerManager() );
|
||||
channelProcessingManager.addAudioPacketListener( audioManager );
|
||||
|
||||
MapService mapService = new MapService( mSettingsManager );
|
||||
|
||||
@@ -108,7 +108,8 @@ public class ProcessingChain implements IChannelEventListener
|
||||
private Broadcaster<RealBuffer> mUnFilteredRealBufferBroadcaster = new Broadcaster<>();
|
||||
private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>();
|
||||
|
||||
// private ThreadPoolManager mThreadPoolManager;
|
||||
private ThreadPoolManager mThreadPoolManager;
|
||||
private ScheduledFuture<?> mBufferProcessingTask;
|
||||
private AtomicBoolean mRunning = new AtomicBoolean();
|
||||
|
||||
protected Source mSource;
|
||||
@@ -135,11 +136,6 @@ public class ProcessingChain implements IChannelEventListener
|
||||
addCallEventListener( mCallEventModel );
|
||||
}
|
||||
|
||||
public MessageActivityModel getMessageActivityModel()
|
||||
{
|
||||
return mMessageActivityModel;
|
||||
}
|
||||
|
||||
public CallEventModel getCallEventModel()
|
||||
{
|
||||
return mCallEventModel;
|
||||
@@ -150,6 +146,11 @@ public class ProcessingChain implements IChannelEventListener
|
||||
return mChannelState;
|
||||
}
|
||||
|
||||
public MessageActivityModel getMessageActivityModel()
|
||||
{
|
||||
return mMessageActivityModel;
|
||||
}
|
||||
|
||||
public void setMessageActivityModel( MessageActivityModel model )
|
||||
{
|
||||
mMessageActivityModel = model;
|
||||
@@ -526,6 +527,12 @@ public class ProcessingChain implements IChannelEventListener
|
||||
mSource = null;
|
||||
}
|
||||
|
||||
if( mBufferProcessingTask != null )
|
||||
{
|
||||
mThreadPoolManager.cancel( mBufferProcessingTask );
|
||||
mBufferProcessingTask = null;
|
||||
}
|
||||
|
||||
/* Stop each of the modules */
|
||||
for( Module module: mModules )
|
||||
{
|
||||
|
||||
@@ -211,11 +211,12 @@ public class DecoderFactory
|
||||
|
||||
if( channelType == ChannelType.STANDARD )
|
||||
{
|
||||
long timeout = mptConfig.getCallTimeout() * 1000; //convert to milliseconds
|
||||
|
||||
modules.add( new TrafficChannelManager( channelModel, channelProcessingManager,
|
||||
decodeConfig, recordConfig, channel.getSystem(),
|
||||
channel.getSite(), ( aliasList != null ? aliasList.getName() : null ), timeout,
|
||||
mptConfig.getTrafficChannelPoolSize() ) );
|
||||
decodeConfig, recordConfig, channel.getSystem(),
|
||||
channel.getSite(), ( aliasList != null ? aliasList.getName() : null ), timeout,
|
||||
mptConfig.getTrafficChannelPoolSize() ) );
|
||||
}
|
||||
|
||||
modules.add( new FMDemodulatorModule( iqPass, iqStop ) );
|
||||
@@ -256,7 +257,7 @@ public class DecoderFactory
|
||||
if( channelType == ChannelType.STANDARD )
|
||||
{
|
||||
//Set call timeout to 3 seconds for P25 traffic channels
|
||||
timeout = 3000;
|
||||
long timeout = 3000;
|
||||
|
||||
modules.add( new TrafficChannelManager( channelModel,
|
||||
channelProcessingManager, decodeConfig,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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
|
||||
@@ -26,9 +26,11 @@ import java.util.TreeSet;
|
||||
|
||||
import message.Message;
|
||||
import module.decode.DecoderType;
|
||||
import module.decode.config.DecodeConfiguration;
|
||||
import module.decode.event.CallEvent;
|
||||
import module.decode.event.CallEvent.CallEventType;
|
||||
import module.decode.mpt1327.MPT1327Message.IdentType;
|
||||
import module.decode.state.ChangeChannelTimeoutEvent;
|
||||
import module.decode.state.ChangedAttribute;
|
||||
import module.decode.state.DecoderState;
|
||||
import module.decode.state.DecoderStateEvent;
|
||||
@@ -41,6 +43,8 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import alias.Alias;
|
||||
import alias.AliasList;
|
||||
import audio.metadata.Metadata;
|
||||
import audio.metadata.MetadataType;
|
||||
import controller.channel.Channel.ChannelType;
|
||||
import controller.channel.map.ChannelMap;
|
||||
|
||||
@@ -60,15 +64,18 @@ public class MPT1327DecoderState extends DecoderState
|
||||
private ChannelMap mChannelMap;
|
||||
|
||||
private long mFrequency = 0;
|
||||
private long mCallTimeout;
|
||||
|
||||
public MPT1327DecoderState( AliasList aliasList, ChannelMap channelMap,
|
||||
ChannelType channelType )
|
||||
public MPT1327DecoderState( AliasList aliasList,
|
||||
ChannelMap channelMap,
|
||||
ChannelType channelType,
|
||||
long callTimeout )
|
||||
{
|
||||
super( aliasList );
|
||||
|
||||
mChannelMap = channelMap;
|
||||
|
||||
mChannelType = channelType;
|
||||
mCallTimeout = callTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -252,8 +259,42 @@ public class MPT1327DecoderState extends DecoderState
|
||||
|
||||
/* Traffic Channel Events */
|
||||
case CLEAR:
|
||||
mChannelNumber = mpt.getChannel();
|
||||
|
||||
broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) );
|
||||
break;
|
||||
case MAINT:
|
||||
broadcast( new DecoderStateEvent( this, Event.END, State.END ) );
|
||||
if( mChannelType == ChannelType.STANDARD )
|
||||
{
|
||||
/**
|
||||
* When we receive a MAINT message and we're configured
|
||||
* as a standard channel, we need to apply the call
|
||||
* timeout specified by the user. Otherwise we'll
|
||||
* be using the shorter default call timeout
|
||||
*/
|
||||
broadcast( new ChangeChannelTimeoutEvent( this,
|
||||
mChannelType, mCallTimeout ) );
|
||||
|
||||
if( mCurrentCallEvent == null )
|
||||
{
|
||||
mCurrentCallEvent = new MPT1327CallEvent.Builder( CallEventType.CALL )
|
||||
.aliasList( getAliasList() )
|
||||
.channel( String.valueOf( mChannelNumber ) )
|
||||
.details( "MONITORED TRAFFIC CHANNEL" )
|
||||
.frequency( mFrequency )
|
||||
.to( mpt.getToID() )
|
||||
.build();
|
||||
|
||||
broadcast( mCurrentCallEvent );
|
||||
}
|
||||
|
||||
broadcast(new Metadata(MetadataType.TO,
|
||||
mpt.getToID(), mpt.getToIDAlias(), true));
|
||||
|
||||
broadcast( new DecoderStateEvent( this, Event.START, State.CALL ) );
|
||||
|
||||
setToTalkgroup( mpt.getToID() );
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -276,6 +317,25 @@ public class MPT1327DecoderState extends DecoderState
|
||||
|
||||
mToTalkgroup = null;
|
||||
broadcast( ChangedAttribute.TO_TALKGROUP );
|
||||
|
||||
/**
|
||||
* If this is a standard channel, reset the fade timeout to the default
|
||||
* timeout. Once processing is underway, if we get a MAINT message,
|
||||
* this indicates we're processing a traffic channel as a standard
|
||||
* channel, so we'll issue a different call timeout then.
|
||||
*/
|
||||
if( mChannelType == ChannelType.STANDARD )
|
||||
{
|
||||
broadcast( new ChangeChannelTimeoutEvent( this, mChannelType,
|
||||
DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS * 1000 ) );
|
||||
|
||||
if( mCurrentCallEvent != null )
|
||||
{
|
||||
mCurrentCallEvent.end();
|
||||
broadcast( mCurrentCallEvent );
|
||||
mCurrentCallEvent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getSite()
|
||||
@@ -552,4 +612,4 @@ public class MPT1327DecoderState extends DecoderState
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,7 +437,7 @@ public class P25DecoderState extends DecoderState
|
||||
{
|
||||
if( tdulc.getOpcode() == LinkControlOpcode.CALL_TERMINATION_OR_CANCELLATION )
|
||||
{
|
||||
broadcast( new DecoderStateEvent( this, Event.END, State.END ) );
|
||||
broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) );
|
||||
|
||||
if( mCurrentCallEvent != null )
|
||||
{
|
||||
@@ -1057,7 +1057,7 @@ public class P25DecoderState extends DecoderState
|
||||
}
|
||||
break;
|
||||
case CALL_TERMINATION_OR_CANCELLATION:
|
||||
broadcast( new DecoderStateEvent( this, Event.END, State.END ) );
|
||||
broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) );
|
||||
mCurrentCallEvent = null;
|
||||
|
||||
if( !( ldu instanceof CallTermination ) )
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
@@ -17,20 +17,14 @@
|
||||
******************************************************************************/
|
||||
package module.decode.state;
|
||||
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import module.Module;
|
||||
import module.decode.config.DecodeConfiguration;
|
||||
import module.decode.event.CallEvent;
|
||||
import module.decode.event.ICallEventProvider;
|
||||
import module.decode.state.DecoderStateEvent.Event;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sample.Listener;
|
||||
import audio.metadata.IMetadataProvider;
|
||||
import audio.metadata.Metadata;
|
||||
import audio.metadata.MetadataReset;
|
||||
@@ -40,38 +34,13 @@ import audio.squelch.SquelchState;
|
||||
import controller.ThreadPoolManager;
|
||||
import controller.ThreadPoolManager.ThreadType;
|
||||
import controller.channel.Channel.ChannelType;
|
||||
import module.Module;
|
||||
import module.decode.config.DecodeConfiguration;
|
||||
import module.decode.event.CallEvent;
|
||||
import module.decode.event.ICallEventProvider;
|
||||
import module.decode.state.DecoderStateEvent.Event;
|
||||
import sample.Listener;
|
||||
|
||||
/**
|
||||
* Channel state tracks the overall state of all processing modules and decoders
|
||||
* configured for the channel and provides squelch control events.
|
||||
*
|
||||
* Primary channel state provides a state machine for tracking decode events
|
||||
* during a voice or data call through it's life cycle of CALL/DATA, FADE, END,
|
||||
* and then back to an IDLE state. This class also provides squelch control
|
||||
* to support any audio processing.
|
||||
*
|
||||
* State Descriptions:
|
||||
* IDLE No voice or data call activity
|
||||
* CALL/DATA A voice or data call has started, or is continuing
|
||||
* CONTROL Control channel
|
||||
* FADE The phase after a voice or data call when either an explicit
|
||||
* call end has been received, or when no new signalling updates
|
||||
* have been received, and the fade timer has expired. This phase
|
||||
* allows for gui updates to signal to the user that the call is
|
||||
* ended, while continuing to display the call details for the user
|
||||
* END The phase after the voice or data call FADE, when the call is
|
||||
* now over, and the gui should be reset to IDLE.
|
||||
*
|
||||
* When a call is started, invoke mTimerService.startFadeTimer() to start the
|
||||
* call fade monitoring timer process that will automatically signal a call end
|
||||
* once no more call signalling has been detected after the fade timeout has
|
||||
* occurred, by invoking the fade() method.
|
||||
*
|
||||
* When a call ends, invoke the mTimerService.stopFadeTimer() and then invoke
|
||||
* the mTimerService.startResetTimer(). Once the reset timeout occurs,
|
||||
* the reset() method will be called, allowing the channel state to perform
|
||||
* any reset actions, like clearing status and id labels on any view panels.
|
||||
*/
|
||||
public class ChannelState extends Module implements ICallEventProvider,
|
||||
IChangedAttributeProvider, IDecoderStateEventListener,
|
||||
IDecoderStateEventProvider, IMetadataProvider, ISquelchStateProvider
|
||||
@@ -83,8 +52,6 @@ public class ChannelState extends Module implements ICallEventProvider,
|
||||
public static final long RESET_TIMEOUT_DELAY = 2000;
|
||||
|
||||
private State mState = State.IDLE;
|
||||
private long mTrafficChannelTimeout =
|
||||
DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS;
|
||||
|
||||
private Listener<CallEvent> mCallEventListener;
|
||||
private Listener<ChangedAttribute> mChangedAttributeListener;
|
||||
@@ -97,12 +64,15 @@ public class ChannelState extends Module implements ICallEventProvider,
|
||||
private boolean mSquelchLocked = false;
|
||||
private boolean mSelected = false;
|
||||
|
||||
private long mStandardChannelFadeTimeout = FADE_TIMEOUT_DELAY;
|
||||
private long mTrafficChannelFadeTimeout =
|
||||
DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS * 1000;
|
||||
private long mFadeTimeout;
|
||||
private long mResetTimeout;
|
||||
private long mEndTimeout;
|
||||
|
||||
private StateMonitor mStateMonitor = new StateMonitor();
|
||||
private AtomicBoolean mMonitoring = new AtomicBoolean();
|
||||
private ScheduledFuture<?> mRunningMonitor;
|
||||
private ScheduledFuture<?> mStateMonitorFuture;
|
||||
|
||||
private ThreadPoolManager mThreadPoolManager;
|
||||
private ChannelType mChannelType;
|
||||
@@ -110,32 +80,97 @@ public class ChannelState extends Module implements ICallEventProvider,
|
||||
private TrafficChannelStatusListener mTrafficChannelStatusListener;
|
||||
private CallEvent mTrafficChannelCallEvent;
|
||||
|
||||
/**
|
||||
* Channel state tracks the overall state of all processing modules and decoders
|
||||
* configured for the channel and provides squelch control and decoder state
|
||||
* reset events.
|
||||
*
|
||||
* Uses a state enumeration that defines allowable channel state transitions in
|
||||
* order to track a call or data decode event from start to finish. Uses a
|
||||
* timer to monitor for inactivity and to provide a FADE period that indicates
|
||||
* to the user that the activity has stopped while continuing to provide details
|
||||
* about the call, before the state is reset to IDLE.
|
||||
*
|
||||
* Since this class is multi-threaded between decoder events and the internal
|
||||
* timer that monitors for inactivity, this class uses the mState object for
|
||||
* thread synchronization.
|
||||
*
|
||||
* State Descriptions:
|
||||
* IDLE Normal state. No voice or data call activity
|
||||
* CALL/DATA/ENCRYPTED/CONTROL
|
||||
* Decoding states.
|
||||
* FADE The phase after a voice or data call when either an explicit
|
||||
* call end has been received, or when no new signalling updates
|
||||
* have been received, and the fade timer has expired. This phase
|
||||
* allows for gui updates to signal to the user that the call is
|
||||
* ended, while continuing to display the call details for the user
|
||||
* TEARDOWN Indicates a traffic channel that will be torn down for reuse.
|
||||
*/
|
||||
public ChannelState( ThreadPoolManager manager, ChannelType channelType )
|
||||
{
|
||||
mThreadPoolManager = manager;
|
||||
mChannelType = channelType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this channel state and prepares it for reuse.
|
||||
*/
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
broadcast( new DecoderStateEvent( this, Event.RESET, State.IDLE ) );
|
||||
broadcast( new MetadataReset() );
|
||||
|
||||
mState = State.IDLE;
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
//Start a monitoring task to check for FADE and END event timeouts
|
||||
if( mMonitoring.compareAndSet( false, true ) )
|
||||
{
|
||||
if( mStateMonitorFuture == null && mStateMonitor != null )
|
||||
{
|
||||
try
|
||||
{
|
||||
mStateMonitorFuture = mThreadPoolManager.scheduleFixedRate(
|
||||
ThreadType.DECODER, mStateMonitor, 20, TimeUnit.MILLISECONDS );
|
||||
}
|
||||
catch( RejectedExecutionException ree )
|
||||
{
|
||||
mLog.error( "state monitor scheduled task rejected", ree );
|
||||
mMonitoring.set( false );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
mSquelchLocked = false;
|
||||
|
||||
//Shutdown the state montoring task
|
||||
if( mMonitoring.compareAndSet( true, false ) )
|
||||
{
|
||||
if( mStateMonitorFuture != null )
|
||||
{
|
||||
boolean success = mThreadPoolManager.cancel( mStateMonitorFuture );
|
||||
|
||||
if( !success )
|
||||
{
|
||||
mLog.error("Couldn't stop monitoring scheduled future" );
|
||||
}
|
||||
}
|
||||
|
||||
mStateMonitorFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
stopMonitor();
|
||||
|
||||
mCallEventListener = null;
|
||||
mChangedAttributeListener = null;
|
||||
mDecoderStateListener = null;
|
||||
@@ -144,9 +179,34 @@ public class ChannelState extends Module implements ICallEventProvider,
|
||||
mStateMonitor = null;
|
||||
}
|
||||
|
||||
private boolean isStandardChannel()
|
||||
{
|
||||
return mChannelType == ChannelType.STANDARD;
|
||||
}
|
||||
|
||||
private boolean isTrafficChannel()
|
||||
{
|
||||
return mChannelType == ChannelType.TRAFFIC;
|
||||
}
|
||||
|
||||
public void setStandardChannelTimeout( long milliseconds )
|
||||
{
|
||||
mStandardChannelFadeTimeout = milliseconds;
|
||||
|
||||
if( mChannelType == ChannelType.STANDARD )
|
||||
{
|
||||
mFadeTimeout = mStandardChannelFadeTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
public void setTrafficChannelTimeout( long milliseconds )
|
||||
{
|
||||
mTrafficChannelTimeout = milliseconds;
|
||||
mTrafficChannelFadeTimeout = milliseconds;
|
||||
|
||||
if( mChannelType == ChannelType.TRAFFIC )
|
||||
{
|
||||
mFadeTimeout = mTrafficChannelFadeTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelected( boolean selected )
|
||||
@@ -164,71 +224,18 @@ public class ChannelState extends Module implements ICallEventProvider,
|
||||
return mState;
|
||||
}
|
||||
|
||||
private boolean isIdleOrEndingState()
|
||||
{
|
||||
return mState == State.FADE ||
|
||||
mState == State.END ||
|
||||
mState == State.IDLE;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts the state monitor running when we're not in IDLE state. Calls to
|
||||
* start an already started monitor are ignored.
|
||||
*/
|
||||
private void startMonitor()
|
||||
{
|
||||
if( mMonitoring.compareAndSet( false, true ) )
|
||||
{
|
||||
if( mRunningMonitor == null && mStateMonitor != null )
|
||||
{
|
||||
mRunningMonitor = mThreadPoolManager.scheduleFixedRate(
|
||||
ThreadType.DECODER, mStateMonitor, 20, TimeUnit.MILLISECONDS );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuntimeException( "Channel state monitor's scheduled "
|
||||
+ "future pointer was not null" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the state monitor when we're in an IDLE state. Calls to stop an
|
||||
* already stopped monitor are ignored.
|
||||
*/
|
||||
private void stopMonitor()
|
||||
{
|
||||
if( mMonitoring.compareAndSet( true, false ) )
|
||||
{
|
||||
if( mRunningMonitor != null )
|
||||
{
|
||||
mThreadPoolManager.cancel( mRunningMonitor );
|
||||
}
|
||||
|
||||
mRunningMonitor = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the fade timeout threshold to the current time plus delay
|
||||
*/
|
||||
private void updateFadeTimeout()
|
||||
{
|
||||
/* Standard channel uses the FADE_TIMEOUT_DELAY and traffic channels
|
||||
* use the TRAFFIC_FADE_TIMEOUT_DELAY. Some traffic channels (e.g. MPT)
|
||||
* don't have signaling on the traffic channel that can be used to
|
||||
* update the fade timeout, so we set the timeout value larger to ensure
|
||||
* the channel doesn't fade before the end of the call. */
|
||||
long current = System.currentTimeMillis();
|
||||
|
||||
long timeout = current +
|
||||
( mChannelType == ChannelType.STANDARD ? FADE_TIMEOUT_DELAY :
|
||||
mTrafficChannelTimeout );
|
||||
|
||||
if( timeout > mFadeTimeout )
|
||||
if( isTrafficChannel() )
|
||||
{
|
||||
mFadeTimeout = timeout;
|
||||
mFadeTimeout = System.currentTimeMillis() + mTrafficChannelFadeTimeout;
|
||||
}
|
||||
else
|
||||
{
|
||||
mFadeTimeout = System.currentTimeMillis() + mStandardChannelFadeTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,13 +244,13 @@ public class ChannelState extends Module implements ICallEventProvider,
|
||||
*/
|
||||
private void updateResetTimeout()
|
||||
{
|
||||
if( mChannelType == ChannelType.TRAFFIC )
|
||||
if( isTrafficChannel() )
|
||||
{
|
||||
mResetTimeout = System.currentTimeMillis();
|
||||
mEndTimeout = System.currentTimeMillis();
|
||||
}
|
||||
else
|
||||
{
|
||||
mResetTimeout = System.currentTimeMillis() + RESET_TIMEOUT_DELAY;
|
||||
mEndTimeout = System.currentTimeMillis() + RESET_TIMEOUT_DELAY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,120 +292,120 @@ public class ChannelState extends Module implements ICallEventProvider,
|
||||
*/
|
||||
protected void setState( State state )
|
||||
{
|
||||
if( mState == state )
|
||||
synchronized( mState )
|
||||
{
|
||||
switch( state )
|
||||
if( state == mState )
|
||||
{
|
||||
case CALL:
|
||||
case CONTROL:
|
||||
case DATA:
|
||||
case ENCRYPTED:
|
||||
updateFadeTimeout();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if( mState.canChangeTo( state ) )
|
||||
{
|
||||
switch( state )
|
||||
{
|
||||
case CONTROL:
|
||||
//Don't allow traffic channels to be control channels,
|
||||
//otherwise they won't get call tear down
|
||||
if( mChannelType != ChannelType.TRAFFIC )
|
||||
{
|
||||
broadcast( SquelchState.SQUELCH );
|
||||
updateFadeTimeout();
|
||||
startMonitor();
|
||||
mState = state;
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
}
|
||||
break;
|
||||
case DATA:
|
||||
case ENCRYPTED:
|
||||
broadcast( SquelchState.SQUELCH );
|
||||
updateFadeTimeout();
|
||||
startMonitor();
|
||||
mState = state;
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
break;
|
||||
case CALL:
|
||||
broadcast( SquelchState.UNSQUELCH );
|
||||
updateFadeTimeout();
|
||||
startMonitor();
|
||||
mState = state;
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
break;
|
||||
case FADE:
|
||||
broadcast( SquelchState.SQUELCH );
|
||||
updateResetTimeout();
|
||||
startMonitor();
|
||||
mState = state;
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
break;
|
||||
case END:
|
||||
broadcast( SquelchState.SQUELCH );
|
||||
broadcast( new DecoderStateEvent( this, Event.RESET, State.IDLE ) );
|
||||
broadcast( new MetadataReset() );
|
||||
mState = state;
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
break;
|
||||
case IDLE:
|
||||
broadcast( SquelchState.SQUELCH );
|
||||
stopMonitor();
|
||||
|
||||
if( mChannelType == ChannelType.TRAFFIC )
|
||||
{
|
||||
if( mTrafficChannelStatusListener != null )
|
||||
{
|
||||
mTrafficChannelStatusListener.callEnd();
|
||||
mTrafficChannelStatusListener = null;
|
||||
}
|
||||
|
||||
if( mTrafficChannelCallEvent != null )
|
||||
{
|
||||
mTrafficChannelCallEvent.end();
|
||||
|
||||
broadcast( mTrafficChannelCallEvent );
|
||||
mTrafficChannelCallEvent = null;
|
||||
}
|
||||
}
|
||||
|
||||
mState = state;
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class StateMonitor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if( getState() == State.FADE )
|
||||
{
|
||||
if( mResetTimeout <= System.currentTimeMillis() )
|
||||
if( State.CALL_STATES.contains( state ) )
|
||||
{
|
||||
setState( State.END );
|
||||
updateFadeTimeout();
|
||||
}
|
||||
}
|
||||
else if( getState() == State.END )
|
||||
else if( state != mState && mState.canChangeTo( state ) )
|
||||
{
|
||||
setState( State.IDLE );
|
||||
switch( state )
|
||||
{
|
||||
case CONTROL:
|
||||
//Don't allow traffic channels to be control channels,
|
||||
//otherwise they can't transition to call tear down
|
||||
if( isStandardChannel() )
|
||||
{
|
||||
broadcast( SquelchState.SQUELCH );
|
||||
updateFadeTimeout();
|
||||
mState = state;
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
}
|
||||
break;
|
||||
case DATA:
|
||||
case ENCRYPTED:
|
||||
broadcast( SquelchState.SQUELCH );
|
||||
updateFadeTimeout();
|
||||
mState = state;
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
break;
|
||||
case CALL:
|
||||
broadcast( SquelchState.UNSQUELCH );
|
||||
updateFadeTimeout();
|
||||
mState = state;
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
break;
|
||||
case FADE:
|
||||
processFadeState();
|
||||
break;
|
||||
case IDLE:
|
||||
processIdleState();
|
||||
break;
|
||||
case TEARDOWN:
|
||||
processTeardownState();
|
||||
break;
|
||||
case RESET:
|
||||
mState = State.IDLE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( mFadeTimeout <= System.currentTimeMillis() )
|
||||
{
|
||||
setState( State.FADE );
|
||||
}
|
||||
// mLog.debug( "Can't change from [" + mState + "] to [" + state + "]" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state and processes related actions
|
||||
*/
|
||||
private void processFadeState()
|
||||
{
|
||||
updateResetTimeout();
|
||||
mState = State.FADE;
|
||||
|
||||
broadcast( SquelchState.SQUELCH );
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
}
|
||||
|
||||
private void processIdleState()
|
||||
{
|
||||
broadcast( SquelchState.SQUELCH );
|
||||
|
||||
if( mState == State.FADE )
|
||||
{
|
||||
broadcast( new DecoderStateEvent( this, Event.RESET, State.IDLE ) );
|
||||
broadcast( new MetadataReset() );
|
||||
}
|
||||
|
||||
mState = State.IDLE;
|
||||
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
}
|
||||
|
||||
private void processTeardownState()
|
||||
{
|
||||
broadcast( SquelchState.SQUELCH );
|
||||
|
||||
mState = State.TEARDOWN;
|
||||
|
||||
broadcast( ChangedAttribute.CHANNEL_STATE );
|
||||
|
||||
if( mTrafficChannelCallEvent != null )
|
||||
{
|
||||
mTrafficChannelCallEvent.end();
|
||||
|
||||
broadcast( mTrafficChannelCallEvent );
|
||||
mTrafficChannelCallEvent = null;
|
||||
}
|
||||
|
||||
if( mTrafficChannelStatusListener != null )
|
||||
{
|
||||
mTrafficChannelStatusListener.callEnd();
|
||||
mTrafficChannelStatusListener = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error( "Traffic channel status listener was null - couldn't "
|
||||
+ "teardown the channel");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the call event to the registered listener
|
||||
@@ -590,35 +597,95 @@ public class ChannelState extends Module implements ICallEventProvider,
|
||||
broadcast( SquelchState.UNSQUELCH );
|
||||
mSquelchLocked = true;
|
||||
break;
|
||||
case CHANGE_CALL_TIMEOUT:
|
||||
if( event instanceof ChangeChannelTimeoutEvent )
|
||||
{
|
||||
ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event;
|
||||
|
||||
if( timeout.getChannelType() == ChannelType.STANDARD )
|
||||
{
|
||||
setStandardChannelTimeout( timeout.getCallTimeout() );
|
||||
}
|
||||
else
|
||||
{
|
||||
setTrafficChannelTimeout( timeout.getCallTimeout() );
|
||||
}
|
||||
}
|
||||
case CONTINUATION:
|
||||
if( isIdleOrEndingState() )
|
||||
{
|
||||
setState( event.getState() );
|
||||
}
|
||||
else
|
||||
{
|
||||
updateFadeTimeout();
|
||||
}
|
||||
break;
|
||||
case DECODE:
|
||||
if( getState() == State.IDLE )
|
||||
case START:
|
||||
if( State.CALL_STATES.contains( event.getState() ) )
|
||||
{
|
||||
setState( event.getState() );
|
||||
}
|
||||
break;
|
||||
case END:
|
||||
setState( State.FADE );
|
||||
if( isTrafficChannel() )
|
||||
{
|
||||
setState( State.TEARDOWN );
|
||||
}
|
||||
else
|
||||
{
|
||||
setState( State.FADE );
|
||||
}
|
||||
break;
|
||||
case RESET:
|
||||
/* Channel State does not respond to reset events */
|
||||
break;
|
||||
case START:
|
||||
setState( event.getState() );
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors decoder state events to automatically transition the channel
|
||||
* state to IDLE (standard channel) or to TEARDOWN (traffic channel) when
|
||||
* decoding stops or the monitored channel returns to a no signal state.
|
||||
*
|
||||
* Provides a FADE transition state to allow for momentary decoding dropouts
|
||||
* and to allow the user access to call details for a fade period upon
|
||||
* call end.
|
||||
*/
|
||||
public class StateMonitor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
synchronized( mState )
|
||||
{
|
||||
if( State.CALL_STATES.contains( mState ) &&
|
||||
mFadeTimeout <= System.currentTimeMillis() )
|
||||
{
|
||||
mState = State.FADE;
|
||||
processFadeState();
|
||||
}
|
||||
else if( mState == State.FADE &&
|
||||
mEndTimeout <= System.currentTimeMillis() )
|
||||
{
|
||||
if( isTrafficChannel() )
|
||||
{
|
||||
processTeardownState();
|
||||
}
|
||||
else
|
||||
{
|
||||
processIdleState();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
mLog.error( "Exception thrown while state monitor is running " +
|
||||
"- state [" + getState() +
|
||||
"] current [" + System.currentTimeMillis() +
|
||||
"] mResetTimeout [" + mEndTimeout +
|
||||
"] mFadeTimeout [" + mFadeTimeout +
|
||||
"]", e );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,38 +254,8 @@ public class ChannelStatePanel extends JPanel
|
||||
|
||||
Paint p = null;
|
||||
|
||||
final Channel channel = mChannel;
|
||||
|
||||
final ChannelState channelState = ( channel == null ? null : channel.getChannelState() );
|
||||
|
||||
if( channelState != null )
|
||||
switch( mChannelState.getState() )
|
||||
{
|
||||
<<<<<<< Upstream, based on origin/master
|
||||
switch( channelState.getState() )
|
||||
{
|
||||
case CALL:
|
||||
p = getGradient( mColorTopCall, mColorMiddleCall );
|
||||
break;
|
||||
case CONTROL:
|
||||
p = getGradient( mColorTopControl, mColorMiddleControl );
|
||||
break;
|
||||
case DATA:
|
||||
p = getGradient( mColorTopData, mColorMiddleData );
|
||||
break;
|
||||
case FADE:
|
||||
p = getGradient( mColorTopFade, mColorMiddleFade );
|
||||
break;
|
||||
case IDLE:
|
||||
default:
|
||||
p = getGradient( mColorTopIdle, mColorMiddleIdle );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( p == null )
|
||||
{
|
||||
p = getGradient( mColorTopIdle, mColorMiddleIdle );
|
||||
=======
|
||||
case CALL:
|
||||
p = getGradient( mColorTopCall, mColorMiddleCall );
|
||||
break;
|
||||
@@ -298,13 +268,11 @@ public class ChannelStatePanel extends JPanel
|
||||
case FADE:
|
||||
p = getGradient( mColorTopFade, mColorMiddleFade );
|
||||
break;
|
||||
case END:
|
||||
case IDLE:
|
||||
default:
|
||||
p = getGradient( mColorTopIdle, mColorMiddleIdle );
|
||||
break;
|
||||
>>>>>>> 0e01381 Updates.
|
||||
}
|
||||
}
|
||||
|
||||
g2.setPaint( p );
|
||||
g2.fillRect( 0, 0, getWidth(), getHeight() );
|
||||
|
||||
@@ -56,6 +56,7 @@ public class DecoderStateEvent
|
||||
public enum Event
|
||||
{
|
||||
ALWAYS_UNSQUELCH,
|
||||
CHANGE_CALL_TIMEOUT,
|
||||
CONTINUATION,
|
||||
DECODE,
|
||||
END,
|
||||
@@ -64,4 +65,4 @@ public class DecoderStateEvent
|
||||
START,
|
||||
TRAFFIC_CHANNEL_ALLOCATION;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,27 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package module.decode.state;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* Details the set of states for a channel and the allowable transition states
|
||||
*/
|
||||
public enum State
|
||||
{
|
||||
IDLE( "IDLE" )
|
||||
@@ -7,7 +29,8 @@ public enum State
|
||||
@Override
|
||||
public boolean canChangeTo( State state )
|
||||
{
|
||||
return true;
|
||||
return state != TEARDOWN &&
|
||||
state != RESET;
|
||||
}
|
||||
},
|
||||
CALL( "CALL" )
|
||||
@@ -15,11 +38,11 @@ public enum State
|
||||
@Override
|
||||
public boolean canChangeTo( State state )
|
||||
{
|
||||
return state == CALL ||
|
||||
state == CONTROL ||
|
||||
return state == CONTROL ||
|
||||
state == DATA ||
|
||||
state == ENCRYPTED ||
|
||||
state == FADE;
|
||||
state == FADE ||
|
||||
state == TEARDOWN;
|
||||
}
|
||||
},
|
||||
DATA( "DATA" )
|
||||
@@ -29,9 +52,9 @@ public enum State
|
||||
{
|
||||
return state == CALL ||
|
||||
state == CONTROL ||
|
||||
state == DATA ||
|
||||
state == State.ENCRYPTED ||
|
||||
state == FADE;
|
||||
state == ENCRYPTED ||
|
||||
state == FADE ||
|
||||
state == TEARDOWN;
|
||||
}
|
||||
},
|
||||
ENCRYPTED( "ENCRYPTED" )
|
||||
@@ -39,7 +62,7 @@ public enum State
|
||||
@Override
|
||||
public boolean canChangeTo( State state )
|
||||
{
|
||||
return state == FADE;
|
||||
return state == FADE || state == TEARDOWN;
|
||||
}
|
||||
},
|
||||
CONTROL( "CONTROL" )
|
||||
@@ -47,7 +70,8 @@ public enum State
|
||||
@Override
|
||||
public boolean canChangeTo( State state )
|
||||
{
|
||||
return true;
|
||||
return state == IDLE ||
|
||||
state == FADE;
|
||||
}
|
||||
},
|
||||
FADE( "FADE" )
|
||||
@@ -55,20 +79,36 @@ public enum State
|
||||
@Override
|
||||
public boolean canChangeTo( State state )
|
||||
{
|
||||
return state != FADE; //All states except fade allowed
|
||||
}
|
||||
return state != FADE &&
|
||||
state != RESET;
|
||||
}
|
||||
},
|
||||
END( "END" )
|
||||
RESET( "RESET" )
|
||||
{
|
||||
@Override
|
||||
public boolean canChangeTo( State state )
|
||||
{
|
||||
return state != FADE; //Fade is only disallowed state
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public boolean canChangeTo(State state)
|
||||
{
|
||||
return state == IDLE;
|
||||
}
|
||||
},
|
||||
|
||||
TEARDOWN( "TEARDOWN" )
|
||||
{
|
||||
@Override
|
||||
public boolean canChangeTo(State state)
|
||||
{
|
||||
return state == RESET;
|
||||
}
|
||||
};
|
||||
|
||||
private String mDisplayValue;
|
||||
|
||||
public static final EnumSet<State> CALL_STATES =
|
||||
EnumSet.of( CALL, CONTROL, DATA, ENCRYPTED );
|
||||
|
||||
public static final EnumSet<State> IDLE_STATES =
|
||||
EnumSet.of( IDLE, FADE );
|
||||
|
||||
private State( String displayValue )
|
||||
{
|
||||
mDisplayValue = displayValue;
|
||||
@@ -80,4 +120,4 @@ public enum State
|
||||
{
|
||||
return mDisplayValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public class PlaylistManager implements ChannelEventListener
|
||||
private boolean mPlaylistLoading = false;
|
||||
|
||||
/**
|
||||
* Playlist manager - manages all channel configurationc, channel maps, and
|
||||
* Playlist manager - manages all channel configurations, channel maps, and
|
||||
* alias lists and handles loading or persisting to a playlist.xml file
|
||||
*
|
||||
* Monitors playlist changes to automatically save configuration changes
|
||||
@@ -103,8 +103,11 @@ public class PlaylistManager implements ChannelEventListener
|
||||
|
||||
mPlaylistLoading = true;
|
||||
|
||||
mLog.debug( "Transferring aliases to model" );
|
||||
mAliasModel.addAliases( mPlaylist.getAliases() );
|
||||
mLog.debug( "Transferring channels to model" );
|
||||
mChannelModel.addChannels( mPlaylist.getChannels() );
|
||||
mLog.debug( "Transfer to models is complete" );
|
||||
|
||||
mPlaylistLoading = false;
|
||||
}
|
||||
@@ -126,48 +129,6 @@ public class PlaylistManager implements ChannelEventListener
|
||||
props.get( "playlist.currentfilename", defaultPlaylistFile );
|
||||
|
||||
load( playlistFolder.resolve( playlistFile ) );
|
||||
|
||||
transferPlaylistToModels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel event listener method. Monitors channel events for events that
|
||||
* indicate that the playlist has changed and queues automatic playlist
|
||||
* saving.
|
||||
*/
|
||||
@Override
|
||||
public void channelChanged( ChannelEvent event )
|
||||
{
|
||||
//Only save playlist for changes to standard channels (not traffic)
|
||||
if( event.getChannel().getChannelType() == ChannelType.STANDARD )
|
||||
{
|
||||
switch( event.getEvent() )
|
||||
{
|
||||
case NOTIFICATION_ADD:
|
||||
case NOTIFICATION_CONFIGURATION_CHANGE:
|
||||
case NOTIFICATION_DELETE:
|
||||
case NOTIFICATION_PROCESSING_START:
|
||||
case NOTIFICATION_PROCESSING_STOP:
|
||||
schedulePlaylistSave();
|
||||
break;
|
||||
case NOTIFICATION_ENABLE_REJECTED:
|
||||
case NOTIFICATION_SELECTION_CHANGE:
|
||||
case NOTIFICATION_STATE_RESET:
|
||||
case REQUEST_DELETE:
|
||||
case REQUEST_DESELECT:
|
||||
case REQUEST_DISABLE:
|
||||
case REQUEST_ENABLE:
|
||||
case REQUEST_SELECT:
|
||||
//Do nothing for these event types
|
||||
break;
|
||||
default:
|
||||
//When a new event enum entry is added and received, throw an
|
||||
//exception here to ensure developer adds support for the
|
||||
//use case
|
||||
throw new IllegalArgumentException( "Unrecognized Channel "
|
||||
+ "Event [" + event.getEvent().name() + "]" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -426,7 +387,6 @@ public class PlaylistManager implements ChannelEventListener
|
||||
saveRequired = true;
|
||||
}
|
||||
|
||||
|
||||
//Check for and convert from legacy play list format
|
||||
if( mPlaylist.hasSystemList() || mPlaylist.hasAliasDirectory() )
|
||||
{
|
||||
|
||||
@@ -23,6 +23,14 @@ public class MixerChannelConfiguration
|
||||
return mMixerChannel;
|
||||
}
|
||||
|
||||
public boolean matches( String mixer, String channels )
|
||||
{
|
||||
return mixer != null &&
|
||||
channels != null &&
|
||||
mMixer.getMixerInfo().getName().contentEquals( mixer ) &&
|
||||
mMixerChannel.name().contentEquals( channels );
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* 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
|
||||
@@ -28,7 +28,6 @@ import source.SourceException;
|
||||
import source.tuner.frequency.FrequencyController;
|
||||
import source.tuner.frequency.FrequencyController.Tunable;
|
||||
import source.tuner.frequency.IFrequencyChangeProcessor;
|
||||
import source.tuner.frequency.IFrequencyChangeListener;
|
||||
import controller.ThreadPoolManager;
|
||||
|
||||
public abstract class TunerController implements Tunable
|
||||
@@ -37,11 +36,7 @@ public abstract class TunerController implements Tunable
|
||||
LoggerFactory.getLogger( TunerController.class );
|
||||
|
||||
/* List of currently tuned channels being served to demod channels */
|
||||
<<<<<<< Upstream, based on origin/master
|
||||
private SortedSet<TunerChannel> mTunedChannels = new ConcurrentSkipListSet<>();
|
||||
=======
|
||||
protected ArrayList<TunerChannel> mTunedChannels = new ArrayList<>();
|
||||
>>>>>>> 20e3bee Work in progress. -Started work on new channel editor channel viewer/controller -Removed a number of unused buffer package classes -Moved automatic frequency control to be a module and removed special handling -Added IUnfilteredRealBufferListener & Provider interfaces -Completely removed the ResourceManager class
|
||||
protected FrequencyController mFrequencyController;
|
||||
private int mMiddleUnusable;
|
||||
private double mUsableBandwidthPercentage;
|
||||
|
||||
Reference in New Issue
Block a user