Resolves issue #23.

Adds audio output display and management options supporting mono and 
stereo output options.

Provides audio output supporting prioritization by talkgroup and/or 
channel selection.

Supports muting all channels, or muting individual channels and
individual audio gain/volume controls.
This commit is contained in:
denny
2015-10-03 17:50:03 -04:00
parent 20f5d06104
commit 3e05a9eb03
27 changed files with 1174 additions and 458 deletions

View File

@@ -5,8 +5,8 @@
<!-- version properties -->
<property name="major" value="0"/>
<property name="minor" value="1"/>
<property name="build" value="5"/>
<property name="minor" value="2"/>
<property name="build" value="0"/>
<!-- Build properties -->
<property name="classes" location="../classes"/>

BIN
images/audio_muted.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
images/audio_unmuted.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -3,17 +3,10 @@ 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 javax.swing.JSeparator;
import net.miginfocom.swing.MigLayout;
@@ -22,15 +15,16 @@ import org.slf4j.LoggerFactory;
import sample.Listener;
import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.Setting;
import settings.SettingChangeListener;
import settings.SettingsManager;
import settings.ColorSetting.ColorSettingName;
import alias.Alias;
import audio.AudioEvent.Type;
import audio.metadata.AudioMetadata;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
import audio.output.AudioOutput;
public class AudioChannelPanel extends JPanel
implements Listener<AudioEvent>, SettingChangeListener
@@ -44,85 +38,60 @@ public class AudioChannelPanel extends JPanel
private Color mDetailsColor;
private SettingsManager mSettingsManager;
private IAudioController mController;
private String mChannel;
private AudioOutput mAudioOutput;
private JLabel mChannelName = new JLabel( " " );
private JLabel mToLabel = new JLabel( "TO:" );
private JLabel mTo = new JLabel( "" );
private JLabel mToAlias = new JLabel( "" );
private JLabel mMutedLabel = new JLabel( " " );
private JLabel mFromLabel = new JLabel( "FROM:" );
private JLabel mFrom = new JLabel( "" );
private JLabel mFromAlias = new JLabel( "" );
private JLabel mChannelLabel;
private JLabel mChannelName;
private JLabel mMutedLabel;
private boolean mConfigured = false;
private BooleanControl mMuteControl;
private FloatControl mGainControl;
public AudioChannelPanel( SettingsManager settingsManager,
IAudioController controller,
String channel )
public AudioChannelPanel( SettingsManager settingsManager, AudioOutput audioOutput )
{
mSettingsManager = settingsManager;
mSettingsManager.addListener( this );
mAudioOutput = audioOutput;
if( mAudioOutput != null )
{
mAudioOutput.addAudioEventListener( this );
mAudioOutput.setAudioMetadataListener( new AudioMetadataProcessor() );
}
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[]" ) );
setLayout( new MigLayout( "insets 0 0 0 0", "[][right][][grow,fill]", "[]0[]0[grow,fill]" ) );
setBackground( Color.BLACK );
addMouseListener( new MouseSelectionListener() );
mChannelName = new JLabel( mAudioOutput != null ? mAudioOutput.getChannelName() : " " );
mChannelName.setFont( mFont );
mChannelName.setForeground( mDetailsColor );
add( mChannelName );
mToLabel.setFont( mFont );
mToLabel.setForeground( mLabelColor );
if( mAudioOutput != null )
{
mToLabel.setForeground( mLabelColor );
}
else
{
mToLabel.setForeground( getBackground() );
}
add( mToLabel );
mTo.setFont( mFont );
@@ -133,8 +102,19 @@ public class AudioChannelPanel extends JPanel
mToAlias.setForeground( mLabelColor );
add( mToAlias,"wrap" );
mMutedLabel.setFont( mFont );
mMutedLabel.setForeground( Color.RED );
add( mMutedLabel );
mFromLabel.setFont( mFont );
mFromLabel.setForeground( mLabelColor );
if( mAudioOutput != null )
{
mFromLabel.setForeground( mLabelColor );
}
else
{
mFromLabel.setForeground( getBackground() );
}
add( mFromLabel );
mFrom.setFont( mFont );
@@ -144,54 +124,40 @@ public class AudioChannelPanel extends JPanel
mFromAlias.setFont( mFont );
mFromAlias.setForeground( mLabelColor );
add( mFromAlias,"wrap" );
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" );
JSeparator separator = new JSeparator( JSeparator.HORIZONTAL );
separator.setBackground( Color.DARK_GRAY );
add( separator, "span,growx" );
}
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 )
switch( audioEvent.getType() )
{
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
case AUDIO_STOPPED:
EventQueue.invokeLater( new Runnable()
{
resetLabels();
}
} );
@Override
public void run()
{
resetLabels();
}
} );
break;
case AUDIO_MUTED:
case AUDIO_UNMUTED:
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
mMutedLabel.setText( mAudioOutput.isMuted() ? "M" : " " );
}
} );
break;
default:
break;
}
}
@@ -240,43 +206,6 @@ public class AudioChannelPanel extends JPanel
}
}
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

View File

@@ -0,0 +1,37 @@
package audio;
import java.awt.Color;
import java.util.List;
import javax.swing.JPanel;
import net.miginfocom.swing.MigLayout;
import settings.SettingsManager;
import audio.output.AudioOutput;
public class AudioChannelsPanel extends JPanel
{
private static final long serialVersionUID = 1L;
public AudioChannelsPanel( SettingsManager settingsManager,
IAudioController controller )
{
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[]0[]") );
setBackground( Color.BLACK );
List<AudioOutput> outputs = controller.getAudioOutputs();
for( AudioOutput audioOutput: controller.getAudioOutputs() )
{
add( new AudioChannelPanel( settingsManager, audioOutput ), "wrap" );
}
/* Add an empty channel panel so that the panel is sized appropriately
* for either a single channel or two channels */
if( outputs.size() == 1 )
{
add( new AudioChannelPanel( settingsManager, null ), "wrap" );
}
}
}

View File

@@ -23,8 +23,13 @@ public class AudioEvent
public enum Type
{
AUDIO_MUTED,
AUDIO_UNMUTED,
AUDIO_SQUELCHED,
AUDIO_UNSQUELCHED,
AUDIO_STARTED,
AUDIO_STOPPED,
AUDIO_CONFIGURATION_CHANGED;
AUDIO_CONFIGURATION_CHANGE_STARTED,
AUDIO_CONFIGURATION_CHANGE_COMPLETE;
}
}

View File

