Initial commit of redesigned internal processing and audio management.

Note: Several of the decoders are currently non-functioning in this
commit.  If you want the latest functioning codebase, please checkout
the commit prior to this one.  All decoders will eventually be updated
with this new architecture.

Currently broken:
- Logging and recording
- Aux decoders (Fleetsync/MDC/LoJack/Tait)
- LTR/Passport decoders

Changes in this commit:
- Module architecture for decoders and components with central
provisioning by the DecoderFactory
- Transition from Complex sample stream processing to float buffer
processing for improved memory management
- Redesigned filter structures - to support buffers and streams
- Resolved several memory leak issues
- Universal sharing of the thread pool manager
- Added centralized audio management with call/user prioritization and
new gui with mute/volume controls
- Embedded audio metadata for eventual streaming support
- Redesigned ProcessingChain to support independent re-use
- Added new TrafficChannelManager to handle traffic channel allocations
- Traffic Channel pool size and Call timeout available in P25 and
MPT1327 configs
- Redesigned channel and decoder gui components
This commit is contained in:
denny
2015-09-01 07:10:26 -04:00
parent 538a299246
commit f4d8ea281d
570 changed files with 21578 additions and 17012 deletions

View File

@@ -0,0 +1,356 @@
package audio;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.FloatControl;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import settings.ColorSetting;
import settings.Setting;
import settings.SettingChangeListener;
import settings.SettingsManager;
import settings.ColorSetting.ColorSettingName;
import alias.Alias;
import alias.Metadata;
import alias.MetadataType;
import audio.AudioEvent.Type;
import audio.metadata.AudioMetadata;
public class AudioChannelPanel extends JPanel
implements Listener<AudioEvent>, SettingChangeListener
{
private static final long serialVersionUID = 1L;
private static final Logger mLog = LoggerFactory.getLogger( AudioChannelPanel.class );
private Font mFont = new Font( Font.MONOSPACED, Font.PLAIN, 12 );
private Color mLabelColor;
private Color mDetailsColor;
private SettingsManager mSettingsManager;
private IAudioController mController;
private String mChannel;
private JLabel mChannelLabel;
private JLabel mChannelName;
private JLabel mMutedLabel;
private JLabel mFromLabel = new JLabel( "From:" );
private JLabel mFrom = new JLabel( "" );
private JLabel mFromAlias = new JLabel( "" );
private JLabel mToLabel = new JLabel( "To:" );
private JLabel mTo = new JLabel( "" );
private JLabel mToAlias = new JLabel( "" );
private boolean mConfigured = false;
private BooleanControl mMuteControl;
private FloatControl mGainControl;
public AudioChannelPanel( SettingsManager settingsManager,
IAudioController controller,
String channel )
{
mSettingsManager = settingsManager;
mSettingsManager.addListener( this );
mLabelColor = mSettingsManager.getColorSetting(
ColorSettingName.CHANNEL_STATE_LABEL_DECODER ).getColor();
mDetailsColor = mSettingsManager.getColorSetting(
ColorSettingName.CHANNEL_STATE_LABEL_DETAILS ).getColor();
mController = controller;
mChannel = channel;
try
{
mMuteControl = mController.getMuteControl( mChannel );
}
catch ( AudioException e )
{
mLog.error( "Couldn't obtain mute control from audio channel [" +
mChannel + "]", e );
}
try
{
mGainControl = mController.getGainControl( mChannel );
}
catch ( AudioException e )
{
mLog.error( "Couldn't obtain gain control from audio channel [" +
mChannel + "]", e );
}
try
{
mController.addAudioEventListener( mChannel, this );
}
catch( AudioException e )
{
mLog.error( "Couldn't register as audio event listener on channel [" +
mChannel + "]", e );
}
mController.setAudioMetadataListener( mChannel, new AudioMetadataProcessor() );
init();
}
private void init()
{
setLayout( new MigLayout( "insets 0 0 0 0", "[right][][grow,fill]", "[grow,fill]0[]0[]" ) );
setBackground( Color.BLACK );
addMouseListener( new MouseSelectionListener() );
mChannelLabel = new JLabel( "Channel:" );
mChannelLabel.setFont( mFont );
mChannelLabel.setForeground( mDetailsColor );
add( mChannelLabel );
mChannelName = new JLabel( mChannel );
mChannelName.setFont( mFont );
mChannelName.setForeground( mDetailsColor );
add( mChannelName );
mMutedLabel = new JLabel( "" );
mMutedLabel.setFont( mFont );
mMutedLabel.setForeground( Color.RED );
add( mMutedLabel, "wrap" );
mFromLabel.setFont( mFont );
mFromLabel.setForeground( mLabelColor );
add( mFromLabel );
mFrom.setFont( mFont );
mFrom.setForeground( mLabelColor );
add( mFrom );
mFromAlias.setFont( mFont );
mFromAlias.setForeground( mLabelColor );
add( mFromAlias,"wrap" );
mToLabel.setFont( mFont );
mToLabel.setForeground( mLabelColor );
add( mToLabel );
mTo.setFont( mFont );
mTo.setForeground( mLabelColor );
add( mTo );
mToAlias.setFont( mFont );
mToAlias.setForeground( mLabelColor );
add( mToAlias );
}
private void updateMuteState()
{
if( mMuteControl != null )
{
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
mMutedLabel.setText( mMuteControl.getValue() ? "Muted" : "" );
}
} );
}
}
@Override
public void receive( final AudioEvent audioEvent )
{
if( audioEvent.getType() == Type.AUDIO_STOPPED )
{
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
resetLabels();
}
} );
}
}
/**
* Resets the from and to labels. Note: this does not happen on the swing
* event thread. Only invoke from the swing thread.
*/
private void resetLabels()
{
mFrom.setText( "" );
updateAlias( mFromAlias, null );
mTo.setText( "" );
updateAlias( mToAlias, null );
mConfigured = false;
}
/**
* Updates the alias label with text and icon from the alias. Note: this
* does not occur on the Swing event thread -- wrap any calls to this
* method with an event thread call.
*/
private void updateAlias( JLabel label, Alias alias )
{
if( alias != null )
{
label.setText( alias.getName() );
String icon = alias.getIconName();
if( icon != null )
{
label.setIcon( mSettingsManager.getImageIcon( icon, 12 ) );
}
else
{
label.setIcon( null );
}
}
else
{
label.setText( "" );
label.setIcon( null );
}
}
public class MouseSelectionListener implements MouseListener
{
@Override
public void mouseClicked( MouseEvent event )
{
JPopupMenu popup = new JPopupMenu();
if( mMuteControl != null )
{
JMenuItem mute = new JMenuItem( mMuteControl.getValue() ?
"Unmute Channel: " + mChannel : "Mute Channel: " + mChannel );
mute.addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
mMuteControl.setValue( !mMuteControl.getValue() );
updateMuteState();
}
} );
popup.add( mute );
}
popup.show( event.getComponent(), event.getX(), event.getY() );
}
@Override
public void mousePressed( MouseEvent e ) {}
@Override
public void mouseReleased( MouseEvent e ) {}
@Override
public void mouseEntered( MouseEvent e ) {}
@Override
public void mouseExited( MouseEvent e ) {}
}
/**
* Processes audio metadata to update this panel's display values
*/
public class AudioMetadataProcessor implements Listener<AudioMetadata>
{
@Override
public void receive( AudioMetadata audioMetadata )
{
if( !mConfigured || audioMetadata.isUpdated() )
{
final Metadata from = audioMetadata.getMetadata( MetadataType.FROM_TALKGROUP );
final Metadata to = audioMetadata.getMetadata( MetadataType.TO );
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
mFrom.setText( from.getValue() );
updateAlias( mFromAlias, from.getAlias() );
mTo.setText( to.getValue() );
updateAlias( mToAlias, to.getAlias() );
}
} );
mConfigured = true;
}
}
}
@Override
public void settingChanged( Setting setting )
{
if( setting instanceof ColorSetting )
{
ColorSetting colorSetting = (ColorSetting)setting;
switch( colorSetting.getColorSettingName() )
{
case CHANNEL_STATE_LABEL_DECODER:
if( mFrom != null )
{
mFrom.setForeground( mLabelColor );
}
if( mFromAlias != null )
{
mFromAlias.setForeground( mLabelColor );
}
if( mTo != null )
{
mTo.setForeground( mLabelColor );
}
if( mToAlias != null )
{
mToAlias.setForeground( mLabelColor );
}
break;
case CHANNEL_STATE_LABEL_DETAILS:
if( mFromLabel != null )
{
mFromLabel.setForeground( mDetailsColor );
}
if( mToLabel != null )
{
mToLabel.setForeground( mDetailsColor );
}
break;
}
}
}
@Override
public void settingDeleted( Setting setting ) {}
}

