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

@@ -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;
}
}