@@ -1,6 +1,9 @@
package audio;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line;
import javax.sound.sampled.SourceDataLine;
public class AudioFormats
{
@@ -18,8 +21,10 @@ public class AudioFormats
public static final int IMBE_FRAME_SIZE_BYTES = 18;
public static final int IMBE_SAMPLE_SIZE_BITS = 144;
public static final int ONE_CHANNEL = 1;
public static final int TWO_CHANNELS = 2;
public static final int PCM_SAMPLE_SIZE_BITS = 16;
public static final int PCM_FRAME_SIZE_BYTES = 2;
public static final int PCM_FRAME_SIZE_BYTES_MONO = 2;
public static final int PCM_FRAME_SIZE_BYTES_STEREO = 4;
public static AudioFormat IMBE_AUDIO_FORMAT =
new AudioFormat( IMBE_ENCODING,
@@ -35,16 +40,37 @@ public class AudioFormats
PCM_8KHZ_RATE,
PCM_SAMPLE_SIZE_BITS,
ONE_CHANNEL,
PCM_FRAME_SIZE_BYTES,
PCM_FRAME_SIZE_BYTES_MONO,
PCM_8KHZ_RATE,
LITTLE_ENDIAN );
public static AudioFormat PCM_SIGNED_48KHZ_16BITS =
public static AudioFormat PCM_SIGNED_48KHZ_16BITS_MONO =
new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
PCM_48KHZ_RATE,
PCM_SAMPLE_SIZE_BITS,
ONE_CHANNEL,
PCM_FRAME_SIZE_BYTES,
PCM_FRAME_SIZE_BYTES_MONO,
PCM_48KHZ_RATE,
LITTLE_ENDIAN );
public static AudioFormat PCM_SIGNED_48KHZ_16BITS_STEREO =
new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
PCM_48KHZ_RATE,
PCM_SAMPLE_SIZE_BITS,
TWO_CHANNELS,
PCM_FRAME_SIZE_BYTES_STEREO,
PCM_48KHZ_RATE,
LITTLE_ENDIAN );
/**
* Source Data Line Info for a 48 kHz, 16-bits signed PCM one channel
*/
public static final Line.Info MONO_SOURCE_DATALINE_INFO =
new DataLine.Info( SourceDataLine.class, PCM_SIGNED_48KHZ_16BITS_MONO );
/**
* Source Data Line Info for a 48 kHz, 16-bits signed PCM two channels
*/
public static final Line.Info STEREO_SOURCE_DATALINE_INFO =
new DataLine.Info( SourceDataLine.class, PCM_SIGNED_48KHZ_16BITS_STEREO );
}

View File

@@ -1,6 +1,8 @@
package audio;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -10,16 +12,20 @@ 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 javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Mixer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Broadcaster;
import sample.Listener;
import audio.metadata.AudioMetadata;
import source.mixer.MixerChannel;
import source.mixer.MixerChannelConfiguration;
import audio.AudioEvent.Type;
import audio.output.AudioOutput;
import audio.output.MonoAudioOutput;
import audio.output.StereoAudioOutput;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
@@ -29,23 +35,30 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
public static final int AUDIO_TIMEOUT = 2000; //2 seconds
public static final int NO_SOURCE = 0;
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null );
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null );
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 Map<String,AudioOutput> mAudioOutputMap = new HashMap<>();
private List<String> mChannelNames = new ArrayList<>();
private AudioOutput mAudioOutput;
private Listener<AudioEvent> mConfigurationChangeListener;
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture<?> mProcessingTask;
private MixerChannelConfiguration mMixerChannel;
/**
* Handles all audio produced by the decoding channels and routes audio
* Processes all audio produced by the decoding channels and routes audio
* packets to any combination of outputs based on any alias audio routing
* options specified by the user.
*/
@@ -53,14 +66,24 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
{
mThreadPoolManager = manager;
mAudioOutput = new MonoAudioOutput();
mAvailableAudioOutputs.add( mAudioOutput );
mChannelNames.add( mAudioOutput.getChannelName() );
mChannelMap.put( mAudioOutput.getChannelName(), mAudioOutput );
//TODO: save audio system preferences to the settings manager
mProcessingTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_PROCESSING, new AudioPacketProcessor(), 15,
TimeUnit.MILLISECONDS );
/* Use the system default mixer and mono channel as default startup */
Mixer mixer = AudioSystem.getMixer( null );
if( mixer != null )
{
mMixerChannel = new MixerChannelConfiguration( mixer, MixerChannel.MONO );
try
{
setMixerChannelConfiguration( mMixerChannel );
}
catch ( AudioException e )
{
mLog.error( "Couldn't create an audio output device", e );
}
}
}
public void dispose()
@@ -74,8 +97,6 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mThreadPoolManager = null;
mProcessingTask = null;
mAudioOutput.dispose();
}
@Override
@@ -221,6 +242,14 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
}
}
public void endAllAudioAssignments()
{
for( AudioOutput output: mAudioOutputMap.values() )
{
output.receive( new AudioPacket( AudioPacket.Type.END, null ) );
}
}
public class AudioChannelAssignment implements Listener<AudioEvent>
{
private static final long CALL_END_DELAY = 2000; //2 seconds
@@ -280,102 +309,91 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
}
}
}
@Override
public List<String> getAudioChannels()
public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException
{
return mChannelNames;
}
@Override
public BooleanControl getMuteControl( String channel )
throws AudioException
{
if( mChannelMap.containsKey( channel ) )
if( entry != null &&
( entry.getMixerChannel() == MixerChannel.MONO ||
entry.getMixerChannel() == MixerChannel.STEREO ) )
{
return mChannelMap.get( channel ).getMuteControl();
}
mControllerBroadcaster.broadcast( CONFIGURATION_CHANGE_STARTED );
if( mThreadPoolManager != null && mProcessingTask != null )
{
mThreadPoolManager.cancel( mProcessingTask );
}
throw new AudioException( "Channel [" + channel + "] is not a valid channel" );
}
endAllAudioAssignments();
mAvailableAudioOutputs.clear();
mChannelNames.clear();
mAudioOutputMap.clear();
@Override
public boolean hasMuteControl( String channel )
{
return mChannelMap.containsKey( channel ) &&
mChannelMap.get( channel ) != null &&
mChannelMap.get( channel ).getMuteControl() != null;
}
switch( entry.getMixerChannel() )
{
case MONO:
AudioOutput mono = new MonoAudioOutput( entry.getMixer() );
mAvailableAudioOutputs.add( mono );
mChannelNames.add( mono.getChannelName() );
mAudioOutputMap.put( mono.getChannelName(), mono );
break;
case STEREO:
AudioOutput left = new StereoAudioOutput( entry.getMixer(), MixerChannel.LEFT );
mAvailableAudioOutputs.add( left );
mChannelNames.add( left.getChannelName() );
mAudioOutputMap.put( left.getChannelName(), left );
AudioOutput right = new StereoAudioOutput( entry.getMixer(), MixerChannel.RIGHT );
mAvailableAudioOutputs.add( right );
mChannelNames.add( right.getChannelName() );
mAudioOutputMap.put( right.getChannelName(), right );
break;
default:
throw new AudioException( "Unsupported mixer channel "
+ "configuration: " + entry.getMixerChannel() );
}
@Override
public FloatControl getGainControl( String channel ) throws AudioException
{
if( mChannelMap.containsKey( channel ) )
{
return mChannelMap.get( channel ).getGainControl();
}
mProcessingTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_PROCESSING, new AudioPacketProcessor(), 15,
TimeUnit.MILLISECONDS );
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" );
mControllerBroadcaster.broadcast( CONFIGURATION_CHANGE_COMPLETE );
}
}
@Override
public void removeAudioEventListener( String channel, Listener<AudioEvent> listener )
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException
{
if( mChannelMap.containsKey( channel ) )
{
mChannelMap.get( channel ).removeAudioEventListener( listener );
}
return mMixerChannel;
}
@Override
public void setAudioMetadataListener( String channel,
Listener<AudioMetadata> listener )
public List<AudioOutput> getAudioOutputs()
{
if( mChannelMap.containsKey( channel ) )
List<AudioOutput> outputs = new ArrayList<>( mAudioOutputMap.values() );
Collections.sort( outputs, new Comparator<AudioOutput>()
{
mChannelMap.get( channel ).setAudioMetadataListener( listener );
}
@Override
public int compare( AudioOutput first, AudioOutput second )
{
return first.getChannelName().compareTo( second.getChannelName() );
}
} );
return outputs;
}
@Override
public void removeAudioMetadataListener( String channel )
public void addControllerListener( Listener<AudioEvent> listener )
{
if( mChannelMap.containsKey( channel ) )
{
mChannelMap.get( channel ).removeAudioMetadataListener();
}
mControllerBroadcaster.addListener( listener );
}
@Override
public void removeControllerListener( Listener<AudioEvent> listener )
{
mControllerBroadcaster.removeListener( listener );
}
}