30
src/audio/AudioEvent.java Normal file
View File

@@ -0,0 +1,30 @@
package audio;
public class AudioEvent
{
private Type mType;
private String mChannel;
public AudioEvent( Type event, String channel )
{
mType = event;
mChannel = channel;
}
public Type getType()
{
return mType;
}
public String getChannel()
{
return mChannel;
}
public enum Type
{
AUDIO_STARTED,
AUDIO_STOPPED,
AUDIO_CONFIGURATION_CHANGED;
}
}

View File

@@ -0,0 +1,16 @@
package audio;
public class AudioException extends Exception
{
private static final long serialVersionUID = 1L;
public AudioException( String description )
{
super( description );
}
public AudioException( String description, Throwable throwable )
{
super( description, throwable );
}
}

379
src/audio/AudioManager.java Normal file
View File

@@ -0,0 +1,379 @@
package audio;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.FloatControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import audio.metadata.AudioMetadata;
import audio.output.AudioOutput;
import audio.output.MonoAudioOutput;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
public class AudioManager implements Listener<AudioPacket>, IAudioController
{
private static final Logger mLog = LoggerFactory.getLogger( AudioManager.class );
public static final int AUDIO_TIMEOUT = 2000; //2 seconds
public static final int NO_SOURCE = 0;
private LinkedTransferQueue<AudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
private Map<Integer,AudioChannelAssignment> mChannelAssignments = new HashMap<>();
private int mLowestPrioritySource;
private List<AudioOutput> mAvailableAudioOutputs = new ArrayList<>();
private Map<String,AudioOutput> mChannelMap = new HashMap<>();
private List<String> mChannelNames = new ArrayList<>();
private AudioOutput mAudioOutput;
private boolean mMuted = false;
private Listener<AudioEvent> mConfigurationChangeListener;
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture<?> mProcessingTask;
/**
* Handles 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.
*/
public AudioManager( ThreadPoolManager manager )
{
mThreadPoolManager = manager;
mAudioOutput = new MonoAudioOutput();
mAvailableAudioOutputs.add( mAudioOutput );
mChannelNames.add( mAudioOutput.getChannelName() );
mChannelMap.put( mAudioOutput.getChannelName(), mAudioOutput );
mProcessingTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_PROCESSING, new AudioPacketProcessor(), 15,
TimeUnit.MILLISECONDS );
}
public void dispose()
{
if( mThreadPoolManager != null && mProcessingTask != null )
{
mThreadPoolManager.cancel( mProcessingTask );
}
mAudioPacketQueue.clear();
mThreadPoolManager = null;
mProcessingTask = null;
mAudioOutput.dispose();
}
@Override
public synchronized void receive( AudioPacket packet )
{
mAudioPacketQueue.add( packet );
}
public class AudioPacketProcessor implements Runnable
{
@Override
public void run()
{
if( !mMuted )
{
}
removeCompletedChannelAssignments();
if( mAudioPacketQueue != null )
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mAudioPacketQueue.drainTo( packets );
for( AudioPacket packet: packets )
{
int source = packet.getMetadata().getSource();
/* Existing channel assignment */
if( mChannelAssignments.containsKey( source ) )
{
mChannelAssignments.get( source ).getAudioOutput()
.receive( packet );
}
/* Start a new channel assignment */
else if( !mAvailableAudioOutputs.isEmpty() )
{
AudioOutput channel = mAvailableAudioOutputs.remove( 0 );
mChannelAssignments.put( packet.getMetadata().getSource(),
new AudioChannelAssignment( channel, packet.getMetadata().getPriority() ) );
updateLowestPriorityAssignment();
channel.receive( packet );
}
/* Override the lowest priority assignment */
else
{
if( mLowestPrioritySource > -1 )
{
int priority = packet.getMetadata().getPriority();
if( packet.getMetadata().isSelected() ||
priority < mChannelAssignments
.get( mLowestPrioritySource ).getPriority() )
{
AudioChannelAssignment assignment =
mChannelAssignments.remove( mLowestPrioritySource );
if( assignment != null )
{
assignment.setPriority( priority );
int overrideSource = packet.getMetadata().getSource();
mChannelAssignments.put( overrideSource, assignment );
mLowestPrioritySource = overrideSource;
assignment.getAudioOutput().receive( packet );
}
else
{
mLowestPrioritySource = -1;
}
}
}
}
}
}
}
}
/**
* Checks each audio channel assignment and removes any completed channels.
*/
private void removeCompletedChannelAssignments()
{
if( !mChannelAssignments.isEmpty() )
{
Iterator<Entry<Integer,AudioChannelAssignment>> it =
mChannelAssignments.entrySet().iterator();
Entry<Integer,AudioChannelAssignment> entry;
boolean removed = false;
while( it.hasNext() )
{
entry = it.next();
if( entry.getValue().isComplete() )
{
mAvailableAudioOutputs.add( entry.getValue().getAudioOutput() );
entry.getValue().dispose();
it.remove();
removed = true;
}
}
if( removed )
{
updateLowestPriorityAssignment();
}
}
}
/**
* Identifies the lowest priority channel assignment ... the one that can
* be overridden with a higher priority call
*/
private void updateLowestPriorityAssignment()
{
int lowestPriority = -1;
mLowestPrioritySource = -1;
for( int source: mChannelAssignments.keySet() )
{
AudioChannelAssignment assignment = mChannelAssignments.get( source );
if( assignment.getPriority() > lowestPriority )
{
lowestPriority = assignment.getPriority();
mLowestPrioritySource = source;
}
}
}
public class AudioChannelAssignment implements Listener<AudioEvent>
{
private static final long CALL_END_DELAY = 2000; //2 seconds
private AudioOutput mAudioOutput;
private int mPriority;
private long mAudioStoppedTime = Long.MAX_VALUE;
public AudioChannelAssignment( AudioOutput audioOutput, int priority )
{
mAudioOutput = audioOutput;
mPriority = priority;
mAudioOutput.addAudioEventListener( this );
}
public boolean isComplete()
{
return mAudioStoppedTime != Long.MAX_VALUE &&
mAudioStoppedTime + CALL_END_DELAY < System.currentTimeMillis();
}
public void dispose()
{
mAudioOutput.removeAudioEventListener( this );
mAudioOutput = null;
}
public AudioOutput getAudioOutput()
{
return mAudioOutput;
}
public int getPriority()
{
return mPriority;
}
public void setPriority( int priority )
{
mPriority = priority;
}
@Override
public void receive( AudioEvent event )
{
switch( event.getType() )
{
case AUDIO_STARTED:
mAudioStoppedTime = Long.MAX_VALUE;
break;
case AUDIO_STOPPED:
mAudioStoppedTime = System.currentTimeMillis();
break;
default:
break;
}
}
}
@Override
public List<String> getAudioChannels()
{
return mChannelNames;
}
@Override
public BooleanControl getMuteControl( String channel )
throws AudioException
{
if( mChannelMap.containsKey( channel ) )
{
return mChannelMap.get( channel ).getMuteControl();
}
throw new AudioException( "Channel [" + channel + "] is not a valid channel" );
}
@Override
public boolean hasMuteControl( String channel )
{
return mChannelMap.containsKey( channel ) &&
mChannelMap.get( channel ) != null &&
mChannelMap.get( channel ).getMuteControl() != null;
}
@Override
public FloatControl getGainControl( String channel ) throws AudioException
{
if( mChannelMap.containsKey( channel ) )
{
return mChannelMap.get( channel ).getGainControl();
}
throw new AudioException( "Channel [" + channel + "] is not a valid channel" );
}
@Override
public boolean hasGainControl( String channel )
{
return mChannelMap.containsKey( channel ) &&
mChannelMap.get( channel ) != null &&
mChannelMap.get( channel ).getGainControl() != null;
}
/**
* Sets the listener to receive audio events, specifically, audio
* configuration change events
*/
@Override
public void setConfigurationChangeListener( Listener<AudioEvent> listener )
{
mConfigurationChangeListener = listener;
}
@Override
public void addAudioEventListener( String channel, Listener<AudioEvent> listener )
throws AudioException
{
if( mChannelMap.containsKey( channel ) )
{
mChannelMap.get( channel ).addAudioEventListener( listener );
}
else
{
throw new AudioException( "Channel [" + channel + "] is not a valid "
+ "channel. Audio event listener was not registered" );
}
}
@Override
public void removeAudioEventListener( String channel, Listener<AudioEvent> listener )
{
if( mChannelMap.containsKey( channel ) )
{
mChannelMap.get( channel ).removeAudioEventListener( listener );
}
}
@Override
public void setAudioMetadataListener( String channel,
Listener<AudioMetadata> listener )
{
if( mChannelMap.containsKey( channel ) )
{
mChannelMap.get( channel ).setAudioMetadataListener( listener );
}
}
@Override
public void removeAudioMetadataListener( String channel )
{
if( mChannelMap.containsKey( channel ) )
{
mChannelMap.get( channel ).removeAudioMetadataListener();
}
}
}

