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 <denny@denny-desktop>
This commit is contained in:
Denny Sheirer
2020-03-20 12:31:35 +00:00
committed by GitHub
parent 23a3ee39cc
commit 604c8ab6e5
6 changed files with 299 additions and 25 deletions

View File

@@ -161,7 +161,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
*/
public boolean isDoNotMonitor()
{
return mMonitorPriority.get() == Priority.DO_NOT_MONITOR;
return mMonitorPriority.get() <= Priority.DO_NOT_MONITOR;
}
/**

View File

@@ -85,6 +85,8 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
private IntegerProperty mAudioPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
private ByteBuffer mAudioSegmentStartTone;
private ByteBuffer mAudioSegmentPreemptTone;
private ByteBuffer mAudioSegmentDropTone;
private boolean mRunning = false;
/**
* Single audio channel playback with automatic starting and stopping of the
@@ -147,9 +149,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
}
//Run the queue processor task every 250 milliseconds or 4 times a second
//Run the queue processor task every 100 milliseconds or 10 times a second
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 250, TimeUnit.MILLISECONDS);
0, 100, TimeUnit.MILLISECONDS);
}
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
@@ -255,12 +257,13 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
/**
* Updates audio segment start and preempt tone insertion clips
* Updates audio segment start, drop and preempt tone insertion clips
*/
private void updateToneInsertionAudioClips()
{
mAudioSegmentStartTone = null;
mAudioSegmentPreemptTone = null;
mAudioSegmentDropTone = null;
float[] start = mUserPreferences.getPlaybackPreference().getStartTone();
@@ -275,6 +278,13 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
mAudioSegmentPreemptTone = convert(preempt);
}
float[] drop = mUserPreferences.getPlaybackPreference().getDropTone();
if(drop != null)
{
mAudioSegmentDropTone = convert(drop);
}
}
/**
@@ -310,6 +320,15 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
return mAudioSegmentPreemptTone;
}
/**
* Generates a tone indicating that the current audio segment playback has been dropped because the audio segment
* has been flagged as Do Not Monitor after playback has started.
*/
private ByteBuffer getAudioSegmentDropTone()
{
return mAudioSegmentDropTone;
}
/**
* Writes the audio buffer data to the source data line. If the data line is not currently playing, write as
* much of the buffer to the data line as will fit, start the dataline, and then finish writing the residual
@@ -362,8 +381,18 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
try
{
//Remove an assigned audio segment that's subsequently flagged as do not monitor before playback
if(mNextAudioSegment.isDoNotMonitor())
{
mNextAudioSegment = null;
if(mCurrentAudioSegment == null)
{
mEmptyProperty.set(true);
}
}
//For linked audio segments, allow the linked segment to complete first before assigning the next
if(mNextAudioSegment.isLinked())
else if(mNextAudioSegment.isLinked())
{
if(mCurrentAudioSegment == null)
{
@@ -427,10 +456,15 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
if(mCurrentAudioSegment != null)
{
//Check for completed audio segment
if(mCurrentAudioSegment.completeProperty().get() &&
mCurrentBufferIndex >= 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<IdentifierUp
//while processing, exit the loop so that we can evaluate the next for higher priority preempt. If the next
//segment is a linked segment, ignore it so that we can close out the current segment.
while((mNextAudioSegment == null || mNextAudioSegment.isLinked()) &&
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount() &&
!mCurrentAudioSegment.isDoNotMonitor())
{
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
@@ -595,6 +630,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
if(mCanProcessAudio && !mOutput.isRunning() && mOutput.available() <= mBufferStartThreshold)
{
mOutput.start();
mRunning = true;
}
}
@@ -606,11 +642,26 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
*/
private void checkStop()
{
if(mCanProcessAudio && mOutput.isRunning() && mOutput.available() >= 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);
}
}
}

View File

@@ -67,6 +67,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
private MixerChannelConfiguration mMixerChannelConfiguration;
private List<AudioOutput> mAudioOutputs = new ArrayList<>();
private List<AudioSegment> mAudioSegments = new ArrayList<>();
private List<AudioSegment> mPendingAudioSegments = new ArrayList<>();
private LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
/**
@@ -109,9 +110,50 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, 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<AudioSegment> 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<AudioSegment> it = mAudioSegments.iterator();
@@ -119,15 +161,15 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, 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<AudioSegment>, 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<AudioSegment>, 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<AudioSegment>, 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<AudioSegment>, IAudioContr
}
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 250, TimeUnit.MILLISECONDS);
0, 100, TimeUnit.MILLISECONDS);
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
mMixerChannelConfiguration = entry;
}

View File

@@ -422,7 +422,6 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
for(Channel channel : channelsToStop)
{
mLog.info("Stopping channel: " + channel.toString());
stopProcessing(channel, true);
}
}

View File

@@ -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<ToneFrequency> mStartToneFrequencyComboBox;
private ComboBox<ToneVolume> mStartToneVolumeComboBox;
private ComboBox<ToneFrequency> mPreemptToneFrequencyComboBox;
private ComboBox<ToneVolume> mPreemptToneVolumeComboBox;
private ComboBox<ToneFrequency> mDropToneFrequencyComboBox;
private ComboBox<ToneVolume> 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<Boolean>()
{
@Override
public void changed(ObservableValue<? extends Boolean> 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<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
play(mPlaybackPreference.getDropTone());
}
});
mTestDropToneButton.disableProperty().bind(getUseAudioSegmentDropToneSwitch().selectedProperty().not());
}
return mTestDropToneButton;
}
public ComboBox<ToneFrequency> 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<ToneVolume> 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<ToneFrequency> getStartToneFrequencyComboBox()
{
if(mStartToneFrequencyComboBox == null)

View File

@@ -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
*/