View File

@@ -4,10 +4,21 @@ import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.FloatControl;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import net.miginfocom.swing.MigLayout;
@@ -16,68 +27,329 @@ import org.slf4j.LoggerFactory;
import sample.Listener;
import settings.SettingsManager;
import source.mixer.MixerChannelConfiguration;
import source.mixer.MixerManager;
import audio.output.AudioOutput;
public class AudioPanel extends JPanel implements Listener<AudioEvent>
{
private static final long serialVersionUID = 1L;
private static final Logger mLog = LoggerFactory.getLogger( AudioPanel.class );
private static ImageIcon MUTED_ICON = SettingsManager.getScaledIcon(
new ImageIcon( "images/audio_muted.png" ), 38 );
private static ImageIcon UNMUTED_ICON = SettingsManager.getScaledIcon(
new ImageIcon( "images/audio_unmuted.png" ), 38 );
private SettingsManager mSettingsManager;
private IAudioController mController;
private MuteButton mMuteButton;
private JButton mMuteButton;
private AudioChannelsPanel mAudioChannelsPanel;
public AudioPanel( SettingsManager settingsManager, IAudioController controller )
{
mSettingsManager = settingsManager;
mController = controller;
mMuteButton = new MuteButton( mController );
mController.addControllerListener( this );
init();
}
private void init()
{
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[]0[]") );
setLayout( new MigLayout( "insets 0 0 0 0", "[]0[grow,fill]", "[fill]0[]") );
setBackground( Color.BLACK );
add( mMuteButton, "span" );
mMuteButton = new MuteButton();
mMuteButton.setBackground( getBackground() );
add( mMuteButton );
for( String channel: mController.getAudioChannels() )
{
add( new AudioChannelPanel( mSettingsManager, mController, channel ), "span" );
}
mAudioChannelsPanel = new AudioChannelsPanel( mSettingsManager, mController );
add( mAudioChannelsPanel );
addMouseListener( new MouseSelectionListener() );
}
public int getPreferredHeight()
{
return mMuteButton.getHeight() +
( mController.getAudioChannels().size() * 20 );
}
@Override
public void receive( AudioEvent event )
{
if( event.getType() == AudioEvent.Type.AUDIO_CONFIGURATION_CHANGED )
switch( event.getType() )
{
mLog.debug( "Audio configuration change detected" );
case AUDIO_CONFIGURATION_CHANGE_STARTED:
break;
case AUDIO_CONFIGURATION_CHANGE_COMPLETE:
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
remove( mAudioChannelsPanel );
mAudioChannelsPanel = new AudioChannelsPanel(
mSettingsManager, mController );
add( mAudioChannelsPanel );
mAudioChannelsPanel.repaint();
revalidate();
repaint();
}
} );
break;
default:
break;
}
}
/**
* Audio output mute control menu item.
*/
public class AudioOutputMuteItem extends JMenuItem
{
private static final long serialVersionUID = 1L;
private AudioOutput mAudioOutput;
public AudioOutputMuteItem( AudioOutput audioOutput )
{
super( audioOutput.isMuted() ? "Unmute" : "Mute" );
mAudioOutput = audioOutput;
addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
mAudioOutput.setMuted( !mAudioOutput.isMuted() );
}
} );
}
}
/**
* Mouse listener
*/
public class MouseSelectionListener implements MouseListener
{
@Override
public void mouseClicked( MouseEvent event )
{
if( SwingUtilities.isRightMouseButton( event ) )
{
JPopupMenu popup = new JPopupMenu();
/* Audio mixer/output selection menus */
JMenu outputMenu = new JMenu( "Audio Output" );
MixerChannelConfiguration[] mixerConfigurations =
MixerManager.getInstance().getOutputMixers();
for( MixerChannelConfiguration mixerConfig: mixerConfigurations )
{
MixerSelectionItem mixerItem = new MixerSelectionItem( mixerConfig );
try
{
MixerChannelConfiguration current = mController.getMixerChannelConfiguration();
if( current != null && current.equals( mixerConfig ) )
{
mixerItem.setSelected( true );
}
}
catch( AudioException e )
{
mLog.debug( "Error while detecting current mixer "
+ "channel configuration", e );
}
outputMenu.add( mixerItem );
}
popup.add( outputMenu );
/* Audio output mute and volume control */
for( AudioOutput output: mController.getAudioOutputs() )
{
JMenu menu = new JMenu( "Channel: " + output.getChannelName() );
menu.add( new AudioOutputMuteItem( output ) );
if( output.hasGainControl() )
{
JMenu volume = new JMenu( "Volume" );
volume.add( new VolumeSlider( output.getGainControl() ) );
menu.add( volume );
}
popup.add( menu );
}
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 ) {}
}
/**
* Audio volume (gain) adjustment slider control
*/
public class VolumeSlider extends JSlider
{
private static final long serialVersionUID = 1L;
private FloatControl mFloatControl;
public VolumeSlider( FloatControl control )
{
super( 0, 100, 0 );
setMajorTickSpacing( 25 );
setMinorTickSpacing( 5 );
setPaintTicks( true );
setPaintLabels( true );
mFloatControl = control;
setValue( getIntegerValue( mFloatControl.getValue() ) );
addChangeListener( new ChangeListener()
{
@Override
public void stateChanged( ChangeEvent event )
{
mFloatControl.shift( mFloatControl.getValue(),
getFloatValue( VolumeSlider.this.getValue() ),
1000 );
}
} );
addMouseListener( new MouseListener()
{
@Override
public void mouseClicked( MouseEvent event )
{
if( event.getClickCount() == 2 )
{
VolumeSlider.this.setValue( 50 );
}
}
public void mouseReleased( MouseEvent arg0 ) {}
public void mousePressed( MouseEvent arg0 ) {}
public void mouseExited( MouseEvent arg0 ) {}
public void mouseEntered( MouseEvent arg0 ) {}
} );
}
/**
* Converts the integer value to a floating point value to use in the
* float control. Assumes an integer value of 50 is the 0.0 dB mid
* point (ie no gain ) value.
*/
private int getIntegerValue( float value )
{
if( value == 0.0f )
{
return 50;
}
else if( value < 0.0f )
{
float ratio = value / mFloatControl.getMinimum();
return 50 - (int)( ratio * 50.0f );
}
else
{
float ratio = value / mFloatControl.getMaximum();
return 50 + (int)( ratio * 50.0f );
}
}
private float getFloatValue( int value )
{
if( value == 50 )
{
return 0.0f;
}
else if( value < 50 )
{
return (float)( 50 - value ) / 50.0f * mFloatControl.getMinimum();
}
else
{
return (float)( value - 50 ) / 50.0f * mFloatControl.getMaximum();
}
}
}
/**
* Mixer/Channel configuration selection item
*/
public class MixerSelectionItem extends JMenuItem
{
private static final long serialVersionUID = 1L;
private MixerChannelConfiguration mConfiguration;
public MixerSelectionItem( MixerChannelConfiguration config )
{
super( config.toString() );
mConfiguration = config;
addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
try
{
mController.setMixerChannelConfiguration( mConfiguration );
}
catch ( AudioException e1 )
{
mLog.error( "Couldn't set mixer channel configuration "
+ "to: " + mConfiguration.toString() );
JOptionPane.showMessageDialog( MixerSelectionItem.this,
"Couldn't set [" + mConfiguration.toString() +
"] as the audio output device" );
}
}
} );
}
}
/**
* Mute button to mute all audio output channels exposed by the audio
* controller
*/
public class MuteButton extends JButton
{
private IAudioController mController;
private static final long serialVersionUID = 1L;
private boolean mMuted = false;
public MuteButton( IAudioController controller )
public MuteButton()
{
super( "Mute Audio" );
setBackground( Color.GREEN );
mController = controller;
setIcon( UNMUTED_ICON );
setBorderPainted( false );
addActionListener( new ActionListener()
{
@@ -86,31 +358,17 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
{
mMuted = !mMuted;
for( String channel: mController.getAudioChannels() )
for( AudioOutput output: mController.getAudioOutputs() )
{
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 );
}
output.setMuted( mMuted );
}
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
MuteButton.this.setText( ( mMuted ? "Unmute Audio" : "Mute Audio" ) );
MuteButton.this.setBackground( ( mMuted ? Color.RED : Color.GREEN ) );
setIcon( mMuted ? MUTED_ICON : UNMUTED_ICON );
}
} );
}