191
src/audio/AudioModule.java Normal file
View File

@@ -0,0 +1,191 @@
package audio;
import module.Module;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import sample.real.IRealBufferListener;
import sample.real.RealBuffer;
import alias.Metadata;
import audio.metadata.AudioMetadata;
import audio.metadata.IMetadataListener;
import audio.squelch.ISquelchStateListener;
import audio.squelch.SquelchState;
import controller.channel.ChannelEvent;
import controller.channel.IChannelEventListener;
import dsp.filter.Filters;
import dsp.filter.dc.AveragingDCRemovalFilter_RB;
import dsp.filter.dc.DCRemovalFilter_RB;
import dsp.filter.fir.real.RealFIRFilter_R_R;
/**
* Provides packaging of demodulated audio sample buffers into audio packets for
* broadcast to registered audio packet listeners. Includes audio packet
* metadata in constructed audio packets.
*
* Incorporates audio squelch state listener to control if audio packets are
* broadcast or ignored.
*/
public class AudioModule extends Module implements IAudioPacketProvider,
IChannelEventListener,
IMetadataListener,
IRealBufferListener,
ISquelchStateListener,
Listener<RealBuffer>
{
protected static final Logger mLog = LoggerFactory.getLogger( AudioModule.class );
/* Determines responsiveness of DC filter to frequency changes */
private static final float DC_REMOVAL_RATIO = 0.000003f;
/* Provides a unique identifier for this audio module instance to use as a
* source identifier for all audio packets */
private static int UNIQUE_ID = 0;
private int mSourceID;
private AudioMetadata mAudioMetadata;
private ChannelEventListener mChannelEventListener = new ChannelEventListener();
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private SquelchState mSquelchState = SquelchState.SQUELCH;
private RealFIRFilter_R_R mAudioFilter = new RealFIRFilter_R_R(
Filters.FIR_BANDPASS_AUDIO_48KHZ.getCoefficients(), 2.0f );
private DCRemovalFilter_RB mDCRemovalFilter;
private Listener<AudioPacket> mAudioPacketListener;
private boolean mRemoveDC = false;
public AudioModule()
{
this( false );
}
public AudioModule( boolean removeDC )
{
mSourceID = ++UNIQUE_ID;
mAudioMetadata = new AudioMetadata( mSourceID );
mRemoveDC = removeDC;
if( mRemoveDC )
{
mDCRemovalFilter = new AveragingDCRemovalFilter_RB( DC_REMOVAL_RATIO );
mDCRemovalFilter.setListener( this );
}
}
@Override
public void dispose()
{
mSquelchStateListener = null;
mAudioPacketListener = null;
}
@Override
public void init()
{
mAudioMetadata.reset();
}
/**
* Processes demodulated audio samples into audio packets with current audio
* metadata and sends to the registered listener
*/
@Override
public void receive( RealBuffer buffer )
{
if( mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH )
{
float[] audio = buffer.getSamples();
mAudioFilter.filter( audio );
AudioPacket packet = new AudioPacket( audio, mAudioMetadata.copyOf() );
mAudioPacketListener.receive( packet );
}
}
@Override
public Listener<RealBuffer> getRealBufferListener()
{
if( mRemoveDC )
{
return mDCRemovalFilter;
}
return this;
}
@Override
public Listener<Metadata> getMetadataListener()
{
return mAudioMetadata;
}
@Override
public void setAudioPacketListener( Listener<AudioPacket> listener )
{
mAudioPacketListener = listener;
}
@Override
public void removeAudioPacketListener()
{
mAudioPacketListener = null;
}
@Override
public Listener<ChannelEvent> getChannelEventListener()
{
return mChannelEventListener;
}
@Override
public Listener<SquelchState> getSquelchStateListener()
{
return mSquelchStateListener;
}
/**
* Wrapper for channel event listener. Responds to channel state reset
* events to remove/cleanup current audio metadata
*/
public class ChannelEventListener implements Listener<ChannelEvent>
{
@Override
public void receive( ChannelEvent event )
{
switch( event.getEvent() )
{
case CHANNEL_STATE_RESET:
mAudioMetadata.reset();
break;
case CHANGE_SELECTED:
mAudioMetadata.setSelected( event.getChannel().isSelected() );
break;
default:
break;
}
}
}
/**
* Wrapper for squelch state listener
*/
public class SquelchStateListener implements Listener<SquelchState>
{
@Override
public void receive( SquelchState state )
{
mSquelchState = state;
}
}
}

