From 604c8ab6e575ae8ecf2e3a638362a58ab6fbcf8c Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Fri, 20 Mar 2020 12:31:35 +0000 Subject: [PATCH] Resolves #680 Audio Do Not Monitor is not working. Adds audio segment drop tone to indicate that an audio playback is dropped because the do not monitor setting was applied after the audio segment started. (#681) Co-authored-by: Denny --- .../github/dsheirer/audio/AudioSegment.java | 2 +- .../dsheirer/audio/playback/AudioOutput.java | 75 +++++++++++--- .../audio/playback/AudioPlaybackManager.java | 59 +++++++++-- .../channel/ChannelProcessingManager.java | 1 - .../playback/PlaybackPreferenceEditor.java | 90 +++++++++++++++++ .../playback/PlaybackPreference.java | 97 ++++++++++++++++++- 6 files changed, 299 insertions(+), 25 deletions(-) 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 */