View File

@@ -2,35 +2,23 @@ package audio;
import java.util.List;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.FloatControl;
import sample.Listener;
import audio.metadata.AudioMetadata;
import source.mixer.MixerChannelConfiguration;
import audio.output.AudioOutput;
/**
* Interface for controlling one or more audio channels
*/
public interface IAudioController
{
public List<String> getAudioChannels();
/* Current Mixer and MixerChannel configuration */
public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException;
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException;
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 );
/* Audio Output(s) */
public List<AudioOutput> getAudioOutputs();
/* Controller Audio Event Listener */
public void addControllerListener( Listener<AudioEvent> listener );
public void removeControllerListener( Listener<AudioEvent> listener );
}

View File

@@ -1,5 +1,7 @@
package audio.output;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.BooleanControl;
@@ -11,6 +13,7 @@ import org.slf4j.LoggerFactory;
import sample.Broadcaster;
import sample.Listener;
import audio.AudioEvent;
import audio.AudioEvent.Type;
import audio.AudioPacket;
import audio.metadata.AudioMetadata;
@@ -20,10 +23,24 @@ public abstract class AudioOutput implements Listener<AudioPacket>
private AtomicBoolean mSquelched = new AtomicBoolean( false );
private Listener<AudioMetadata> mAudioMetadataListener;
protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
protected LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
protected ScheduledExecutorService mExecutorService;
protected boolean mCanProcessAudio;
protected long mLastBufferReceived;
public void dispose()
{
mCanProcessAudio = false;
mBuffer.clear();
if( mExecutorService != null )
{
mExecutorService.shutdown();
}
mAudioEventBroadcaster.dispose();
mAudioEventBroadcaster = null;
mAudioMetadataListener = null;
@@ -31,8 +48,8 @@ public abstract class AudioOutput implements Listener<AudioPacket>
public abstract String getChannelName();
public abstract BooleanControl getMuteControl();
public abstract boolean hasMuteControl();
public abstract void setMuted( boolean muted );
public abstract boolean isMuted();
public abstract FloatControl getGainControl();
public abstract boolean hasGainControl();
@@ -45,9 +62,17 @@ public abstract class AudioOutput implements Listener<AudioPacket>
}
}
protected void broadcast( AudioEvent audioEvent )
{
mAudioEventBroadcaster.broadcast( audioEvent );
}
public void setSquelched( boolean squelched )
{
mSquelched.set( squelched );
broadcast( new AudioEvent( squelched ? Type.AUDIO_SQUELCHED :
Type.AUDIO_UNSQUELCHED, getChannelName() ) );
}
public boolean isSquelched()
@@ -81,4 +106,23 @@ public abstract class AudioOutput implements Listener<AudioPacket>
{
mAudioMetadataListener = null;
}
/**
* Timestamp when most recent buffer was received
*/
public long getLastBufferReceived()
{
return mLastBufferReceived;
}
@Override
public void receive( AudioPacket packet )
{
if( mCanProcessAudio )
{
mLastBufferReceived = System.currentTimeMillis();
mBuffer.add( packet );
}
}
}

