diff --git a/build/build.xml b/build/build.xml index 3667c7ff..27ab0551 100644 --- a/build/build.xml +++ b/build/build.xml @@ -5,9 +5,9 @@ - + - + diff --git a/src/alias/Alias.java b/src/alias/Alias.java index 1c6ddb87..7def2d64 100644 --- a/src/alias/Alias.java +++ b/src/alias/Alias.java @@ -34,7 +34,7 @@ public class Alias private String mName; private int mColor; private String mIconName; - private ArrayList mAliasIDs = new ArrayList(); + private List mAliasIDs = new ArrayList(); private List mAliasActions = new ArrayList(); public Alias() @@ -121,7 +121,7 @@ public class Alias mIconName = iconName; } - public ArrayList getId() + public List getId() { return mAliasIDs; } diff --git a/src/alias/id/talkgroup/TalkgroupIDEditor.java b/src/alias/id/talkgroup/TalkgroupIDEditor.java index feec6d2d..1feda866 100644 --- a/src/alias/id/talkgroup/TalkgroupIDEditor.java +++ b/src/alias/id/talkgroup/TalkgroupIDEditor.java @@ -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 " diff --git a/src/audio/AudioManager.java b/src/audio/AudioManager.java index 6fa1a9f7..a8aae418 100644 --- a/src/audio/AudioManager.java +++ b/src/audio/AudioManager.java @@ -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, 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, IAudioController new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null ); private LinkedTransferQueue mAudioPacketQueue = new LinkedTransferQueue<>(); - private Map mChannelAssignments = new HashMap<>(); - private int mLowestPrioritySource; + private Map mChannelConnectionMap = new HashMap<>(); + private List mAudioOutputConnections = new ArrayList<>(); + private AudioOutputConnection mLowestPriorityConnection; + private int mAvailableConnectionCount; - private List mAvailableAudioOutputs = new ArrayList<>(); private Map mAudioOutputMap = new HashMap<>(); - private List mChannelNames = new ArrayList<>(); private Broadcaster 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, 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, IAudioController mAudioPacketQueue.add( packet ); } - public class AudioPacketProcessor implements Runnable - { - @Override - public void run() - { - removeCompletedChannelAssignments(); - - if( mAudioPacketQueue != null ) - { - List packets = new ArrayList(); - - 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> it = - mChannelAssignments.entrySet().iterator(); - - Entry 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 - { - 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, IAudioController { mThreadPoolManager.cancel( mProcessingTask ); } - - endAllAudioAssignments(); - for( Entry 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, 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, IAudioController @Override public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException { - return mMixerChannel; + return mMixerChannelConfiguration; } @Override @@ -449,4 +331,244 @@ public class AudioManager implements Listener, 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 packets = new ArrayList(); + + 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 + { + 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; + } + } + } +} \ No newline at end of file diff --git a/src/audio/output/AudioOutput.java b/src/audio/output/AudioOutput.java index d9506472..31f4300f 100644 --- a/src/audio/output/AudioOutput.java +++ b/src/audio/output/AudioOutput.java @@ -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 +public abstract class AudioOutput implements Listener, LineListener { private final static Logger mLog = LoggerFactory.getLogger( AudioOutput.class ); - private AtomicBoolean mSquelched = new AtomicBoolean( false ); private Listener mAudioMetadataListener; - protected Broadcaster mAudioEventBroadcaster = new Broadcaster<>(); protected LinkedTransferQueue 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 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 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 mBuffer.add( packet ); } } -} +} \ No newline at end of file diff --git a/src/audio/output/MonoAudioOutput.java b/src/audio/output/MonoAudioOutput.java index 76bf2324..f1aad5cd 100644 --- a/src/audio/output/MonoAudioOutput.java +++ b/src/audio/output/MonoAudioOutput.java @@ -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 packets = new ArrayList(); - - 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 packets = new ArrayList(); - 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 ); + } + } +} \ No newline at end of file diff --git a/src/audio/output/StereoAudioOutput.java b/src/audio/output/StereoAudioOutput.java index 81892ca5..8a624d18 100644 --- a/src/audio/output/StereoAudioOutput.java +++ b/src/audio/output/StereoAudioOutput.java @@ -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 packets = new ArrayList(); - - 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 packets = new ArrayList(); - 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 ); + } + } +} \ No newline at end of file diff --git a/src/controller/ConfigurationControllerModel.java b/src/controller/ConfigurationControllerModel.java index 692ee7ee..22d3232c 100644 --- a/src/controller/ConfigurationControllerModel.java +++ b/src/controller/ConfigurationControllerModel.java @@ -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; diff --git a/src/controller/ThreadPoolManager.java b/src/controller/ThreadPoolManager.java index c5527970..c076422a 100644 --- a/src/controller/ThreadPoolManager.java +++ b/src/controller/ThreadPoolManager.java @@ -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 ) diff --git a/src/controller/channel/Channel.java b/src/controller/channel/Channel.java index a86d4aae..be2bf97f 100644 --- a/src/controller/channel/Channel.java +++ b/src/controller/channel/Channel.java @@ -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; + } } diff --git a/src/gui/SDRTrunk.java b/src/gui/SDRTrunk.java index db22429c..ef21d641 100644 --- a/src/gui/SDRTrunk.java +++ b/src/gui/SDRTrunk.java @@ -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 ); diff --git a/src/module/ProcessingChain.java b/src/module/ProcessingChain.java index 68c94256..e48e644e 100644 --- a/src/module/ProcessingChain.java +++ b/src/module/ProcessingChain.java @@ -108,7 +108,8 @@ public class ProcessingChain implements IChannelEventListener private Broadcaster mUnFilteredRealBufferBroadcaster = new Broadcaster<>(); private Broadcaster 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 ) { diff --git a/src/module/decode/DecoderFactory.java b/src/module/decode/DecoderFactory.java index 02a41441..447e7865 100644 --- a/src/module/decode/DecoderFactory.java +++ b/src/module/decode/DecoderFactory.java @@ -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, diff --git a/src/module/decode/mpt1327/MPT1327DecoderState.java b/src/module/decode/mpt1327/MPT1327DecoderState.java index ad40aa52..a5b94f27 100644 --- a/src/module/decode/mpt1327/MPT1327DecoderState.java +++ b/src/module/decode/mpt1327/MPT1327DecoderState.java @@ -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; } } -} +} \ No newline at end of file diff --git a/src/module/decode/p25/P25DecoderState.java b/src/module/decode/p25/P25DecoderState.java index 0ffe62ce..55340dba 100644 --- a/src/module/decode/p25/P25DecoderState.java +++ b/src/module/decode/p25/P25DecoderState.java @@ -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 ) ) diff --git a/src/module/decode/state/ChannelState.java b/src/module/decode/state/ChannelState.java index 3b538349..450ed92d 100644 --- a/src/module/decode/state/ChannelState.java +++ b/src/module/decode/state/ChannelState.java @@ -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 mCallEventListener; private Listener 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 ); + } + } + } +} \ No newline at end of file diff --git a/src/module/decode/state/ChannelStatePanel.java b/src/module/decode/state/ChannelStatePanel.java index e133989f..9b9dd7cb 100644 --- a/src/module/decode/state/ChannelStatePanel.java +++ b/src/module/decode/state/ChannelStatePanel.java @@ -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() ); diff --git a/src/module/decode/state/DecoderStateEvent.java b/src/module/decode/state/DecoderStateEvent.java index 19623bbc..a4ebd68f 100644 --- a/src/module/decode/state/DecoderStateEvent.java +++ b/src/module/decode/state/DecoderStateEvent.java @@ -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; } -} +} \ No newline at end of file diff --git a/src/module/decode/state/State.java b/src/module/decode/state/State.java index 2820429d..1f1bc79b 100644 --- a/src/module/decode/state/State.java +++ b/src/module/decode/state/State.java @@ -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 + ******************************************************************************/ 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 CALL_STATES = + EnumSet.of( CALL, CONTROL, DATA, ENCRYPTED ); + + public static final EnumSet IDLE_STATES = + EnumSet.of( IDLE, FADE ); + private State( String displayValue ) { mDisplayValue = displayValue; @@ -80,4 +120,4 @@ public enum State { return mDisplayValue; } -} +} \ No newline at end of file diff --git a/src/playlist/PlaylistManager.java b/src/playlist/PlaylistManager.java index ae97cfbb..167f5180 100644 --- a/src/playlist/PlaylistManager.java +++ b/src/playlist/PlaylistManager.java @@ -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() ) { diff --git a/src/source/mixer/MixerChannelConfiguration.java b/src/source/mixer/MixerChannelConfiguration.java index 0be3b358..b8520bcc 100644 --- a/src/source/mixer/MixerChannelConfiguration.java +++ b/src/source/mixer/MixerChannelConfiguration.java @@ -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(); diff --git a/src/source/tuner/TunerController.java b/src/source/tuner/TunerController.java index 44682e7a..faf36961 100644 --- a/src/source/tuner/TunerController.java +++ b/src/source/tuner/TunerController.java @@ -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 mTunedChannels = new ConcurrentSkipListSet<>(); -======= - protected ArrayList 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;