diff --git a/src/main/java/io/github/dsheirer/audio/AudioSegment.java b/src/main/java/io/github/dsheirer/audio/AudioSegment.java index c6c38e9d..a4b89a41 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioSegment.java +++ b/src/main/java/io/github/dsheirer/audio/AudioSegment.java @@ -161,7 +161,7 @@ public class AudioSegment implements Listener */ public boolean isDoNotMonitor() { - return mMonitorPriority.get() == Priority.DO_NOT_MONITOR; + return mMonitorPriority.get() <= Priority.DO_NOT_MONITOR; } /** diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java index b7e2141e..5d7f97dc 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java @@ -85,6 +85,8 @@ public abstract class AudioOutput implements LineListener, Listener= mCurrentAudioSegment.getAudioBufferCount()) + //Check for completed audio segment or a segment flagged as Do Not Monitor + if(mCurrentAudioSegment.isDoNotMonitor() || (mCurrentAudioSegment.completeProperty().get() && + mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount())) { + if(mCurrentAudioSegment.isDoNotMonitor()) + { + playAudio(getAudioSegmentDropTone()); + } + dispose(mCurrentAudioSegment); mCurrentAudioSegment = null; @@ -455,7 +489,8 @@ public abstract class AudioOutput implements LineListener, Listener= mBufferStopThreshold) + if(mRunning) { - mOutput.drain(); - mOutput.stop(); - broadcast(null); + //If the output buffer falls below the threshold then drain the output and stop playback + if(mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold) + { + mOutput.drain(); + mOutput.stop(); + mRunning = false; + } + //If output playback stopped on its own because the buffer emptied, then cleanup + else if(!mOutput.isRunning()) + { + mRunning = false; + } + + //If we stopped audio playback, broadcast a null identifier to clear the gui panel + if(!mRunning) + { + broadcast(null); + } } } diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java b/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java index 62ea4515..b849263c 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java @@ -67,6 +67,7 @@ public class AudioPlaybackManager implements Listener, IAudioContr private MixerChannelConfiguration mMixerChannelConfiguration; private List mAudioOutputs = new ArrayList<>(); private List mAudioSegments = new ArrayList<>(); + private List mPendingAudioSegments = new ArrayList<>(); private LinkedTransferQueue mNewAudioSegmentQueue = new LinkedTransferQueue<>(); /** @@ -109,9 +110,50 @@ public class AudioPlaybackManager implements Listener, IAudioContr */ private void processAudioSegments() { - //Transfer new audio segments out of the concurrent queue and into the managed segments list. - mNewAudioSegmentQueue.drainTo(mAudioSegments); + //Process new audio segments queue. If segment has audio, queue it for replay, otherwise place in pending queue + AudioSegment newSegment = mNewAudioSegmentQueue.poll(); + while(newSegment != null) + { + if(newSegment.hasAudio()) + { + mAudioSegments.add(newSegment); + } + else + { + mPendingAudioSegments.add(newSegment); + } + + newSegment = mNewAudioSegmentQueue.poll(); + } + + //Transfer pending audio segments that now have audio or that completed without ever having audio + if(!mPendingAudioSegments.isEmpty()) + { + Iterator it = mPendingAudioSegments.iterator(); + + AudioSegment audioSegment; + + while(it.hasNext()) + { + audioSegment = it.next(); + + if(audioSegment.hasAudio()) + { + //Queue it up for replay + it.remove(); + mAudioSegments.add(audioSegment); + } + else if(audioSegment.completeProperty().get()) + { + //Rare situation: the audio segment completed but never had audio ... dispose it + it.remove(); + audioSegment.decrementConsumerCount(); + } + } + } + + //Process all audio segments that have audio if(!mAudioSegments.isEmpty()) { Iterator it = mAudioSegments.iterator(); @@ -119,15 +161,15 @@ public class AudioPlaybackManager implements Listener, IAudioContr //Remove any audio segments flagged as do not monitor. Don't remove completed segments yet, because //we want to give them a brief chance at playback. Automatically assign linked audio segments to the - //current audio output for audio continutity + //current audio output for audio continuity while(it.hasNext()) { audioSegment = it.next(); if(audioSegment.isDoNotMonitor()) { - audioSegment.decrementConsumerCount(); it.remove(); + audioSegment.decrementConsumerCount(); } else if(audioSegment.isLinked()) { @@ -135,14 +177,14 @@ public class AudioPlaybackManager implements Listener, IAudioContr { if(audioOutput.isLinkedTo(audioSegment)) { - audioOutput.play(audioSegment); it.remove(); + audioOutput.play(audioSegment); } } } } - //Assign audio segments to empty audio outputs + //Sort audio segments by playback priority and assign to empty audio outputs if(!mAudioSegments.isEmpty()) { Collections.sort(mAudioSegments, Comparator.comparingInt(o -> o.monitorPriorityProperty().get())); @@ -165,7 +207,6 @@ public class AudioPlaybackManager implements Listener, IAudioContr //Preempt ongoing audio segment playback when higher priority audio segments are available if(!mAudioSegments.isEmpty()) { - //Assign empty audio outputs first for(AudioOutput audioOutput: mAudioOutputs) { if(mAudioSegments.get(0).monitorPriorityProperty().get() < audioOutput.audioPriorityProperty().get()) @@ -180,7 +221,7 @@ public class AudioPlaybackManager implements Listener, IAudioContr } } - //Remove any audio segments marked as completed + //Remove any audio segments marked as complete that didn't get assigned to an output it = mAudioSegments.iterator(); //reset the iterator while(it.hasNext()) { @@ -275,7 +316,7 @@ public class AudioPlaybackManager implements Listener, IAudioContr } mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(), - 0, 250, TimeUnit.MILLISECONDS); + 0, 100, TimeUnit.MILLISECONDS); mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE); mMixerChannelConfiguration = entry; } diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java index b0ac2b95..69be2ebe 100644 --- a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java +++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java @@ -422,7 +422,6 @@ public class ChannelProcessingManager implements Listener for(Channel channel : channelsToStop) { - mLog.info("Stopping channel: " + channel.toString()); stopProcessing(channel, true); } } diff --git a/src/main/java/io/github/dsheirer/gui/preference/playback/PlaybackPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/playback/PlaybackPreferenceEditor.java index 21948030..b2d9c706 100644 --- a/src/main/java/io/github/dsheirer/gui/preference/playback/PlaybackPreferenceEditor.java +++ b/src/main/java/io/github/dsheirer/gui/preference/playback/PlaybackPreferenceEditor.java @@ -71,10 +71,14 @@ public class PlaybackPreferenceEditor extends HBox private Button mTestStartToneButton; private ToggleSwitch mUseAudioSegmentPreemptToneSwitch; private Button mTestPreemptToneButton; + private ToggleSwitch mUseAudioSegmentDropToneSwitch; + private Button mTestDropToneButton; private ComboBox mStartToneFrequencyComboBox; private ComboBox mStartToneVolumeComboBox; private ComboBox mPreemptToneFrequencyComboBox; private ComboBox mPreemptToneVolumeComboBox; + private ComboBox mDropToneFrequencyComboBox; + private ComboBox mDropToneVolumeComboBox; public PlaybackPreferenceEditor(UserPreferences userPreferences) { @@ -100,6 +104,7 @@ public class PlaybackPreferenceEditor extends HBox mEditorPane.add(getMixerTestButton(), 5, row); mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, ++row, 6, 1); mEditorPane.add(new Label("Audio Playback Insert Tones"), 0, ++row, 2, 1); + mEditorPane.add(getUseAudioSegmentStartToneSwitch(), 0, ++row); mEditorPane.add(new Label("Start Tone"), 1, row, 3, 1); Label startFrequencyLabel = new Label("Frequency:"); @@ -111,6 +116,7 @@ public class PlaybackPreferenceEditor extends HBox mEditorPane.add(startVolumeLabel, 3, row); mEditorPane.add(getStartToneVolumeComboBox(), 4, row); mEditorPane.add(getTestStartToneButton(), 5, row); + mEditorPane.add(getUseAudioSegmentPreemptToneSwitch(), 0, ++row); mEditorPane.add(new Label("Priority Preempt Tone"), 1, row, 3, 1); Label preemptFrequencyLabel = new Label("Frequency:"); @@ -122,6 +128,18 @@ public class PlaybackPreferenceEditor extends HBox mEditorPane.add(preemptVolumeLabel, 3, row); mEditorPane.add(getPreemptToneVolumeComboBox(), 4, row); mEditorPane.add(getTestPreemptToneButton(), 5, row); + + mEditorPane.add(getUseAudioSegmentDropToneSwitch(), 0, ++row); + mEditorPane.add(new Label("Drop Tone - Do Not Monitor"), 1, row, 3, 1); + Label dropFrequencyLabel = new Label("Frequency:"); + GridPane.setHalignment(dropFrequencyLabel, HPos.RIGHT); + mEditorPane.add(dropFrequencyLabel, 1, ++row); + mEditorPane.add(getDropToneFrequencyComboBox(), 2, row); + Label dropVolumeLabel = new Label("Volume:"); + GridPane.setHalignment(dropVolumeLabel, HPos.RIGHT); + mEditorPane.add(dropVolumeLabel, 3, row); + mEditorPane.add(getDropToneVolumeComboBox(), 4, row); + mEditorPane.add(getTestDropToneButton(), 5, row); } return mEditorPane; @@ -194,6 +212,25 @@ public class PlaybackPreferenceEditor extends HBox return mUseAudioSegmentPreemptToneSwitch; } + private ToggleSwitch getUseAudioSegmentDropToneSwitch() + { + if(mUseAudioSegmentDropToneSwitch == null) + { + mUseAudioSegmentDropToneSwitch = new ToggleSwitch(); + mUseAudioSegmentDropToneSwitch.setSelected(mPlaybackPreference.getUseAudioSegmentDropTone()); + mUseAudioSegmentDropToneSwitch.selectedProperty().addListener(new ChangeListener() + { + @Override + public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) + { + mPlaybackPreference.setUseAudioSegmentDropTone(newValue); + } + }); + } + + return mUseAudioSegmentDropToneSwitch; + } + public Button getTestStartToneButton() { if(mTestStartToneButton == null) @@ -240,6 +277,59 @@ public class PlaybackPreferenceEditor extends HBox return mTestPreemptToneButton; } + public Button getTestDropToneButton() + { + if(mTestDropToneButton == null) + { + mTestDropToneButton = new Button("Test"); + IconNode iconNode = new IconNode(FontAwesome.PLAY); + iconNode.setFill(Color.CORNFLOWERBLUE); + mTestDropToneButton.setGraphic(iconNode); + mTestDropToneButton.setOnAction(new EventHandler() + { + @Override + public void handle(ActionEvent event) + { + play(mPlaybackPreference.getDropTone()); + } + }); + + mTestDropToneButton.disableProperty().bind(getUseAudioSegmentDropToneSwitch().selectedProperty().not()); + } + + return mTestDropToneButton; + } + + public ComboBox getDropToneFrequencyComboBox() + { + if(mDropToneFrequencyComboBox == null) + { + mDropToneFrequencyComboBox = new ComboBox<>(); + mDropToneFrequencyComboBox.getItems().addAll(ToneFrequency.values()); + mDropToneFrequencyComboBox.getSelectionModel().select(mPlaybackPreference.getDropToneFrequency()); + mDropToneFrequencyComboBox.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> mPlaybackPreference.setDropToneFrequency(newValue)); + mDropToneFrequencyComboBox.disableProperty().bind(getUseAudioSegmentDropToneSwitch().selectedProperty().not()); + } + + return mDropToneFrequencyComboBox; + } + + public ComboBox getDropToneVolumeComboBox() + { + if(mDropToneVolumeComboBox == null) + { + mDropToneVolumeComboBox = new ComboBox<>(); + mDropToneVolumeComboBox.getItems().addAll(ToneVolume.values()); + mDropToneVolumeComboBox.getSelectionModel().select(mPlaybackPreference.getDropToneVolume()); + mDropToneVolumeComboBox.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> mPlaybackPreference.setDropToneVolume(newValue)); + mDropToneVolumeComboBox.disableProperty().bind(getUseAudioSegmentDropToneSwitch().selectedProperty().not()); + } + + return mDropToneVolumeComboBox; + } + public ComboBox getStartToneFrequencyComboBox() { if(mStartToneFrequencyComboBox == null) diff --git a/src/main/java/io/github/dsheirer/preference/playback/PlaybackPreference.java b/src/main/java/io/github/dsheirer/preference/playback/PlaybackPreference.java index c17d4c2e..dd2d8726 100644 --- a/src/main/java/io/github/dsheirer/preference/playback/PlaybackPreference.java +++ b/src/main/java/io/github/dsheirer/preference/playback/PlaybackPreference.java @@ -38,12 +38,18 @@ import java.util.prefs.Preferences; */ public class PlaybackPreference extends Preference { + private static final String PREFERENCE_KEY_USE_AUDIO_SEGMENT_DROP_TONE = "audio.playback.segment.drop.tone"; + private static final String PREFERENCE_KEY_DROP_TONE_FREQUENCY = "audio.playback.segment.drop.frequency"; + private static final String PREFERENCE_KEY_DROP_TONE_VOLUME = "audio.playback.segment.drop.volume"; + private static final String PREFERENCE_KEY_USE_AUDIO_SEGMENT_PREEMPT_TONE = "audio.playback.segment.preempt.tone"; + private static final String PREFERENCE_KEY_PREEMPT_TONE_FREQUENCY = "audio.playback.segment.preempt.frequency"; + private static final String PREFERENCE_KEY_PREEMPT_TONE_VOLUME = "audio.playback.segment.preempt.volume"; + private static final String PREFERENCE_KEY_USE_AUDIO_SEGMENT_START_TONE = "audio.playback.segment.start.tone"; private static final String PREFERENCE_KEY_START_TONE_FREQUENCY = "audio.playback.segment.start.frequency"; private static final String PREFERENCE_KEY_START_TONE_VOLUME = "audio.playback.segment.start.volume"; - private static final String PREFERENCE_KEY_PREEMPT_TONE_FREQUENCY = "audio.playback.segment.preempt.frequency"; - private static final String PREFERENCE_KEY_PREEMPT_TONE_VOLUME = "audio.playback.segment.preempt.volume"; + private static final String PREFERENCE_KEY_MIXER_CHANNEL_CONFIG = "audio.playback.mixer.channel.configuration"; private static final int TONE_LENGTH_SAMPLES = 180; @@ -51,10 +57,13 @@ public class PlaybackPreference extends Preference private Preferences mPreferences = Preferences.userNodeForPackage(PlaybackPreference.class); private Boolean mUseAudioSegmentStartTone; private Boolean mUseAudioSegmentPreemptTone; + private Boolean mUseAudioSegmentDropTone; private ToneFrequency mStartToneFrequency; private ToneVolume mStartToneVolume; private ToneFrequency mPreemptToneFrequency; private ToneVolume mPreemptToneVolume; + private ToneFrequency mDropToneFrequency; + private ToneVolume mDropToneVolume; private MixerChannelConfiguration mMixerChannelConfiguration; /** @@ -72,6 +81,29 @@ public class PlaybackPreference extends Preference return PreferenceType.PLAYBACK; } + /** + * Indicates if an audio segment drop tone should be used. + */ + public boolean getUseAudioSegmentDropTone() + { + if(mUseAudioSegmentDropTone == null) + { + mUseAudioSegmentDropTone = mPreferences.getBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_DROP_TONE, true); + } + + return mUseAudioSegmentDropTone; + } + + /** + * Sets the preference for using an audio segment drop tone + */ + public void setUseAudioSegmentDropTone(boolean use) + { + mUseAudioSegmentDropTone = use; + mPreferences.putBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_DROP_TONE, use); + notifyPreferenceUpdated(); + } + /** * Indicates if an audio segment start tone should be used. */ @@ -118,6 +150,30 @@ public class PlaybackPreference extends Preference notifyPreferenceUpdated(); } + /** + * Frequency for the drop tone + */ + public ToneFrequency getDropToneFrequency() + { + if(mDropToneFrequency == null) + { + int frequency = mPreferences.getInt(PREFERENCE_KEY_DROP_TONE_FREQUENCY, ToneFrequency.F500.getValue()); + mDropToneFrequency = ToneFrequency.fromValue(frequency); + } + + return mDropToneFrequency; + } + + /** + * Sets the frequency for the drop tone + */ + public void setDropToneFrequency(ToneFrequency toneFrequency) + { + mDropToneFrequency = toneFrequency; + mPreferences.putInt(PREFERENCE_KEY_DROP_TONE_FREQUENCY, toneFrequency.getValue()); + notifyPreferenceUpdated(); + } + /** * Frequency for the start tone */ @@ -166,6 +222,30 @@ public class PlaybackPreference extends Preference notifyPreferenceUpdated(); } + /** + * Drop tone volume + */ + public ToneVolume getDropToneVolume() + { + if(mDropToneVolume == null) + { + int volume = mPreferences.getInt(PREFERENCE_KEY_DROP_TONE_VOLUME, ToneVolume.V3.getValue()); + mDropToneVolume = ToneVolume.fromValue(volume); + } + + return mDropToneVolume; + } + + /** + * Sets the drop tone volume + */ + public void setDropToneVolume(ToneVolume toneVolume) + { + mDropToneVolume = toneVolume; + mPreferences.putInt(PREFERENCE_KEY_DROP_TONE_VOLUME, toneVolume.getValue()); + notifyPreferenceUpdated(); + } + /** * Start tone volume */ @@ -240,6 +320,19 @@ public class PlaybackPreference extends Preference return null; } + /** + * Buffer with samples for the audio segment drop tone + */ + public float[] getDropTone() + { + if(getUseAudioSegmentDropTone()) + { + return ToneUtil.getTone(getDropToneFrequency(), getDropToneVolume(), TONE_LENGTH_SAMPLES); + } + + return null; + } + /** * Test tone to use for testing the currently selected mixer output */