mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 01:57:39 -05:00
#2356 Audio playback redesign. Resolves audio playback issue on some (e.g. Kubuntu) systems. For stereo playback, (#2359)
consolidates left and right channels into a single playback channel. Implements continuous audio streaming using silence to fill the gaps to avoid mixer source data line start/stop issues. Code structure is ready to layer on multi-channel (ie 5.1 or 7.1) audio playback devices, but that hasn't been tested. Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -61,7 +61,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
*
|
*
|
||||||
* Producers will add audio buffers and update identifiers throughout the life-cycle of an audio segment. The producer
|
* Producers will add audio buffers and update identifiers throughout the life-cycle of an audio segment. The producer
|
||||||
* will signal the completion of an audio segment by setting the complete property to true. This allows consumers the
|
* will signal the completion of an audio segment by setting the complete property to true. This allows consumers the
|
||||||
* option to process the audio buffers throughout the life-cycle of the segment, or to process all of the buffers once
|
* option to process the audio buffers throughout the life-cycle of the segment, or to process all the buffers once
|
||||||
* the segment is complete.
|
* the segment is complete.
|
||||||
*/
|
*/
|
||||||
public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
package io.github.dsheirer.audio;
|
package io.github.dsheirer.audio;
|
||||||
|
|
||||||
import io.github.dsheirer.audio.playback.AudioOutput;
|
import io.github.dsheirer.audio.playback.AudioChannel;
|
||||||
|
import io.github.dsheirer.audio.playback.AudioPlaybackDeviceDescriptor;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,13 +30,13 @@ import java.util.List;
|
|||||||
public interface IAudioController
|
public interface IAudioController
|
||||||
{
|
{
|
||||||
/* Current Mixer and MixerChannel configuration */
|
/* Current Mixer and MixerChannel configuration */
|
||||||
public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException;
|
public void setAudioPlaybackDevice(AudioPlaybackDeviceDescriptor device ) throws AudioException;
|
||||||
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException;
|
public AudioPlaybackDeviceDescriptor getAudioPlaybackDevice() throws AudioException;
|
||||||
|
|
||||||
/* Audio Output(s) */
|
/* Audio Channel(s) */
|
||||||
public List<AudioOutput> getAudioOutputs();
|
public List<AudioChannel> getAudioChannels();
|
||||||
|
|
||||||
/* Controller Audio Event Listener */
|
/* Controller Audio Event Listener */
|
||||||
public void addControllerListener( Listener<AudioEvent> listener );
|
public void addAudioEventListener(Listener<AudioEvent> listener );
|
||||||
public void removeControllerListener( Listener<AudioEvent> listener );
|
public void removeAudioEventListener(Listener<AudioEvent> listener );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,629 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import io.github.dsheirer.audio.AudioEvent;
|
||||||
|
import io.github.dsheirer.audio.AudioSegment;
|
||||||
|
import io.github.dsheirer.eventbus.MyEventBus;
|
||||||
|
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||||
|
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||||
|
import io.github.dsheirer.preference.PreferenceType;
|
||||||
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
|
import io.github.dsheirer.sample.Broadcaster;
|
||||||
|
import io.github.dsheirer.sample.Listener;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.LinkedTransferQueue;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the incoming audio segments and provides access to audio buffers from those segments.
|
||||||
|
*/
|
||||||
|
public class AudioChannel implements Listener<IdentifierUpdateNotification>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Quantity of audio samples to deliver per getAudio() method invocation, equivalent to 20 milliseconds at an 8 kHz
|
||||||
|
* sample rate.
|
||||||
|
*/
|
||||||
|
public static final int SAMPLES_PER_INTERVAL = 160;
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(AudioChannel.class);
|
||||||
|
private final AudioBuffer mAudioBuffer = new AudioBuffer();
|
||||||
|
private final Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
|
||||||
|
private final LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||||
|
private final UserPreferences mUserPreferences;
|
||||||
|
private final String mChannelName;
|
||||||
|
|
||||||
|
private AudioSegment mCurrentAudioSegment;
|
||||||
|
private Listener<IdentifierCollection> mIdentifierCollectionListener;
|
||||||
|
private boolean mDropDuplicates;
|
||||||
|
private boolean mMetadataSent = false;
|
||||||
|
private boolean mMuted;
|
||||||
|
private boolean mDisabled;
|
||||||
|
private float[] mAudioSegmentStartTone;
|
||||||
|
private float[] mAudioSegmentDropTone;
|
||||||
|
private int mCurrentBufferIndex = -1;
|
||||||
|
private int mNoAudioFromSegmentIntervalCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
*/
|
||||||
|
public AudioChannel(UserPreferences userPreferences, String channelName)
|
||||||
|
{
|
||||||
|
mUserPreferences = userPreferences;
|
||||||
|
mChannelName = channelName;
|
||||||
|
|
||||||
|
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||||
|
updateToneInsertionAudioClips();
|
||||||
|
|
||||||
|
//Register to receive preference update notifications so we can update the preference items
|
||||||
|
MyEventBus.getGlobalEventBus().register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables this audio channel.
|
||||||
|
* @param disabled state
|
||||||
|
*/
|
||||||
|
public void setDisabled(boolean disabled)
|
||||||
|
{
|
||||||
|
mDisabled = disabled;
|
||||||
|
|
||||||
|
if(disabled)
|
||||||
|
{
|
||||||
|
AudioSegment segment = mAudioSegmentQueue.poll();
|
||||||
|
|
||||||
|
while(segment != null)
|
||||||
|
{
|
||||||
|
segment.decrementConsumerCount();
|
||||||
|
segment = mAudioSegmentQueue.poll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this audio channel is enabled.
|
||||||
|
*/
|
||||||
|
public boolean isDisabled()
|
||||||
|
{
|
||||||
|
return mDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches this channel's muted state
|
||||||
|
* @param muted true to mute and false to unmute
|
||||||
|
*/
|
||||||
|
public void setMuted(boolean muted)
|
||||||
|
{
|
||||||
|
mMuted = muted;
|
||||||
|
notify(muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this channel is muted
|
||||||
|
* @return muted state
|
||||||
|
*/
|
||||||
|
public boolean isMuted()
|
||||||
|
{
|
||||||
|
return mMuted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays the test audio sample data at higher priority that current audio queue.
|
||||||
|
* @param samples of test audio to play.
|
||||||
|
*/
|
||||||
|
public void playTest(float[] samples)
|
||||||
|
{
|
||||||
|
if(isDisabled())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mAudioBuffer.insert(samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this channel has an audio segment, indicating that it is right in the middle of playback and
|
||||||
|
* the audio output should temporarily pause until more audio is available from the segment.
|
||||||
|
* @return true if this channel is processing a non-complete audio segment.
|
||||||
|
*/
|
||||||
|
public boolean hasAudioSegment()
|
||||||
|
{
|
||||||
|
return mCurrentAudioSegment != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts a null identifier collection to the UI to clear any displayed metadata (TO/FROM) identifiers
|
||||||
|
*/
|
||||||
|
public void clearMetadata()
|
||||||
|
{
|
||||||
|
if(mMetadataSent)
|
||||||
|
{
|
||||||
|
broadcast(null);
|
||||||
|
mMetadataSent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides audio for this channel from the audio segment queue.
|
||||||
|
* @return audio or null if there are no audio segments or test audio to play.
|
||||||
|
*/
|
||||||
|
public float[] getAudio()
|
||||||
|
{
|
||||||
|
if(mAudioBuffer.isFull())
|
||||||
|
{
|
||||||
|
return mAudioBuffer.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mCurrentAudioSegment == null)
|
||||||
|
{
|
||||||
|
loadNextAudioSegment();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
|
||||||
|
while(isThrowaway(mCurrentAudioSegment))
|
||||||
|
{
|
||||||
|
if(mCurrentBufferIndex > 0)
|
||||||
|
{
|
||||||
|
//Flush the buffer and insert the drop tone
|
||||||
|
mAudioBuffer.replace(mAudioSegmentDropTone);
|
||||||
|
}
|
||||||
|
|
||||||
|
disposeCurrentAudioSegment();
|
||||||
|
loadNextAudioSegment();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mCurrentAudioSegment != null)
|
||||||
|
{
|
||||||
|
mNoAudioFromSegmentIntervalCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
while(!mAudioBuffer.isFull() && mCurrentAudioSegment != null &&
|
||||||
|
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())
|
||||||
|
{
|
||||||
|
if(mCurrentBufferIndex < 0)
|
||||||
|
{
|
||||||
|
mAudioBuffer.add(mAudioSegmentStartTone);
|
||||||
|
mAudioBuffer.add(mCurrentAudioSegment.getAudioBuffer(0));
|
||||||
|
mCurrentBufferIndex = 1;
|
||||||
|
broadcast(mCurrentAudioSegment.getIdentifierCollection());
|
||||||
|
mMetadataSent = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mAudioBuffer.add(mCurrentAudioSegment.getAudioBuffer(mCurrentBufferIndex++));
|
||||||
|
}
|
||||||
|
|
||||||
|
mNoAudioFromSegmentIntervalCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Dispose completed/fully-played audio segments and segments that have stalled/failed to progress
|
||||||
|
if(mCurrentAudioSegment != null && ((mNoAudioFromSegmentIntervalCount >= 6) ||
|
||||||
|
(mCurrentAudioSegment.isComplete() && (mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))))
|
||||||
|
{
|
||||||
|
disposeCurrentAudioSegment();
|
||||||
|
mNoAudioFromSegmentIntervalCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float[] audio = null;
|
||||||
|
|
||||||
|
if(mAudioBuffer.isFull())
|
||||||
|
{
|
||||||
|
audio = mAudioBuffer.get();
|
||||||
|
}
|
||||||
|
else if(mCurrentAudioSegment == null && !mAudioBuffer.isEmpty())
|
||||||
|
{
|
||||||
|
audio = mAudioBuffer.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Finally, if we have buffer audio and we're muted, return silence
|
||||||
|
if(isMuted() && audio != null)
|
||||||
|
{
|
||||||
|
audio = new float[SAMPLES_PER_INTERVAL];
|
||||||
|
}
|
||||||
|
|
||||||
|
return audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies listener(s) of the audio event.
|
||||||
|
* @param eventType to notify for this channel
|
||||||
|
*/
|
||||||
|
public void notify(AudioEvent.Type eventType)
|
||||||
|
{
|
||||||
|
broadcastAudioEvent(new AudioEvent(eventType, getChannelName()));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Registers a single listener to receive audio start and audio stop events
|
||||||
|
*/
|
||||||
|
public void addAudioEventListener(Listener<AudioEvent> listener)
|
||||||
|
{
|
||||||
|
mAudioEventBroadcaster.addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the audio event listener
|
||||||
|
* @param listener to remove
|
||||||
|
*/
|
||||||
|
public void removeAudioEventListener(Listener<AudioEvent> listener)
|
||||||
|
{
|
||||||
|
mAudioEventBroadcaster.removeListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts an audio event to the registered listener
|
||||||
|
*/
|
||||||
|
private void broadcastAudioEvent(AudioEvent audioEvent)
|
||||||
|
{
|
||||||
|
mAudioEventBroadcaster.broadcast(audioEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the audio segment should be thrown away because it is marked as duplicate or do-not-monitor.
|
||||||
|
* @param audioSegment to evaluate
|
||||||
|
* @return true if the audio should be thrown away.
|
||||||
|
*/
|
||||||
|
private boolean isThrowaway(AudioSegment audioSegment)
|
||||||
|
{
|
||||||
|
return audioSegment != null && (audioSegment.isDoNotMonitor() || mDropDuplicates && (audioSegment.isDuplicate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Channel name
|
||||||
|
*/
|
||||||
|
public String getChannelName()
|
||||||
|
{
|
||||||
|
return mChannelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts audio identifier collection metadata to the registered listener
|
||||||
|
*/
|
||||||
|
private void broadcast(IdentifierCollection identifierCollection)
|
||||||
|
{
|
||||||
|
if(mIdentifierCollectionListener != null)
|
||||||
|
{
|
||||||
|
mIdentifierCollectionListener.receive(identifierCollection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a single listener to receive the audio metadata from each
|
||||||
|
* audio packet
|
||||||
|
*/
|
||||||
|
public void setIdentifierCollectionListener(Listener<IdentifierCollection> listener)
|
||||||
|
{
|
||||||
|
mIdentifierCollectionListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters the current audio metadata listener
|
||||||
|
*/
|
||||||
|
public void removeAudioMetadataListener()
|
||||||
|
{
|
||||||
|
mIdentifierCollectionListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares this audio output for disposal.
|
||||||
|
*/
|
||||||
|
public void dispose()
|
||||||
|
{
|
||||||
|
|
||||||
|
MyEventBus.getGlobalEventBus().unregister(this);
|
||||||
|
disposeCurrentAudioSegment();
|
||||||
|
mAudioEventBroadcaster.clear();
|
||||||
|
mIdentifierCollectionListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guava event bus notifications that the preferences have been updated, so that we can update audio segment tones.
|
||||||
|
*/
|
||||||
|
@Subscribe
|
||||||
|
public void preferenceUpdated(PreferenceType preferenceType)
|
||||||
|
{
|
||||||
|
if(preferenceType == PreferenceType.PLAYBACK)
|
||||||
|
{
|
||||||
|
updateToneInsertionAudioClips();
|
||||||
|
}
|
||||||
|
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
|
||||||
|
{
|
||||||
|
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the audio segment is linked to the current playback audio segment
|
||||||
|
*/
|
||||||
|
public boolean isLinkedTo(AudioSegment audioSegment)
|
||||||
|
{
|
||||||
|
return audioSegment.isLinked() && audioSegment.isLinkedTo(mCurrentAudioSegment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive and process audio identifier update notifications.
|
||||||
|
*/
|
||||||
|
public void receive(IdentifierUpdateNotification identifierUpdateNotification)
|
||||||
|
{
|
||||||
|
if(mCurrentAudioSegment != null)
|
||||||
|
{
|
||||||
|
IdentifierCollection identifierCollection = mCurrentAudioSegment.getIdentifierCollection();
|
||||||
|
|
||||||
|
if(identifierCollection != null)
|
||||||
|
{
|
||||||
|
broadcast(identifierCollection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates audio segment start, drop and preempt tone insertion clips
|
||||||
|
*/
|
||||||
|
private void updateToneInsertionAudioClips()
|
||||||
|
{
|
||||||
|
mAudioSegmentStartTone = mUserPreferences.getPlaybackPreference().getStartTone();
|
||||||
|
mAudioSegmentDropTone = mUserPreferences.getPlaybackPreference().getDropTone();;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this audio output doesn't currently have any audio segments queued for playback.
|
||||||
|
* @return true if empty
|
||||||
|
*/
|
||||||
|
public boolean isEmpty()
|
||||||
|
{
|
||||||
|
return mAudioSegmentQueue.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules the audio segment for playback. The audio segment user count should already be incremented by the
|
||||||
|
* calling entity.
|
||||||
|
*
|
||||||
|
* @param audioSegment to schedule for playback.
|
||||||
|
*/
|
||||||
|
public void play(AudioSegment audioSegment)
|
||||||
|
{
|
||||||
|
if(audioSegment != null)
|
||||||
|
{
|
||||||
|
//Audio segment user count has already been incremented by the external caller.
|
||||||
|
if(isDisabled())
|
||||||
|
{
|
||||||
|
audioSegment.decrementConsumerCount();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mAudioSegmentQueue.add(audioSegment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose of the currently assigned audio segment.
|
||||||
|
*/
|
||||||
|
private void disposeCurrentAudioSegment()
|
||||||
|
{
|
||||||
|
if(mCurrentAudioSegment != null)
|
||||||
|
{
|
||||||
|
mCurrentAudioSegment.decrementConsumerCount();
|
||||||
|
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
|
||||||
|
mCurrentAudioSegment = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the next audio segment from the queue.
|
||||||
|
*/
|
||||||
|
private void loadNextAudioSegment()
|
||||||
|
{
|
||||||
|
AudioSegment audioSegment = mAudioSegmentQueue.poll();
|
||||||
|
|
||||||
|
boolean verificationInProgress = (audioSegment != null);
|
||||||
|
|
||||||
|
while(verificationInProgress)
|
||||||
|
{
|
||||||
|
if(audioSegment != null)
|
||||||
|
{
|
||||||
|
//Throw away the audio segment if it has been flagged as do not monitor or is duplicate
|
||||||
|
if(isThrowaway(audioSegment))
|
||||||
|
{
|
||||||
|
audioSegment.decrementConsumerCount();
|
||||||
|
audioSegment = mAudioSegmentQueue.poll();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
verificationInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
verificationInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentAudioSegment = audioSegment;
|
||||||
|
|
||||||
|
if(audioSegment != null)
|
||||||
|
{
|
||||||
|
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
|
||||||
|
broadcast(mCurrentAudioSegment.getIdentifierCollection());
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentBufferIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages a stream of float PCM audio samples. Employs thread locking of the audio buffer to support both the
|
||||||
|
* audio processor thread and user initiated test audio replay.
|
||||||
|
*/
|
||||||
|
static class AudioBuffer
|
||||||
|
{
|
||||||
|
private final Lock mLock = new ReentrantLock();
|
||||||
|
private float[] mBuffer = new float[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the buffer has at least one interval of samples available
|
||||||
|
*/
|
||||||
|
public boolean isFull()
|
||||||
|
{
|
||||||
|
return mBuffer.length >= SAMPLES_PER_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the buffer is empty
|
||||||
|
*/
|
||||||
|
public boolean isEmpty()
|
||||||
|
{
|
||||||
|
return mBuffer.length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends the audio to the buffer
|
||||||
|
* @param audio to add
|
||||||
|
*/
|
||||||
|
public void add(float[] audio)
|
||||||
|
{
|
||||||
|
if(audio != null)
|
||||||
|
{
|
||||||
|
mLock.lock();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mBuffer = concatenate(mBuffer, audio);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
mLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts the audio to the front of the buffer. This can be used to insert test audio ahead of any queued audio.
|
||||||
|
* @param audio to insert
|
||||||
|
*/
|
||||||
|
public void insert(float[] audio)
|
||||||
|
{
|
||||||
|
if(audio != null)
|
||||||
|
{
|
||||||
|
mLock.lock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mBuffer = concatenate(audio, mBuffer);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
mLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the content in the buffer with the audio argument
|
||||||
|
* @param audio to replace into the buffer
|
||||||
|
*/
|
||||||
|
public void replace(float[] audio)
|
||||||
|
{
|
||||||
|
if(audio != null)
|
||||||
|
{
|
||||||
|
mLock.lock();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mBuffer = audio;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
mLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenate two arrays. Note: this does not null check either array.
|
||||||
|
* @param a first array
|
||||||
|
* @param b second array
|
||||||
|
* @return concatenated array
|
||||||
|
*/
|
||||||
|
private static float[] concatenate(float[] a, float[] b)
|
||||||
|
{
|
||||||
|
a = Arrays.copyOf(a, a.length + b.length);
|
||||||
|
System.arraycopy(b, 0, a, a.length - b.length, b.length);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an array of 160 samples if available, otherwise returns null.
|
||||||
|
* @return 160 samples or null.
|
||||||
|
*/
|
||||||
|
public float[] get()
|
||||||
|
{
|
||||||
|
if(mBuffer.length >= SAMPLES_PER_INTERVAL)
|
||||||
|
{
|
||||||
|
mLock.lock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
float[] fragment = Arrays.copyOf(mBuffer, SAMPLES_PER_INTERVAL);
|
||||||
|
mBuffer = Arrays.copyOfRange(mBuffer, SAMPLES_PER_INTERVAL, mBuffer.length);
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
mLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes remaining samples from the buffer when the quantity is less than a full sample set.
|
||||||
|
* @return remaining samples or null.
|
||||||
|
*/
|
||||||
|
public float[] flush()
|
||||||
|
{
|
||||||
|
mLock.lock();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(mBuffer.length > 0)
|
||||||
|
{
|
||||||
|
float[] fragment = Arrays.copyOf(mBuffer, SAMPLES_PER_INTERVAL);
|
||||||
|
|
||||||
|
if(mBuffer.length > SAMPLES_PER_INTERVAL)
|
||||||
|
{
|
||||||
|
mBuffer = Arrays.copyOfRange(mBuffer, SAMPLES_PER_INTERVAL, mBuffer.length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mBuffer = new float[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
mLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* ******************************************************************************
|
* *****************************************************************************
|
||||||
* sdrtrunk
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* *****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
package io.github.dsheirer.audio.playback;
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
@@ -40,13 +39,6 @@ import io.github.dsheirer.settings.ColorSetting;
|
|||||||
import io.github.dsheirer.settings.Setting;
|
import io.github.dsheirer.settings.Setting;
|
||||||
import io.github.dsheirer.settings.SettingChangeListener;
|
import io.github.dsheirer.settings.SettingChangeListener;
|
||||||
import io.github.dsheirer.settings.SettingsManager;
|
import io.github.dsheirer.settings.SettingsManager;
|
||||||
import net.miginfocom.swing.MigLayout;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.swing.ImageIcon;
|
|
||||||
import javax.swing.JLabel;
|
|
||||||
import javax.swing.JPanel;
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.EventQueue;
|
import java.awt.EventQueue;
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
@@ -54,7 +46,17 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.swing.ImageIcon;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI to wrap an audio channel and provide display of metadata and playback state information.
|
||||||
|
*/
|
||||||
public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, SettingChangeListener
|
public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, SettingChangeListener
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
@@ -66,30 +68,36 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
|||||||
public static final String PROPERTY_COLOR_MUTED = PROPERTY_PREFIX + "muted";
|
public static final String PROPERTY_COLOR_MUTED = PROPERTY_PREFIX + "muted";
|
||||||
public static final String PROPERTY_COLOR_VALUE = PROPERTY_PREFIX + "value";
|
public static final String PROPERTY_COLOR_VALUE = PROPERTY_PREFIX + "value";
|
||||||
|
|
||||||
private Font mFont = new Font(Font.MONOSPACED, Font.PLAIN, 16);
|
private final AudioChannel mAudioChannel;
|
||||||
|
private final AliasModel mAliasModel;
|
||||||
private Color mBackgroundColor;
|
private final IconModel mIconModel;
|
||||||
private Color mLabelColor;
|
private final SettingsManager mSettingsManager;
|
||||||
private Color mMutedColor;
|
private final UserPreferences mUserPreferences;
|
||||||
private Color mValueColor;
|
private final TalkgroupFormatPreference mTalkgroupFormatPreference;
|
||||||
|
|
||||||
private IconModel mIconModel;
|
|
||||||
private SettingsManager mSettingsManager;
|
|
||||||
private UserPreferences mUserPreferences;
|
|
||||||
private TalkgroupFormatPreference mTalkgroupFormatPreference;
|
|
||||||
private AudioOutput mAudioOutput;
|
|
||||||
|
|
||||||
private JLabel mMutedLabel = new JLabel("M");
|
|
||||||
private JLabel mChannelName = new JLabel(" ");
|
|
||||||
private JLabel mIconLabel = new JLabel(" ");
|
|
||||||
private JLabel mIdentifierLabel = new JLabel("-----");
|
|
||||||
private Identifier mIdentifier;
|
private Identifier mIdentifier;
|
||||||
private List<Alias> mAliases = Collections.EMPTY_LIST;
|
private List<Alias> mAliases = Collections.EMPTY_LIST;
|
||||||
private AliasModel mAliasModel;
|
private final Lock mLock = new ReentrantLock();
|
||||||
private Lock mLock = new ReentrantLock();
|
|
||||||
|
|
||||||
public AudioChannelPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
|
private final Font mFont = new Font(Font.MONOSPACED, Font.PLAIN, 16);
|
||||||
AudioOutput audioOutput, AliasModel aliasModel)
|
private final Color mBackgroundColor;
|
||||||
|
private final Color mLabelColor;
|
||||||
|
private final Color mMutedColor;
|
||||||
|
private final Color mValueColor;
|
||||||
|
private final JLabel mMutedLabel = new JLabel("M");
|
||||||
|
private JLabel mChannelName = new JLabel(" ");
|
||||||
|
private final JLabel mIconLabel = new JLabel(" ");
|
||||||
|
private final JLabel mIdentifierLabel = new JLabel("-----");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param audioChannel to wrap by this panel
|
||||||
|
* @param aliasModel for alias lookup
|
||||||
|
* @param iconModel for icon lookup
|
||||||
|
* @param settingsManager for monitoring changes to tone insertion
|
||||||
|
* @param userPreferences for lookup of tone and other preferences
|
||||||
|
*/
|
||||||
|
public AudioChannelPanel(AudioChannel audioChannel, AliasModel aliasModel, IconModel iconModel,
|
||||||
|
SettingsManager settingsManager, UserPreferences userPreferences)
|
||||||
{
|
{
|
||||||
mIconModel = iconModel;
|
mIconModel = iconModel;
|
||||||
mSettingsManager = settingsManager;
|
mSettingsManager = settingsManager;
|
||||||
@@ -97,12 +105,12 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
|||||||
mAliasModel = aliasModel;
|
mAliasModel = aliasModel;
|
||||||
mUserPreferences = userPreferences;
|
mUserPreferences = userPreferences;
|
||||||
mTalkgroupFormatPreference = mUserPreferences.getTalkgroupFormatPreference();
|
mTalkgroupFormatPreference = mUserPreferences.getTalkgroupFormatPreference();
|
||||||
mAudioOutput = audioOutput;
|
mAudioChannel = audioChannel;
|
||||||
|
|
||||||
if(mAudioOutput != null)
|
if(mAudioChannel != null)
|
||||||
{
|
{
|
||||||
mAudioOutput.addAudioEventListener(this);
|
mAudioChannel.addAudioEventListener(this);
|
||||||
mAudioOutput.setIdentifierCollectionListener(new AudioMetadataProcessor());
|
mAudioChannel.setIdentifierCollectionListener(new AudioMetadataProcessor());
|
||||||
}
|
}
|
||||||
|
|
||||||
mBackgroundColor = SystemProperties.getInstance().get(PROPERTY_COLOR_BACKGROUND, Color.BLACK);
|
mBackgroundColor = SystemProperties.getInstance().get(PROPERTY_COLOR_BACKGROUND, Color.BLACK);
|
||||||
@@ -131,10 +139,10 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
|||||||
//Deregister from receiving preference update notifications
|
//Deregister from receiving preference update notifications
|
||||||
MyEventBus.getGlobalEventBus().unregister(this);
|
MyEventBus.getGlobalEventBus().unregister(this);
|
||||||
|
|
||||||
if(mAudioOutput != null)
|
if(mAudioChannel != null)
|
||||||
{
|
{
|
||||||
mAudioOutput.removeAudioEventListener(this);
|
mAudioChannel.removeAudioEventListener(this);
|
||||||
mAudioOutput.removeAudioMetadataListener();
|
mAudioChannel.removeAudioMetadataListener();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +160,7 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
|||||||
mMutedLabel.setVisible(false);
|
mMutedLabel.setVisible(false);
|
||||||
add(mMutedLabel);
|
add(mMutedLabel);
|
||||||
|
|
||||||
mChannelName = new JLabel(mAudioOutput != null ? mAudioOutput.getChannelName() : " ");
|
mChannelName = new JLabel(mAudioChannel != null ? mAudioChannel.getChannelName() : " ");
|
||||||
mChannelName.setFont(mFont);
|
mChannelName.setFont(mFont);
|
||||||
mChannelName.setForeground(mLabelColor);
|
mChannelName.setForeground(mLabelColor);
|
||||||
add(mChannelName);
|
add(mChannelName);
|
||||||
@@ -172,25 +180,11 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
|||||||
switch(audioEvent.getType())
|
switch(audioEvent.getType())
|
||||||
{
|
{
|
||||||
case AUDIO_STOPPED:
|
case AUDIO_STOPPED:
|
||||||
EventQueue.invokeLater(new Runnable()
|
EventQueue.invokeLater(this::resetLabels);
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
resetLabels();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case AUDIO_MUTED:
|
case AUDIO_MUTED:
|
||||||
case AUDIO_UNMUTED:
|
case AUDIO_UNMUTED:
|
||||||
EventQueue.invokeLater(new Runnable()
|
EventQueue.invokeLater(() -> mMutedLabel.setVisible(mAudioChannel.isMuted()));
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
mMutedLabel.setVisible(mAudioOutput.isMuted());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* ******************************************************************************
|
* *****************************************************************************
|
||||||
* sdrtrunk
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* *****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
package io.github.dsheirer.audio.playback;
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
@@ -24,18 +23,26 @@ import io.github.dsheirer.audio.IAudioController;
|
|||||||
import io.github.dsheirer.icon.IconModel;
|
import io.github.dsheirer.icon.IconModel;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.settings.SettingsManager;
|
import io.github.dsheirer.settings.SettingsManager;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Component;
|
||||||
import net.miginfocom.swing.MigLayout;
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JSeparator;
|
import javax.swing.JSeparator;
|
||||||
import java.awt.Color;
|
|
||||||
import java.awt.Component;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays one or more audio channel panels.
|
||||||
|
*/
|
||||||
public class AudioChannelsPanel extends JPanel
|
public class AudioChannelsPanel extends JPanel
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 1L;
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param iconModel for icon access
|
||||||
|
* @param userPreferences to monitor for audio playback changes
|
||||||
|
* @param settingsManager for tone insertion settings
|
||||||
|
* @param controller for the audio channels
|
||||||
|
* @param aliasModel for accessing aliases
|
||||||
|
*/
|
||||||
public AudioChannelsPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
|
public AudioChannelsPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
|
||||||
IAudioController controller, AliasModel aliasModel)
|
IAudioController controller, AliasModel aliasModel)
|
||||||
{
|
{
|
||||||
@@ -44,15 +51,13 @@ public class AudioChannelsPanel extends JPanel
|
|||||||
|
|
||||||
setBackground(Color.BLACK);
|
setBackground(Color.BLACK);
|
||||||
|
|
||||||
List<AudioOutput> outputs = controller.getAudioOutputs();
|
|
||||||
|
|
||||||
addSeparator();
|
addSeparator();
|
||||||
|
|
||||||
for(int x = 0; x < outputs.size(); x++)
|
for(int x = 0; x < controller.getAudioChannels().size(); x++)
|
||||||
{
|
{
|
||||||
add(new AudioChannelPanel(iconModel, userPreferences, settingsManager, outputs.get(x), aliasModel));
|
add(new AudioChannelPanel(controller.getAudioChannels().get(x), aliasModel, iconModel, settingsManager, userPreferences));
|
||||||
|
|
||||||
if(x < outputs.size() - 1)
|
if(x < controller.getAudioChannels().size() - 1)
|
||||||
{
|
{
|
||||||
addSeparator();
|
addSeparator();
|
||||||
}
|
}
|
||||||
@@ -60,10 +65,10 @@ public class AudioChannelsPanel extends JPanel
|
|||||||
|
|
||||||
/* Add an empty channel panel so that the panel is sized appropriately
|
/* Add an empty channel panel so that the panel is sized appropriately
|
||||||
* for either a single channel or two channels */
|
* for either a single channel or two channels */
|
||||||
if(outputs.size() == 1)
|
if(controller.getAudioChannels().size() == 1)
|
||||||
{
|
{
|
||||||
addSeparator();
|
addSeparator();
|
||||||
add(new AudioChannelPanel(iconModel, userPreferences, settingsManager, null, aliasModel), "growx");
|
add(new AudioChannelPanel(null, aliasModel, iconModel, settingsManager, userPreferences), "growx");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +86,9 @@ public class AudioChannelsPanel extends JPanel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a separator to this panel.
|
||||||
|
*/
|
||||||
private void addSeparator()
|
private void addSeparator()
|
||||||
{
|
{
|
||||||
JSeparator separator = new JSeparator(JSeparator.VERTICAL);
|
JSeparator separator = new JSeparator(JSeparator.VERTICAL);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -18,22 +18,14 @@
|
|||||||
*/
|
*/
|
||||||
package io.github.dsheirer.audio.playback;
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
|
||||||
import io.github.dsheirer.audio.AudioEvent;
|
import io.github.dsheirer.audio.AudioEvent;
|
||||||
import io.github.dsheirer.audio.AudioSegment;
|
|
||||||
import io.github.dsheirer.controller.NamingThreadFactory;
|
import io.github.dsheirer.controller.NamingThreadFactory;
|
||||||
import io.github.dsheirer.eventbus.MyEventBus;
|
|
||||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
|
||||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
|
||||||
import io.github.dsheirer.log.LoggingSuppressor;
|
import io.github.dsheirer.log.LoggingSuppressor;
|
||||||
import io.github.dsheirer.preference.PreferenceType;
|
import io.github.dsheirer.preference.playback.PlayTestAudioRequest;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
|
||||||
import io.github.dsheirer.sample.Broadcaster;
|
|
||||||
import io.github.dsheirer.sample.Listener;
|
|
||||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.LinkedTransferQueue;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -41,11 +33,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.sound.sampled.AudioFormat;
|
import javax.sound.sampled.AudioSystem;
|
||||||
import javax.sound.sampled.BooleanControl;
|
|
||||||
import javax.sound.sampled.Control;
|
import javax.sound.sampled.Control;
|
||||||
|
import javax.sound.sampled.DataLine;
|
||||||
import javax.sound.sampled.FloatControl;
|
import javax.sound.sampled.FloatControl;
|
||||||
import javax.sound.sampled.Line;
|
|
||||||
import javax.sound.sampled.LineEvent;
|
import javax.sound.sampled.LineEvent;
|
||||||
import javax.sound.sampled.LineListener;
|
import javax.sound.sampled.LineListener;
|
||||||
import javax.sound.sampled.LineUnavailableException;
|
import javax.sound.sampled.LineUnavailableException;
|
||||||
@@ -56,418 +47,214 @@ import javax.sound.sampled.SourceDataLine;
|
|||||||
* Audio output/playback channel for a single audio mixer channel. Providers support for playback of audio segments
|
* Audio output/playback channel for a single audio mixer channel. Providers support for playback of audio segments
|
||||||
* and broadcasts audio segment metadata to registered listeners (ie gui components).
|
* and broadcasts audio segment metadata to registered listeners (ie gui components).
|
||||||
*/
|
*/
|
||||||
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>
|
public class AudioOutput implements LineListener
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
private static final Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
||||||
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog);
|
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog);
|
||||||
private int mBufferStartThreshold;
|
private static final int BUFFER_SIZE_SAMPLES = 1000; //At 8 kHz audio
|
||||||
private int mBufferStopThreshold;
|
private final AudioPlaybackDeviceDescriptor mAudioPlaybackDeviceDescriptor;
|
||||||
private Listener<IdentifierCollection> mIdentifierCollectionListener;
|
private final AudioProvider mAudioProvider;
|
||||||
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
|
|
||||||
private SourceDataLine mOutput;
|
|
||||||
private Mixer mMixer;
|
|
||||||
private MixerChannel mMixerChannel;
|
|
||||||
private FloatControl mGainControl;
|
private FloatControl mGainControl;
|
||||||
private BooleanControl mMuteControl;
|
|
||||||
private AudioEvent mAudioStartEvent;
|
|
||||||
private AudioEvent mAudioStopEvent;
|
|
||||||
private boolean mCanProcessAudio = false;
|
|
||||||
private LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
|
|
||||||
private AudioSegment mCurrentAudioSegment;
|
|
||||||
private int mCurrentBufferIndex = 0;
|
|
||||||
private UserPreferences mUserPreferences;
|
|
||||||
private ByteBuffer mAudioSegmentStartTone;
|
|
||||||
private ByteBuffer mAudioSegmentDropTone;
|
|
||||||
private boolean mRunning = false;
|
|
||||||
private ScheduledExecutorService mScheduledExecutorService;
|
private ScheduledExecutorService mScheduledExecutorService;
|
||||||
private ScheduledFuture<?> mProcessorFuture;
|
private ScheduledFuture<?> mProcessorFuture;
|
||||||
private boolean mDropDuplicates;
|
private SourceDataLine mSourceDataLine;
|
||||||
private long mOutputLastTimestamp = 0;
|
private boolean mCanProcessAudio = false;
|
||||||
private static final long STALE_PLAYBACK_THRESHOLD_MS = 500;
|
private boolean mRunning = false;
|
||||||
private AudioFormat mAudioFormat;
|
|
||||||
private Line.Info mLineInfo;
|
|
||||||
private int mRequestedBufferSize;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single audio channel playback with automatic starting and stopping of the
|
* Audio output for the selected audio playback device and provider. Opens a SourceDataLine from the miser for the
|
||||||
* underlying sourcedataline specified by the mixer and mixer channel
|
* audio format specified in the descriptor. Employs a single threaded executor to process audio 10x a second and
|
||||||
* arguments.
|
* manages the start/stop control for the dataline based on audio availability.
|
||||||
*
|
*
|
||||||
* Maintains an internal non-blocking audio packet queue and processes this
|
* @param descriptor for the mixer and audio format
|
||||||
* queue 25 times a second (every 40 ms).
|
* @param audioProvider for access to audio from audio segments
|
||||||
*
|
|
||||||
* @param mixer to obtain source data line
|
|
||||||
* @param mixerChannel either mono or left/right stereo
|
|
||||||
* @param audioFormat to use during playback
|
|
||||||
* @param lineInfo to use when obtaining the source data line
|
|
||||||
* @param requestedBufferSize of approximately 1 second of audio
|
|
||||||
*/
|
*/
|
||||||
public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioFormat, Line.Info lineInfo,
|
public AudioOutput(AudioPlaybackDeviceDescriptor descriptor, AudioProvider audioProvider)
|
||||||
int requestedBufferSize, UserPreferences userPreferences)
|
|
||||||
{
|
{
|
||||||
mMixer = mixer;
|
mAudioProvider = audioProvider;
|
||||||
mMixerChannel = mixerChannel;
|
Mixer mixer = AudioSystem.getMixer(descriptor.getMixerInfo());
|
||||||
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
|
|
||||||
"sdrtrunk audio output " + mixerChannel.name()));
|
|
||||||
mUserPreferences = userPreferences;
|
|
||||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
|
||||||
mAudioFormat = audioFormat;
|
|
||||||
mLineInfo = lineInfo;
|
|
||||||
mRequestedBufferSize = requestedBufferSize;
|
|
||||||
|
|
||||||
try
|
if(mixer == null)
|
||||||
{
|
{
|
||||||
mOutput = (SourceDataLine) mMixer.getLine(mLineInfo);
|
List<AudioPlaybackDeviceDescriptor> descriptors = AudioPlaybackDeviceManager.getAudioPlaybackDevices();
|
||||||
|
|
||||||
if(mOutput != null)
|
int selected = 0;
|
||||||
|
|
||||||
|
while((mixer == null) && (selected < descriptors.size()))
|
||||||
{
|
{
|
||||||
mOutput.open(audioFormat, requestedBufferSize);
|
descriptor = descriptors.get(selected++);
|
||||||
|
mixer = AudioSystem.getMixer(descriptor.getMixerInfo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Start threshold: buffer is full with 10% or less of capacity remaining
|
mAudioPlaybackDeviceDescriptor = descriptor;
|
||||||
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
|
|
||||||
|
|
||||||
//Stop threshold: buffer is empty with 90% or more capacity available
|
if(mixer != null)
|
||||||
mBufferStopThreshold = (int) (mOutput.getBufferSize() * 0.90);
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mSourceDataLine = (SourceDataLine) mixer.getLine(new DataLine.Info(SourceDataLine.class,
|
||||||
|
descriptor.getAudioFormat()));
|
||||||
|
|
||||||
mOutput.addLineListener(this);
|
if(mSourceDataLine != null)
|
||||||
|
|
||||||
if(mOutput != null)
|
|
||||||
{
|
{
|
||||||
|
mSourceDataLine.addLineListener(this);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
|
Control gain = mSourceDataLine.getControl(FloatControl.Type.MASTER_GAIN);
|
||||||
mGainControl = (FloatControl) gain;
|
mGainControl = (FloatControl) gain;
|
||||||
}
|
}
|
||||||
catch(IllegalArgumentException iae)
|
catch(IllegalArgumentException iae)
|
||||||
{
|
{
|
||||||
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
|
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
|
||||||
"MASTER GAIN control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
|
"MASTER GAIN control for stereo line [" + mixer.getMixerInfo().getName() + "]");
|
||||||
getChannelName() + "]");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
mCanProcessAudio = true;
|
||||||
{
|
|
||||||
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
|
|
||||||
mMuteControl = (BooleanControl) mute;
|
|
||||||
}
|
|
||||||
catch(IllegalArgumentException iae)
|
|
||||||
{
|
|
||||||
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
|
|
||||||
"MUTE control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
|
|
||||||
getChannelName() + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
//Run the queue processor task every 100 milliseconds or 10 times a second
|
|
||||||
mProcessorFuture = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
|
||||||
0, 100, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch(LineUnavailableException e)
|
||||||
|
{
|
||||||
|
mLog.error("Couldn't open source data line for mixer: " + descriptor.getMixerInfo().getName(), e);
|
||||||
|
}
|
||||||
|
|
||||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
|
if(mCanProcessAudio)
|
||||||
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
|
{
|
||||||
mCanProcessAudio = true;
|
openSourceDataLine();
|
||||||
|
|
||||||
|
//The audio provider gives us audio 160 samples per interval representing 20 milliseconds of audio.
|
||||||
|
// Run the scheduled executor at just slightly faster than that time, recognizing that it will
|
||||||
|
// block against the mixer source data line until it can write all 160 samples.
|
||||||
|
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
|
||||||
|
"sdrtrunk audio output " + descriptor.getMixerInfo().getName()));
|
||||||
|
mProcessorFuture = mScheduledExecutorService.scheduleAtFixedRate(new AudioProcessor(),
|
||||||
|
0, 19, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Disable each of the audio channels so they don't queue audio segments.
|
||||||
|
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
|
||||||
|
{
|
||||||
|
audioChannel.setDisabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens and starts the source data line.
|
||||||
|
*/
|
||||||
|
private void openSourceDataLine()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(mSourceDataLine != null && !mSourceDataLine.isOpen())
|
||||||
|
{
|
||||||
|
int bufferSize = BUFFER_SIZE_SAMPLES * 2 * mAudioPlaybackDeviceDescriptor.getAudioFormat().getChannels();
|
||||||
|
mSourceDataLine.open(mAudioProvider.getAudioFormat(), bufferSize);
|
||||||
|
bufferSize = mSourceDataLine.getBufferSize();
|
||||||
|
//Fill the line with silence so we can start it
|
||||||
|
mSourceDataLine.write(new byte[bufferSize], 0, bufferSize);
|
||||||
|
mSourceDataLine.start();
|
||||||
|
|
||||||
|
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
|
||||||
|
{
|
||||||
|
audioChannel.setDisabled(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(LineUnavailableException e)
|
catch(LineUnavailableException e)
|
||||||
{
|
{
|
||||||
mLog.error("Couldn't obtain audio source data line for audio output - mixer [" +
|
LOGGING_SUPPRESSOR.error("Can't open", 3, "Error opening source data line", e);
|
||||||
mMixer.getMixerInfo().getName() + "]");
|
mRunning = false;
|
||||||
}
|
|
||||||
|
|
||||||
updateToneInsertionAudioClips();
|
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
|
||||||
|
|
||||||
//Register to receive preference update notifications so we can update the preference items
|
|
||||||
MyEventBus.getGlobalEventBus().register(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates if this audio output doesn't currently have any audio segments queued for playback.
|
|
||||||
* @return true if empty
|
|
||||||
*/
|
|
||||||
public boolean isEmpty()
|
|
||||||
{
|
|
||||||
return mAudioSegmentQueue.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedules the audio segment for playback. The audio segment user count should already be incremented by the
|
|
||||||
* calling entity.
|
|
||||||
*
|
|
||||||
* @param audioSegment to schedule for playback.
|
|
||||||
*/
|
|
||||||
public void play(AudioSegment audioSegment)
|
|
||||||
{
|
|
||||||
if(audioSegment != null)
|
|
||||||
{
|
|
||||||
//Audio segment use count has already been incremented by the external caller.
|
|
||||||
mAudioSegmentQueue.add(audioSegment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates if the audio segment is linked to the current playback audio segment
|
|
||||||
*/
|
|
||||||
public boolean isLinkedTo(AudioSegment audioSegment)
|
|
||||||
{
|
|
||||||
return audioSegment.isLinked() && audioSegment.isLinkedTo(mCurrentAudioSegment);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Receive and process audio identifier update notifications.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void receive(IdentifierUpdateNotification identifierUpdateNotification)
|
|
||||||
{
|
|
||||||
if(mCurrentAudioSegment != null)
|
|
||||||
{
|
|
||||||
IdentifierCollection identifierCollection = mCurrentAudioSegment.getIdentifierCollection();
|
|
||||||
|
|
||||||
if(identifierCollection != null)
|
|
||||||
{
|
{
|
||||||
broadcast(identifierCollection);
|
audioChannel.setDisabled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Guava event bus notifications that the preferences have been updated, so that we can update audio segment tones.
|
* Plays the test audio over the selected audio channel(s)
|
||||||
|
* @param request to play test audio
|
||||||
*/
|
*/
|
||||||
@Subscribe
|
public void playTestAudio(PlayTestAudioRequest request)
|
||||||
public void preferenceUpdated(PreferenceType preferenceType)
|
|
||||||
{
|
{
|
||||||
if(preferenceType == PreferenceType.PLAYBACK)
|
//If we're not running, resize the audio samples to fill the buffer enough to trigger playback.
|
||||||
|
float[] audio = mRunning ? request.audio() : Arrays.copyOf(request.audio(), BUFFER_SIZE_SAMPLES);
|
||||||
|
|
||||||
|
List<AudioChannel> channels = mAudioProvider.getAudioChannels();
|
||||||
|
|
||||||
|
if(request.isAllChannels())
|
||||||
{
|
{
|
||||||
updateToneInsertionAudioClips();
|
for(AudioChannel channel : channels)
|
||||||
}
|
|
||||||
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
|
|
||||||
{
|
|
||||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates audio segment start, drop and preempt tone insertion clips
|
|
||||||
*/
|
|
||||||
private void updateToneInsertionAudioClips()
|
|
||||||
{
|
|
||||||
mAudioSegmentStartTone = null;
|
|
||||||
mAudioSegmentDropTone = null;
|
|
||||||
|
|
||||||
float[] start = mUserPreferences.getPlaybackPreference().getStartTone();
|
|
||||||
|
|
||||||
if(start != null)
|
|
||||||
{
|
|
||||||
mAudioSegmentStartTone = convert(start);
|
|
||||||
}
|
|
||||||
|
|
||||||
float[] drop = mUserPreferences.getPlaybackPreference().getDropTone();
|
|
||||||
|
|
||||||
if(drop != null)
|
|
||||||
{
|
|
||||||
mAudioSegmentDropTone = convert(drop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* buffer content to the data line as a blocking call.
|
|
||||||
*
|
|
||||||
* @param buffer of audio to playback
|
|
||||||
*/
|
|
||||||
private void playAudio(ByteBuffer buffer)
|
|
||||||
{
|
|
||||||
if(mOutput == null)
|
|
||||||
{
|
|
||||||
LOGGING_SUPPRESSOR.error("null output", 2, "Audio Output is null - ignoring audio playback request");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(buffer != null && buffer.array().length > 0)
|
|
||||||
{
|
|
||||||
int wrote = 0;
|
|
||||||
|
|
||||||
//If the output data line is not running, we can only write up to the available capacity. So, only write
|
|
||||||
//what will fit initially, start playback, and then use a blocking write for the remainder.
|
|
||||||
if(!mOutput.isRunning())
|
|
||||||
{
|
{
|
||||||
int toWrite = mOutput.available();
|
channel.playTest(audio);
|
||||||
|
|
||||||
if(toWrite > buffer.array().length)
|
|
||||||
{
|
|
||||||
toWrite = buffer.array().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Top off the buffer and check if we can start it
|
|
||||||
wrote += mOutput.write(buffer.array(), 0, toWrite);
|
|
||||||
mOutputLastTimestamp = System.currentTimeMillis();
|
|
||||||
|
|
||||||
checkStart();
|
|
||||||
|
|
||||||
//Something is causing the source data line to fail to accept audio byte data via the write() method
|
|
||||||
//and this seems to have started around JDK22, maybe. When this happens, close and then re-open the
|
|
||||||
//data line to clear the error state. Note: in testing this error condition, the line is showing
|
|
||||||
//the buffer is empty and the capacity is fully available, so it should have accepted attempts to write
|
|
||||||
//data, but it failed, as indicated by wrote=0.
|
|
||||||
if(!mOutput.isRunning() && wrote <= 0)
|
|
||||||
{
|
|
||||||
mOutput.close();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
mOutput.open();
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
mLog.error("Error after closing and attempting to reopen audio output", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(mOutput.isRunning() && wrote < buffer.array().length)
|
|
||||||
{
|
|
||||||
//This will block until the buffer is fully written to the data line
|
|
||||||
mOutput.write(buffer.array(), wrote, buffer.array().length - wrote);
|
|
||||||
mOutputLastTimestamp = System.currentTimeMillis();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
else if(request.channel() < channels.size())
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose of the currently assigned audio segment.
|
|
||||||
*/
|
|
||||||
private void disposeCurrentAudioSegment()
|
|
||||||
{
|
|
||||||
if(mCurrentAudioSegment != null)
|
|
||||||
{
|
{
|
||||||
mCurrentAudioSegment.decrementConsumerCount();
|
channels.get(request.channel()).playTest(audio);
|
||||||
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
|
}
|
||||||
mCurrentAudioSegment = null;
|
else if(!channels.isEmpty())
|
||||||
broadcast(null);
|
{
|
||||||
|
channels.getFirst().playTest(audio);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGING_SUPPRESSOR.info("No Audio Channels", 2,
|
||||||
|
"Unable to play test audio - no audio channels configured currently");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the next audio segment from the queue.
|
* Audio playback device descriptor for this audio output
|
||||||
*/
|
*/
|
||||||
private void loadNextAudioSegment()
|
public AudioPlaybackDeviceDescriptor getAudioPlaybackDeviceDescriptor()
|
||||||
{
|
{
|
||||||
AudioSegment audioSegment = mAudioSegmentQueue.poll();
|
return mAudioPlaybackDeviceDescriptor;
|
||||||
|
|
||||||
boolean verificationInProgress = (audioSegment != null);
|
|
||||||
|
|
||||||
while(verificationInProgress)
|
|
||||||
{
|
|
||||||
if(audioSegment != null)
|
|
||||||
{
|
|
||||||
//Throw away the audio segment if it has been flagged as do not monitor or is duplicate
|
|
||||||
if(isThrowaway(audioSegment))
|
|
||||||
{
|
|
||||||
audioSegment.decrementConsumerCount();
|
|
||||||
audioSegment = mAudioSegmentQueue.poll();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
verificationInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
verificationInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mCurrentAudioSegment = audioSegment;
|
|
||||||
|
|
||||||
if(audioSegment != null)
|
|
||||||
{
|
|
||||||
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
|
|
||||||
broadcast(mCurrentAudioSegment.getIdentifierCollection());
|
|
||||||
}
|
|
||||||
|
|
||||||
mCurrentBufferIndex = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if the audio segment should be thrown away because it is marked as duplicate or do-not-monitor.
|
* Audio provider managed by this audio output
|
||||||
* @param audioSegment to evaluate
|
|
||||||
* @return true if the audio should be thrown away.
|
|
||||||
*/
|
*/
|
||||||
private boolean isThrowaway(AudioSegment audioSegment)
|
public AudioProvider getAudioProvider()
|
||||||
{
|
{
|
||||||
return audioSegment != null && (audioSegment.isDoNotMonitor() || mDropDuplicates && (audioSegment.isDuplicate()));
|
return mAudioProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage audio segment playback and process audio segment buffers. This method is designed to be called
|
* Process audio from the audio provider and writes it to the mixer's source data line. Calls to this method can
|
||||||
* by a threaded processor repeatedly to playback the current audio segment and check for and start newly
|
* block on the source data line until it has capacity available to accept the audio.
|
||||||
* added audio segments. It also handles starting and stopping the playback source data line to avoid audio
|
|
||||||
* discontinuities due to buffer underruns.
|
|
||||||
*/
|
*/
|
||||||
private void processAudio()
|
private void processAudio()
|
||||||
{
|
{
|
||||||
if(mCurrentAudioSegment == null)
|
if(mSourceDataLine == null)
|
||||||
{
|
{
|
||||||
loadNextAudioSegment();
|
LOGGING_SUPPRESSOR.error("null output", 2,
|
||||||
|
"Audio Output source data line is null - ignoring audio playback request");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
|
ByteBuffer buffer = mAudioProvider.getAudio();
|
||||||
while(isThrowaway(mCurrentAudioSegment))
|
|
||||||
|
if(buffer != null)
|
||||||
{
|
{
|
||||||
if(mCurrentBufferIndex > 0)
|
//This is a blocking method call.
|
||||||
|
int wrote = mSourceDataLine.write(buffer.array(), 0, buffer.array().length);
|
||||||
|
|
||||||
|
//Around JDK 22 something started causing the source data line to fail to accept audio byte data via the
|
||||||
|
//write() method. When this happens, close and reopen the data line to clear the error state.
|
||||||
|
if(wrote <= 0)
|
||||||
{
|
{
|
||||||
playAudio(mAudioSegmentDropTone);
|
LOGGING_SUPPRESSOR.info("Stalled Data Line", 3,
|
||||||
}
|
"Audio playback data line has stopped accepting samples - recycling to clear the error");
|
||||||
|
mSourceDataLine.close();
|
||||||
disposeCurrentAudioSegment();
|
openSourceDataLine();
|
||||||
loadNextAudioSegment();
|
|
||||||
}
|
|
||||||
|
|
||||||
while(mCurrentAudioSegment != null && mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())
|
|
||||||
{
|
|
||||||
//Continuously evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
|
|
||||||
if(isThrowaway(mCurrentAudioSegment))
|
|
||||||
{
|
|
||||||
if(mCurrentBufferIndex > 0)
|
|
||||||
{
|
|
||||||
playAudio(mAudioSegmentDropTone);
|
|
||||||
}
|
|
||||||
|
|
||||||
disposeCurrentAudioSegment();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if(mCurrentBufferIndex == 0)
|
|
||||||
{
|
|
||||||
playAudio(mAudioSegmentStartTone);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
|
|
||||||
|
|
||||||
if(audioBuffer != null)
|
|
||||||
{
|
|
||||||
ByteBuffer audio = convert(audioBuffer);
|
|
||||||
//This call blocks until all audio bytes are dumped into the data line.
|
|
||||||
playAudio(audio);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
mLog.error("Error while processing audio for [" + mMixerChannel.name() + "]", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check for completed and fully-played audio segment to closeout
|
|
||||||
if(mCurrentAudioSegment != null &&
|
|
||||||
mCurrentAudioSegment.isComplete() &&
|
|
||||||
(mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
|
|
||||||
{
|
|
||||||
disposeCurrentAudioSegment();
|
|
||||||
}
|
|
||||||
|
|
||||||
checkStop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -475,156 +262,30 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
*/
|
*/
|
||||||
public void dispose()
|
public void dispose()
|
||||||
{
|
{
|
||||||
MyEventBus.getGlobalEventBus().unregister(this);
|
|
||||||
mCanProcessAudio = false;
|
|
||||||
|
|
||||||
if(mProcessorFuture != null)
|
if(mProcessorFuture != null)
|
||||||
{
|
{
|
||||||
mProcessorFuture.cancel(true);
|
mProcessorFuture.cancel(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
mProcessorFuture = null;
|
mProcessorFuture = null;
|
||||||
disposeCurrentAudioSegment();
|
|
||||||
mAudioEventBroadcaster.clear();
|
|
||||||
mIdentifierCollectionListener = null;
|
|
||||||
|
|
||||||
if(mOutput != null)
|
if(mScheduledExecutorService != null)
|
||||||
{
|
{
|
||||||
mOutput.close();
|
mScheduledExecutorService.shutdownNow();
|
||||||
|
mScheduledExecutorService = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mOutput = null;
|
mAudioProvider.dispose();
|
||||||
|
mCanProcessAudio = false;
|
||||||
|
|
||||||
|
if(mSourceDataLine != null)
|
||||||
|
{
|
||||||
|
mSourceDataLine.stop();
|
||||||
|
mSourceDataLine.flush();
|
||||||
|
mSourceDataLine.close();
|
||||||
|
}
|
||||||
|
mSourceDataLine = null;
|
||||||
mGainControl = null;
|
mGainControl = null;
|
||||||
mMuteControl = null;
|
mRunning = false;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the audio buffer data into a byte buffer format appropriate for the underlying source data line.
|
|
||||||
*/
|
|
||||||
protected abstract ByteBuffer convert(float[] buffer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Audio output channel name
|
|
||||||
*/
|
|
||||||
public String getChannelName()
|
|
||||||
{
|
|
||||||
return mMixerChannel.getLabel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mixer Channel for this audio output
|
|
||||||
*/
|
|
||||||
protected MixerChannel getMixerChannel()
|
|
||||||
{
|
|
||||||
return mMixerChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a single listener to receive audio start and audio stop events
|
|
||||||
*/
|
|
||||||
public void addAudioEventListener(Listener<AudioEvent> listener)
|
|
||||||
{
|
|
||||||
mAudioEventBroadcaster.addListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeAudioEventListener(Listener<AudioEvent> listener)
|
|
||||||
{
|
|
||||||
mAudioEventBroadcaster.removeListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcasts an audio event to the registered listener
|
|
||||||
*/
|
|
||||||
private void broadcastAudioEvent(AudioEvent audioEvent)
|
|
||||||
{
|
|
||||||
mAudioEventBroadcaster.broadcast(audioEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a single listener to receive the audio metadata from each
|
|
||||||
* audio packet
|
|
||||||
*/
|
|
||||||
public void setIdentifierCollectionListener(Listener<IdentifierCollection> listener)
|
|
||||||
{
|
|
||||||
mIdentifierCollectionListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregisters the current audio metadata listener
|
|
||||||
*/
|
|
||||||
public void removeAudioMetadataListener()
|
|
||||||
{
|
|
||||||
mIdentifierCollectionListener = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcasts audio identifier collection metadata to the registered listener
|
|
||||||
*/
|
|
||||||
private void broadcast(IdentifierCollection identifierCollection)
|
|
||||||
{
|
|
||||||
if(mIdentifierCollectionListener != null)
|
|
||||||
{
|
|
||||||
mIdentifierCollectionListener.receive(identifierCollection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts audio playback once audio buffer is almost full and remaining capacity falls below the start threshold.
|
|
||||||
*
|
|
||||||
* Note: this method should only be invoked from the processAudio() method
|
|
||||||
*/
|
|
||||||
private void checkStart()
|
|
||||||
{
|
|
||||||
if(mCanProcessAudio && !mOutput.isRunning() && mOutput.available() <= mBufferStartThreshold)
|
|
||||||
{
|
|
||||||
mOutput.start();
|
|
||||||
mRunning = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops audio playback and drains the audio buffer to empty when the audio buffer is mostly empty and the
|
|
||||||
* available buffer capacity exceeds the stop threshold
|
|
||||||
*
|
|
||||||
* Note: this method should only be invoked from the processAudio() method
|
|
||||||
*/
|
|
||||||
private void checkStop()
|
|
||||||
{
|
|
||||||
if(mRunning)
|
|
||||||
{
|
|
||||||
//If the output buffer falls below the threshold then drain the output and stop playback
|
|
||||||
if(mOutput.isRunning())
|
|
||||||
{
|
|
||||||
if(mOutput.available() >= mBufferStopThreshold)
|
|
||||||
{
|
|
||||||
mOutput.drain();
|
|
||||||
mOutput.stop();
|
|
||||||
mRunning = false;
|
|
||||||
}
|
|
||||||
//Detect when we have a partially filled output that will neither start nor stop and the last write
|
|
||||||
//timestamp was more than the staleness threshold (500 ms) -- stop the output and discard stale contents
|
|
||||||
else if(mOutput.available() < mBufferStopThreshold &&
|
|
||||||
mOutput.available() > mBufferStartThreshold &&
|
|
||||||
(System.currentTimeMillis() - mOutputLastTimestamp >= STALE_PLAYBACK_THRESHOLD_MS))
|
|
||||||
{
|
|
||||||
mOutput.stop();
|
|
||||||
//Discard the stale buffer contents.
|
|
||||||
mOutput.flush();
|
|
||||||
mRunning = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//If output playback stopped on its own because the buffer emptied, then cleanup
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
//If we stopped audio playback, broadcast a null identifier to clear the gui panel
|
|
||||||
if(!mRunning)
|
|
||||||
{
|
|
||||||
broadcast(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -632,11 +293,13 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
*/
|
*/
|
||||||
public void setMuted(boolean muted)
|
public void setMuted(boolean muted)
|
||||||
{
|
{
|
||||||
if(mMuteControl != null)
|
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
|
||||||
{
|
{
|
||||||
mMuteControl.setValue(muted);
|
audioChannel.setMuted(muted);
|
||||||
broadcastAudioEvent(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED, getChannelName()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AudioEvent.Type type = muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED;
|
||||||
|
mAudioProvider.notify(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -644,9 +307,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
*/
|
*/
|
||||||
public boolean isMuted()
|
public boolean isMuted()
|
||||||
{
|
{
|
||||||
if(mMuteControl != null)
|
if(!mAudioProvider.getAudioChannels().isEmpty())
|
||||||
{
|
{
|
||||||
return mMuteControl.getValue();
|
return mAudioProvider.getAudioChannels().getFirst().isMuted();
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -679,20 +342,20 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
|
|
||||||
if(type == LineEvent.Type.START)
|
if(type == LineEvent.Type.START)
|
||||||
{
|
{
|
||||||
mAudioEventBroadcaster.broadcast(mAudioStartEvent);
|
mAudioProvider.notify(AudioEvent.Type.AUDIO_STARTED);
|
||||||
}
|
}
|
||||||
else if(type == LineEvent.Type.STOP)
|
else if(type == LineEvent.Type.STOP)
|
||||||
{
|
{
|
||||||
mAudioEventBroadcaster.broadcast(mAudioStopEvent);
|
mAudioProvider.notify(AudioEvent.Type.AUDIO_STOPPED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runnable audio segment processor
|
* Audio Processor thread
|
||||||
*/
|
*/
|
||||||
public class AudioSegmentProcessor implements Runnable
|
public class AudioProcessor implements Runnable
|
||||||
{
|
{
|
||||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
private final AtomicBoolean mProcessing = new AtomicBoolean();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run()
|
public void run()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -20,8 +20,6 @@ package io.github.dsheirer.audio.playback;
|
|||||||
|
|
||||||
import io.github.dsheirer.alias.AliasModel;
|
import io.github.dsheirer.alias.AliasModel;
|
||||||
import io.github.dsheirer.audio.AudioEvent;
|
import io.github.dsheirer.audio.AudioEvent;
|
||||||
import io.github.dsheirer.audio.AudioException;
|
|
||||||
import io.github.dsheirer.audio.IAudioController;
|
|
||||||
import io.github.dsheirer.eventbus.MyEventBus;
|
import io.github.dsheirer.eventbus.MyEventBus;
|
||||||
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||||
@@ -29,81 +27,77 @@ import io.github.dsheirer.icon.IconModel;
|
|||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.settings.SettingsManager;
|
import io.github.dsheirer.settings.SettingsManager;
|
||||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
import java.awt.Color;
|
||||||
|
import java.awt.EventQueue;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.event.MouseListener;
|
||||||
import jiconfont.icons.font_awesome.FontAwesome;
|
import jiconfont.icons.font_awesome.FontAwesome;
|
||||||
import jiconfont.swing.IconFontSwing;
|
import jiconfont.swing.IconFontSwing;
|
||||||
import net.miginfocom.swing.MigLayout;
|
import net.miginfocom.swing.MigLayout;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.sound.sampled.FloatControl;
|
import javax.sound.sampled.FloatControl;
|
||||||
import javax.swing.Icon;
|
import javax.swing.Icon;
|
||||||
import javax.swing.ImageIcon;
|
import javax.swing.ImageIcon;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JMenu;
|
|
||||||
import javax.swing.JMenuItem;
|
import javax.swing.JMenuItem;
|
||||||
import javax.swing.JOptionPane;
|
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JPopupMenu;
|
import javax.swing.JPopupMenu;
|
||||||
import javax.swing.JSlider;
|
import javax.swing.JSlider;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
import javax.swing.event.ChangeEvent;
|
|
||||||
import javax.swing.event.ChangeListener;
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.awt.EventQueue;
|
|
||||||
import java.awt.event.ActionEvent;
|
|
||||||
import java.awt.event.ActionListener;
|
|
||||||
import java.awt.event.MouseEvent;
|
|
||||||
import java.awt.event.MouseListener;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio playback panel
|
||||||
|
*/
|
||||||
public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 1L;
|
private static final ImageIcon MUTED_ICON = IconModel.getScaledIcon("images/audio_muted.png", 20);
|
||||||
|
private static final ImageIcon UNMUTED_ICON = IconModel.getScaledIcon("images/audio_unmuted.png", 20);
|
||||||
private static final Logger mLog = LoggerFactory.getLogger(AudioPanel.class);
|
private final AliasModel mAliasModel;
|
||||||
|
private final AudioPlaybackManager mAudioPlaybackManager;
|
||||||
private static ImageIcon MUTED_ICON = IconModel.getScaledIcon("images/audio_muted.png", 20);
|
private final IconModel mIconModel;
|
||||||
private static ImageIcon UNMUTED_ICON = IconModel.getScaledIcon("images/audio_unmuted.png", 20);
|
private final SettingsManager mSettingsManager;
|
||||||
|
private final UserPreferences mUserPreferences;
|
||||||
private IconModel mIconModel;
|
|
||||||
private SettingsManager mSettingsManager;
|
|
||||||
private IAudioController mController;
|
|
||||||
private AliasModel mAliasModel;
|
|
||||||
private UserPreferences mUserPreferences;
|
|
||||||
|
|
||||||
private JButton mMuteButton;
|
|
||||||
private AudioChannelsPanel mAudioChannelsPanel;
|
private AudioChannelsPanel mAudioChannelsPanel;
|
||||||
|
private JButton mMuteButton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param iconModel for icon lookup
|
||||||
|
* @param userPreferences for preference lookup
|
||||||
|
* @param settingsManager to monitor for changes
|
||||||
|
* @param audioPlaybackManager for accessing the audio output
|
||||||
|
* @param aliasModel for alias lookup
|
||||||
|
*/
|
||||||
public AudioPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
|
public AudioPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
|
||||||
IAudioController controller, AliasModel aliasModel)
|
AudioPlaybackManager audioPlaybackManager, AliasModel aliasModel)
|
||||||
{
|
{
|
||||||
mIconModel = iconModel;
|
mIconModel = iconModel;
|
||||||
mSettingsManager = settingsManager;
|
mSettingsManager = settingsManager;
|
||||||
mController = controller;
|
mAudioPlaybackManager = audioPlaybackManager;
|
||||||
mAliasModel = aliasModel;
|
mAliasModel = aliasModel;
|
||||||
mUserPreferences = userPreferences;
|
mUserPreferences = userPreferences;
|
||||||
|
mAudioPlaybackManager.addAudioEventListener(this);
|
||||||
mController.addControllerListener(this);
|
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the display
|
||||||
|
*/
|
||||||
private void init()
|
private void init()
|
||||||
{
|
{
|
||||||
setLayout(new MigLayout("insets 0 0 0 0", "[]0[grow,fill]", "[fill]0[]"));
|
setLayout(new MigLayout("insets 0 0 0 0", "[]0[grow,fill]", "[fill]0[]"));
|
||||||
setBackground(Color.BLACK);
|
setBackground(Color.BLACK);
|
||||||
|
|
||||||
mMuteButton = new MuteButton();
|
mMuteButton = new MuteButton();
|
||||||
mMuteButton.setBackground(getBackground());
|
mMuteButton.setBackground(getBackground());
|
||||||
add(mMuteButton);
|
add(mMuteButton);
|
||||||
|
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mAudioPlaybackManager, mAliasModel);
|
||||||
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
|
|
||||||
|
|
||||||
add(mAudioChannelsPanel);
|
add(mAudioChannelsPanel);
|
||||||
|
|
||||||
addMouseListener(new MouseSelectionListener());
|
addMouseListener(new MouseSelectionListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive audio event notifications from the audio playback controller
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void receive(AudioEvent event)
|
public void receive(AudioEvent event)
|
||||||
{
|
{
|
||||||
@@ -112,19 +106,14 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
|||||||
case AUDIO_CONFIGURATION_CHANGE_STARTED:
|
case AUDIO_CONFIGURATION_CHANGE_STARTED:
|
||||||
break;
|
break;
|
||||||
case AUDIO_CONFIGURATION_CHANGE_COMPLETE:
|
case AUDIO_CONFIGURATION_CHANGE_COMPLETE:
|
||||||
EventQueue.invokeLater(new Runnable()
|
EventQueue.invokeLater(() -> {
|
||||||
{
|
remove(mAudioChannelsPanel);
|
||||||
@Override
|
mAudioChannelsPanel.dispose();
|
||||||
public void run()
|
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mAudioPlaybackManager, mAliasModel);
|
||||||
{
|
add(mAudioChannelsPanel);
|
||||||
remove(mAudioChannelsPanel);
|
mAudioChannelsPanel.repaint();
|
||||||
mAudioChannelsPanel.dispose();
|
revalidate();
|
||||||
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
|
repaint();
|
||||||
add(mAudioChannelsPanel);
|
|
||||||
mAudioChannelsPanel.repaint();
|
|
||||||
revalidate();
|
|
||||||
repaint();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -135,26 +124,19 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
|||||||
/**
|
/**
|
||||||
* Audio output mute control menu item.
|
* Audio output mute control menu item.
|
||||||
*/
|
*/
|
||||||
public class AudioOutputMuteItem extends JMenuItem
|
public static class AudioOutputMuteItem extends JMenuItem
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 1L;
|
private final AudioOutput mAudioOutput;
|
||||||
|
|
||||||
private AudioOutput mAudioOutput;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param audioOutput to mute/unmute
|
||||||
|
*/
|
||||||
public AudioOutputMuteItem(AudioOutput audioOutput)
|
public AudioOutputMuteItem(AudioOutput audioOutput)
|
||||||
{
|
{
|
||||||
super(audioOutput.isMuted() ? "Unmute" : "Mute");
|
super(audioOutput.isMuted() ? "Unmute" : "Mute");
|
||||||
|
|
||||||
mAudioOutput = audioOutput;
|
mAudioOutput = audioOutput;
|
||||||
|
addActionListener(e -> mAudioOutput.setMuted(!mAudioOutput.isMuted()));
|
||||||
addActionListener(new ActionListener()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e)
|
|
||||||
{
|
|
||||||
mAudioOutput.setMuted(!mAudioOutput.isMuted());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,37 +151,23 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
|||||||
if(SwingUtilities.isRightMouseButton(event))
|
if(SwingUtilities.isRightMouseButton(event))
|
||||||
{
|
{
|
||||||
JPopupMenu popup = new JPopupMenu();
|
JPopupMenu popup = new JPopupMenu();
|
||||||
|
JMenuItem outputMenu = new JMenuItem("Audio Playback Device ...");
|
||||||
/* Audio mixer/output selection menus */
|
|
||||||
JMenuItem outputMenu = new JMenuItem("Configure ...");
|
|
||||||
Icon icon = IconFontSwing.buildIcon(FontAwesome.COG, 14);
|
Icon icon = IconFontSwing.buildIcon(FontAwesome.COG, 14);
|
||||||
outputMenu.setIcon(icon);
|
outputMenu.setIcon(icon);
|
||||||
outputMenu.addActionListener(new ActionListener()
|
outputMenu.addActionListener(e -> MyEventBus.getGlobalEventBus()
|
||||||
{
|
.post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT)));
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e)
|
|
||||||
{
|
|
||||||
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
popup.add(outputMenu);
|
popup.add(outputMenu);
|
||||||
popup.add(new JPopupMenu.Separator());
|
|
||||||
|
|
||||||
/* Audio output mute and volume control */
|
//Add optional gain controller for th eaudio output
|
||||||
for(AudioOutput output : mController.getAudioOutputs())
|
if(mAudioPlaybackManager.getAudioOutput() != null && mAudioPlaybackManager.getAudioOutput().hasGainControl())
|
||||||
{
|
{
|
||||||
JMenu menu = new JMenu("Channel: " + output.getChannelName());
|
popup.add(new JPopupMenu.Separator());
|
||||||
|
JMenuItem volume = new JMenuItem("Audio Volume");
|
||||||
menu.add(new AudioOutputMuteItem(output));
|
volume.setEnabled(false);
|
||||||
|
Icon volumeIcon = IconFontSwing.buildIcon(FontAwesome.VOLUME_UP, 14);
|
||||||
if(output.hasGainControl())
|
volume.setIcon(volumeIcon);
|
||||||
{
|
popup.add(volume);
|
||||||
JMenu volume = new JMenu("Volume");
|
popup.add(new VolumeSlider(mAudioPlaybackManager.getAudioOutput().getGainControl()));
|
||||||
volume.add(new VolumeSlider(output.getGainControl()));
|
|
||||||
menu.add(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.add(menu);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
popup.show(event.getComponent(), event.getX(), event.getY());
|
popup.show(event.getComponent(), event.getX(), event.getY());
|
||||||
@@ -230,12 +198,14 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
|||||||
/**
|
/**
|
||||||
* Audio volume (gain) adjustment slider control
|
* Audio volume (gain) adjustment slider control
|
||||||
*/
|
*/
|
||||||
public class VolumeSlider extends JSlider
|
public static class VolumeSlider extends JSlider
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 1L;
|
private final FloatControl mFloatControl;
|
||||||
|
|
||||||
private FloatControl mFloatControl;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param control to be controlled
|
||||||
|
*/
|
||||||
public VolumeSlider(FloatControl control)
|
public VolumeSlider(FloatControl control)
|
||||||
{
|
{
|
||||||
super(0, 100, 0);
|
super(0, 100, 0);
|
||||||
@@ -244,21 +214,11 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
|||||||
setMinorTickSpacing(5);
|
setMinorTickSpacing(5);
|
||||||
setPaintTicks(true);
|
setPaintTicks(true);
|
||||||
setPaintLabels(true);
|
setPaintLabels(true);
|
||||||
|
|
||||||
mFloatControl = control;
|
mFloatControl = control;
|
||||||
|
|
||||||
setValue(getIntegerValue(mFloatControl.getValue()));
|
setValue(getIntegerValue(mFloatControl.getValue()));
|
||||||
|
addChangeListener(event -> mFloatControl.shift(mFloatControl.getValue(),
|
||||||
addChangeListener(new ChangeListener()
|
getFloatValue(VolumeSlider.this.getValue()),
|
||||||
{
|
1000));
|
||||||
@Override
|
|
||||||
public void stateChanged(ChangeEvent event)
|
|
||||||
{
|
|
||||||
mFloatControl.shift(mFloatControl.getValue(),
|
|
||||||
getFloatValue(VolumeSlider.this.getValue()),
|
|
||||||
1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addMouseListener(new MouseListener()
|
addMouseListener(new MouseListener()
|
||||||
{
|
{
|
||||||
@@ -270,22 +230,10 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
|||||||
VolumeSlider.this.setValue(50);
|
VolumeSlider.this.setValue(50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public void mouseReleased(MouseEvent arg0) {}
|
||||||
public void mouseReleased(MouseEvent arg0)
|
public void mousePressed(MouseEvent arg0) {}
|
||||||
{
|
public void mouseExited(MouseEvent arg0) {}
|
||||||
}
|
public void mouseEntered(MouseEvent arg0) {}
|
||||||
|
|
||||||
public void mousePressed(MouseEvent arg0)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void mouseExited(MouseEvent arg0)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void mouseEntered(MouseEvent arg0)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,52 +279,12 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mixer/Channel configuration selection item
|
|
||||||
*/
|
|
||||||
public class MixerSelectionItem extends JMenuItem
|
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
private MixerChannelConfiguration mConfiguration;
|
|
||||||
|
|
||||||
public MixerSelectionItem(MixerChannelConfiguration config)
|
|
||||||
{
|
|
||||||
super(config.toString());
|
|
||||||
|
|
||||||
mConfiguration = config;
|
|
||||||
|
|
||||||
addActionListener(new ActionListener()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
mController.setMixerChannelConfiguration(mConfiguration);
|
|
||||||
}
|
|
||||||
catch(AudioException e1)
|
|
||||||
{
|
|
||||||
mLog.error("Couldn't set mixer channel configuration "
|
|
||||||
+ "to: " + mConfiguration.toString());
|
|
||||||
|
|
||||||
JOptionPane.showMessageDialog(MixerSelectionItem.this,
|
|
||||||
"Couldn't set [" + mConfiguration.toString() +
|
|
||||||
"] as the audio output device");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mute button to mute all audio output channels exposed by the audio
|
* Mute button to mute all audio output channels exposed by the audio
|
||||||
* controller
|
* controller
|
||||||
*/
|
*/
|
||||||
public class MuteButton extends JButton
|
public class MuteButton extends JButton
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
private boolean mMuted = false;
|
private boolean mMuted = false;
|
||||||
|
|
||||||
public MuteButton()
|
public MuteButton()
|
||||||
@@ -384,29 +292,13 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
|||||||
setIcon(UNMUTED_ICON);
|
setIcon(UNMUTED_ICON);
|
||||||
setBorderPainted(false);
|
setBorderPainted(false);
|
||||||
getAccessibleContext().setAccessibleName("Mute");
|
getAccessibleContext().setAccessibleName("Mute");
|
||||||
|
addActionListener(e -> {
|
||||||
addActionListener(new ActionListener()
|
mMuted = !mMuted;
|
||||||
{
|
mAudioPlaybackManager.getAudioOutput().setMuted(mMuted);
|
||||||
@Override
|
EventQueue.invokeLater(() -> {
|
||||||
public void actionPerformed(ActionEvent e)
|
setIcon(mMuted ? MUTED_ICON : UNMUTED_ICON);
|
||||||
{
|
getAccessibleContext().setAccessibleName(mMuted ? "Unmute" : "Mute");
|
||||||
mMuted = !mMuted;
|
});
|
||||||
|
|
||||||
for(AudioOutput output : mController.getAudioOutputs())
|
|
||||||
{
|
|
||||||
output.setMuted(mMuted);
|
|
||||||
}
|
|
||||||
|
|
||||||
EventQueue.invokeLater(new Runnable()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
setIcon(mMuted ? MUTED_ICON : UNMUTED_ICON);
|
|
||||||
getAccessibleContext().setAccessibleName(mMuted ? "Unmute" : "Mute");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioFormat;
|
||||||
|
import javax.sound.sampled.AudioSystem;
|
||||||
|
import javax.sound.sampled.Mixer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio playback device descriptor for a mixer and a supported audio output format (ie number of channels).
|
||||||
|
*/
|
||||||
|
public class AudioPlaybackDeviceDescriptor implements Comparable<AudioPlaybackDeviceDescriptor>
|
||||||
|
{
|
||||||
|
private final Mixer.Info mMixerInfo;
|
||||||
|
private final AudioFormat mAudioFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
*
|
||||||
|
* @param mixerInfo for the mixer
|
||||||
|
* @param audioFormat supported by the mixer
|
||||||
|
*/
|
||||||
|
public AudioPlaybackDeviceDescriptor(Mixer.Info mixerInfo, AudioFormat audioFormat)
|
||||||
|
{
|
||||||
|
mMixerInfo = mixerInfo;
|
||||||
|
mAudioFormat = audioFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return getCleanDescription(getMixerInfo()) + "(" + getMixerInfo().getName() + ") - " +
|
||||||
|
getChannelDescription(getAudioFormat());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getCleanDescription(Mixer.Info mixerInfo)
|
||||||
|
{
|
||||||
|
return mixerInfo.getDescription().replace("Direct Audio Device: ", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a description of the channels for the audio format
|
||||||
|
* @param audioFormat with channels
|
||||||
|
* @return pretty string
|
||||||
|
*/
|
||||||
|
private static String getChannelDescription(AudioFormat audioFormat)
|
||||||
|
{
|
||||||
|
return switch(audioFormat.getChannels())
|
||||||
|
{
|
||||||
|
case 1 -> "Mono";
|
||||||
|
case 2 -> "Stereo";
|
||||||
|
case AudioSystem.NOT_SPECIFIED -> "Unknown number of channels";
|
||||||
|
default -> audioFormat.getChannels() + " Channels";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio format supported by the mixer
|
||||||
|
*
|
||||||
|
* @return audio format
|
||||||
|
*/
|
||||||
|
public AudioFormat getAudioFormat()
|
||||||
|
{
|
||||||
|
return mAudioFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mixer information for the available system mixer.
|
||||||
|
*/
|
||||||
|
public Mixer.Info getMixerInfo()
|
||||||
|
{
|
||||||
|
return mMixerInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(AudioPlaybackDeviceDescriptor other)
|
||||||
|
{
|
||||||
|
if(other == null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(other.getMixerInfo().getDescription().equals(getMixerInfo().getDescription()))
|
||||||
|
{
|
||||||
|
return Integer.compare(getAudioFormat().getChannels(), other.getAudioFormat().getChannels());
|
||||||
|
}
|
||||||
|
|
||||||
|
return getMixerInfo().getDescription().compareTo(other.getMixerInfo().getDescription());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioFormat;
|
||||||
|
import javax.sound.sampled.AudioSystem;
|
||||||
|
import javax.sound.sampled.DataLine;
|
||||||
|
import javax.sound.sampled.Line;
|
||||||
|
import javax.sound.sampled.Mixer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio playback device manager.
|
||||||
|
*/
|
||||||
|
public class AudioPlaybackDeviceManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Always returns the first available device in the system.
|
||||||
|
*/
|
||||||
|
public static AudioPlaybackDeviceDescriptor getDefaultAudioPLaybackDevice()
|
||||||
|
{
|
||||||
|
List<AudioPlaybackDeviceDescriptor> devices = getAudioPlaybackDevices();
|
||||||
|
|
||||||
|
if(!devices.isEmpty())
|
||||||
|
{
|
||||||
|
return devices.getFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the matching audio playback device
|
||||||
|
* @param name of the device
|
||||||
|
* @param channels count
|
||||||
|
* @return matching entry or the default playback device.
|
||||||
|
*/
|
||||||
|
public static AudioPlaybackDeviceDescriptor getAudioPlaybackDevice(String name, int channels)
|
||||||
|
{
|
||||||
|
for(AudioPlaybackDeviceDescriptor device: getAudioPlaybackDevices())
|
||||||
|
{
|
||||||
|
if(device.getMixerInfo().getName().equals(name) && device.getAudioFormat().getChannels() == channels)
|
||||||
|
{
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDefaultAudioPLaybackDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<AudioPlaybackDeviceDescriptor> getAudioPlaybackDevices()
|
||||||
|
{
|
||||||
|
List<AudioPlaybackDeviceDescriptor> devices = new ArrayList<>();
|
||||||
|
|
||||||
|
Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo();
|
||||||
|
|
||||||
|
for(Mixer.Info mixerInfo : mixerInfos)
|
||||||
|
{
|
||||||
|
Mixer mixer = AudioSystem.getMixer(mixerInfo);
|
||||||
|
|
||||||
|
if(mixer != null)
|
||||||
|
{
|
||||||
|
List<AudioFormat> discoveredFormats = new ArrayList<>();
|
||||||
|
|
||||||
|
for(Line.Info sourceLineInfo : mixer.getSourceLineInfo())
|
||||||
|
{
|
||||||
|
if(sourceLineInfo instanceof DataLine.Info dataLineInfo)
|
||||||
|
{
|
||||||
|
for(AudioFormat audioFormat : dataLineInfo.getFormats())
|
||||||
|
{
|
||||||
|
if(!audioFormat.isBigEndian() &&
|
||||||
|
audioFormat.getSampleSizeInBits() == 16 &&
|
||||||
|
!discoveredFormats.contains(audioFormat) &&
|
||||||
|
audioFormat.getChannels() <= 2) //TODO: remove the 2 channels max restriction once supported
|
||||||
|
{
|
||||||
|
if(audioFormat.getSampleRate() == AudioSystem.NOT_SPECIFIED)
|
||||||
|
{
|
||||||
|
discoveredFormats.add(audioFormat);
|
||||||
|
//Modify the format for 8 kHz sample rate
|
||||||
|
AudioFormat updatedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
|
||||||
|
8000, 16, audioFormat.getChannels(), audioFormat.getFrameSize(),
|
||||||
|
8000, false);
|
||||||
|
devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, updatedFormat));
|
||||||
|
}
|
||||||
|
else if(audioFormat.getSampleRate() == 8000)
|
||||||
|
{
|
||||||
|
discoveredFormats.add(audioFormat);
|
||||||
|
devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, audioFormat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(devices);
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void main()
|
||||||
|
{
|
||||||
|
List<AudioPlaybackDeviceDescriptor> devices = getAudioPlaybackDevices();
|
||||||
|
for(AudioPlaybackDeviceDescriptor device : devices)
|
||||||
|
{
|
||||||
|
System.out.println(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -27,11 +27,11 @@ import io.github.dsheirer.controller.NamingThreadFactory;
|
|||||||
import io.github.dsheirer.eventbus.MyEventBus;
|
import io.github.dsheirer.eventbus.MyEventBus;
|
||||||
import io.github.dsheirer.preference.PreferenceType;
|
import io.github.dsheirer.preference.PreferenceType;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
|
import io.github.dsheirer.preference.playback.PlayTestAudioRequest;
|
||||||
import io.github.dsheirer.sample.Broadcaster;
|
import io.github.dsheirer.sample.Broadcaster;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
|
||||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -51,23 +51,16 @@ import org.slf4j.LoggerFactory;
|
|||||||
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController
|
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController
|
||||||
{
|
{
|
||||||
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
|
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
|
||||||
|
private final AudioSegmentPrioritySorter mAudioSegmentPrioritySorter = new AudioSegmentPrioritySorter();
|
||||||
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
|
private final Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
|
||||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
|
private final List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||||
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
|
private final List<AudioSegment> mPendingAudioSegments = new ArrayList<>();
|
||||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
|
private final LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||||
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
|
private final ReentrantLock mAudioChannelsLock = new ReentrantLock();
|
||||||
|
private final UserPreferences mUserPreferences;
|
||||||
|
private AudioPlaybackDeviceDescriptor mAudioPlaybackDevice;
|
||||||
|
private AudioOutput mAudioOutput;
|
||||||
private ScheduledFuture<?> mProcessingTask;
|
private ScheduledFuture<?> mProcessingTask;
|
||||||
private UserPreferences mUserPreferences;
|
|
||||||
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<>();
|
|
||||||
private ScheduledExecutorService mScheduledExecutorService =
|
|
||||||
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
|
|
||||||
private AudioSegmentPrioritySorter mAudioSegmentPrioritySorter = new AudioSegmentPrioritySorter();
|
|
||||||
private ReentrantLock mAudioOutputLock = new ReentrantLock();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance.
|
* Constructs an instance.
|
||||||
@@ -78,20 +71,18 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
{
|
{
|
||||||
mUserPreferences = userPreferences;
|
mUserPreferences = userPreferences;
|
||||||
MyEventBus.getGlobalEventBus().register(this);
|
MyEventBus.getGlobalEventBus().register(this);
|
||||||
|
AudioPlaybackDeviceDescriptor device = mUserPreferences.getPlaybackPreference().getAudioPlaybackDevice();
|
||||||
|
|
||||||
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
|
if(device != null)
|
||||||
|
|
||||||
if(configuration != null)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
setMixerChannelConfiguration(configuration);
|
setAudioPlaybackDevice(device);
|
||||||
}
|
}
|
||||||
catch(AudioException ae)
|
catch(AudioException ae)
|
||||||
{
|
{
|
||||||
mLog.error("Error during setup of audio playback configuration. Attempted to use audio mixer [" +
|
mLog.error("Error during setup of audio playback configuration. Attempted to use device [" +
|
||||||
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
|
device + "]", ae);
|
||||||
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -99,13 +90,16 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
mLog.warn("No audio output devices available");
|
mLog.warn("No audio output devices available");
|
||||||
}
|
}
|
||||||
|
|
||||||
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
//Even if we don't have an audio device, setup the queue processor to always process the audio segment queue
|
||||||
|
ScheduledExecutorService scheduledExecutorService =
|
||||||
|
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
|
||||||
|
mProcessingTask = scheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||||
0, 100, TimeUnit.MILLISECONDS);
|
0, 100, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives audio segments from channel audio modules.
|
* Receives audio segments from channel audio modules.
|
||||||
* @param audioSegment
|
* @param audioSegment to receive and process
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void receive(AudioSegment audioSegment)
|
public void receive(AudioSegment audioSegment)
|
||||||
@@ -195,11 +189,11 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
}
|
}
|
||||||
else if(audioSegment.isLinked())
|
else if(audioSegment.isLinked())
|
||||||
{
|
{
|
||||||
mAudioOutputLock.lock();
|
mAudioChannelsLock.lock();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for(AudioOutput audioOutput: mAudioOutputs)
|
for(AudioChannel audioOutput: mAudioOutput.getAudioProvider().getAudioChannels())
|
||||||
{
|
{
|
||||||
if(audioOutput.isLinkedTo(audioSegment))
|
if(audioOutput.isLinkedTo(audioSegment))
|
||||||
{
|
{
|
||||||
@@ -210,7 +204,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
mAudioOutputLock.unlock();
|
mAudioChannelsLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,18 +213,16 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
if(!mAudioSegments.isEmpty())
|
if(!mAudioSegments.isEmpty())
|
||||||
{
|
{
|
||||||
mAudioSegments.sort(mAudioSegmentPrioritySorter);
|
mAudioSegments.sort(mAudioSegmentPrioritySorter);
|
||||||
|
mAudioChannelsLock.lock();
|
||||||
mAudioOutputLock.lock();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
//Assign empty audio outputs first
|
//Assign empty audio outputs first
|
||||||
for(AudioOutput audioOutput: mAudioOutputs)
|
for(AudioChannel audioChannel: mAudioOutput.getAudioProvider().getAudioChannels())
|
||||||
{
|
{
|
||||||
if(audioOutput.isEmpty())
|
if(audioChannel.isEmpty())
|
||||||
{
|
{
|
||||||
audioOutput.play(mAudioSegments.remove(0));
|
audioChannel.play(mAudioSegments.removeFirst());
|
||||||
|
|
||||||
if(mAudioSegments.isEmpty())
|
if(mAudioSegments.isEmpty())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -240,7 +232,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
mAudioOutputLock.unlock();
|
mAudioChannelsLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +265,19 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
mAudioSegments.clear();
|
mAudioSegments.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives a request from the global event bus to playback a test audio sequence via the specified audio channel
|
||||||
|
* @param request with test audio and channel number
|
||||||
|
*/
|
||||||
|
@Subscribe
|
||||||
|
public void playTestAudio(PlayTestAudioRequest request)
|
||||||
|
{
|
||||||
|
if(mAudioOutput != null)
|
||||||
|
{
|
||||||
|
mAudioOutput.playTestAudio(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receive user preference update notifications so that we can detect when the user changes the audio output
|
* Receive user preference update notifications so that we can detect when the user changes the audio output
|
||||||
* device in the user preferences editor.
|
* device in the user preferences editor.
|
||||||
@@ -282,100 +287,112 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
{
|
{
|
||||||
if(preferenceType == PreferenceType.PLAYBACK)
|
if(preferenceType == PreferenceType.PLAYBACK)
|
||||||
{
|
{
|
||||||
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
|
AudioPlaybackDeviceDescriptor device = mUserPreferences.getPlaybackPreference().getAudioPlaybackDevice();
|
||||||
|
|
||||||
if(configuration != null && !configuration.equals(mMixerChannelConfiguration))
|
if(device != null && !device.equals(getAudioPlaybackDevice()))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
setMixerChannelConfiguration(configuration);
|
setAudioPlaybackDevice(device);
|
||||||
}
|
}
|
||||||
catch(AudioException ae)
|
catch(AudioException ae)
|
||||||
{
|
{
|
||||||
mLog.error("Error changing audio output to [" + configuration + "]", ae);
|
mLog.error("Error changing audio output to [" + device + "]", ae);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures audio playback to use the configuration specified in the entry argument.
|
* Configures audio playback to use the audio device argument.
|
||||||
*
|
*
|
||||||
* @param entry to use in configuring the audio playback setup.
|
* @param audioDevice to use in configuring the audio playback setup.
|
||||||
* @throws AudioException if there is an error
|
* @throws AudioException if there is an error
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException
|
public void setAudioPlaybackDevice(AudioPlaybackDeviceDescriptor audioDevice) throws AudioException
|
||||||
{
|
{
|
||||||
if(entry != null)
|
if(audioDevice != null)
|
||||||
{
|
{
|
||||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
|
mControllerBroadcaster.broadcast(new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED,
|
||||||
|
audioDevice.getMixerInfo().getName()));
|
||||||
|
|
||||||
mAudioOutputLock.lock();
|
mAudioChannelsLock.lock();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for(AudioOutput audioOutput: mAudioOutputs)
|
if(mAudioOutput != null)
|
||||||
{
|
{
|
||||||
audioOutput.dispose();
|
mAudioOutput.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
mAudioOutputs.clear();
|
int channelCount = audioDevice.getAudioFormat().getChannels();
|
||||||
|
|
||||||
switch(entry.getMixerChannel())
|
switch(channelCount)
|
||||||
{
|
{
|
||||||
case MONO:
|
case 1:
|
||||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
|
mAudioOutput = new AudioOutput(audioDevice, new AudioProviderMono(mUserPreferences));
|
||||||
mAudioOutputs.add(mono);
|
|
||||||
break;
|
break;
|
||||||
case STEREO:
|
case 2:
|
||||||
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
|
mAudioOutput = new AudioOutput(audioDevice, new AudioProviderStereo(mUserPreferences));
|
||||||
mAudioOutputs.add(left);
|
|
||||||
|
|
||||||
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
|
|
||||||
mAudioOutputs.add(right);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
|
// mAudioOutput = new AudioOutput(audioDevice, new AudioProviderMultiChannel(mUserPreferences,
|
||||||
|
// audioDevice.getAudioFormat()));
|
||||||
|
throw new AudioException("Unsupported mixer channel configuration channel count: " + channelCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Note: audio output can use an alternate device if the requested device can't be used, so we assign
|
||||||
|
//the descriptor that was actually used by the audio output
|
||||||
|
mAudioPlaybackDevice = mAudioOutput.getAudioPlaybackDeviceDescriptor();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
mAudioOutputLock.unlock();
|
mAudioChannelsLock.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
|
mControllerBroadcaster.broadcast(new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE,
|
||||||
mMixerChannelConfiguration = entry;
|
mAudioPlaybackDevice.getMixerInfo().getName()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio output device. Note: this can be null depending on when it is accessed.
|
||||||
|
* @return audio output or null.
|
||||||
|
*/
|
||||||
|
public AudioOutput getAudioOutput()
|
||||||
|
{
|
||||||
|
return mAudioOutput;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current audio playback mixer channel configuration setting.
|
* Current audio playback mixer channel configuration setting.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public MixerChannelConfiguration getMixerChannelConfiguration()
|
public AudioPlaybackDeviceDescriptor getAudioPlaybackDevice()
|
||||||
{
|
{
|
||||||
return mMixerChannelConfiguration;
|
return mAudioPlaybackDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of sorted audio outputs available for the current mixer channel configuration
|
* List of sorted audio outputs available for the current mixer channel configuration
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<AudioOutput> getAudioOutputs()
|
public List<AudioChannel> getAudioChannels()
|
||||||
{
|
{
|
||||||
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputs);
|
if(mAudioOutput != null)
|
||||||
|
{
|
||||||
|
return mAudioOutput.getAudioProvider().getAudioChannels();
|
||||||
|
}
|
||||||
|
|
||||||
outputs.sort(Comparator.comparing(AudioOutput::getChannelName));
|
return Collections.emptyList();
|
||||||
|
|
||||||
return outputs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an audio event listener to receive audio event notifications.
|
* Adds an audio event listener to receive audio event notifications.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void addControllerListener(Listener<AudioEvent> listener)
|
public void addAudioEventListener(Listener<AudioEvent> listener)
|
||||||
{
|
{
|
||||||
mControllerBroadcaster.addListener(listener);
|
mControllerBroadcaster.addListener(listener);
|
||||||
}
|
}
|
||||||
@@ -384,7 +401,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
* Removes an audio event listener from receiving audio event notifications.
|
* Removes an audio event listener from receiving audio event notifications.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void removeControllerListener(Listener<AudioEvent> listener)
|
public void removeAudioEventListener(Listener<AudioEvent> listener)
|
||||||
{
|
{
|
||||||
mControllerBroadcaster.removeListener(listener);
|
mControllerBroadcaster.removeListener(listener);
|
||||||
}
|
}
|
||||||
@@ -394,7 +411,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
*/
|
*/
|
||||||
public class AudioSegmentProcessor implements Runnable
|
public class AudioSegmentProcessor implements Runnable
|
||||||
{
|
{
|
||||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
private final AtomicBoolean mProcessing = new AtomicBoolean();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run()
|
public void run()
|
||||||
@@ -418,7 +435,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
|||||||
/**
|
/**
|
||||||
* Audio segment comparator for sorting audio segments by: 1)Playback priority and 2)Segment start time
|
* Audio segment comparator for sorting audio segments by: 1)Playback priority and 2)Segment start time
|
||||||
*/
|
*/
|
||||||
public class AudioSegmentPrioritySorter implements Comparator<AudioSegment>
|
public static class AudioSegmentPrioritySorter implements Comparator<AudioSegment>
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public int compare(AudioSegment segment1, AudioSegment segment2)
|
public int compare(AudioSegment segment1, AudioSegment segment2)
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
|
import io.github.dsheirer.audio.AudioEvent;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract audio provider implementation
|
||||||
|
*/
|
||||||
|
public abstract class AudioProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Audio format for this provider
|
||||||
|
*/
|
||||||
|
public abstract AudioFormat getAudioFormat();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide a non-null, 160-sample/20-millisecond buffer of audio at 8 kHz
|
||||||
|
* @return audio buffer
|
||||||
|
*/
|
||||||
|
public abstract ByteBuffer getAudio();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to the audio channels for this provider
|
||||||
|
* @return list of audio channels
|
||||||
|
*/
|
||||||
|
public abstract List<AudioChannel> getAudioChannels();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies audio event listener(s) of the event
|
||||||
|
* @param eventType to broadcast
|
||||||
|
*/
|
||||||
|
public void notify(AudioEvent.Type eventType)
|
||||||
|
{
|
||||||
|
for(AudioChannel audioChannel : getAudioChannels())
|
||||||
|
{
|
||||||
|
audioChannel.notify(eventType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes the audio channel to prepare for audio output changes or shutdown.
|
||||||
|
*/
|
||||||
|
public void dispose()
|
||||||
|
{
|
||||||
|
for(AudioChannel audioChannel: getAudioChannels())
|
||||||
|
{
|
||||||
|
audioChannel.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
|
import io.github.dsheirer.audio.AudioFormats;
|
||||||
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.ShortBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single channel (mono) audio provider wraps a single audio channel and provides converted audio.
|
||||||
|
*/
|
||||||
|
public class AudioProviderMono extends AudioProvider
|
||||||
|
{
|
||||||
|
private static final ByteBuffer SILENCE = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 2);
|
||||||
|
private final AudioChannel mAudioChannel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param userPreferences for monitoring preference changes
|
||||||
|
*/
|
||||||
|
public AudioProviderMono(UserPreferences userPreferences)
|
||||||
|
{
|
||||||
|
mAudioChannel = new AudioChannel(userPreferences, "MONO");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AudioChannel> getAudioChannels()
|
||||||
|
{
|
||||||
|
return List.of(mAudioChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AudioFormat getAudioFormat()
|
||||||
|
{
|
||||||
|
return AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getAudio()
|
||||||
|
{
|
||||||
|
float[] audio = mAudioChannel.getAudio();
|
||||||
|
|
||||||
|
if(audio == null)
|
||||||
|
{
|
||||||
|
if(!mAudioChannel.hasAudioSegment())
|
||||||
|
{
|
||||||
|
mAudioChannel.clearMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SILENCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||||
|
|
||||||
|
for(int x = 0; x < AudioChannel.SAMPLES_PER_INTERVAL; x++)
|
||||||
|
{
|
||||||
|
shortBuffer.put((short) (audio[x] * Short.MAX_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.ShortBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-Channel audio provider implementation. Provides an interleaved stream of audio from multiple channels.
|
||||||
|
*
|
||||||
|
* Note: this implementation has not been tested for use.
|
||||||
|
*/
|
||||||
|
public class AudioProviderMultiChannel extends AudioProvider
|
||||||
|
{
|
||||||
|
private static ByteBuffer SILENCE;
|
||||||
|
private final List<AudioChannel> mAudioChannels = new ArrayList<>();
|
||||||
|
private final AudioFormat mAudioFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a multi-channel audio provider
|
||||||
|
* @param userPreferences for this provider
|
||||||
|
* @param audioFormat for this provider
|
||||||
|
*/
|
||||||
|
public AudioProviderMultiChannel(UserPreferences userPreferences, AudioFormat audioFormat)
|
||||||
|
{
|
||||||
|
SILENCE = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * audioFormat.getChannels());
|
||||||
|
|
||||||
|
for(int channel = 0; channel < audioFormat.getChannels(); channel++)
|
||||||
|
{
|
||||||
|
mAudioChannels.add(new AudioChannel(userPreferences, String.valueOf(channel)));
|
||||||
|
}
|
||||||
|
|
||||||
|
mAudioFormat = audioFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AudioFormat getAudioFormat()
|
||||||
|
{
|
||||||
|
return mAudioFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access the next buffer of audio.
|
||||||
|
* @return next buffer or null if there is no audio available
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getAudio()
|
||||||
|
{
|
||||||
|
float[][] samples = new float[mAudioChannels.size()][];
|
||||||
|
boolean hasAudio = false;
|
||||||
|
|
||||||
|
AudioChannel channel = null;
|
||||||
|
|
||||||
|
for(int x = 0; x < mAudioChannels.size(); x++)
|
||||||
|
{
|
||||||
|
channel = mAudioChannels.get(x);
|
||||||
|
samples[x] = channel.getAudio();
|
||||||
|
|
||||||
|
if(samples[x] == null)
|
||||||
|
{
|
||||||
|
if(!channel.hasAudioSegment())
|
||||||
|
{
|
||||||
|
channel.clearMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
samples[x] = new float[AudioChannel.SAMPLES_PER_INTERVAL];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hasAudio = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!hasAudio)
|
||||||
|
{
|
||||||
|
return SILENCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 2 *
|
||||||
|
mAudioChannels.size()).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||||
|
|
||||||
|
for(int sampleCounter = 0; sampleCounter < AudioChannel.SAMPLES_PER_INTERVAL; sampleCounter++)
|
||||||
|
{
|
||||||
|
for(int channelNumber = 0; channelNumber < mAudioChannels.size(); channelNumber++)
|
||||||
|
{
|
||||||
|
shortBuffer.put((short)(samples[channelNumber][sampleCounter] * Short.MAX_VALUE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AudioChannel> getAudioChannels()
|
||||||
|
{
|
||||||
|
return Collections.unmodifiableList(mAudioChannels);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
|
import io.github.dsheirer.audio.AudioFormats;
|
||||||
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.ShortBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an interleaved stream of left and right audio samples from audio channels.
|
||||||
|
*
|
||||||
|
* Note: the getAudio() method is optimized to provide buffers of 160 samples (x2) of audio per interval, equivalent to
|
||||||
|
* 20 milliseconds of audio at 8,000 Hertz sample rate.
|
||||||
|
*/
|
||||||
|
public class AudioProviderStereo extends AudioProvider
|
||||||
|
{
|
||||||
|
private final AudioChannel mAudioChannelLeft;
|
||||||
|
private final AudioChannel mAudioChannelRight;
|
||||||
|
private static final ByteBuffer SILENCE = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 4);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance.
|
||||||
|
* @param userPreferences for monitoring preference changes
|
||||||
|
*/
|
||||||
|
public AudioProviderStereo(UserPreferences userPreferences)
|
||||||
|
{
|
||||||
|
mAudioChannelLeft = new AudioChannel(userPreferences, "LEFT");
|
||||||
|
mAudioChannelRight = new AudioChannel(userPreferences, "RIGHT");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AudioFormat getAudioFormat()
|
||||||
|
{
|
||||||
|
return AudioFormats.PCM_SIGNED_8000_HZ_16BITS_STEREO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AudioChannel> getAudioChannels()
|
||||||
|
{
|
||||||
|
return List.of(mAudioChannelLeft, mAudioChannelRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access the next buffer of audio.
|
||||||
|
* @return next buffer or null if there is no audio available
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getAudio()
|
||||||
|
{
|
||||||
|
float[] left = mAudioChannelLeft.getAudio();
|
||||||
|
|
||||||
|
if(left == null && !mAudioChannelLeft.hasAudioSegment())
|
||||||
|
{
|
||||||
|
mAudioChannelLeft.clearMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
float[] right = mAudioChannelRight.getAudio();
|
||||||
|
|
||||||
|
if(right == null && !mAudioChannelRight.hasAudioSegment())
|
||||||
|
{
|
||||||
|
mAudioChannelRight.clearMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
//If we didn't get audio from either channel send silence buffer
|
||||||
|
if(left == null && right == null)
|
||||||
|
{
|
||||||
|
return SILENCE;
|
||||||
|
}
|
||||||
|
else if(left == null)
|
||||||
|
{
|
||||||
|
left = new float[AudioChannel.SAMPLES_PER_INTERVAL];
|
||||||
|
}
|
||||||
|
else if(right == null)
|
||||||
|
{
|
||||||
|
right = new float[AudioChannel.SAMPLES_PER_INTERVAL];
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 4).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||||
|
|
||||||
|
for(int x = 0; x < AudioChannel.SAMPLES_PER_INTERVAL; x++)
|
||||||
|
{
|
||||||
|
shortBuffer.put((short)(left[x] * Short.MAX_VALUE));
|
||||||
|
shortBuffer.put((short)(right[x] * Short.MAX_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* *****************************************************************************
|
|
||||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
* ****************************************************************************
|
|
||||||
*/
|
|
||||||
package io.github.dsheirer.audio.playback;
|
|
||||||
|
|
||||||
import io.github.dsheirer.audio.AudioFormats;
|
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
|
||||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
|
||||||
|
|
||||||
import javax.sound.sampled.Mixer;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.nio.ShortBuffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mono Audio output implementation
|
|
||||||
*/
|
|
||||||
public class MonoAudioOutput extends AudioOutput
|
|
||||||
{
|
|
||||||
private final static int BUFFER_SIZE = 8000;
|
|
||||||
|
|
||||||
public MonoAudioOutput(Mixer mixer, UserPreferences userPreferences)
|
|
||||||
{
|
|
||||||
super(mixer, MixerChannel.MONO, AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO,
|
|
||||||
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the audio packet data into mono audio frames.
|
|
||||||
*/
|
|
||||||
protected ByteBuffer convert(float[] samples)
|
|
||||||
{
|
|
||||||
ByteBuffer buffer = null;
|
|
||||||
|
|
||||||
if(samples.length > 0)
|
|
||||||
{
|
|
||||||
/* Little-endian byte buffer */
|
|
||||||
buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
|
|
||||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
|
||||||
|
|
||||||
for(float sample : samples)
|
|
||||||
{
|
|
||||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
* *****************************************************************************
|
|
||||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
* ****************************************************************************
|
|
||||||
*/
|
|
||||||
package io.github.dsheirer.audio.playback;
|
|
||||||
|
|
||||||
import io.github.dsheirer.audio.AudioFormats;
|
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
|
||||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
|
||||||
|
|
||||||
import javax.sound.sampled.Mixer;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.nio.ShortBuffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stereo audio output implementation.
|
|
||||||
*/
|
|
||||||
public class StereoAudioOutput extends AudioOutput
|
|
||||||
{
|
|
||||||
private final static int BUFFER_SIZE = 16000;
|
|
||||||
|
|
||||||
public StereoAudioOutput(Mixer mixer, MixerChannel channel, UserPreferences userPreferences)
|
|
||||||
{
|
|
||||||
super(mixer, channel, AudioFormats.PCM_SIGNED_8000_HZ_16BITS_STEREO, AudioFormats.STEREO_SOURCE_DATALINE_INFO,
|
|
||||||
BUFFER_SIZE, userPreferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the audio packet data into stereo audio frames with the mixer
|
|
||||||
* channel containing the audio and the other channel containing zero
|
|
||||||
* valued (silent) samples.
|
|
||||||
*/
|
|
||||||
protected ByteBuffer convert(float[] samples)
|
|
||||||
{
|
|
||||||
ByteBuffer buffer = null;
|
|
||||||
|
|
||||||
if(samples.length > 0)
|
|
||||||
{
|
|
||||||
/* Little-endian byte buffer */
|
|
||||||
buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
|
|
||||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
|
||||||
|
|
||||||
if(getMixerChannel() == MixerChannel.LEFT)
|
|
||||||
{
|
|
||||||
for(float sample : samples)
|
|
||||||
{
|
|
||||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
|
||||||
shortBuffer.put((short) 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for(float sample : samples)
|
|
||||||
{
|
|
||||||
shortBuffer.put((short) 0);
|
|
||||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -30,7 +30,7 @@ public enum PreferenceEditorType
|
|||||||
JMBE_LIBRARY("JMBE Audio Library"),
|
JMBE_LIBRARY("JMBE Audio Library"),
|
||||||
AUDIO_MP3("MP3"),
|
AUDIO_MP3("MP3"),
|
||||||
AUDIO_RECORD("Record"),
|
AUDIO_RECORD("Record"),
|
||||||
AUDIO_OUTPUT("Output/Tones"),
|
AUDIO_OUTPUT("Playback/Tones"),
|
||||||
AUDIO_CALL_MANAGEMENT("Call Management"),
|
AUDIO_CALL_MANAGEMENT("Call Management"),
|
||||||
SOURCE_TUNERS("Tuners"),
|
SOURCE_TUNERS("Tuners"),
|
||||||
TALKGROUP_FORMAT("Talkgroup & Radio ID"),
|
TALKGROUP_FORMAT("Talkgroup & Radio ID"),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -19,14 +19,12 @@
|
|||||||
|
|
||||||
package io.github.dsheirer.gui.preference.playback;
|
package io.github.dsheirer.gui.preference.playback;
|
||||||
|
|
||||||
import io.github.dsheirer.audio.AudioFormats;
|
import io.github.dsheirer.audio.playback.AudioPlaybackDeviceDescriptor;
|
||||||
|
import io.github.dsheirer.audio.playback.AudioPlaybackDeviceManager;
|
||||||
|
import io.github.dsheirer.eventbus.MyEventBus;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
|
import io.github.dsheirer.preference.playback.PlayTestAudioRequest;
|
||||||
import io.github.dsheirer.preference.playback.PlaybackPreference;
|
import io.github.dsheirer.preference.playback.PlaybackPreference;
|
||||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
|
||||||
import io.github.dsheirer.source.mixer.MixerManager;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.nio.ShortBuffer;
|
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Orientation;
|
import javafx.geometry.Orientation;
|
||||||
@@ -45,10 +43,6 @@ import org.controlsfx.control.ToggleSwitch;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.sound.sampled.AudioSystem;
|
|
||||||
import javax.sound.sampled.Clip;
|
|
||||||
import javax.sound.sampled.DataLine;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preference settings for audio playback
|
* Preference settings for audio playback
|
||||||
@@ -58,8 +52,8 @@ public class PlaybackPreferenceEditor extends HBox
|
|||||||
private final static Logger mLog = LoggerFactory.getLogger(PlaybackPreferenceEditor.class);
|
private final static Logger mLog = LoggerFactory.getLogger(PlaybackPreferenceEditor.class);
|
||||||
private final PlaybackPreference mPlaybackPreference;
|
private final PlaybackPreference mPlaybackPreference;
|
||||||
private GridPane mEditorPane;
|
private GridPane mEditorPane;
|
||||||
private ComboBox<MixerChannelConfiguration> mMixerComboBox;
|
private ComboBox<AudioPlaybackDeviceDescriptor> mAudioPlaybackDevicesCombo;
|
||||||
private Button mMixerTestButton;
|
private Button mPlaybackDeviceTestButton;
|
||||||
private ToggleSwitch mUseAudioSegmentStartToneSwitch;
|
private ToggleSwitch mUseAudioSegmentStartToneSwitch;
|
||||||
private Button mTestStartToneButton;
|
private Button mTestStartToneButton;
|
||||||
private ToggleSwitch mUseAudioSegmentDropToneSwitch;
|
private ToggleSwitch mUseAudioSegmentDropToneSwitch;
|
||||||
@@ -86,11 +80,11 @@ public class PlaybackPreferenceEditor extends HBox
|
|||||||
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
|
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
|
||||||
mEditorPane.setHgap(10);
|
mEditorPane.setHgap(10);
|
||||||
mEditorPane.setVgap(10);
|
mEditorPane.setVgap(10);
|
||||||
Label outputLabel = new Label("Audio Output Device");
|
Label outputLabel = new Label("Audio Playback Device");
|
||||||
GridPane.setHalignment(outputLabel, HPos.RIGHT);
|
GridPane.setHalignment(outputLabel, HPos.RIGHT);
|
||||||
mEditorPane.add(outputLabel, 0, row, 2, 1);
|
mEditorPane.add(outputLabel, 0, row, 2, 1);
|
||||||
mEditorPane.add(getMixerComboBox(), 2, row, 3, 1);
|
mEditorPane.add(getAudioPlaybackDevicesCombo(), 2, row, 3, 1);
|
||||||
mEditorPane.add(getMixerTestButton(), 5, row);
|
mEditorPane.add(getPlaybackDeviceTestButton(), 5, row);
|
||||||
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, ++row, 6, 1);
|
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, ++row, 6, 1);
|
||||||
mEditorPane.add(new Label("Audio Playback Insert Tones"), 0, ++row, 2, 1);
|
mEditorPane.add(new Label("Audio Playback Insert Tones"), 0, ++row, 2, 1);
|
||||||
|
|
||||||
@@ -122,32 +116,34 @@ public class PlaybackPreferenceEditor extends HBox
|
|||||||
return mEditorPane;
|
return mEditorPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ComboBox<MixerChannelConfiguration> getMixerComboBox()
|
private ComboBox<AudioPlaybackDeviceDescriptor> getAudioPlaybackDevicesCombo()
|
||||||
{
|
{
|
||||||
if(mMixerComboBox == null)
|
if(mAudioPlaybackDevicesCombo == null)
|
||||||
{
|
{
|
||||||
mMixerComboBox = new ComboBox<>();
|
mAudioPlaybackDevicesCombo = new ComboBox<>();
|
||||||
mMixerComboBox.getItems().addAll(MixerManager.getOutputMixers());
|
mAudioPlaybackDevicesCombo.getItems().addAll(AudioPlaybackDeviceManager.getAudioPlaybackDevices());
|
||||||
mMixerComboBox.getSelectionModel().select(mPlaybackPreference.getMixerChannelConfiguration());
|
mAudioPlaybackDevicesCombo.getSelectionModel().select(mPlaybackPreference.getAudioPlaybackDevice());
|
||||||
mMixerComboBox.getSelectionModel().selectedItemProperty()
|
mAudioPlaybackDevicesCombo.getSelectionModel().selectedItemProperty()
|
||||||
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setMixerChannelConfiguration(newValue));
|
.addListener((observable, oldValue,
|
||||||
|
newValue) -> mPlaybackPreference.setAudioPlaybackDevice(newValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
return mMixerComboBox;
|
return mAudioPlaybackDevicesCombo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Button getMixerTestButton()
|
public Button getPlaybackDeviceTestButton()
|
||||||
{
|
{
|
||||||
if(mMixerTestButton == null)
|
if(mPlaybackDeviceTestButton == null)
|
||||||
{
|
{
|
||||||
mMixerTestButton = new Button("Test");
|
mPlaybackDeviceTestButton = new Button("Test");
|
||||||
IconNode iconNode = new IconNode(FontAwesome.PLAY);
|
IconNode iconNode = new IconNode(FontAwesome.PLAY);
|
||||||
iconNode.setFill(Color.CORNFLOWERBLUE);
|
iconNode.setFill(Color.CORNFLOWERBLUE);
|
||||||
mMixerTestButton.setGraphic(iconNode);
|
mPlaybackDeviceTestButton.setGraphic(iconNode);
|
||||||
mMixerTestButton.setOnAction(event -> play(mPlaybackPreference.getMixerTestTone()));
|
mPlaybackDeviceTestButton.setOnAction(event ->
|
||||||
|
play(mPlaybackPreference.getAudioPlaybackTestTone(), PlayTestAudioRequest.ALL_CHANNELS));
|
||||||
}
|
}
|
||||||
|
|
||||||
return mMixerTestButton;
|
return mPlaybackDeviceTestButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ToggleSwitch getUseAudioSegmentStartToneSwitch()
|
private ToggleSwitch getUseAudioSegmentStartToneSwitch()
|
||||||
@@ -185,8 +181,9 @@ public class PlaybackPreferenceEditor extends HBox
|
|||||||
IconNode iconNode = new IconNode(FontAwesome.PLAY);
|
IconNode iconNode = new IconNode(FontAwesome.PLAY);
|
||||||
iconNode.setFill(Color.CORNFLOWERBLUE);
|
iconNode.setFill(Color.CORNFLOWERBLUE);
|
||||||
mTestStartToneButton.setGraphic(iconNode);
|
mTestStartToneButton.setGraphic(iconNode);
|
||||||
mTestStartToneButton.setOnAction(event -> play(mPlaybackPreference.getStartTone()));
|
mTestStartToneButton.setOnAction(_ ->
|
||||||
|
play(mPlaybackPreference.getStartTone(PlaybackPreference.TONE_LENGTH_SAMPLES * 3),
|
||||||
|
PlayTestAudioRequest.ALL_CHANNELS));
|
||||||
mTestStartToneButton.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not());
|
mTestStartToneButton.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,8 +198,9 @@ public class PlaybackPreferenceEditor extends HBox
|
|||||||
IconNode iconNode = new IconNode(FontAwesome.PLAY);
|
IconNode iconNode = new IconNode(FontAwesome.PLAY);
|
||||||
iconNode.setFill(Color.CORNFLOWERBLUE);
|
iconNode.setFill(Color.CORNFLOWERBLUE);
|
||||||
mTestDropToneButton.setGraphic(iconNode);
|
mTestDropToneButton.setGraphic(iconNode);
|
||||||
mTestDropToneButton.setOnAction(event -> play(mPlaybackPreference.getDropTone()));
|
mTestDropToneButton.setOnAction(_ ->
|
||||||
|
play(mPlaybackPreference.getDropTone(PlaybackPreference.TONE_LENGTH_SAMPLES * 3),
|
||||||
|
PlayTestAudioRequest.ALL_CHANNELS));
|
||||||
mTestDropToneButton.disableProperty().bind(getUseAudioSegmentDropToneSwitch().selectedProperty().not());
|
mTestDropToneButton.disableProperty().bind(getUseAudioSegmentDropToneSwitch().selectedProperty().not());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,43 +268,12 @@ public class PlaybackPreferenceEditor extends HBox
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays the audio buffer over the default mono playback device
|
* Sends a request to play the audio samples over the specified audio playback channel
|
||||||
* @param audioSamples with 8 kHz mono PCM samples
|
* @param audioSamples with 8 kHz mono PCM samples
|
||||||
|
* @param channel number (0=mono/left, 1=right, etc.)
|
||||||
*/
|
*/
|
||||||
private void play(float[] audioSamples)
|
private void play(float[] audioSamples, int channel)
|
||||||
{
|
{
|
||||||
if(audioSamples != null)
|
MyEventBus.getGlobalEventBus().post(new PlayTestAudioRequest(audioSamples, channel));
|
||||||
{
|
|
||||||
/* Little-endian byte buffer */
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(audioSamples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
|
|
||||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
|
||||||
|
|
||||||
for(float sample : audioSamples)
|
|
||||||
{
|
|
||||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] bytes = buffer.array();
|
|
||||||
|
|
||||||
DataLine.Info info = new DataLine.Info(Clip.class, AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO);
|
|
||||||
|
|
||||||
if(!AudioSystem.isLineSupported(info))
|
|
||||||
{
|
|
||||||
mLog.error("Audio clip playback is not supported on this system");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Clip clip = (Clip)AudioSystem.getLine(info);
|
|
||||||
clip.open(AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO, bytes, 0, bytes.length);
|
|
||||||
clip.start();
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
mLog.error("Error attempting to play audio test tone", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.preference.playback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to play a test tone over the currently employed audio playback device.
|
||||||
|
*
|
||||||
|
* @param audio to play (160 samples in length = 20 milliseconds)
|
||||||
|
* @param channel number for the audio channel (0 = mono/left, 1 = right, etc.)
|
||||||
|
*/
|
||||||
|
public record PlayTestAudioRequest(float[] audio, int channel)
|
||||||
|
{
|
||||||
|
public static final int ALL_CHANNELS = -1;
|
||||||
|
|
||||||
|
public boolean isAllChannels()
|
||||||
|
{
|
||||||
|
return channel == ALL_CHANNELS;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -19,14 +19,14 @@
|
|||||||
|
|
||||||
package io.github.dsheirer.preference.playback;
|
package io.github.dsheirer.preference.playback;
|
||||||
|
|
||||||
|
import io.github.dsheirer.audio.playback.AudioPlaybackDeviceDescriptor;
|
||||||
|
import io.github.dsheirer.audio.playback.AudioPlaybackDeviceManager;
|
||||||
import io.github.dsheirer.gui.preference.playback.ToneFrequency;
|
import io.github.dsheirer.gui.preference.playback.ToneFrequency;
|
||||||
import io.github.dsheirer.gui.preference.playback.ToneUtil;
|
import io.github.dsheirer.gui.preference.playback.ToneUtil;
|
||||||
import io.github.dsheirer.gui.preference.playback.ToneVolume;
|
import io.github.dsheirer.gui.preference.playback.ToneVolume;
|
||||||
import io.github.dsheirer.preference.Preference;
|
import io.github.dsheirer.preference.Preference;
|
||||||
import io.github.dsheirer.preference.PreferenceType;
|
import io.github.dsheirer.preference.PreferenceType;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
|
||||||
import io.github.dsheirer.source.mixer.MixerManager;
|
|
||||||
import java.util.prefs.Preferences;
|
import java.util.prefs.Preferences;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -44,8 +44,9 @@ public class PlaybackPreference extends Preference
|
|||||||
private static final String PREFERENCE_KEY_START_TONE_FREQUENCY = "audio.playback.segment.start.frequency";
|
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_START_TONE_VOLUME = "audio.playback.segment.start.volume";
|
||||||
|
|
||||||
private static final String PREFERENCE_KEY_MIXER_CHANNEL_CONFIG = "audio.playback.mixer.channel.configuration";
|
private static final String PREFERENCE_KEY_AUDIO_DEVICE_NAME = "audio.playback.device.name";
|
||||||
private static final int TONE_LENGTH_SAMPLES = 180;
|
private static final String PREFERENCE_KEY_AUDIO_CHANNEL_COUNT = "audio.playback.channel.count";
|
||||||
|
public static final int TONE_LENGTH_SAMPLES = 160;
|
||||||
|
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(PlaybackPreference.class);
|
private final static Logger mLog = LoggerFactory.getLogger(PlaybackPreference.class);
|
||||||
private Preferences mPreferences = Preferences.userNodeForPackage(PlaybackPreference.class);
|
private Preferences mPreferences = Preferences.userNodeForPackage(PlaybackPreference.class);
|
||||||
@@ -55,7 +56,7 @@ public class PlaybackPreference extends Preference
|
|||||||
private ToneVolume mStartToneVolume;
|
private ToneVolume mStartToneVolume;
|
||||||
private ToneFrequency mDropToneFrequency;
|
private ToneFrequency mDropToneFrequency;
|
||||||
private ToneVolume mDropToneVolume;
|
private ToneVolume mDropToneVolume;
|
||||||
private MixerChannelConfiguration mMixerChannelConfiguration;
|
private AudioPlaybackDeviceDescriptor mAudioPlaybackDeviceDescriptor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs this preference with an update listener
|
* Constructs this preference with an update listener
|
||||||
@@ -221,12 +222,17 @@ public class PlaybackPreference extends Preference
|
|||||||
{
|
{
|
||||||
if(getUseAudioSegmentStartTone())
|
if(getUseAudioSegmentStartTone())
|
||||||
{
|
{
|
||||||
return ToneUtil.getTone(getStartToneFrequency(), getStartToneVolume(), TONE_LENGTH_SAMPLES);
|
return getStartTone(TONE_LENGTH_SAMPLES);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public float[] getStartTone(int length)
|
||||||
|
{
|
||||||
|
return ToneUtil.getTone(getStartToneFrequency(), getStartToneVolume(), length);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buffer with samples for the audio segment drop tone
|
* Buffer with samples for the audio segment drop tone
|
||||||
*/
|
*/
|
||||||
@@ -234,62 +240,60 @@ public class PlaybackPreference extends Preference
|
|||||||
{
|
{
|
||||||
if(getUseAudioSegmentDropTone())
|
if(getUseAudioSegmentDropTone())
|
||||||
{
|
{
|
||||||
return ToneUtil.getTone(getDropToneFrequency(), getDropToneVolume(), TONE_LENGTH_SAMPLES);
|
return getDropTone(TONE_LENGTH_SAMPLES);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public float[] getDropTone(int length)
|
||||||
|
{
|
||||||
|
return ToneUtil.getTone(getDropToneFrequency(), getDropToneVolume(), length);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test tone to use for testing the currently selected mixer output
|
* Test tone to use for testing the currently selected mixer output
|
||||||
*/
|
*/
|
||||||
public float[] getMixerTestTone()
|
public float[] getAudioPlaybackTestTone()
|
||||||
{
|
{
|
||||||
return ToneUtil.getTone(ToneFrequency.F1200, ToneVolume.V10, 800);
|
return ToneUtil.getTone(ToneFrequency.F1200, ToneVolume.V10, TONE_LENGTH_SAMPLES * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the preferred output mixer to use
|
* Preferred audio playback device
|
||||||
*/
|
*/
|
||||||
public MixerChannelConfiguration getMixerChannelConfiguration()
|
public AudioPlaybackDeviceDescriptor getAudioPlaybackDevice()
|
||||||
{
|
{
|
||||||
if(mMixerChannelConfiguration == null)
|
if(mAudioPlaybackDeviceDescriptor == null)
|
||||||
{
|
{
|
||||||
MixerChannelConfiguration defaultConfig = MixerManager.getDefaultOutputMixer();
|
AudioPlaybackDeviceDescriptor descriptor = AudioPlaybackDeviceManager.getDefaultAudioPLaybackDevice();
|
||||||
|
|
||||||
if(defaultConfig != null)
|
if(descriptor != null)
|
||||||
{
|
{
|
||||||
String configName = mPreferences.get(PREFERENCE_KEY_MIXER_CHANNEL_CONFIG, defaultConfig.toString());
|
String name = mPreferences.get(PREFERENCE_KEY_AUDIO_DEVICE_NAME, descriptor.getMixerInfo().getName());
|
||||||
|
int channelCount = mPreferences.getInt(PREFERENCE_KEY_AUDIO_CHANNEL_COUNT, descriptor.getAudioFormat().getChannels());
|
||||||
for(MixerChannelConfiguration config: MixerManager.getOutputMixers())
|
mAudioPlaybackDeviceDescriptor = AudioPlaybackDeviceManager.getAudioPlaybackDevice(name, channelCount);
|
||||||
{
|
|
||||||
if(config.toString().contentEquals(configName))
|
|
||||||
{
|
|
||||||
mMixerChannelConfiguration = config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(mMixerChannelConfiguration == null)
|
|
||||||
{
|
|
||||||
mMixerChannelConfiguration = defaultConfig;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
mLog.error("Error - no system audio devices available");
|
mLog.error("Error - no audio playback devices available");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mMixerChannelConfiguration;
|
return mAudioPlaybackDeviceDescriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the preferred output mixer to use
|
* Sets the preferred audio playback device
|
||||||
*/
|
*/
|
||||||
public void setMixerChannelConfiguration(MixerChannelConfiguration configuration)
|
public void setAudioPlaybackDevice(AudioPlaybackDeviceDescriptor descriptor)
|
||||||
{
|
{
|
||||||
mMixerChannelConfiguration = configuration;
|
if(descriptor != null)
|
||||||
mPreferences.put(PREFERENCE_KEY_MIXER_CHANNEL_CONFIG, configuration.toString());
|
{
|
||||||
notifyPreferenceUpdated();
|
mAudioPlaybackDeviceDescriptor = descriptor;
|
||||||
|
mPreferences.put(PREFERENCE_KEY_AUDIO_DEVICE_NAME, descriptor.getMixerInfo().getName());
|
||||||
|
mPreferences.putInt(PREFERENCE_KEY_AUDIO_CHANNEL_COUNT, descriptor.getAudioFormat().getChannels());
|
||||||
|
notifyPreferenceUpdated();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* SDR Trunk
|
|
||||||
* Copyright (C) 2014 Dennis Sheirer
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
******************************************************************************/
|
|
||||||
package io.github.dsheirer.source.mixer;
|
|
||||||
|
|
||||||
import javax.sound.sampled.AudioFormat;
|
|
||||||
import javax.sound.sampled.AudioSystem;
|
|
||||||
import javax.sound.sampled.DataLine;
|
|
||||||
import javax.sound.sampled.Line;
|
|
||||||
import javax.sound.sampled.LineUnavailableException;
|
|
||||||
import javax.sound.sampled.Mixer;
|
|
||||||
import javax.sound.sampled.TargetDataLine;
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* THIS CLASS IS SCHEDULED FOR DELETION *****************************
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class SystemMixerDevices {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a hashmap of mixer names and target data lines that support the
|
|
||||||
* audio format argument
|
|
||||||
*
|
|
||||||
* @param format - requested audio format
|
|
||||||
* @return - mixer target data lines that support the requested audio format
|
|
||||||
*/
|
|
||||||
public static HashMap<String,TargetDataLine> getMixers( AudioFormat format )
|
|
||||||
{
|
|
||||||
HashMap<String,TargetDataLine> retVal = new HashMap<String,TargetDataLine>();
|
|
||||||
|
|
||||||
DataLine.Info datalineInfo=new DataLine.Info(TargetDataLine.class, format );
|
|
||||||
|
|
||||||
for( Mixer.Info mixerInfo: AudioSystem.getMixerInfo() )
|
|
||||||
{
|
|
||||||
Mixer mixer = AudioSystem.getMixer( mixerInfo );
|
|
||||||
|
|
||||||
if( mixer != null )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
TargetDataLine tdl = (TargetDataLine) mixer.getLine( datalineInfo );
|
|
||||||
|
|
||||||
if( tdl != null )
|
|
||||||
{
|
|
||||||
retVal.put( mixerInfo.getName(), tdl );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch( IllegalArgumentException iae )
|
|
||||||
{
|
|
||||||
//Do nothing ... mixer doesn't support the audio format
|
|
||||||
}
|
|
||||||
catch( LineUnavailableException e )
|
|
||||||
{
|
|
||||||
//Do nothing ... mixer doesn't support the audio format
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getSoundDevices()
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
for( Mixer.Info mixerInfo: AudioSystem.getMixerInfo() )
|
|
||||||
{
|
|
||||||
sb.append( "\n--------------------------------------------------" );
|
|
||||||
sb.append("\nMIXER name:").append(mixerInfo.getName())
|
|
||||||
.append("\n desc:").append(mixerInfo.getDescription())
|
|
||||||
.append("\n vendor:").append(mixerInfo.getVendor())
|
|
||||||
.append("\n version:").append(mixerInfo.getVersion())
|
|
||||||
.append("\n");
|
|
||||||
|
|
||||||
Mixer mixer = AudioSystem.getMixer( mixerInfo );
|
|
||||||
|
|
||||||
Line.Info[] sourceLines = mixer.getSourceLineInfo();
|
|
||||||
|
|
||||||
for( Line.Info lineInfo: sourceLines )
|
|
||||||
{
|
|
||||||
sb.append(" SOURCE LINE desc:").append(lineInfo)
|
|
||||||
.append("\n class:").append(lineInfo.getClass())
|
|
||||||
.append("\n lineclass:").append(lineInfo.getLineClass())
|
|
||||||
.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
Line.Info[] targetLines = mixer.getTargetLineInfo();
|
|
||||||
|
|
||||||
for( Line.Info lineInfo: targetLines )
|
|
||||||
{
|
|
||||||
sb.append(" TARGET LINE desc:").append(lineInfo)
|
|
||||||
.append("\n class:").append(lineInfo.getClass())
|
|
||||||
.append("\n lineclass:").append(lineInfo.getLineClass())
|
|
||||||
.append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user