View File

@@ -1,124 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package audio;
import java.nio.ByteBuffer;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.real.RealSampleListener;
import audio.inverted.AudioAdapter;
import audio.inverted.AudioType;
import audio.inverted.IAudioTypeListener;
import buffer.FloatSampleBufferAssembler;
public class AudioOutputImpl implements RealSampleListener,
SquelchListener, IAudioTypeListener, IAudioOutput
{
private final static Logger mLog =
LoggerFactory.getLogger( AudioOutputImpl.class );
private static final int sAUDIO_BLOCK_SIZE = 24000;
private static final AudioFormat sAUDIO_FORMAT =
new AudioFormat( 48000, //SampleRate
16, //Sample Size
1, //Channels
true, //Signed
false ); //Little Endian
private AudioAdapter mAdapter = new AudioAdapter( 48000 );
private FloatSampleBufferAssembler mBuffer =
new FloatSampleBufferAssembler( sAUDIO_BLOCK_SIZE );
private SourceDataLine mOutput;
private boolean mEnabled = false;
private SquelchState mSquelchState = SquelchState.SQUELCH;
public AudioOutputImpl( String channelName )
{
try
{
mOutput = AudioSystem.getSourceDataLine( sAUDIO_FORMAT );
mOutput.open( sAUDIO_FORMAT, (int)( sAUDIO_FORMAT.getSampleRate() ) );
mOutput.start();
}
catch ( LineUnavailableException e )
{
mLog.error( "AudioOutput - couldn't open audio speakers "
+ "for playback", e );
}
}
@Override
public void setSquelch( SquelchState state )
{
mSquelchState = state;
}
@Override
public void setAudioPlaybackEnabled( boolean enabled )
{
mEnabled = enabled;
}
@Override
public void dispose()
{
mOutput.stop();
mOutput.close();
mOutput = null;
}
@Override
public void receive( float sample )
{
float outputSample = mAdapter.get( sample );
if( mEnabled && mSquelchState == SquelchState.UNSQUELCH )
{
mBuffer.put( outputSample );
}
else
{
/* Use 0-valued samples when we're disabled or squelched */
mBuffer.put( 0.0f );
}
if( !mBuffer.hasRemaining() )
{
ByteBuffer buffer = mBuffer.get();
mOutput.write( buffer.array(), 0, buffer.array().length );
mBuffer.reset();
}
}
@Override
public void setAudioType( AudioType type )
{
mAdapter.setAudioType( type );
}
}

