mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 01:57:39 -05:00
Merges master 894ebeb - patch to audio playback where audio output
channels were locking up after a period of time
This commit is contained in:
@@ -430,19 +430,17 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
|||||||
* output and maintains current state information about the audio activity
|
* output and maintains current state information about the audio activity
|
||||||
* received form the source.
|
* received form the source.
|
||||||
*/
|
*/
|
||||||
public class AudioOutputConnection implements Listener<AudioEvent>
|
public class AudioOutputConnection
|
||||||
{
|
{
|
||||||
private static final int DISCONNECTED = -1;
|
private static final int DISCONNECTED = -1;
|
||||||
|
|
||||||
private AudioOutput mAudioOutput;
|
private AudioOutput mAudioOutput;
|
||||||
private int mPriority = 0;
|
private int mPriority = 0;
|
||||||
private long mLastActivity = System.currentTimeMillis();
|
|
||||||
private int mSource = DISCONNECTED;
|
private int mSource = DISCONNECTED;
|
||||||
|
|
||||||
public AudioOutputConnection( AudioOutput audioOutput )
|
public AudioOutputConnection( AudioOutput audioOutput )
|
||||||
{
|
{
|
||||||
mAudioOutput = audioOutput;
|
mAudioOutput = audioOutput;
|
||||||
mAudioOutput.addAudioEventListener( this );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void receive( AudioPacket packet )
|
public void receive( AudioPacket packet )
|
||||||
@@ -458,8 +456,6 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
|||||||
updateLowestPriorityAssignment();
|
updateLowestPriorityAssignment();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTimestamp();
|
|
||||||
|
|
||||||
if( mAudioOutput != null )
|
if( mAudioOutput != null )
|
||||||
{
|
{
|
||||||
mAudioOutput.receive( packet );
|
mAudioOutput.receive( packet );
|
||||||
@@ -482,11 +478,6 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTimestamp()
|
|
||||||
{
|
|
||||||
mLastActivity = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Terminates the audio output and prepares this connection for disposal
|
* Terminates the audio output and prepares this connection for disposal
|
||||||
*/
|
*/
|
||||||
@@ -519,7 +510,8 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
|||||||
public void connect( int source )
|
public void connect( int source )
|
||||||
{
|
{
|
||||||
mSource = source;
|
mSource = source;
|
||||||
updateTimestamp();
|
|
||||||
|
mAudioOutput.updateTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -537,6 +529,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
|||||||
public void disconnect()
|
public void disconnect()
|
||||||
{
|
{
|
||||||
mSource = DISCONNECTED;
|
mSource = DISCONNECTED;
|
||||||
|
mPriority = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -545,28 +538,12 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
|
|||||||
*/
|
*/
|
||||||
public boolean isInactive()
|
public boolean isInactive()
|
||||||
{
|
{
|
||||||
return mLastActivity != Long.MAX_VALUE &&
|
return ( mAudioOutput.getLastActivityTimestamp() + AUDIO_TIMEOUT ) < System.currentTimeMillis();
|
||||||
mLastActivity + AUDIO_TIMEOUT < System.currentTimeMillis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getPriority()
|
public int getPriority()
|
||||||
{
|
{
|
||||||
return mPriority;
|
return mPriority;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void receive( AudioEvent event )
|
|
||||||
{
|
|
||||||
switch( event.getType() )
|
|
||||||
{
|
|
||||||
case AUDIO_STARTED:
|
|
||||||
case AUDIO_STOPPED:
|
|
||||||
case AUDIO_CONTINUATION:
|
|
||||||
updateTimestamp();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,11 @@ public class AudioPacket
|
|||||||
return mAudioData;
|
return mAudioData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasAudioBuffer()
|
||||||
|
{
|
||||||
|
return mAudioData != null;
|
||||||
|
}
|
||||||
|
|
||||||
public enum Type
|
public enum Type
|
||||||
{
|
{
|
||||||
AUDIO,
|
AUDIO,
|
||||||
|
|||||||
@@ -1,34 +1,165 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* SDR Trunk
|
||||||
|
* Copyright (C) 2014-2016 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 audio.output;
|
package audio.output;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.LinkedTransferQueue;
|
import java.util.concurrent.LinkedTransferQueue;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioFormat;
|
||||||
|
import javax.sound.sampled.BooleanControl;
|
||||||
|
import javax.sound.sampled.Control;
|
||||||
import javax.sound.sampled.FloatControl;
|
import javax.sound.sampled.FloatControl;
|
||||||
|
import javax.sound.sampled.Line;
|
||||||
|
import javax.sound.sampled.LineEvent;
|
||||||
import javax.sound.sampled.LineListener;
|
import javax.sound.sampled.LineListener;
|
||||||
|
import javax.sound.sampled.LineUnavailableException;
|
||||||
|
import javax.sound.sampled.Mixer;
|
||||||
|
import javax.sound.sampled.SourceDataLine;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import sample.Broadcaster;
|
import sample.Broadcaster;
|
||||||
import sample.Listener;
|
import sample.Listener;
|
||||||
|
import source.mixer.MixerChannel;
|
||||||
import audio.AudioEvent;
|
import audio.AudioEvent;
|
||||||
import audio.AudioPacket;
|
import audio.AudioPacket;
|
||||||
|
import audio.AudioPacket.Type;
|
||||||
import audio.metadata.AudioMetadata;
|
import audio.metadata.AudioMetadata;
|
||||||
import controller.ThreadPoolManager;
|
import controller.ThreadPoolManager;
|
||||||
|
import controller.ThreadPoolManager.ThreadType;
|
||||||
|
|
||||||
public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger( AudioOutput.class );
|
private final static Logger mLog = LoggerFactory.getLogger( AudioOutput.class );
|
||||||
|
|
||||||
private Listener<AudioMetadata> mAudioMetadataListener;
|
private LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
|
||||||
protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
|
private int mBufferStartThreshold;
|
||||||
protected LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
|
private int mBufferStopThreshold;
|
||||||
protected ThreadPoolManager mThreadPoolManager;
|
|
||||||
protected boolean mCanProcessAudio;
|
|
||||||
protected long mLastActivity;
|
|
||||||
|
|
||||||
public AudioOutput( ThreadPoolManager threadPoolManager )
|
private Listener<AudioMetadata> mAudioMetadataListener;
|
||||||
|
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
|
||||||
|
|
||||||
|
private ThreadPoolManager mThreadPoolManager;
|
||||||
|
private ScheduledFuture<?> mProcessorTask;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single audio channel playback with automatic starting and stopping of the
|
||||||
|
* underlying sourcedataline specified by the mixer and mixer channel
|
||||||
|
* arguments.
|
||||||
|
*
|
||||||
|
* Maintains an internal non-blocking audio packet queue and processes this
|
||||||
|
* queue 25 times a second (every 40 ms).
|
||||||
|
*
|
||||||
|
* @param threadPoolManager for assigning buffer processing schedule task
|
||||||
|
* @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( ThreadPoolManager threadPoolManager,
|
||||||
|
Mixer mixer,
|
||||||
|
MixerChannel mixerChannel,
|
||||||
|
AudioFormat audioFormat,
|
||||||
|
Line.Info lineInfo,
|
||||||
|
int requestedBufferSize )
|
||||||
{
|
{
|
||||||
mThreadPoolManager = threadPoolManager;
|
mThreadPoolManager = threadPoolManager;
|
||||||
|
mMixer = mixer;
|
||||||
|
mMixerChannel = mixerChannel;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mOutput = (SourceDataLine)mMixer.getLine( lineInfo );
|
||||||
|
|
||||||
|
if( mOutput != null )
|
||||||
|
{
|
||||||
|
mOutput.open( audioFormat, requestedBufferSize );
|
||||||
|
|
||||||
|
//Start threshold: buffer is full with 10% or less of capacity remaining
|
||||||
|
mBufferStartThreshold = (int)( mOutput.getBufferSize() * 0.10 );
|
||||||
|
|
||||||
|
//Stop threshold: buffer is empty with 90% or more capacity available
|
||||||
|
mBufferStopThreshold = (int)( mOutput.getBufferSize() * 0.90 );
|
||||||
|
|
||||||
|
mOutput.addLineListener( this );
|
||||||
|
|
||||||
|
if( mOutput != null )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN );
|
||||||
|
mGainControl = (FloatControl)gain;
|
||||||
|
}
|
||||||
|
catch( IllegalArgumentException iae )
|
||||||
|
{
|
||||||
|
mLog.warn( "Couldn't obtain MASTER GAIN control for stereo line [" +
|
||||||
|
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]" );
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Control mute = mOutput.getControl( BooleanControl.Type.MUTE );
|
||||||
|
mMuteControl = (BooleanControl)mute;
|
||||||
|
}
|
||||||
|
catch( IllegalArgumentException iae )
|
||||||
|
{
|
||||||
|
mLog.warn( "Couldn't obtain MUTE control for stereo line [" +
|
||||||
|
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]" );
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Run the queue processor task every 40 milliseconds or 25 times a second */
|
||||||
|
mProcessorTask = mThreadPoolManager.scheduleFixedRate(
|
||||||
|
ThreadType.AUDIO_PROCESSING, new BufferProcessor(),
|
||||||
|
40, TimeUnit.MILLISECONDS );
|
||||||
|
}
|
||||||
|
|
||||||
|
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() + "]" );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reset()
|
public void reset()
|
||||||
@@ -41,34 +172,50 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
|||||||
{
|
{
|
||||||
mCanProcessAudio = false;
|
mCanProcessAudio = false;
|
||||||
|
|
||||||
|
if( mProcessorTask != null && mThreadPoolManager != null )
|
||||||
|
{
|
||||||
|
mThreadPoolManager.cancel( mProcessorTask );
|
||||||
|
}
|
||||||
|
|
||||||
|
mThreadPoolManager = null;
|
||||||
|
mProcessorTask = null;
|
||||||
|
|
||||||
mBuffer.clear();
|
mBuffer.clear();
|
||||||
|
|
||||||
mAudioEventBroadcaster.dispose();
|
mAudioEventBroadcaster.dispose();
|
||||||
mAudioEventBroadcaster = null;
|
mAudioEventBroadcaster = null;
|
||||||
mAudioMetadataListener = null;
|
mAudioMetadataListener = null;
|
||||||
|
|
||||||
mThreadPoolManager = null;
|
if( mOutput != null )
|
||||||
}
|
|
||||||
|
|
||||||
public abstract String getChannelName();
|
|
||||||
|
|
||||||
public abstract void setMuted( boolean muted );
|
|
||||||
public abstract boolean isMuted();
|
|
||||||
|
|
||||||
public abstract FloatControl getGainControl();
|
|
||||||
public abstract boolean hasGainControl();
|
|
||||||
|
|
||||||
protected void broadcast( AudioMetadata metadata )
|
|
||||||
{
|
|
||||||
if( mAudioMetadataListener != null )
|
|
||||||
{
|
{
|
||||||
mAudioMetadataListener.receive( metadata );
|
mOutput.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mOutput = null;
|
||||||
|
mGainControl = null;
|
||||||
|
mMuteControl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void broadcast( AudioEvent audioEvent )
|
/**
|
||||||
|
* Converts the audio packet data into a byte buffer format appropriate for
|
||||||
|
* the underlying source data line.
|
||||||
|
*/
|
||||||
|
protected abstract ByteBuffer convert( AudioPacket packet );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio output channel name
|
||||||
|
*/
|
||||||
|
public String getChannelName()
|
||||||
{
|
{
|
||||||
mAudioEventBroadcaster.broadcast( audioEvent );
|
return mMixerChannel.getLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mixer Channel for this audio output
|
||||||
|
*/
|
||||||
|
protected MixerChannel getMixerChannel()
|
||||||
|
{
|
||||||
|
return mMixerChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,6 +231,14 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
|||||||
mAudioEventBroadcaster.removeListener( listener );
|
mAudioEventBroadcaster.removeListener( listener );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts an audio event to the registered listener
|
||||||
|
*/
|
||||||
|
private void broadcast( AudioEvent audioEvent )
|
||||||
|
{
|
||||||
|
mAudioEventBroadcaster.broadcast( audioEvent );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a single listener to receive the audio metadata from each
|
* Registers a single listener to receive the audio metadata from each
|
||||||
* audio packet
|
* audio packet
|
||||||
@@ -99,21 +254,200 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp when most recent buffer was received
|
* Broadcasts audio metadata to the registered listener
|
||||||
*/
|
*/
|
||||||
public long getLastBufferReceived()
|
private void broadcast( AudioMetadata metadata )
|
||||||
|
{
|
||||||
|
if( mAudioMetadataListener != null )
|
||||||
|
{
|
||||||
|
mAudioMetadataListener.receive( metadata );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp of either last buffer received or last buffer processed
|
||||||
|
*/
|
||||||
|
public long getLastActivityTimestamp()
|
||||||
{
|
{
|
||||||
return mLastActivity;
|
return mLastActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the last activity timestamp to current system time
|
||||||
|
*/
|
||||||
|
public void updateTimestamp()
|
||||||
|
{
|
||||||
|
mLastActivity = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void receive( AudioPacket packet )
|
public void receive( AudioPacket packet )
|
||||||
{
|
{
|
||||||
if( mCanProcessAudio )
|
if( mCanProcessAudio )
|
||||||
{
|
{
|
||||||
mLastActivity = System.currentTimeMillis();
|
//Update the activity timestamp so that this audio output doesn't
|
||||||
|
//get disconnected before it starts processing the audio stream
|
||||||
|
updateTimestamp();
|
||||||
|
|
||||||
mBuffer.add( packet );
|
mBuffer.add( packet );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class BufferProcessor implements Runnable
|
||||||
|
{
|
||||||
|
private AtomicBoolean mProcessing = new AtomicBoolean();
|
||||||
|
|
||||||
|
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 ) )
|
||||||
|
{
|
||||||
|
List<AudioPacket> packets = new ArrayList<AudioPacket>();
|
||||||
|
|
||||||
|
mBuffer.drainTo( packets );
|
||||||
|
|
||||||
|
for( AudioPacket packet: packets )
|
||||||
|
{
|
||||||
|
if( packet.getType() == Type.AUDIO )
|
||||||
|
{
|
||||||
|
broadcast( packet.getAudioMetadata() );
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the mute state for this audio output channel
|
||||||
|
*/
|
||||||
|
public void setMuted( boolean muted )
|
||||||
|
{
|
||||||
|
if( mMuteControl != null )
|
||||||
|
{
|
||||||
|
mMuteControl.setValue( muted );
|
||||||
|
|
||||||
|
broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED :
|
||||||
|
AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current mute state for this audio output channel
|
||||||
|
*/
|
||||||
|
public boolean isMuted()
|
||||||
|
{
|
||||||
|
if( mMuteControl != null )
|
||||||
|
{
|
||||||
|
return mMuteControl.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gain/volume control for this audio output channel, if one is available.
|
||||||
|
*/
|
||||||
|
public FloatControl getGainControl()
|
||||||
|
{
|
||||||
|
return mGainControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void update( LineEvent event )
|
||||||
|
{
|
||||||
|
LineEvent.Type type = event.getType();
|
||||||
|
|
||||||
|
if( type == LineEvent.Type.START )
|
||||||
|
{
|
||||||
|
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
|
||||||
|
}
|
||||||
|
else if( type == LineEvent.Type.STOP )
|
||||||
|
{
|
||||||
|
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,282 +1,74 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* SDR Trunk
|
||||||
|
* Copyright (C) 2014-2016 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 audio.output;
|
package audio.output;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.nio.ShortBuffer;
|
import java.nio.ShortBuffer;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.ScheduledFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
import javax.sound.sampled.BooleanControl;
|
|
||||||
import javax.sound.sampled.Control;
|
|
||||||
import javax.sound.sampled.FloatControl;
|
|
||||||
import javax.sound.sampled.LineEvent;
|
|
||||||
import javax.sound.sampled.LineUnavailableException;
|
|
||||||
import javax.sound.sampled.Mixer;
|
import javax.sound.sampled.Mixer;
|
||||||
import javax.sound.sampled.SourceDataLine;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import source.mixer.MixerChannel;
|
import source.mixer.MixerChannel;
|
||||||
import audio.AudioEvent;
|
|
||||||
import audio.AudioFormats;
|
import audio.AudioFormats;
|
||||||
import audio.AudioPacket;
|
import audio.AudioPacket;
|
||||||
import audio.AudioPacket.Type;
|
|
||||||
import controller.ThreadPoolManager;
|
import controller.ThreadPoolManager;
|
||||||
import controller.ThreadPoolManager.ThreadType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mono Audio output with automatic flow control based on the availability of
|
* Mono Audio output implementation
|
||||||
* audio data packets.
|
|
||||||
*/
|
*/
|
||||||
public class MonoAudioOutput extends AudioOutput
|
public class MonoAudioOutput extends AudioOutput
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger( MonoAudioOutput.class );
|
|
||||||
|
|
||||||
/* Output line buffer size is set at one half second of audio at 48 kHz and
|
|
||||||
* two bytes per sample */
|
|
||||||
private final static int BUFFER_SIZE = 48000;
|
private final static int BUFFER_SIZE = 48000;
|
||||||
|
|
||||||
/* The queue processor will run every 20 milliseconds checking for inbound
|
|
||||||
* audio and automatically starting or stopping audio playback. We set the
|
|
||||||
* data line available space threshold to start when we're 20 ms away from
|
|
||||||
* being full, and the stop threshold when 20 ms away from being empty */
|
|
||||||
private final static int SAMPLE_SIZE_40_MS = BUFFER_SIZE / 25;
|
|
||||||
private final static int START_THRESHOLD = SAMPLE_SIZE_40_MS;
|
|
||||||
private final static int STOP_THRESHOLD = BUFFER_SIZE - SAMPLE_SIZE_40_MS;
|
|
||||||
|
|
||||||
private SourceDataLine mOutput;
|
|
||||||
private FloatControl mGainControl;
|
|
||||||
private BooleanControl mMuteControl;
|
|
||||||
private AudioEvent mAudioStartEvent;
|
|
||||||
private AudioEvent mAudioStopEvent;
|
|
||||||
private AudioEvent mAudioContinuationEvent;
|
|
||||||
private ScheduledFuture<?> mProcessorTask;
|
|
||||||
|
|
||||||
public MonoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer )
|
public MonoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer )
|
||||||
{
|
{
|
||||||
super( threadPoolManager );
|
super( threadPoolManager,
|
||||||
|
mixer,
|
||||||
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
|
MixerChannel.MONO,
|
||||||
getChannelName() );
|
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO,
|
||||||
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
|
AudioFormats.MONO_SOURCE_DATALINE_INFO,
|
||||||
getChannelName() );
|
BUFFER_SIZE );
|
||||||
mAudioContinuationEvent = new AudioEvent( AudioEvent.Type.AUDIO_CONTINUATION,
|
|
||||||
getChannelName() );
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
mOutput = (SourceDataLine)mixer.getLine(
|
|
||||||
AudioFormats.MONO_SOURCE_DATALINE_INFO );
|
|
||||||
|
|
||||||
if( mOutput != null )
|
|
||||||
{
|
|
||||||
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE );
|
|
||||||
mOutput.addLineListener( this );
|
|
||||||
mCanProcessAudio = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch ( LineUnavailableException e )
|
|
||||||
{
|
|
||||||
mLog.error( "Couldn't obtain source data line for 48kHz PCM mono "
|
|
||||||
+ "audio output" );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( mOutput != null )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN );
|
|
||||||
mGainControl = (FloatControl)gain;
|
|
||||||
}
|
|
||||||
catch( IllegalArgumentException iae )
|
|
||||||
{
|
|
||||||
mLog.warn( "Couldn't obtain MASTER GAIN control for mono line [" +
|
|
||||||
mixer.getMixerInfo().getName() + "]" );
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Control mute = mOutput.getControl( BooleanControl.Type.MUTE );
|
|
||||||
mMuteControl = (BooleanControl)mute;
|
|
||||||
}
|
|
||||||
catch( IllegalArgumentException iae )
|
|
||||||
{
|
|
||||||
mLog.warn( "Couldn't obtain MUTE control for mono line [" +
|
|
||||||
mixer.getMixerInfo().getName() + "]" );
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Run the queue processor task every 40 milliseconds */
|
|
||||||
mProcessorTask = mThreadPoolManager.scheduleFixedRate(
|
|
||||||
ThreadType.AUDIO_PROCESSING, new QueueProcessor(),
|
|
||||||
40, TimeUnit.MILLISECONDS );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public String getChannelName()
|
* Converts the audio packet data into mono audio frames.
|
||||||
|
*/
|
||||||
|
protected ByteBuffer convert( AudioPacket packet )
|
||||||
{
|
{
|
||||||
return MixerChannel.MONO.getLabel();
|
if( packet.hasAudioBuffer() )
|
||||||
}
|
|
||||||
|
|
||||||
public void dispose()
|
|
||||||
{
|
|
||||||
if( mProcessorTask != null && mThreadPoolManager != null )
|
|
||||||
{
|
{
|
||||||
mThreadPoolManager.cancel( mProcessorTask );
|
float[] samples = packet.getAudioBuffer().getSamples();
|
||||||
}
|
|
||||||
|
|
||||||
mProcessorTask = null;
|
/* Little-endian byte buffer */
|
||||||
|
ByteBuffer buffer =
|
||||||
|
ByteBuffer.allocate( samples.length * 2 )
|
||||||
|
.order( ByteOrder.LITTLE_ENDIAN );
|
||||||
|
|
||||||
super.dispose();
|
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||||
|
|
||||||
if( mOutput != null )
|
for( float sample: samples )
|
||||||
{
|
|
||||||
mOutput.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
mOutput = null;
|
|
||||||
mGainControl = null;
|
|
||||||
mMuteControl = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class QueueProcessor implements Runnable
|
|
||||||
{
|
|
||||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
/* The mProcessing flag ensures that only one instance of the
|
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
|
||||||
* processor can run at any given time */
|
|
||||||
if( mProcessing.compareAndSet( false, true ) )
|
|
||||||
{
|
|
||||||
List<AudioPacket> packets = new ArrayList<AudioPacket>();
|
|
||||||
|
|
||||||
mBuffer.drainTo( packets );
|
|
||||||
|
|
||||||
if( packets.size() > 0 )
|
|
||||||
{
|
|
||||||
for( AudioPacket packet: packets )
|
|
||||||
{
|
|
||||||
broadcast( mAudioContinuationEvent );
|
|
||||||
|
|
||||||
if( mCanProcessAudio )
|
|
||||||
{
|
|
||||||
if( packet.getType() == Type.AUDIO )
|
|
||||||
{
|
|
||||||
float[] samples = packet.getAudioBuffer().getSamples();
|
|
||||||
|
|
||||||
/* Little-endian byte buffer */
|
|
||||||
ByteBuffer 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 ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
//This is a blocking write
|
|
||||||
mOutput.write( buffer.array(), 0, buffer.array().length );
|
|
||||||
|
|
||||||
checkStart();
|
|
||||||
|
|
||||||
broadcast( packet.getAudioMetadata() );
|
|
||||||
|
|
||||||
mLastActivity = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
checkStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
mProcessing.set( false );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
mLog.error( "Error while processing audio buffers", e );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkStart()
|
return null;
|
||||||
{
|
|
||||||
if( mCanProcessAudio &&
|
|
||||||
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
|
|
||||||
{
|
|
||||||
mOutput.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkStop()
|
|
||||||
{
|
|
||||||
if( mCanProcessAudio && mOutput.isRunning() &&
|
|
||||||
mOutput.available() >= STOP_THRESHOLD )
|
|
||||||
{
|
|
||||||
mOutput.drain();
|
|
||||||
mOutput.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setMuted( boolean muted )
|
|
||||||
{
|
|
||||||
if( mMuteControl != null )
|
|
||||||
{
|
|
||||||
mMuteControl.setValue( muted );
|
|
||||||
|
|
||||||
broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED :
|
|
||||||
AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isMuted()
|
|
||||||
{
|
|
||||||
if( mMuteControl != null )
|
|
||||||
{
|
|
||||||
return mMuteControl.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FloatControl getGainControl()
|
|
||||||
{
|
|
||||||
return mGainControl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasGainControl()
|
|
||||||
{
|
|
||||||
return mGainControl != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void update( LineEvent event )
|
|
||||||
{
|
|
||||||
LineEvent.Type type = event.getType();
|
|
||||||
|
|
||||||
if( type == LineEvent.Type.START )
|
|
||||||
{
|
|
||||||
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
|
|
||||||
}
|
|
||||||
else if( type == LineEvent.Type.STOP )
|
|
||||||
{
|
|
||||||
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,312 +1,88 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* SDR Trunk
|
||||||
|
* Copyright (C) 2014-2016 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 audio.output;
|
package audio.output;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.nio.ShortBuffer;
|
import java.nio.ShortBuffer;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.ScheduledFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
import javax.sound.sampled.BooleanControl;
|
|
||||||
import javax.sound.sampled.Control;
|
|
||||||
import javax.sound.sampled.FloatControl;
|
|
||||||
import javax.sound.sampled.LineEvent;
|
|
||||||
import javax.sound.sampled.LineUnavailableException;
|
|
||||||
import javax.sound.sampled.Mixer;
|
import javax.sound.sampled.Mixer;
|
||||||
import javax.sound.sampled.SourceDataLine;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import source.mixer.MixerChannel;
|
import source.mixer.MixerChannel;
|
||||||
import audio.AudioEvent;
|
|
||||||
import audio.AudioFormats;
|
import audio.AudioFormats;
|
||||||
import audio.AudioPacket;
|
import audio.AudioPacket;
|
||||||
import audio.AudioPacket.Type;
|
|
||||||
import controller.ThreadPoolManager;
|
import controller.ThreadPoolManager;
|
||||||
import controller.ThreadPoolManager.ThreadType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stereo Audio output in stereo format with automatic flow control based on the
|
* Stereo audio output implementation.
|
||||||
* availability of audio data packets.
|
|
||||||
*/
|
*/
|
||||||
public class StereoAudioOutput extends AudioOutput
|
public class StereoAudioOutput extends AudioOutput
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger( StereoAudioOutput.class );
|
|
||||||
|
|
||||||
/* Output line buffer size is set at one half second of audio at 48 kHz and
|
|
||||||
* two bytes per sample for two channels */
|
|
||||||
private final static int BUFFER_SIZE = 96000;
|
private final static int BUFFER_SIZE = 96000;
|
||||||
|
|
||||||
/* The queue processor will run every 20 milliseconds checking for inbound
|
|
||||||
* audio and automatically start or stop audio playback. We set the
|
|
||||||
* data line available space threshold to start when we're 20 ms away from
|
|
||||||
* being full and the stop threshold when 20 ms away from being empty */
|
|
||||||
private final static int SAMPLE_SIZE_40_MS = BUFFER_SIZE / 25;
|
|
||||||
private final static int START_THRESHOLD = SAMPLE_SIZE_40_MS;
|
|
||||||
private final static int STOP_THRESHOLD = BUFFER_SIZE - SAMPLE_SIZE_40_MS;
|
|
||||||
|
|
||||||
private SourceDataLine mOutput;
|
|
||||||
private FloatControl mGainControl;
|
|
||||||
private BooleanControl mMuteControl;
|
|
||||||
private AudioEvent mAudioStartEvent;
|
|
||||||
private AudioEvent mAudioStopEvent;
|
|
||||||
private AudioEvent mAudioContinuationEvent;
|
|
||||||
private MixerChannel mMixerChannel;
|
|
||||||
private ScheduledFuture<?> mProcessorTask;
|
|
||||||
|
|
||||||
public StereoAudioOutput( ThreadPoolManager threadPoolManager,
|
public StereoAudioOutput( ThreadPoolManager threadPoolManager,
|
||||||
Mixer mixer, MixerChannel channel )
|
Mixer mixer, MixerChannel channel )
|
||||||
{
|
{
|
||||||
super( threadPoolManager );
|
super( threadPoolManager,
|
||||||
|
mixer,
|
||||||
assert( channel == MixerChannel.LEFT || channel == MixerChannel.RIGHT );
|
channel,
|
||||||
|
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO,
|
||||||
mMixerChannel = channel;
|
AudioFormats.STEREO_SOURCE_DATALINE_INFO,
|
||||||
|
BUFFER_SIZE );
|
||||||
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
|
|
||||||
getChannelName() );
|
|
||||||
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
|
|
||||||
getChannelName() );
|
|
||||||
mAudioContinuationEvent = new AudioEvent( AudioEvent.Type.AUDIO_CONTINUATION,
|
|
||||||
getChannelName() );
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
mOutput = (SourceDataLine)mixer.getLine(
|
|
||||||
AudioFormats.STEREO_SOURCE_DATALINE_INFO );
|
|
||||||
|
|
||||||
if( mOutput != null )
|
|
||||||
{
|
|
||||||
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, BUFFER_SIZE );
|
|
||||||
mOutput.addLineListener( this );
|
|
||||||
mCanProcessAudio = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch ( LineUnavailableException e )
|
|
||||||
{
|
|
||||||
mLog.error( "Couldn't obtain source data line for 48kHz PCM stereo "
|
|
||||||
+ "audio output - mixer [" + mixer.getMixerInfo().getName() +
|
|
||||||
"] channel [" + mMixerChannel.name() + "]" );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( mOutput != null )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN );
|
|
||||||
mGainControl = (FloatControl)gain;
|
|
||||||
}
|
|
||||||
catch( IllegalArgumentException iae )
|
|
||||||
{
|
|
||||||
mLog.warn( "Couldn't obtain MASTER GAIN control for stereo line [" +
|
|
||||||
mixer.getMixerInfo().getName() + " | " + channel.name() + "]" );
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Control mute = mOutput.getControl( BooleanControl.Type.MUTE );
|
|
||||||
mMuteControl = (BooleanControl)mute;
|
|
||||||
}
|
|
||||||
catch( IllegalArgumentException iae )
|
|
||||||
{
|
|
||||||
mLog.warn( "Couldn't obtain MUTE control for stereo line [" +
|
|
||||||
mixer.getMixerInfo().getName() + " | " + channel.name() + "]" );
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Run the queue processor task every 40 milliseconds */
|
|
||||||
mProcessorTask = mThreadPoolManager.scheduleFixedRate(
|
|
||||||
ThreadType.AUDIO_PROCESSING, new QueueProcessor(),
|
|
||||||
40, TimeUnit.MILLISECONDS );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public String getChannelName()
|
* 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( AudioPacket packet )
|
||||||
{
|
{
|
||||||
return mMixerChannel.getLabel();
|
if( packet.hasAudioBuffer() )
|
||||||
}
|
|
||||||
|
|
||||||
public void dispose()
|
|
||||||
{
|
|
||||||
if( mProcessorTask != null && mThreadPoolManager != null )
|
|
||||||
{
|
{
|
||||||
mThreadPoolManager.cancel( mProcessorTask );
|
float[] samples = packet.getAudioBuffer().getSamples();
|
||||||
}
|
|
||||||
|
|
||||||
mProcessorTask = null;
|
/* Little-endian byte buffer */
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate( samples.length * 4 )
|
||||||
|
.order( ByteOrder.LITTLE_ENDIAN );
|
||||||
|
|
||||||
super.dispose();
|
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||||
|
|
||||||
if( mOutput != null )
|
if( getMixerChannel() == MixerChannel.LEFT )
|
||||||
{
|
|
||||||
mOutput.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
mOutput = null;
|
|
||||||
mGainControl = null;
|
|
||||||
mMuteControl = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ByteBuffer convert( float[] samples )
|
|
||||||
{
|
|
||||||
/* Little-endian byte buffer */
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate( samples.length * 4 )
|
|
||||||
.order( ByteOrder.LITTLE_ENDIAN );
|
|
||||||
|
|
||||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
|
||||||
|
|
||||||
if( mMixerChannel == MixerChannel.LEFT )
|
|
||||||
{
|
|
||||||
for( float sample: samples )
|
|
||||||
{
|
{
|
||||||
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
|
for( float sample: samples )
|
||||||
shortBuffer.put( (short)0 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for( float sample: samples )
|
|
||||||
{
|
|
||||||
shortBuffer.put( (short)0 );
|
|
||||||
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class QueueProcessor implements Runnable
|
|
||||||
{
|
|
||||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
|
||||||
|
|
||||||
public QueueProcessor()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
/* The mProcessing flag ensures that only one instance of the
|
|
||||||
* processor can run at any given time */
|
|
||||||
if( mProcessing.compareAndSet( false, true ) )
|
|
||||||
{
|
{
|
||||||
List<AudioPacket> packets = new ArrayList<AudioPacket>();
|
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
|
||||||
|
shortBuffer.put( (short)0 );
|
||||||
mBuffer.drainTo( packets );
|
|
||||||
|
|
||||||
if( packets.size() > 0 )
|
|
||||||
{
|
|
||||||
|
|
||||||
for( AudioPacket packet: packets )
|
|
||||||
{
|
|
||||||
broadcast( mAudioContinuationEvent );
|
|
||||||
|
|
||||||
if( mCanProcessAudio )
|
|
||||||
{
|
|
||||||
if( packet.getType() == Type.AUDIO )
|
|
||||||
{
|
|
||||||
ByteBuffer buffer = convert(
|
|
||||||
packet.getAudioBuffer().getSamples() );
|
|
||||||
|
|
||||||
mOutput.write( buffer.array(), 0,
|
|
||||||
buffer.array().length );
|
|
||||||
|
|
||||||
checkStart();
|
|
||||||
|
|
||||||
broadcast( packet.getAudioMetadata() );
|
|
||||||
|
|
||||||
mLastActivity = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
checkStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
mProcessing.set( false );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch( Exception e )
|
else
|
||||||
{
|
{
|
||||||
mLog.error( "Error while processing audio buffers", e );
|
for( float sample: samples )
|
||||||
|
{
|
||||||
|
shortBuffer.put( (short)0 );
|
||||||
|
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkStart()
|
return null;
|
||||||
{
|
|
||||||
if( mCanProcessAudio &&
|
|
||||||
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
|
|
||||||
{
|
|
||||||
mOutput.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkStop()
|
|
||||||
{
|
|
||||||
if( mCanProcessAudio && mOutput.isRunning() &&
|
|
||||||
mOutput.available() >= STOP_THRESHOLD )
|
|
||||||
{
|
|
||||||
mOutput.drain();
|
|
||||||
mOutput.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setMuted( boolean muted )
|
|
||||||
{
|
|
||||||
if( mMuteControl != null )
|
|
||||||
{
|
|
||||||
mMuteControl.setValue( muted );
|
|
||||||
|
|
||||||
broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED :
|
|
||||||
AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isMuted()
|
|
||||||
{
|
|
||||||
if( mMuteControl != null )
|
|
||||||
{
|
|
||||||
return mMuteControl.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FloatControl getGainControl()
|
|
||||||
{
|
|
||||||
return mGainControl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasGainControl()
|
|
||||||
{
|
|
||||||
return mGainControl != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void update( LineEvent event )
|
|
||||||
{
|
|
||||||
LineEvent.Type type = event.getType();
|
|
||||||
|
|
||||||
if( type == LineEvent.Type.START )
|
|
||||||
{
|
|
||||||
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
|
|
||||||
}
|
|
||||||
else if( type == LineEvent.Type.STOP )
|
|
||||||
{
|
|
||||||
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user