View File

@@ -6,20 +6,19 @@ import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
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.Mixer;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import source.mixer.MixerChannel;
import audio.AudioEvent;
import audio.AudioFormats;
import audio.AudioPacket;
@@ -27,14 +26,14 @@ import audio.AudioPacket.Type;
import controller.NamingThreadFactory;
/**
* Audio output with automatic flow control based on the availability of
* Mono Audio output with automatic flow control based on the availability of
* audio data packets.
*/
public class MonoAudioOutput extends AudioOutput
{
private final static Logger mLog = LoggerFactory.getLogger( MonoAudioOutput.class );
/* Output line buffer size is set at one second of audio at 48 kHz and
/* 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;
@@ -42,40 +41,34 @@ public class MonoAudioOutput extends AudioOutput
* 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 = 48000 / 25;
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 LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
private ScheduledExecutorService mExecutorService;
private SourceDataLine mOutput;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private boolean mCanProcessAudio;
private long mLastBufferReceived;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
public MonoAudioOutput()
public MonoAudioOutput( Mixer mixer )
{
super();
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED, getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED, getChannelName() );
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
getChannelName() );
try
{
mOutput = AudioSystem.getSourceDataLine(
AudioFormats.PCM_SIGNED_48KHZ_16BITS );
mOutput = (SourceDataLine)mixer.getLine(
AudioFormats.MONO_SOURCE_DATALINE_INFO );
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS, BUFFER_SIZE );
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE );
mCanProcessAudio = true;
@@ -103,47 +96,18 @@ public class MonoAudioOutput extends AudioOutput
@Override
public String getChannelName()
{
return "Mono";
}
/**
* Timestamp when most recent buffer was received
*/
public long getLastBufferReceived()
{
return mLastBufferReceived;
return MixerChannel.MONO.getLabel();
}
public void dispose()
{
super.dispose();
mCanProcessAudio = false;
mBuffer.clear();
if( mExecutorService != null )
{
mExecutorService.shutdown();
}
if( mOutput != null )
{
mOutput.close();
}
}
@Override
public void receive( AudioPacket packet )
{
if( mCanProcessAudio )
{
mLastBufferReceived = System.currentTimeMillis();
mBuffer.add( packet );
}
}
public class QueueProcessor implements Runnable
{
@@ -233,17 +197,27 @@ public class MonoAudioOutput extends AudioOutput
}
}
@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 BooleanControl getMuteControl()
public boolean isMuted()
{
return mMuteControl;
}
@Override
public boolean hasMuteControl()
{
return mMuteControl != null;
if( mMuteControl != null )
{
return mMuteControl.getValue();
}
return false;
}
@Override

View File

@@ -0,0 +1,259 @@
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;
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.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import source.mixer.MixerChannel;
import audio.AudioEvent;
import audio.AudioFormats;
import audio.AudioPacket;
import audio.AudioPacket.Type;
import controller.NamingThreadFactory;
/**
* Mono Audio output in stereo format with automatic flow control based on the
* availability of audio data packets.
*/
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;
/* 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 MixerChannel mMixerChannel;
public StereoAudioOutput( Mixer mixer, MixerChannel channel )
{
super();
assert( channel == MixerChannel.LEFT || channel == MixerChannel.RIGHT );
mMixerChannel = channel;
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
getChannelName() );
try
{
mOutput = (SourceDataLine)mixer.getLine(
AudioFormats.STEREO_SOURCE_DATALINE_INFO );
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, BUFFER_SIZE );
mCanProcessAudio = true;
mGainControl = (FloatControl)mOutput.getControl(
FloatControl.Type.MASTER_GAIN );
mMuteControl = (BooleanControl)mOutput.getControl(
BooleanControl.Type.MUTE );
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (stereo) output" ) );
/* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(),
0, 40, TimeUnit.MILLISECONDS );
}
}
catch ( LineUnavailableException e )
{
mLog.error( "Couldn't obtain source data line for 48kHz PCM stereo "
+ "audio output" );
}
}
@Override
public String getChannelName()
{
return mMixerChannel.getLabel();
}
public void dispose()
{
super.dispose();
if( mOutput != null )
{
mOutput.close();
}
}
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 ) );
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()
{
/* 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>();
mBuffer.drainTo( packets );
if( packets.size() > 0 )
{
for( AudioPacket packet: packets )
{
if( mCanProcessAudio )
{
if( isSquelched() )
{
checkStop();
}
else if( packet.getType() == Type.AUDIO )
{
ByteBuffer buffer = convert(
packet.getAudioBuffer().getSamples() );
mOutput.write( buffer.array(), 0,
buffer.array().length );
checkStart();
broadcast( packet.getAudioMetadata() );
}
}
}
}
else
{
checkStop();
}
mProcessing.set( false );
}
}
private void checkStart()
{
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 || isSquelched() ) )
{
mOutput.drain();
mOutput.stop();
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}
@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;
}
}

View File

@@ -154,7 +154,6 @@ public class ControllerPanel extends JPanel
AudioPanel audioPanel = new AudioPanel( mResourceManager.getSettingsManager(),
mResourceManager.getAudioManager() );
audioPanel.setPreferredSize( new Dimension( 200, audioPanel.getPreferredHeight() ) );
JideSplitPane audioChannelListSplit = new JideSplitPane( JideSplitPane.VERTICAL_SPLIT );
audioChannelListSplit.setDividerSize( 5 );

View File

@@ -824,7 +824,7 @@ public class Channel extends Configuration
List<Module> modules = DecoderFactory.getModules( mChannelType,
mResourceManager, mDecodeConfiguration, mRecordConfiguration,
mAuxDecodeConfiguration, aliasList, mSystem,
mSite );
mSite, mName );
mProcessingChain.addModules( modules );

View File

@@ -6,6 +6,7 @@ import java.awt.event.ActionListener;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JSeparator;
import module.decode.event.ActivitySummaryFrame;
@@ -101,15 +102,22 @@ public class ChannelUtils
@Override
public void actionPerformed( ActionEvent e )
{
/* Disable the channel */
channel.setEnabled( DISABLED, BROADCAST_CHANGE );
/* Broadcast channel deleted event */
channel.fireChannelEvent( Event.CHANNEL_DELETED );
if( playlistManager != null )
int response = JOptionPane.showConfirmDialog( anchor,
"Do you want to delete channel " + channel.getName() + "?",
"Are you sure?", JOptionPane.YES_NO_CANCEL_OPTION );
if( response == JOptionPane.YES_OPTION )
{
playlistManager.save();
/* Disable the channel */
channel.setEnabled( DISABLED, BROADCAST_CHANGE );
/* Broadcast channel deleted event */
channel.fireChannelEvent( Event.CHANNEL_DELETED );
if( playlistManager != null )
{
playlistManager.save();
}
}
}
} );