View File

@@ -1,25 +0,0 @@
package audio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AudioOutputManager
{
private final static Logger mLog =
LoggerFactory.getLogger( AudioOutputManager.class );
/**
* Handles 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.
*/
public AudioOutputManager()
{
}
public static void main( String[] args )
{
}
}

View File

@@ -1,29 +1,34 @@
package audio;
import audio.metadata.AudioMetadata;
public class AudioPacket
{
private String mSource;
private Type mType;
private byte[] mAudioData;
private int mPriority;
private float[] mAudioData;
private AudioMetadata mAudioMetadata;
public AudioPacket( String source, Type type )
public AudioPacket( Type type )
{
mSource = source;
mType = type;
}
public AudioPacket( String source, byte[] audio, int priority )
public AudioPacket( float[] audio, AudioMetadata metadata )
{
this( source, Type.AUDIO );
this( Type.AUDIO );
mAudioData = audio;
mPriority = priority;
mAudioMetadata = metadata;
}
public String getSource()
public AudioMetadata getMetadata()
{
return mSource;
return mAudioMetadata;
}
public void setMetadata( AudioMetadata metadata )
{
mAudioMetadata = metadata;
}
public Type getType()
@@ -31,16 +36,11 @@ public class AudioPacket
return mType;
}
public byte[] getAudioData()
public float[] getAudioData()
{
return mAudioData;
}
public int getPriority()
{
return mPriority;
}
public enum Type
{
AUDIO,

View File

@@ -0,0 +1,56 @@
package audio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
public class AudioPacketRateCounter implements Listener<AudioPacket>
{
private final static Logger mLog =
LoggerFactory.getLogger( AudioPacketRateCounter.class );
private Listener<AudioPacket> mListener;
private long mCount;
private long mStartTime = -1;
private String mName;
public AudioPacketRateCounter()
{
mName = "UNNAMED";
}
public AudioPacketRateCounter( String name )
{
mName = name;
}
@Override
public void receive( AudioPacket packet )
{
if( mStartTime == -1 )
{
mStartTime = System.currentTimeMillis();
}
mCount += packet.getAudioData().length;
long elapsed = System.currentTimeMillis() - mStartTime;
if( elapsed != 0 )
{
mLog.debug( "[" + mName + "] Audio Rate: " + ( mCount / elapsed ) + " kHz" );
}
if( mListener != null )
{
mListener.receive( packet );
}
}
public void setListener( Listener<AudioPacket> listener )
{
mListener = listener;
}
}

120
src/audio/AudioPanel.java Normal file
View File

@@ -0,0 +1,120 @@
package audio;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.sound.sampled.BooleanControl;
import javax.swing.JButton;
import javax.swing.JPanel;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import settings.SettingsManager;
public class AudioPanel extends JPanel implements Listener<AudioEvent>
{
private static final long serialVersionUID = 1L;
private static final Logger mLog = LoggerFactory.getLogger( AudioPanel.class );
private SettingsManager mSettingsManager;
private IAudioController mController;
private MuteButton mMuteButton;
public AudioPanel( SettingsManager settingsManager, IAudioController controller )
{
mSettingsManager = settingsManager;
mController = controller;
mMuteButton = new MuteButton( mController );
init();
}
private void init()
{
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[]0[]") );
setBackground( Color.BLACK );
add( mMuteButton, "span" );
for( String channel: mController.getAudioChannels() )
{
add( new AudioChannelPanel( mSettingsManager, mController, channel ), "span" );
}
}
public int getPreferredHeight()
{
return mMuteButton.getHeight() +
( mController.getAudioChannels().size() * 20 );
}
@Override
public void receive( AudioEvent event )
{
if( event.getType() == AudioEvent.Type.AUDIO_CONFIGURATION_CHANGED )
{
mLog.debug( "Audio configuration change detected" );
}
}
public class MuteButton extends JButton
{
private IAudioController mController;
private boolean mMuted = false;
public MuteButton( IAudioController controller )
{
super( "Mute Audio" );
setBackground( Color.GREEN );
mController = controller;
addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
mMuted = !mMuted;
for( String channel: mController.getAudioChannels() )
{
try
{
BooleanControl control = mController.getMuteControl( channel );
if( control != null )
{
control.setValue( mMuted );
}
}
catch ( AudioException e1 )
{
mLog.error( "Couldn't obtain mute control for "
+ "channel [" + channel + "] from audio manager", e1 );
}
}
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
MuteButton.this.setText( ( mMuted ? "Unmute Audio" : "Mute Audio" ) );
MuteButton.this.setBackground( ( mMuted ? Color.RED : Color.GREEN ) );
}
} );
}
} );
}
}
}

