mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
#660 Updates audio processing subsystem to use audio segments. Updates audio playback, recording and streaming to use audio segments. Adds support to UserPreferences for sound card selection and audio tone insertion during playback. Adds record preference for WAVE or MP3. Resolves issues with metadata in recordings. (#675)
This commit is contained in:
@@ -127,6 +127,12 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
{
|
||||
//Deregister from receiving preference update notifications
|
||||
MyEventBus.getEventBus().unregister(this);
|
||||
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
mAudioOutput.removeAudioEventListener(this);
|
||||
mAudioOutput.removeAudioMetadataListener();
|
||||
}
|
||||
}
|
||||
|
||||
private void init()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* * Copyright (C) 2014-2020 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
|
||||
@@ -21,13 +21,23 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
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 io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -42,42 +52,39 @@ import javax.sound.sampled.LineUnavailableException;
|
||||
import javax.sound.sampled.Mixer;
|
||||
import javax.sound.sampled.SourceDataLine;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public abstract class AudioOutput implements Listener<ReusableAudioPacket>, LineListener
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
||||
|
||||
private LinkedTransferQueue<ReusableAudioPacket> mBuffer = new LinkedTransferQueue<>();
|
||||
private int mBufferStartThreshold;
|
||||
private int mBufferStopThreshold;
|
||||
private static IdentifierCollection EMPTY_IDENTIFIER_COLLECTION = new IdentifierCollection(0);
|
||||
static
|
||||
{
|
||||
EMPTY_IDENTIFIER_COLLECTION.setUpdated(true);
|
||||
}
|
||||
|
||||
private Listener<IdentifierCollection> mIdentifierCollectionListener;
|
||||
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
|
||||
|
||||
private ScheduledFuture<?> mProcessorTask;
|
||||
|
||||
private ScheduledFuture<?> mProcessorFuture;
|
||||
private SourceDataLine mOutput;
|
||||
private Mixer mMixer;
|
||||
private MixerChannel mMixerChannel;
|
||||
private FloatControl mGainControl;
|
||||
private BooleanControl mMuteControl;
|
||||
|
||||
private AudioEvent mAudioStartEvent;
|
||||
private AudioEvent mAudioStopEvent;
|
||||
|
||||
private boolean mCanProcessAudio = false;
|
||||
private long mLastActivity = System.currentTimeMillis();
|
||||
private AudioSegment mCurrentAudioSegment;
|
||||
private AudioSegment mNextAudioSegment;
|
||||
private ReentrantLock mLock = new ReentrantLock();
|
||||
private int mCurrentBufferIndex = 0;
|
||||
private UserPreferences mUserPreferences;
|
||||
private BooleanProperty mEmptyProperty = new SimpleBooleanProperty(true);
|
||||
private IntegerProperty mAudioPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
|
||||
private ByteBuffer mAudioSegmentStartTone;
|
||||
private ByteBuffer mAudioSegmentPreemptTone;
|
||||
|
||||
/**
|
||||
* Single audio channel playback with automatic starting and stopping of the
|
||||
@@ -94,10 +101,11 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
* @param requestedBufferSize of approximately 1 second of audio
|
||||
*/
|
||||
public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioFormat, Line.Info lineInfo,
|
||||
int requestedBufferSize)
|
||||
int requestedBufferSize, UserPreferences userPreferences)
|
||||
{
|
||||
mMixer = mixer;
|
||||
mMixerChannel = mixerChannel;
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -139,46 +147,362 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
}
|
||||
|
||||
/* Run the queue processor task every 40 milliseconds or 25 times a second */
|
||||
mProcessorTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new BufferProcessor(),
|
||||
0, 40, TimeUnit.MILLISECONDS);
|
||||
//Run the queue processor task every 250 milliseconds or 4 times a second
|
||||
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 250, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED,
|
||||
getChannelName());
|
||||
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED,
|
||||
getChannelName());
|
||||
|
||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
|
||||
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
|
||||
mCanProcessAudio = true;
|
||||
}
|
||||
}
|
||||
catch(LineUnavailableException e)
|
||||
{
|
||||
mLog.error("Couldn't obtain audio source data line for "
|
||||
+ "audio output - mixer [" + mMixer.getMixerInfo().getName() + "]");
|
||||
mLog.error("Couldn't obtain audio source data line for audio output - mixer [" +
|
||||
mMixer.getMixerInfo().getName() + "]");
|
||||
}
|
||||
|
||||
updateToneInsertionAudioClips();
|
||||
|
||||
//Register to receive directory preference update notifications so we can update the preference items
|
||||
MyEventBus.getEventBus().register(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean property that indicates if this audio output has an audio segment queued or in process of playback.
|
||||
*/
|
||||
public BooleanProperty emptyProperty()
|
||||
{
|
||||
return mEmptyProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio playback priority of the current audio segment or default audio priority if nothing is currently loaded.
|
||||
*/
|
||||
public IntegerProperty audioPriorityProperty()
|
||||
{
|
||||
return mAudioPriority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the audio segment for playback.
|
||||
*
|
||||
* Note: if a segment is currently playing and another audio segment is already queued for playback, invoking
|
||||
* this method will overwrite the queued segment with the argument.
|
||||
*
|
||||
* @param audioSegment to schedule for playback.
|
||||
*/
|
||||
public void play(AudioSegment audioSegment)
|
||||
{
|
||||
if(audioSegment != null)
|
||||
{
|
||||
mLock.lock();
|
||||
|
||||
if(mNextAudioSegment != null)
|
||||
{
|
||||
mNextAudioSegment.decrementConsumerCount();
|
||||
mNextAudioSegment = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mNextAudioSegment = audioSegment;
|
||||
mEmptyProperty.set(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reset()
|
||||
/**
|
||||
* Indicates if the audio segment is linked to the current playback audio segment
|
||||
*/
|
||||
public boolean isLinkedTo(AudioSegment audioSegment)
|
||||
{
|
||||
broadcast(new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName()));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates audio segment start and preempt tone insertion clips
|
||||
*/
|
||||
private void updateToneInsertionAudioClips()
|
||||
{
|
||||
mAudioSegmentStartTone = null;
|
||||
mAudioSegmentPreemptTone = null;
|
||||
|
||||
float[] start = mUserPreferences.getPlaybackPreference().getStartTone();
|
||||
|
||||
if(start != null)
|
||||
{
|
||||
mAudioSegmentStartTone = convert(start);
|
||||
}
|
||||
|
||||
float[] preempt = mUserPreferences.getPlaybackPreference().getPreemptTone();
|
||||
|
||||
if(start != null)
|
||||
{
|
||||
mAudioSegmentPreemptTone = convert(preempt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of a processed audio segment and notifies the originator that processing of the segment is complete
|
||||
* by decrementing the consumer count.
|
||||
* @param audioSegment to dispose
|
||||
*/
|
||||
private void dispose(AudioSegment audioSegment)
|
||||
{
|
||||
mAudioPriority.unbind();
|
||||
|
||||
if(audioSegment != null)
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
audioSegment.removeIdentifierUpdateNotificationListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a tone indicating that a new audio segment is starting
|
||||
*/
|
||||
private ByteBuffer getAudioSegmentStartTone()
|
||||
{
|
||||
return mAudioSegmentStartTone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a tone indicating that the current audio segment playback has been preempted for a higher priority
|
||||
* audio segment that is now starting.
|
||||
*/
|
||||
private ByteBuffer getAudioSegmentPreemptionTone()
|
||||
{
|
||||
return mAudioSegmentPreemptTone;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(buffer != null)
|
||||
{
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
checkStart();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage audio segment playback and process audio segment buffers. This method is designed to be called
|
||||
* by a threaded processor repeatedly to playback the current audio segment and check for and start a newly
|
||||
* assigned audio segment. It also handles starting and stopping the playback source data line to avoid audio
|
||||
* discontinuities due to buffer underruns.
|
||||
*/
|
||||
private void processAudio()
|
||||
{
|
||||
if(mNextAudioSegment != null)
|
||||
{
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
//For linked audio segments, allow the linked segment to complete first before assigning the next
|
||||
if(mNextAudioSegment.isLinked())
|
||||
{
|
||||
if(mCurrentAudioSegment == null)
|
||||
{
|
||||
mCurrentAudioSegment = mNextAudioSegment;
|
||||
mNextAudioSegment = null;
|
||||
mCurrentBufferIndex = 0;
|
||||
|
||||
if(mCurrentAudioSegment != null)
|
||||
{
|
||||
mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty());
|
||||
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
|
||||
broadcast(mCurrentAudioSegment.getIdentifierCollection());
|
||||
}
|
||||
else
|
||||
{
|
||||
mAudioPriority.setValue(Priority.DEFAULT_PRIORITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Insert audio segment start or audio priority preemption bonk tone
|
||||
if(mCurrentAudioSegment == null)
|
||||
{
|
||||
playAudio(getAudioSegmentStartTone());
|
||||
}
|
||||
else if(mCurrentBufferIndex > 0 &&
|
||||
(!mCurrentAudioSegment.completeProperty().get() ||
|
||||
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount()))
|
||||
{
|
||||
playAudio(getAudioSegmentPreemptionTone());
|
||||
}
|
||||
else
|
||||
{
|
||||
playAudio(getAudioSegmentStartTone());
|
||||
}
|
||||
|
||||
//Close current audio segment
|
||||
dispose(mCurrentAudioSegment);
|
||||
mCurrentAudioSegment = mNextAudioSegment;
|
||||
mNextAudioSegment = null;
|
||||
mCurrentBufferIndex = 0;
|
||||
|
||||
if(mCurrentAudioSegment != null)
|
||||
{
|
||||
mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty());
|
||||
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
|
||||
broadcast(mCurrentAudioSegment.getIdentifierCollection());
|
||||
}
|
||||
else
|
||||
{
|
||||
mAudioPriority.setValue(Priority.DEFAULT_PRIORITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if(mCurrentAudioSegment != null)
|
||||
{
|
||||
//Check for completed audio segment
|
||||
if(mCurrentAudioSegment.completeProperty().get() &&
|
||||
mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount())
|
||||
{
|
||||
dispose(mCurrentAudioSegment);
|
||||
mCurrentAudioSegment = null;
|
||||
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(mNextAudioSegment == null)
|
||||
{
|
||||
mEmptyProperty.set(true);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//Process any new buffers that have been added to the audio segment. If a next audio segment gets assigned
|
||||
//while processing, exit the loop so that we can evaluate the next for higher priority preempt. If the next
|
||||
//segment is a linked segment, ignore it so that we can close out the current segment.
|
||||
while((mNextAudioSegment == null || mNextAudioSegment.isLinked()) &&
|
||||
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())
|
||||
{
|
||||
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
|
||||
|
||||
if(audioBuffer != null)
|
||||
{
|
||||
ByteBuffer audio = convert(audioBuffer);
|
||||
playAudio(audio);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkStop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares this audio output for disposal.
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
mCanProcessAudio = false;
|
||||
|
||||
if(mProcessorTask != null)
|
||||
if(mProcessorFuture != null)
|
||||
{
|
||||
mProcessorTask.cancel(true);
|
||||
mProcessorFuture.cancel(true);
|
||||
}
|
||||
|
||||
mProcessorTask = null;
|
||||
mProcessorFuture = null;
|
||||
|
||||
mBuffer.clear();
|
||||
mLock.lock();
|
||||
|
||||
mAudioEventBroadcaster.dispose();
|
||||
mAudioEventBroadcaster = null;
|
||||
try
|
||||
{
|
||||
if(mNextAudioSegment != null)
|
||||
{
|
||||
mNextAudioSegment.decrementConsumerCount();
|
||||
mNextAudioSegment = null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
|
||||
dispose(mCurrentAudioSegment);
|
||||
mCurrentAudioSegment = null;
|
||||
|
||||
mAudioEventBroadcaster.clear();
|
||||
mIdentifierCollectionListener = null;
|
||||
|
||||
if(mOutput != null)
|
||||
@@ -192,10 +516,9 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the audio packet data into a byte buffer format appropriate for
|
||||
* the underlying source data line.
|
||||
* Converts the audio buffer data into a byte buffer format appropriate for the underlying source data line.
|
||||
*/
|
||||
protected abstract ByteBuffer convert(ReusableAudioPacket packet);
|
||||
protected abstract ByteBuffer convert(float[] buffer);
|
||||
|
||||
/**
|
||||
* Audio output channel name
|
||||
@@ -229,7 +552,7 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
/**
|
||||
* Broadcasts an audio event to the registered listener
|
||||
*/
|
||||
private void broadcast(AudioEvent audioEvent)
|
||||
private void broadcastAudioEvent(AudioEvent audioEvent)
|
||||
{
|
||||
mAudioEventBroadcaster.broadcast(audioEvent);
|
||||
}
|
||||
@@ -243,6 +566,9 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
mIdentifierCollectionListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the current audio metadata listener
|
||||
*/
|
||||
public void removeAudioMetadataListener()
|
||||
{
|
||||
mIdentifierCollectionListener = null;
|
||||
@@ -260,139 +586,31 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp of either last buffer received or last buffer processed
|
||||
* 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
|
||||
*/
|
||||
public long getLastActivityTimestamp()
|
||||
private void checkStart()
|
||||
{
|
||||
return mLastActivity;
|
||||
if(mCanProcessAudio && !mOutput.isRunning() && mOutput.available() <= mBufferStartThreshold)
|
||||
{
|
||||
mOutput.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the last activity timestamp to current system time
|
||||
* 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
|
||||
*/
|
||||
public void updateTimestamp()
|
||||
private void checkStop()
|
||||
{
|
||||
mLastActivity = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ReusableAudioPacket packet)
|
||||
{
|
||||
if(mCanProcessAudio)
|
||||
if(mCanProcessAudio && mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold)
|
||||
{
|
||||
//Update the activity timestamp so that this audio output doesn't
|
||||
//get disconnected before it starts processing the audio stream
|
||||
updateTimestamp();
|
||||
|
||||
mBuffer.add(packet);
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
public class BufferProcessor implements Runnable
|
||||
{
|
||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
||||
private List<ReusableAudioPacket> mAudioPackets = new ArrayList<ReusableAudioPacket>();
|
||||
|
||||
|
||||
public BufferProcessor()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
/* The processing flag ensures that only one instance of the
|
||||
* processor can run at any given time */
|
||||
if(mProcessing.compareAndSet(false, true))
|
||||
{
|
||||
mBuffer.drainTo(mAudioPackets);
|
||||
|
||||
for(ReusableAudioPacket packet : mAudioPackets)
|
||||
{
|
||||
if(packet.getType() == ReusableAudioPacket.Type.AUDIO)
|
||||
{
|
||||
broadcast(packet.getIdentifierCollection());
|
||||
|
||||
ByteBuffer buffer = convert(packet);
|
||||
|
||||
int wrote = 0;
|
||||
|
||||
if(!mOutput.isRunning())
|
||||
{
|
||||
int toWrite = mOutput.available();
|
||||
|
||||
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);
|
||||
|
||||
checkStart();
|
||||
}
|
||||
|
||||
if(mOutput.isRunning() && wrote < buffer.array().length)
|
||||
{
|
||||
//Blocking write
|
||||
wrote += mOutput.write(buffer.array(), wrote,
|
||||
buffer.array().length - wrote);
|
||||
}
|
||||
|
||||
updateTimestamp();
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
mAudioPackets.clear();
|
||||
|
||||
checkStop();
|
||||
|
||||
mProcessing.set(false);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error while processing audio buffers", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts audio playback once audio buffer is almost full and remaining
|
||||
* capacity falls below the start threshold.
|
||||
*/
|
||||
private void checkStart()
|
||||
{
|
||||
if(mCanProcessAudio &&
|
||||
!mOutput.isRunning() &&
|
||||
mOutput.available() <= mBufferStartThreshold)
|
||||
{
|
||||
mOutput.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private void checkStop()
|
||||
{
|
||||
if(mCanProcessAudio && mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold)
|
||||
{
|
||||
mOutput.drain();
|
||||
mOutput.stop();
|
||||
broadcast(EMPTY_IDENTIFIER_COLLECTION);
|
||||
}
|
||||
mOutput.drain();
|
||||
mOutput.stop();
|
||||
broadcast(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,9 +622,7 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
if(mMuteControl != null)
|
||||
{
|
||||
mMuteControl.setValue(muted);
|
||||
|
||||
broadcast(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED :
|
||||
AudioEvent.Type.AUDIO_UNMUTED, getChannelName()));
|
||||
broadcastAudioEvent(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED, getChannelName()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,14 +647,17 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
return mGainControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio output has a gain control available
|
||||
*/
|
||||
public boolean hasGainControl()
|
||||
{
|
||||
return mGainControl != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors the source data line playback state and broadcasts audio events
|
||||
* to the registered listener as the state changes
|
||||
* Monitors the source data line playback state and broadcasts audio events to the registered listener as the
|
||||
* state changes
|
||||
*/
|
||||
@Override
|
||||
public void update(LineEvent event)
|
||||
@@ -454,4 +673,23 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
mAudioEventBroadcaster.broadcast(mAudioStopEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runnable audio segment processor
|
||||
*/
|
||||
public class AudioSegmentProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
processAudio();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Error while processing audio buffers", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,24 @@ import io.github.dsheirer.alias.AliasModel;
|
||||
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.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.PreferenceEditorViewRequest;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.SourceManager;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import io.github.dsheirer.source.mixer.MixerManager;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.FloatControl;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JMenu;
|
||||
@@ -51,6 +58,7 @@ import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.util.List;
|
||||
|
||||
public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
{
|
||||
@@ -169,34 +177,19 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
|
||||
/* Audio mixer/output selection menus */
|
||||
JMenu outputMenu = new JMenu("Audio Output");
|
||||
|
||||
MixerChannelConfiguration[] mixerConfigurations =
|
||||
mSourceManager.getMixerManager().getOutputMixers();
|
||||
|
||||
for(MixerChannelConfiguration mixerConfig : mixerConfigurations)
|
||||
JMenuItem outputMenu = new JMenuItem("Configure ...");
|
||||
Icon icon = IconFontSwing.buildIcon(FontAwesome.COG, 14);
|
||||
outputMenu.setIcon(icon);
|
||||
outputMenu.addActionListener(new ActionListener()
|
||||
{
|
||||
MixerSelectionItem mixerItem = new MixerSelectionItem(mixerConfig);
|
||||
|
||||
try
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
MixerChannelConfiguration current = mController.getMixerChannelConfiguration();
|
||||
|
||||
if(current != null && current.equals(mixerConfig))
|
||||
{
|
||||
mixerItem.setSelected(true);
|
||||
}
|
||||
MyEventBus.getEventBus().post(new PreferenceEditorViewRequest(PreferenceEditorType.AUDIO_PLAYBACK));
|
||||
}
|
||||
catch(AudioException e)
|
||||
{
|
||||
mLog.error("Error while detecting current mixer "
|
||||
+ "channel configuration", e);
|
||||
}
|
||||
|
||||
outputMenu.add(mixerItem);
|
||||
}
|
||||
|
||||
});
|
||||
popup.add(outputMenu);
|
||||
popup.add(new JPopupMenu.Separator());
|
||||
|
||||
/* Audio output mute and volume control */
|
||||
for(AudioOutput output : mController.getAudioOutputs())
|
||||
|
||||
@@ -19,13 +19,17 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.audio.AudioException;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.audio.IAudioController;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import io.github.dsheirer.source.mixer.MixerManager;
|
||||
@@ -38,113 +42,157 @@ import javax.sound.sampled.Mixer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAudioController
|
||||
/**
|
||||
* Manages scheduling and playback of audio segments to the local users audio system.
|
||||
*/
|
||||
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
|
||||
|
||||
public static final int AUDIO_TIMEOUT = 1000; //1 second
|
||||
|
||||
public static final String AUDIO_CHANNELS_PROPERTY = "audio.manager.channels";
|
||||
public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer";
|
||||
|
||||
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
|
||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
|
||||
|
||||
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
|
||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
|
||||
|
||||
private LinkedTransferQueue<ReusableAudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
|
||||
private Map<Integer,AudioOutputConnection> mChannelConnectionMap = new HashMap<>();
|
||||
private List<AudioOutputConnection> mAudioOutputConnections = new ArrayList<>();
|
||||
private AudioOutputConnection mLowestPriorityConnection;
|
||||
private int mAvailableConnectionCount;
|
||||
|
||||
private Map<String,AudioOutput> mAudioOutputMap = new HashMap<>();
|
||||
|
||||
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
|
||||
|
||||
private ScheduledFuture<?> mProcessingTask;
|
||||
private MixerManager mMixerManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
private MixerChannelConfiguration mMixerChannelConfiguration;
|
||||
|
||||
private List<AudioOutput> mAudioOutputs = new ArrayList<>();
|
||||
private List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||
private LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
|
||||
/**
|
||||
* Processes all audio produced by the decoding channels and routes audio
|
||||
* packets to any combination of outputs based on any alias audio routing
|
||||
* options specified by the user.
|
||||
* Constructs an instance.
|
||||
*
|
||||
* @param userPreferences for audio playback preferences
|
||||
*/
|
||||
public AudioPlaybackManager(MixerManager mixerManager)
|
||||
public AudioPlaybackManager(UserPreferences userPreferences)
|
||||
{
|
||||
mMixerManager = mixerManager;
|
||||
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the saved mixer configuration or a default configuration for audio playback.
|
||||
*/
|
||||
private void loadSettings()
|
||||
{
|
||||
MixerChannelConfiguration configuration = null;
|
||||
|
||||
SystemProperties properties = SystemProperties.getInstance();
|
||||
|
||||
Mixer defaultMixer = AudioSystem.getMixer(null);
|
||||
|
||||
String mixer = properties.get(AUDIO_MIXER_PROPERTY, defaultMixer.getMixerInfo().getName());
|
||||
|
||||
String channels = properties.get(AUDIO_CHANNELS_PROPERTY, MixerChannel.MONO.name());
|
||||
|
||||
MixerChannelConfiguration[] mixerConfigurations = mMixerManager.getOutputMixers();
|
||||
|
||||
for(MixerChannelConfiguration mixerConfig : mixerConfigurations)
|
||||
{
|
||||
if(mixerConfig.matches(mixer, channels))
|
||||
{
|
||||
configuration = mixerConfig;
|
||||
}
|
||||
}
|
||||
|
||||
if(configuration == null)
|
||||
{
|
||||
configuration = getDefaultConfiguration();
|
||||
}
|
||||
mUserPreferences = userPreferences;
|
||||
MyEventBus.getEventBus().register(this);
|
||||
|
||||
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
|
||||
try
|
||||
{
|
||||
setMixerChannelConfiguration(configuration, false);
|
||||
setMixerChannelConfiguration(configuration);
|
||||
}
|
||||
catch(Exception e)
|
||||
catch(AudioException ae)
|
||||
{
|
||||
mLog.error("Couldn't set stored audio mixer/channel configuration - using default", e);
|
||||
|
||||
try
|
||||
{
|
||||
setMixerChannelConfiguration(getDefaultConfiguration());
|
||||
}
|
||||
catch(Exception e2)
|
||||
{
|
||||
mLog.error("Couldn't set default audio mixer/channel configuration - no audio will be available", e2);
|
||||
}
|
||||
mLog.error("Error during setup of audio playback configuration. Attempted to use audio mixer [" +
|
||||
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
|
||||
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default audio playback configuration with a mono audio playback channel.
|
||||
* Receives audio segments from channel audio modules.
|
||||
* @param audioSegment
|
||||
*/
|
||||
private MixerChannelConfiguration getDefaultConfiguration()
|
||||
@Override
|
||||
public void receive(AudioSegment audioSegment)
|
||||
{
|
||||
/* Use the system default mixer and mono channel as default startup */
|
||||
Mixer defaultMixer = AudioSystem.getMixer(null);
|
||||
mNewAudioSegmentQueue.add(audioSegment);
|
||||
}
|
||||
|
||||
return new MixerChannelConfiguration(defaultMixer, MixerChannel.MONO);
|
||||
/**
|
||||
* Processes new audio segments and automatically assigns them to audio outputs.
|
||||
*
|
||||
* Note: this method is intended to be repeatedly invoked by a scheduled processing thread.
|
||||
*/
|
||||
private void processAudioSegments()
|
||||
{
|
||||
//Transfer new audio segments out of the concurrent queue and into the managed segments list.
|
||||
mNewAudioSegmentQueue.drainTo(mAudioSegments);
|
||||
|
||||
if(!mAudioSegments.isEmpty())
|
||||
{
|
||||
Iterator<AudioSegment> it = mAudioSegments.iterator();
|
||||
AudioSegment audioSegment;
|
||||
|
||||
//Remove any audio segments flagged as do not monitor. Don't remove completed segments yet, because
|
||||
//we want to give them a brief chance at playback. Automatically assign linked audio segments to the
|
||||
//current audio output for audio continutity
|
||||
while(it.hasNext())
|
||||
{
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.isDoNotMonitor())
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
it.remove();
|
||||
}
|
||||
else if(audioSegment.isLinked())
|
||||
{
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
if(audioOutput.isLinkedTo(audioSegment))
|
||||
{
|
||||
audioOutput.play(audioSegment);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Assign audio segments to empty audio outputs
|
||||
if(!mAudioSegments.isEmpty())
|
||||
{
|
||||
Collections.sort(mAudioSegments, Comparator.comparingInt(o -> o.monitorPriorityProperty().get()));
|
||||
|
||||
//Assign empty audio outputs first
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
if(audioOutput.emptyProperty().get())
|
||||
{
|
||||
audioOutput.play(mAudioSegments.remove(0));
|
||||
|
||||
if(mAudioSegments.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Preempt ongoing audio segment playback when higher priority audio segments are available
|
||||
if(!mAudioSegments.isEmpty())
|
||||
{
|
||||
//Assign empty audio outputs first
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
if(mAudioSegments.get(0).monitorPriorityProperty().get() < audioOutput.audioPriorityProperty().get())
|
||||
{
|
||||
audioOutput.play(mAudioSegments.remove(0));
|
||||
|
||||
if(mAudioSegments.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Remove any audio segments marked as completed
|
||||
it = mAudioSegments.iterator(); //reset the iterator
|
||||
while(it.hasNext())
|
||||
{
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.completeProperty().get())
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
@@ -152,68 +200,34 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
||||
if(mProcessingTask != null)
|
||||
{
|
||||
mProcessingTask.cancel(true);
|
||||
mProcessingTask = null;
|
||||
}
|
||||
|
||||
mAudioPacketQueue.clear();
|
||||
|
||||
mProcessingTask = null;
|
||||
|
||||
mChannelConnectionMap.clear();
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
connection.dispose();
|
||||
}
|
||||
|
||||
mAudioOutputConnections.clear();
|
||||
mNewAudioSegmentQueue.clear();
|
||||
mAudioSegments.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary ingest point for audio produced by all decoding channels, for distribution to audio playback devices.
|
||||
* Receive user preference update notifications so that we can detect when the user changes the audio output
|
||||
* device in the user preferences editor.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void receive(ReusableAudioPacket packet)
|
||||
@Subscribe
|
||||
public void preferenceUpdated(PreferenceType preferenceType)
|
||||
{
|
||||
mAudioPacketQueue.add(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks each audio channel assignment and disconnects any inactive connections
|
||||
*/
|
||||
private void disconnectInactiveChannelAssignments()
|
||||
{
|
||||
boolean changed = false;
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
if(preferenceType == PreferenceType.PLAYBACK)
|
||||
{
|
||||
if(connection.isInactive() && mChannelConnectionMap.containsKey(connection.getAudioChannelId()))
|
||||
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
|
||||
|
||||
if(configuration != null && !configuration.equals(mMixerChannelConfiguration))
|
||||
{
|
||||
mChannelConnectionMap.remove(connection.getAudioChannelId());
|
||||
connection.disconnect();
|
||||
mAvailableConnectionCount++;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(changed)
|
||||
{
|
||||
updateLowestPriorityAssignment();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the lowest priority channel connection where the a higher value indicates a lower priority.
|
||||
*/
|
||||
private void updateLowestPriorityAssignment()
|
||||
{
|
||||
mLowestPriorityConnection = null;
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
if(connection.isConnected() &&
|
||||
(mLowestPriorityConnection == null || mLowestPriorityConnection.getPriority() < connection.getPriority()))
|
||||
{
|
||||
mLowestPriorityConnection = connection;
|
||||
try
|
||||
{
|
||||
setMixerChannelConfiguration(configuration);
|
||||
}
|
||||
catch(AudioException ae)
|
||||
{
|
||||
mLog.error("Error changing audio output to [" + configuration.toString() + "]", ae);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,19 +241,7 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
||||
@Override
|
||||
public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException
|
||||
{
|
||||
setMixerChannelConfiguration(entry, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures audio playback to use the configuration specified in the entry argument.
|
||||
*
|
||||
* @param entry to use in configuring the audio playback setup.
|
||||
* @param saveSettings to save the audio playback configuration settings in the properties file.
|
||||
* @throws AudioException if there is an error
|
||||
*/
|
||||
public void setMixerChannelConfiguration(MixerChannelConfiguration entry, boolean saveSettings) throws AudioException
|
||||
{
|
||||
if(entry != null && (entry.getMixerChannel() == MixerChannel.MONO || entry.getMixerChannel() == MixerChannel.STEREO))
|
||||
if(entry != null)
|
||||
{
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
|
||||
|
||||
@@ -248,68 +250,37 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
||||
mProcessingTask.cancel(true);
|
||||
}
|
||||
|
||||
disposeCurrentConfiguration();
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
audioOutput.dispose();
|
||||
}
|
||||
|
||||
mAudioOutputs.clear();
|
||||
|
||||
switch(entry.getMixerChannel())
|
||||
{
|
||||
case MONO:
|
||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer());
|
||||
mAudioOutputConnections.add(new AudioOutputConnection(mono));
|
||||
mAvailableConnectionCount++;
|
||||
mAudioOutputMap.put(mono.getChannelName(), mono);
|
||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
|
||||
mAudioOutputs.add(mono);
|
||||
break;
|
||||
case STEREO:
|
||||
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT);
|
||||
mAudioOutputConnections.add(new AudioOutputConnection(left));
|
||||
mAvailableConnectionCount++;
|
||||
mAudioOutputMap.put(left.getChannelName(), left);
|
||||
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
|
||||
mAudioOutputs.add(left);
|
||||
|
||||
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT);
|
||||
mAudioOutputConnections.add(new AudioOutputConnection(right));
|
||||
mAvailableConnectionCount++;
|
||||
mAudioOutputMap.put(right.getChannelName(), right);
|
||||
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
|
||||
mAudioOutputs.add(right);
|
||||
break;
|
||||
default:
|
||||
throw new AudioException("Unsupported mixer channel "
|
||||
+ "configuration: " + entry.getMixerChannel());
|
||||
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
|
||||
}
|
||||
|
||||
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioPacketProcessor(),
|
||||
0, 15, TimeUnit.MILLISECONDS);
|
||||
|
||||
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 250, TimeUnit.MILLISECONDS);
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
|
||||
|
||||
if(saveSettings)
|
||||
{
|
||||
SystemProperties properties = SystemProperties.getInstance();
|
||||
properties.set(AUDIO_MIXER_PROPERTY, entry.getMixer().getMixerInfo().getName());
|
||||
properties.set(AUDIO_CHANNELS_PROPERTY, entry.getMixerChannel().name());
|
||||
}
|
||||
mMixerChannelConfiguration = entry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all channel assignments and terminates all audio outputs in preparation for complete shutdown or change
|
||||
* to another mixer/channel configuration
|
||||
*/
|
||||
private void disposeCurrentConfiguration()
|
||||
{
|
||||
mChannelConnectionMap.clear();
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
connection.dispose();
|
||||
}
|
||||
|
||||
mAvailableConnectionCount = 0;
|
||||
|
||||
mAudioOutputConnections.clear();
|
||||
|
||||
mAudioOutputMap.clear();
|
||||
|
||||
mLowestPriorityConnection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current audio playback mixer channel configuration setting.
|
||||
*/
|
||||
@@ -320,12 +291,12 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
||||
}
|
||||
|
||||
/**
|
||||
* List of audio outputs available for the current mixer channel configuration
|
||||
* List of sorted audio outputs available for the current mixer channel configuration
|
||||
*/
|
||||
@Override
|
||||
public List<AudioOutput> getAudioOutputs()
|
||||
{
|
||||
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputMap.values());
|
||||
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputs);
|
||||
|
||||
Collections.sort(outputs, new Comparator<AudioOutput>()
|
||||
{
|
||||
@@ -358,228 +329,21 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an audio output connection for the packet if one is available, or overrides an existing lower priority
|
||||
* connection. Returns null if no connection is available for the audio packet.
|
||||
*
|
||||
* @param audioPacket from a decoding channel source
|
||||
* @return an audio output connection or null
|
||||
* Scheduled runnable to process incoming audio segments
|
||||
*/
|
||||
private AudioOutputConnection getConnection(ReusableAudioPacket audioPacket)
|
||||
public class AudioSegmentProcessor implements Runnable
|
||||
{
|
||||
int audioChannelId = audioPacket.getAudioChannelId();
|
||||
|
||||
//Use an existing connection
|
||||
if(mChannelConnectionMap.containsKey(audioChannelId))
|
||||
{
|
||||
return mChannelConnectionMap.get(audioChannelId);
|
||||
}
|
||||
|
||||
//Connect to an unused, available connection
|
||||
if(mAvailableConnectionCount > 0)
|
||||
{
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
if(connection.isDisconnected())
|
||||
{
|
||||
connection.connect(audioChannelId, audioPacket.getMonitoringPriority());
|
||||
mChannelConnectionMap.put(audioChannelId, connection);
|
||||
mAvailableConnectionCount--;
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
//Preempt an existing lower priority connection and connect when this is a higher priority packet
|
||||
else
|
||||
{
|
||||
int priority = audioPacket.getMonitoringPriority();
|
||||
|
||||
AudioOutputConnection connection = mLowestPriorityConnection;
|
||||
|
||||
if(connection != null && priority < connection.getPriority())
|
||||
{
|
||||
mChannelConnectionMap.remove(connection.getAudioChannelId());
|
||||
|
||||
connection.connect(audioChannelId, priority);
|
||||
|
||||
mChannelConnectionMap.put(audioChannelId, connection);
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public class AudioPacketProcessor implements Runnable
|
||||
{
|
||||
private List<ReusableAudioPacket> mPackets = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
disconnectInactiveChannelAssignments();
|
||||
|
||||
if(mAudioPacketQueue != null)
|
||||
{
|
||||
mAudioPacketQueue.drainTo(mPackets);
|
||||
|
||||
for(ReusableAudioPacket packet : mPackets)
|
||||
{
|
||||
/* Don't process any packet's marked as do not monitor */
|
||||
if(!packet.isDoNotMonitor() && packet.getType() == ReusableAudioPacket.Type.AUDIO)
|
||||
{
|
||||
AudioOutputConnection connection = getConnection(packet);
|
||||
|
||||
if(connection != null)
|
||||
{
|
||||
connection.receive(packet);
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
mPackets.clear();
|
||||
}
|
||||
processAudioSegments();
|
||||
}
|
||||
catch(Exception e)
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Encountered error while processing audio packets", e);
|
||||
|
||||
while(mPackets.size() > 0)
|
||||
{
|
||||
ReusableAudioPacket audioPacket = mPackets.remove(0);
|
||||
audioPacket.decrementUserCount();
|
||||
}
|
||||
mLog.error("Encountered error while processing audio segments", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio output connection manages a connection between a source and an audio
|
||||
* output and maintains current state information about the audio activity
|
||||
* received form the source.
|
||||
*/
|
||||
public class AudioOutputConnection
|
||||
{
|
||||
private static final int DISCONNECTED = Integer.MIN_VALUE;
|
||||
|
||||
private AudioOutput mAudioOutput;
|
||||
private int mPriority = 0;
|
||||
private int mAudioChannelId = DISCONNECTED;
|
||||
|
||||
public AudioOutputConnection(AudioOutput audioOutput)
|
||||
{
|
||||
mAudioOutput = audioOutput;
|
||||
}
|
||||
|
||||
public void receive(ReusableAudioPacket packet)
|
||||
{
|
||||
if(packet.hasIdentifierCollection() && packet.getAudioChannelId() == mAudioChannelId)
|
||||
{
|
||||
int priority = packet.getMonitoringPriority();
|
||||
|
||||
if(mPriority != priority)
|
||||
{
|
||||
mPriority = priority;
|
||||
updateLowestPriorityAssignment();
|
||||
}
|
||||
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
mAudioOutput.receive(packet);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(packet.hasIdentifierCollection())
|
||||
{
|
||||
mLog.error("Received audio packet from channel metadata [" + packet.getAudioChannelId() +
|
||||
"] however this assignment is currently connected to metadata [" + mAudioChannelId + "]");
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Received audio packet with no metadata - cannot route audio packet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates the audio output and prepares this connection for disposal
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
mAudioOutput.dispose();
|
||||
mAudioOutput = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this assignment is currently disconnected from a channel source
|
||||
*/
|
||||
public boolean isDisconnected()
|
||||
{
|
||||
return mAudioChannelId == DISCONNECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this assignment is currently connected to a channel source
|
||||
*/
|
||||
public boolean isConnected()
|
||||
{
|
||||
return !isDisconnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects this assignment to the indicated source so that audio
|
||||
* packets from this source can be sent to the audio output
|
||||
*/
|
||||
public void connect(int source, int priority)
|
||||
{
|
||||
mAudioChannelId = source;
|
||||
mPriority = priority;
|
||||
updateLowestPriorityAssignment();
|
||||
|
||||
mAudioOutput.updateTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently connected source or -1 if disconnected
|
||||
*/
|
||||
public int getAudioChannelId()
|
||||
{
|
||||
return mAudioChannelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects this assignment from the source and prevents any audio
|
||||
* from being routed to the audio output until another source is assigned
|
||||
*/
|
||||
public void disconnect()
|
||||
{
|
||||
mAudioChannelId = DISCONNECTED;
|
||||
mPriority = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if audio output is current inactive, meaning that the
|
||||
* audio output hasn't recently processed any audio packets.
|
||||
*/
|
||||
public boolean isInactive()
|
||||
{
|
||||
return (mAudioOutput.getLastActivityTimestamp() + AUDIO_TIMEOUT) < System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public int getPriority()
|
||||
{
|
||||
return mPriority;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
@@ -34,23 +34,21 @@ public class MonoAudioOutput extends AudioOutput
|
||||
{
|
||||
private final static int BUFFER_SIZE = 8000;
|
||||
|
||||
public MonoAudioOutput(Mixer mixer)
|
||||
public MonoAudioOutput(Mixer mixer, UserPreferences userPreferences)
|
||||
{
|
||||
super(mixer, MixerChannel.MONO, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO,
|
||||
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE);
|
||||
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the audio packet data into mono audio frames.
|
||||
*/
|
||||
protected ByteBuffer convert(ReusableAudioPacket packet)
|
||||
protected ByteBuffer convert(float[] samples)
|
||||
{
|
||||
ByteBuffer buffer = null;
|
||||
|
||||
if(packet.hasAudioSamples())
|
||||
if(samples.length > 0)
|
||||
{
|
||||
float[] samples = packet.getAudioSamples();
|
||||
|
||||
/* Little-endian byte buffer */
|
||||
buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
@@ -62,8 +60,6 @@ public class MonoAudioOutput extends AudioOutput
|
||||
}
|
||||
}
|
||||
|
||||
packet.decrementUserCount();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
@@ -34,10 +34,10 @@ public class StereoAudioOutput extends AudioOutput
|
||||
{
|
||||
private final static int BUFFER_SIZE = 16000;
|
||||
|
||||
public StereoAudioOutput(Mixer mixer, MixerChannel channel)
|
||||
public StereoAudioOutput(Mixer mixer, MixerChannel channel, UserPreferences userPreferences)
|
||||
{
|
||||
super(mixer, channel, AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO, AudioFormats.STEREO_SOURCE_DATALINE_INFO,
|
||||
BUFFER_SIZE);
|
||||
BUFFER_SIZE, userPreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,14 +45,12 @@ public class StereoAudioOutput extends AudioOutput
|
||||
* channel containing the audio and the other channel containing zero
|
||||
* valued (silent) samples.
|
||||
*/
|
||||
protected ByteBuffer convert(ReusableAudioPacket packet)
|
||||
protected ByteBuffer convert(float[] samples)
|
||||
{
|
||||
ByteBuffer buffer = null;
|
||||
|
||||
if(packet.hasAudioSamples())
|
||||
if(samples.length > 0)
|
||||
{
|
||||
float[] samples = packet.getAudioSamples();
|
||||
|
||||
/* Little-endian byte buffer */
|
||||
buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
@@ -76,8 +74,6 @@ public class StereoAudioOutput extends AudioOutput
|
||||
}
|
||||
}
|
||||
|
||||
packet.decrementUserCount();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user