View File

@@ -109,7 +109,8 @@ public class DecoderViewFrame extends JInternalFrame
auxConfig,
aliasList,
new System( "Instrumented" ),
new Site( "Instrumented" ) );
new Site( "Instrumented" ),
"Channel Name" );
mProcessingChain.addModules( modules );

View File

@@ -135,10 +135,11 @@ public class DecoderFactory
AuxDecodeConfiguration auxConfig,
AliasList aliasList,
System system,
Site site )
Site site,
String channelName )
{
List<Module> modules = getPrimaryModules( channelType, decodeConfig,
recordConfig, aliasList, system, site, resourceManager );
recordConfig, aliasList, system, site, channelName, resourceManager );
modules.addAll( getAuxiliaryDecoders( auxConfig, aliasList ) );
@@ -176,6 +177,7 @@ public class DecoderFactory
AliasList aliasList,
System system,
Site site,
String channelName,
ResourceManager resourceManager )
{
List<Module> modules = new ArrayList<Module>();
@@ -191,12 +193,12 @@ public class DecoderFactory
{
case AM:
modules.add( new AMDecoder( decodeConfig ) );
modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.AM ) );
modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.AM, channelName ) );
break;
case NBFM:
modules.add( new AudioModule( recordAudio, NO_REMOVE_DC ) );
modules.add( new NBFMDecoder( decodeConfig ) );
modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.NBFM ) );
modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.NBFM, channelName ) );
modules.add( getFMDemodulator( decodeConfig, pass, stop, REMOVE_DC ) );
break;
case LTR_STANDARD:

View File

@@ -180,7 +180,7 @@ public class MPT1327DecoderState extends DecoderState
}
mSite = mpt.getSiteID();
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.CONTROL ) );
broadcast( new DecoderStateEvent( this, Event.START, State.CONTROL ) );
break;
case GTC:
String from = mpt.getFromID();

View File

@@ -133,29 +133,37 @@ public class P25AudioModule extends Module implements Listener<Message>,
Class temp = Class.forName( "jmbe.JMBEAudioLibrary" );
library = (AudioConversionLibrary)temp.newInstance();
mAudioConverter = library.getAudioConverter( IMBE_CODEC,
AudioFormats.PCM_SIGNED_48KHZ_16BITS );
if( mAudioConverter != null )
if( ( library.getMajorVersion() == 0 && library.getMinorVersion() >= 3 ) ||
library.getMajorVersion() > 0 )
{
mCanConvertAudio = true;
mAudioConverter = library.getAudioConverter( IMBE_CODEC,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO );
if( !mLibraryLoadStatusLogged )
if( mAudioConverter != null )
{
mLog.info( "JMBE audio conversion library successfully loaded"
+ " - P25 audio will be available" );
mCanConvertAudio = true;
mLibraryLoadStatusLogged = true;
if( !mLibraryLoadStatusLogged )
{
mLog.info( "JMBE audio conversion library successfully loaded"
+ " - P25 audio will be available" );
mLibraryLoadStatusLogged = true;
}
}
else
{
if( !mLibraryLoadStatusLogged )
{
mLog.info( "JMBE audio conversion library NOT FOUND" );
mLibraryLoadStatusLogged = true;
}
}
}
else
{
if( !mLibraryLoadStatusLogged )
{
mLog.info( "JMBE audio conversion library NOT FOUND" );
mLibraryLoadStatusLogged = true;
}
mLog.debug( "JMBE library version 0.3.x or higher is required" );
}
}
catch ( ClassNotFoundException e1 )

View File

@@ -24,6 +24,9 @@ import module.decode.state.DecoderStateEvent.Event;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
/**
* Basic decoder channel state - provides the minimum channel state functionality
* to support an always un-squelched audio decoder.
@@ -34,12 +37,14 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
LoggerFactory.getLogger( AlwaysUnsquelchedDecoderState.class );
private DecoderType mDecoderType;
private String mChannelName;
public AlwaysUnsquelchedDecoderState( DecoderType decoderType )
public AlwaysUnsquelchedDecoderState( DecoderType decoderType, String channelName )
{
super( null );
mDecoderType = decoderType;
mChannelName = channelName;
}
@Override
@@ -87,6 +92,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
@Override
public void start()
{
broadcast( new Metadata( MetadataType.TO, mChannelName, false ) );
broadcast( new DecoderStateEvent( this, Event.ALWAYS_UNSQUELCH, State.IDLE ) );
}

View File

@@ -434,7 +434,7 @@ public class SettingsManager
return null;
}
private ImageIcon getScaledIcon( ImageIcon original, int height )
public static ImageIcon getScaledIcon( ImageIcon original, int height )
{
double scale = (double)original.getIconHeight() / (double)height;

View File

@@ -17,7 +17,40 @@
******************************************************************************/
package source.mixer;
import java.util.EnumSet;
public enum MixerChannel
{
LEFT,RIGHT,MONO;
LEFT( "L" ),
RIGHT( "R" ),
MONO( "M" ),
STEREO( "S" );
private String mLabel;
private MixerChannel( String label )
{
mLabel = label;
}
public String getLabel()
{
return mLabel;
}
/**
* Mixer Target Data Line (ie sample source) channel options
*/
public static EnumSet<MixerChannel> getTargetChannels()
{
return EnumSet.of( LEFT,RIGHT,MONO );
}
/**
* Mixer Source Data Line (ie sound card output) channel options
*/
public static EnumSet<MixerChannel> getSourceChannels()
{
return EnumSet.of( MONO,STEREO );
}
}