View File

@@ -0,0 +1,36 @@
package audio;
import java.util.List;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.FloatControl;
import sample.Listener;
import audio.metadata.AudioMetadata;
/**
* Interface for controlling one or more audio channels
*/
public interface IAudioController
{
public List<String> getAudioChannels();
public BooleanControl getMuteControl( String channel ) throws AudioException;
public boolean hasMuteControl( String channel );
public FloatControl getGainControl( String channel ) throws AudioException;
public boolean hasGainControl( String channel );
public void addAudioEventListener( String channel, Listener<AudioEvent> listener )
throws AudioException;
public void removeAudioEventListener( String channel, Listener<AudioEvent> listener );
public void setAudioMetadataListener( String channel, Listener<AudioMetadata> listener );
public void removeAudioMetadataListener( String channel );
public void setConfigurationChangeListener( Listener<AudioEvent> listener );
}

View File

@@ -1,29 +0,0 @@
package audio;
import audio.SquelchListener.SquelchState;
import audio.inverted.AudioType;
public interface IAudioOutput
{
/**
* Squelch / Unsquelch state for this channel
*/
public abstract void setSquelch( SquelchState state );
/**
* Audio playback defines if this channel is currently playing on the
* system speakers.
*/
public abstract void setAudioPlaybackEnabled( boolean enabled );
/**
* Dispose of any system resources for this audio output
*/
public abstract void dispose();
/**
* Sets the audio to normal or inverted output
*/
public abstract void setAudioType( AudioType type );
}

View File

@@ -0,0 +1,9 @@
package audio;
import sample.Listener;
public interface IAudioPacketProvider
{
public void setAudioPacketListener( Listener<AudioPacket> listener );
public void removeAudioPacketListener();
}

View File

@@ -88,7 +88,9 @@ public class AudioAdapter
default:
retVal = (float)( mPostInversionFilter.get(
mAudioHighPassFilter.get( sample ) *
mSineWaveGenerator.nextFloat() ) );
mSineWaveGenerator.getFloat() ) );
mSineWaveGenerator.rotate();
break;
}

View File

@@ -38,7 +38,7 @@ public class AudioInverter implements RealSampleListener
mSineWaveGenerator = new Oscillator( inversionFrequency, sampleRate );
mPostInversionLowPassFilter = new FloatFIRFilter(
Filters.FIRLP_55TAP_48000FS_3000FC.getCoefficients(), 1.04 );
Filters.FIRLP_55TAP_48000FS_3000FC.getCoefficients(), 1.04f );
mPostInversionLowPassFilter.setListener( new FilteredSampleProcessor() );
}
@@ -61,7 +61,9 @@ public class AudioInverter implements RealSampleListener
if( mPostInversionLowPassFilter != null )
{
mPostInversionLowPassFilter.receive(
sample * mSineWaveGenerator.nextFloat() );
sample * mSineWaveGenerator.getFloat() );
mSineWaveGenerator.rotate();
}
}

View File

@@ -0,0 +1,180 @@
package audio.metadata;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import alias.Metadata;
import alias.MetadataType;
public class AudioMetadata implements Listener<Metadata>
{
protected static final Logger mLog = LoggerFactory.getLogger( AudioMetadata.class );
private int mSource;
private int mPriority = 99;
private boolean mSelected = false;
private List<Metadata> mMetadata = new ArrayList<>();
private boolean mUpdated = false;
public AudioMetadata( int source )
{
mSource = source;
}
public AudioMetadata copyOf()
{
AudioMetadata copy = new AudioMetadata( mSource );
copy.mPriority = mPriority;
copy.mSelected = mSelected;
copy.mMetadata.addAll( mMetadata );
if( mUpdated )
{
copy.mUpdated = true;
mUpdated = false;
}
return copy;
}
/**
* Removes any accumulated temporal metadata and returns to a default set.
*/
public void reset()
{
Iterator<Metadata> it = mMetadata.iterator();
while( it.hasNext() )
{
if( it.next().isTemporal() )
{
it.remove();
}
}
mUpdated = true;
}
/**
* Indicates if this audio metadata contains updated information. The flag
* will only be set when new data is added and will be reset by a copyOf()
* method invocation.
*/
public boolean isUpdated()
{
return mUpdated;
}
/**
* Adds the metadata. Any temporal metadata will be removed when the
* dispose() method is invoked.
*/
@Override
public void receive( Metadata metadata )
{
if( metadata.isReset() )
{
reset();
}
else
{
mMetadata.add( metadata );
if( metadata.hasAlias() )
{
//TODO: assign priority from alias here
}
}
mUpdated = true;
}
/**
* Returns the complete set of metadata
*/
public List<Metadata> getMetadata()
{
return mMetadata;
}
/**
* Returns the first metadata object matching the type, or null if the
* metadata type is not found.
*/
public Metadata getMetadata( MetadataType type )
{
for( Metadata metadata: mMetadata )
{
if( metadata.getMetadataType() == type )
{
return metadata;
}
}
return null;
}
/**
* Source channel ID
*/
public void setSource( int source )
{
mSource = source;
mUpdated = true;
}
public int getSource()
{
return mSource;
}
/**
* Sets the priority of this audio packet in the range of 1 - Max Integer value
*
* @param priority (1 - Max Int)
*/
public void setPriority( int priority )
{
assert( 1 <= priority && priority <= Integer.MAX_VALUE );
mPriority = priority;
mUpdated = true;
}
/**
* Returns the priority of this audio packet or a priority of -1 if the
* audio source is currently selected, so that the priority of this packet
* is higher (lower?) than any other audio source
*
* @return - priority of audio packet where lower numbers are higher priority
*/
public int getPriority()
{
return mSelected ? -1 : mPriority;
}
/**
* Selected audio channel. A selected channel will override the priority
* level so that the audio can be listened to immediately.
*/
public void setSelected( boolean selected )
{
mSelected = selected;
mUpdated = true;
}
public boolean isSelected()
{
return mSelected;
}
}

View File

@@ -0,0 +1,9 @@
package audio.metadata;
import sample.Listener;
import alias.Metadata;
public interface IMetadataListener
{
public Listener<Metadata> getMetadataListener();
}

View File

@@ -0,0 +1,10 @@
package audio.metadata;
import sample.Listener;
import alias.Metadata;
public interface IMetadataProvider
{
public void setMetadataListener( Listener<Metadata> listener );
public void removeMetadataListener();
}

View File

@@ -0,0 +1,84 @@
package audio.output;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.FloatControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Broadcaster;
import sample.Listener;
import audio.AudioEvent;
import audio.AudioPacket;
import audio.metadata.AudioMetadata;
public abstract class AudioOutput implements Listener<AudioPacket>
{
private final static Logger mLog = LoggerFactory.getLogger( AudioOutput.class );
private AtomicBoolean mSquelched = new AtomicBoolean( false );
private Listener<AudioMetadata> mAudioMetadataListener;
protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
public void dispose()
{
mAudioEventBroadcaster.dispose();
mAudioEventBroadcaster = null;
mAudioMetadataListener = null;
}
public abstract String getChannelName();
public abstract BooleanControl getMuteControl();
public abstract boolean hasMuteControl();
public abstract FloatControl getGainControl();
public abstract boolean hasGainControl();
protected void broadcast( AudioMetadata metadata )
{
if( mAudioMetadataListener != null )
{
mAudioMetadataListener.receive( metadata );
}
}
public void setSquelched( boolean squelched )
{
mSquelched.set( squelched );
}
public boolean isSquelched()
{
return mSquelched.get();
}
/**
* Registers a single listener to receive audio start and audio stop events
*/
public void addAudioEventListener( Listener<AudioEvent> listener )
{
mAudioEventBroadcaster.addListener( listener );
}
public void removeAudioEventListener( Listener<AudioEvent> listener )
{
mAudioEventBroadcaster.removeListener( listener );
}
/**
* Registers a single listener to receive the audio metadata from each
* audio packet
*/
public void setAudioMetadataListener( Listener<AudioMetadata> listener )
{
mAudioMetadataListener = listener;
}
public void removeAudioMetadataListener()
{
mAudioMetadataListener = null;
}
}

View File