View File

@@ -0,0 +1,68 @@
package source.mixer;
import javax.sound.sampled.Mixer;
public class MixerChannelConfiguration
{
private Mixer mMixer;
private MixerChannel mMixerChannel;
public MixerChannelConfiguration( Mixer mixer, MixerChannel channel )
{
mMixer = mixer;
mMixerChannel = channel;
}
public Mixer getMixer()
{
return mMixer;
}
public MixerChannel getMixerChannel()
{
return mMixerChannel;
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append( mMixer.getMixerInfo().getName() );
sb.append( " - " );
sb.append( mMixerChannel.name() );
return sb.toString();
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ( ( mMixer == null ) ? 0 : mMixer.hashCode() );
result = prime * result
+ ( ( mMixerChannel == null ) ? 0 : mMixerChannel.hashCode() );
return result;
}
@Override
public boolean equals( Object obj )
{
if ( this == obj )
return true;
if ( obj == null )
return false;
if ( getClass() != obj.getClass() )
return false;
MixerChannelConfiguration other = (MixerChannelConfiguration) obj;
if ( mMixer == null )
{
if ( other.mMixer != null )
return false;
} else if ( !mMixer.equals( other.mMixer ) )
return false;
if ( mMixerChannel != other.mMixerChannel )
return false;
return true;
}
}

View File

@@ -17,6 +17,8 @@
******************************************************************************/
package source.mixer;
import java.util.EnumSet;
import javax.sound.sampled.Mixer;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
@@ -25,13 +27,13 @@ import javax.swing.JLabel;
import source.SourceEditor;
import source.config.SourceConfigMixer;
import source.config.SourceConfiguration;
import source.mixer.MixerManager.DiscoveredMixer;
import source.mixer.MixerManager.InputMixerConfiguration;
import controller.ResourceManager;
public class MixerEditor extends SourceEditor
{
private static final long serialVersionUID = 1L;
private JComboBox<DiscoveredMixer> mComboMixers;
private JComboBox<InputMixerConfiguration> mComboMixers;
private JComboBox<MixerChannel>mComboChannels;
protected Mixer.Info mSelectedMixer = null;
@@ -49,7 +51,7 @@ public class MixerEditor extends SourceEditor
JLabel mixerLabel = new JLabel( "Mixer:" );
add( mixerLabel, "align right" );
mComboMixers = new JComboBox<DiscoveredMixer>();
mComboMixers = new JComboBox<InputMixerConfiguration>();
add( mComboMixers, "wrap" );
@@ -75,7 +77,7 @@ public class MixerEditor extends SourceEditor
{
SourceConfigMixer config = (SourceConfigMixer)mConfig;
DiscoveredMixer selectedMixer =
InputMixerConfiguration selectedMixer =
mComboMixers.getItemAt( mComboMixers.getSelectedIndex() );
if( selectedMixer != null )
@@ -103,10 +105,10 @@ public class MixerEditor extends SourceEditor
@Override
public void run()
{
DiscoveredMixer[] mixers = MixerManager.getInstance().getMixers();
InputMixerConfiguration[] mixers = MixerManager.getInstance().getInputMixers();
mComboMixers.setModel(
new DefaultComboBoxModel<DiscoveredMixer>( mixers ) );
new DefaultComboBoxModel<InputMixerConfiguration>( mixers ) );
SourceConfigMixer config = (SourceConfigMixer)mConfig;
@@ -125,8 +127,8 @@ public class MixerEditor extends SourceEditor
{
mComboMixers.setSelectedIndex( 0 );
DiscoveredMixer selectedMixer =
(DiscoveredMixer)mComboMixers.getSelectedItem();
InputMixerConfiguration selectedMixer =
(InputMixerConfiguration)mComboMixers.getSelectedItem();
config.setMixer( selectedMixer.getMixerName() );
}
@@ -142,8 +144,10 @@ public class MixerEditor extends SourceEditor
@Override
public void run()
{
mComboChannels.setModel(
new DefaultComboBoxModel<MixerChannel>( MixerChannel.values() ) );
EnumSet<MixerChannel> channels = MixerChannel.getTargetChannels();
mComboChannels.setModel( new DefaultComboBoxModel<MixerChannel>(
channels.toArray( new MixerChannel[ channels.size() ] ) ) );
MixerChannel channel = ((SourceConfigMixer)mConfig).getChannel();

View File

@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
@@ -28,6 +29,8 @@ import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.Port;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.TargetDataLine;
import org.slf4j.Logger;
@@ -39,6 +42,7 @@ import source.config.SourceConfigMixer;
import source.config.SourceConfiguration;
import source.tuner.MixerTunerDataLine;
import source.tuner.MixerTunerType;
import audio.AudioFormats;
public class MixerManager
{
@@ -47,23 +51,9 @@ public class MixerManager
private static MixerManager sInstance = null;
private ArrayList<DiscoveredMixer> mDiscoveredMixers =
new ArrayList<DiscoveredMixer>();
private HashMap<String,MixerTunerDataLine> mMixerTuners =
new HashMap<String,MixerTunerDataLine>();
public static AudioFormat MONO = new AudioFormat( 48000, //SampleRate
16, //Sample Size
1, //Channels
true, //Signed
false ); //Little Endian
public static AudioFormat STEREO = new AudioFormat( 48000, //SampleRate
16, //Sample Size
2, //Channels
true, //Signed
false ); //Little Endian
private List<InputMixerConfiguration> mInputMixers = new ArrayList<>();
private List<MixerChannelConfiguration> mOutputMixers = new ArrayList<>();
private HashMap<String,MixerTunerDataLine> mMixerTuners = new HashMap<>();
private MixerManager()
{
@@ -92,7 +82,7 @@ public class MixerManager
if( mixerName != null )
{
DiscoveredMixer mixer = getDiscoveredMixer( mixerName );
InputMixerConfiguration mixer = getInputMixer( mixerName );
if( mixer != null )
{
@@ -104,7 +94,8 @@ public class MixerManager
if( channel == MixerChannel.MONO )
{
DataLine.Info info =
new DataLine.Info(TargetDataLine.class, MONO );
new DataLine.Info(TargetDataLine.class,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO );
TargetDataLine dataLine;
@@ -116,7 +107,7 @@ public class MixerManager
if( dataLine != null )
{
return new RealMixerSource( dataLine,
MONO,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO,
mixerName,
new ShortAdapter() );
}
@@ -132,7 +123,8 @@ public class MixerManager
else
{
DataLine.Info info =
new DataLine.Info(TargetDataLine.class, STEREO );
new DataLine.Info(TargetDataLine.class,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO );
TargetDataLine dataLine;
@@ -143,7 +135,8 @@ public class MixerManager
if( dataLine != null )
{
return new RealMixerSource( dataLine, STEREO,
return new RealMixerSource( dataLine,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO,
mixerName, new ChannelShortAdapter(
mixerConfig.getChannel() ) );
}
@@ -163,15 +156,14 @@ public class MixerManager
return null;
}
public DiscoveredMixer[] getMixers()
public InputMixerConfiguration[] getInputMixers()
{
return mDiscoveredMixers.toArray(
new DiscoveredMixer[ mDiscoveredMixers.size() ] );
return mInputMixers.toArray( new InputMixerConfiguration[ mInputMixers.size() ] );
}
public DiscoveredMixer getDiscoveredMixer( String name )
public InputMixerConfiguration getInputMixer( String name )
{
for( DiscoveredMixer mixer: mDiscoveredMixers )
for( InputMixerConfiguration mixer: mInputMixers )
{
if( mixer.getMixerName().contentEquals( name ) )
{
@@ -181,6 +173,11 @@ public class MixerManager
return null;
}
public MixerChannelConfiguration[] getOutputMixers()
{
return mOutputMixers.toArray( new MixerChannelConfiguration[ mOutputMixers.size() ] );
}
public Collection<MixerTunerDataLine> getMixerTunerDataLines()
{
@@ -204,21 +201,28 @@ public class MixerManager
if( mixer != null )
{
EnumSet<MixerChannel> channels = getSupportedChannels( mixer );
EnumSet<MixerChannel> inputChannels = getSupportedTargetChannels( mixer );
if( channels != null )
if( inputChannels != null )
{
mDiscoveredMixers.add(
new DiscoveredMixer( mixer, channels ) );
mInputMixers.add(
new InputMixerConfiguration( mixer, inputChannels ) );
sb.append( "\t[LOADED] Mixer:" + mixerInfo.getName() +
" CHANNELS: " + channels + "\n" );
sb.append( "\t[LOADED] Input: " + mixerInfo.getName() +
" CHANNELS: " + inputChannels + "\n" );
}
else
EnumSet<MixerChannel> outputChannels = getSupportedSourceChannels( mixer );
if( outputChannels != null )
{
sb.append( "\t[NOT LOADED] Mixer:" +
mixerInfo.getName() +
" - audio format not supported\n" );
for( MixerChannel channel: outputChannels )
{
mOutputMixers.add( new MixerChannelConfiguration( mixer, channel ) );
}
sb.append( "\t[LOADED] Output: " + mixerInfo.getName() +
" CHANNELS: " + outputChannels + "\n" );
}
}
else
@@ -249,10 +253,6 @@ public class MixerManager
break;
}
}
else
{
sb.append( "\t[NOT LOADED] Tuner:" + mixerInfo.getName() + " - couldn't get target data line\n" );
}
}
}
@@ -288,15 +288,17 @@ public class MixerManager
return retVal;
}
private EnumSet<MixerChannel> getSupportedChannels( Mixer mixer )
private EnumSet<MixerChannel> getSupportedTargetChannels( Mixer mixer )
{
DataLine.Info stereoInfo=
new DataLine.Info(TargetDataLine.class, STEREO );
new DataLine.Info(TargetDataLine.class,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO );
boolean stereoSupported = mixer.isLineSupported( stereoInfo );
DataLine.Info monoInfo=
new DataLine.Info(TargetDataLine.class, MONO );
new DataLine.Info(TargetDataLine.class,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO );
boolean monoSupported = mixer.isLineSupported( monoInfo );
@@ -317,6 +319,42 @@ public class MixerManager
return EnumSet.of( MixerChannel.MONO );
}
return null;
}
/**
* Returns enumset of SourceDataLine (audio output) channels
* (MONO and/or STEREO) supported by the mixer, or null if the mixer doesn't
* have any source data lines.
*/
private EnumSet<MixerChannel> getSupportedSourceChannels( Mixer mixer )
{
DataLine.Info stereoInfo=
new DataLine.Info(SourceDataLine.class,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO );
boolean stereoSupported = mixer.isLineSupported( stereoInfo );
DataLine.Info monoInfo=
new DataLine.Info(SourceDataLine.class,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO );
boolean monoSupported = mixer.isLineSupported( monoInfo );
if( stereoSupported && monoSupported )
{
return EnumSet.of( MixerChannel.MONO,
MixerChannel.STEREO );
}
else if( stereoSupported )
{
return EnumSet.of( MixerChannel.STEREO );
}
else if( monoSupported )
{
return EnumSet.of( MixerChannel.MONO );
}
return null;
}
@@ -354,17 +392,24 @@ public class MixerManager
"\n lineclass:" + lineInfo.getLineClass() +
"\n" );
}
Line.Info portInfo = new Line.Info( Port.class );
if( mixer.isLineSupported( portInfo ) )
{
sb.append( "**PORT LINE IS SUPPORTED BY THIS MIXER***\n" );
}
}
return sb.toString();
}
public class DiscoveredMixer
public class InputMixerConfiguration
{
private Mixer mMixer;
private EnumSet<MixerChannel> mChannels;
public DiscoveredMixer( Mixer mixer, EnumSet<MixerChannel> channels )
public InputMixerConfiguration( Mixer mixer, EnumSet<MixerChannel> channels )
{
mMixer = mixer;
mChannels = channels;
@@ -389,10 +434,14 @@ public class MixerManager
{
return mChannels.contains( channel );
}
}
public static void main( String[] args )
{
mLog.debug( "Starting ..." );
public String toString()
{
return getMixerName();
}
MixerManager.getInstance();
mLog.debug( "Finished!" );
}
}

View File

@@ -63,7 +63,7 @@ public class TimeStamp
*/
public static String getFormattedTimeWithMilliseconds( long timestamp )
{
return mSDFTime.format( new Date( timestamp ) );
return mSDFTimeWithMilliseconds.format( new Date( timestamp ) );
}
/**