@@ -1,5 +1,8 @@
package audio;
package audio.output;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
@@ -9,20 +12,25 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import audio.AudioEvent;
import audio.AudioFormats;
import audio.AudioPacket;
import audio.AudioPacket.Type;
import controller.NamingThreadFactory;
/**
* Audio output with automatic flow control based on the availability of
* audio data packets.
*/
public class MonoAudioOutput implements Listener<AudioPacket>
public class MonoAudioOutput extends AudioOutput
{
private final static Logger mLog = LoggerFactory.getLogger( MonoAudioOutput.class );
@@ -43,15 +51,24 @@ public class MonoAudioOutput implements Listener<AudioPacket>
private ScheduledExecutorService mExecutorService;
private SourceDataLine mOutput;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private boolean mCanProcessAudio;
private AtomicBoolean mMuted = new AtomicBoolean( true );
private AtomicBoolean mSquelched = new AtomicBoolean( false );
private long mLastBufferReceived;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
public MonoAudioOutput()
{
try
super();
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED, getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED, getChannelName() );
try
{
mOutput = AudioSystem.getSourceDataLine(
AudioFormats.PCM_SIGNED_48KHZ_16BITS );
@@ -62,7 +79,14 @@ public class MonoAudioOutput implements Listener<AudioPacket>
mCanProcessAudio = true;
mExecutorService = Executors.newSingleThreadScheduledExecutor();
mGainControl = (FloatControl)mOutput.getControl(
FloatControl.Type.MASTER_GAIN );
mMuteControl = (BooleanControl)mOutput.getControl(
BooleanControl.Type.MUTE );
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (mono) output" ) );
/* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(),
@@ -76,19 +100,24 @@ public class MonoAudioOutput implements Listener<AudioPacket>
}
}
public void setMuted( boolean muted )
@Override
public String getChannelName()
{
mMuted.set( muted );
return "Mono";
}
public void setSquelched( boolean squelched )
/**
* Timestamp when most recent buffer was received
*/
public long getLastBufferReceived()
{
mSquelched.set( squelched );
return mLastBufferReceived;
}
public void dispose()
{
super.dispose();
mCanProcessAudio = false;
mBuffer.clear();
@@ -109,10 +138,13 @@ public class MonoAudioOutput implements Listener<AudioPacket>
{
if( mCanProcessAudio )
{
mLastBufferReceived = System.currentTimeMillis();
mBuffer.add( packet );
}
}
public class QueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
@@ -131,23 +163,39 @@ public class MonoAudioOutput implements Listener<AudioPacket>
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo( packets );
if( packets.size() > 0 )
{
for( AudioPacket packet: packets )
{
if( mCanProcessAudio )
{
if( mMuted.get() || mSquelched.get() )
if( isSquelched() )
{
checkStop();
}
else if( packet.getType() == Type.AUDIO )
{
mOutput.write( packet.getAudioData(), 0,
packet.getAudioData().length );
float[] samples = packet.getAudioData();
/* 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 ) );
}
mOutput.write( buffer.array(), 0,
buffer.array().length );
checkStart();
broadcast( packet.getMetadata() );
}
}
}
@@ -163,21 +211,50 @@ public class MonoAudioOutput implements Listener<AudioPacket>
private void checkStart()
{
if( mCanProcessAudio && !mMuted.get() && !mSquelched.get() &&
if( mCanProcessAudio && !isSquelched() &&
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
{
mOutput.start();
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
}
private void checkStop()
{
if( mCanProcessAudio && mOutput.isRunning() &&
( mOutput.available() >= STOP_THRESHOLD || mMuted.get() || mSquelched.get() ) )
( mOutput.available() >= STOP_THRESHOLD || isSquelched() ) )
{
mOutput.drain();
mOutput.stop();
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}
@Override
public BooleanControl getMuteControl()
{
return mMuteControl;
}
@Override
public boolean hasMuteControl()
{
return mMuteControl != null;
}
@Override
public FloatControl getGainControl()
{
return mGainControl;
}
@Override
public boolean hasGainControl()
{
return mGainControl != null;
}
}

View File

@@ -0,0 +1,8 @@
package audio.squelch;
import sample.Listener;
public interface ISquelchStateListener
{
public Listener<SquelchState> getSquelchStateListener();
}

View File

@@ -0,0 +1,9 @@
package audio.squelch;
import sample.Listener;
public interface ISquelchStateProvider
{
public void setSquelchStateListener( Listener<SquelchState> listener );
public void removeSquelchStateListener();
}

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
* Copyright (C) 2015 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
@@ -15,17 +15,26 @@
* 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;
public interface SquelchListener
package audio.squelch;
/**
* Provides squelch control based on presense of decoded audio samples.
*
* NOTE: this class is not yet fully implemented with audio sample processing
* and currently only provides user driven squelch control
*/
public class ManualSquelchController extends SquelchController
{
/**
* Squelch Control:
*/
public void setSquelch( SquelchState state );
public enum SquelchState
public ManualSquelchController()
{
UNSQUELCH, SQUELCH;
mSquelchMode = SquelchMode.NONE;
}
@Override
public void setSquelchMode( SquelchMode mode )
{
//TODO: add functionality
}
}

View File

@@ -0,0 +1,122 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 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.squelch;
import sample.Broadcaster;
import sample.Listener;
import sample.real.RealBuffer;
import sample.real.RealSampleListener;
import sample.real.RealToRealBufferAssembler;
/**
* Squelch control - receives single audio samples or real audio buffers and
* adjusts squelch state either based on the audio samples or based on external
* control inputs.
*
* Assembles received single audio samples into real sample buffers for
* consumption by an external audio processor, like the AudioPacketProcessor
* class.
*/
public abstract class SquelchController
implements Listener<RealBuffer>, RealSampleListener
{
private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>();
private Listener<RealBuffer> mAudioListener;
protected SquelchState mSquelchState = SquelchState.UNSQUELCH;
protected SquelchMode mSquelchMode = SquelchMode.MANUAL;
private RealToRealBufferAssembler mSampleAssembler = new RealToRealBufferAssembler( 512 );
public SquelchController()
{
mSampleAssembler.setListener( this );
}
@Override
public void receive( float sample )
{
mSampleAssembler.receive( sample );
}
@Override
public void receive( RealBuffer buffer )
{
if( mAudioListener != null )
{
mAudioListener.receive( buffer );
}
}
public abstract void setSquelchMode( SquelchMode mode );
public SquelchMode getSquelchMode()
{
return mSquelchMode;
}
public void setSquelchState( SquelchState state )
{
mSquelchState = state;
}
public SquelchState getSquelchState()
{
return mSquelchState;
}
public boolean isSquelched()
{
switch( mSquelchMode )
{
case AUTOMATIC:
case MANUAL:
return mSquelchState == SquelchState.SQUELCH;
case NONE:
return false;
default:
return false;
}
}
public void addSquelchStateListener( Listener<SquelchState> listener )
{
if( listener != null )
{
mSquelchStateBroadcaster.addListener( listener );
/* Send current squelch state to the listener */
listener.receive( mSquelchState );
}
}
public void removeSquelchStateListener( Listener<SquelchState> listener )
{
mSquelchStateBroadcaster.removeListener( listener );
}
public void setAudioListener( Listener<RealBuffer> listener )
{
mAudioListener = listener;
}
public void removeAudioListener( Listener<RealBuffer> listener )
{
mAudioListener = null;
}
}

View File

@@ -0,0 +1,25 @@
package audio.squelch;
/**
* Squelch state for a decoding channel. The state determines the handling of
* any audio packets processed by the channel state.
*/
public enum SquelchMode
{
/**
* Automatic: decoded message activity dictates squelch state and determines
* if audio is passed
*/
AUTOMATIC,
/**
* Manually applied squelch noise level threshold determines if audio is
* passed.
*/
MANUAL,
/**
* No Squelch - no squelch is applied and all audio is passed
*/
NONE;
}

View File

@@ -0,0 +1,7 @@
package audio.squelch;
public enum SquelchState
{
SQUELCH,
UNSQUELCH;
}

View File

@@ -0,0 +1,55 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 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.squelch;
public class TrunkingSquelchController extends SquelchController
{
private boolean mAllowSquelchOverride = true;
public TrunkingSquelchController()
{
}
/**
* Provides control over audio stream as directed by an external trunking
* channel state.
*
* Override automatic squelch control by setting the squelch mode to none.
*
* Disable automatic control override by setting allowManualOverride to false;
*
* Note: this control does not respond to squelch mode manual.
*/
public TrunkingSquelchController( boolean allowSquelchOverride )
{
mAllowSquelchOverride = allowSquelchOverride;
mSquelchMode = SquelchMode.AUTOMATIC;
}
@Override
public void setSquelchMode( SquelchMode mode )
{
if( mode == SquelchMode.AUTOMATIC ||
( mode == SquelchMode.NONE && mAllowSquelchOverride ) )
{
mSquelchMode = mode;
}
}
}