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 --> <!-- version properties -->
<property name="major" value="0"/> <property name="major" value="0"/>
<property name="minor" value="1"/> <property name="minor" value="2"/>
<property name="build" value="5"/> <property name="build" value="0"/>
<!-- Build properties --> <!-- Build properties -->
<property name="classes" location="../classes"/> <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.Color;
import java.awt.EventQueue; import java.awt.EventQueue;
import java.awt.Font; 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.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.JPopupMenu; import javax.swing.JSeparator;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
@@ -22,15 +15,16 @@ import org.slf4j.LoggerFactory;
import sample.Listener; import sample.Listener;
import settings.ColorSetting; import settings.ColorSetting;
import settings.ColorSetting.ColorSettingName;
import settings.Setting; import settings.Setting;
import settings.SettingChangeListener; import settings.SettingChangeListener;
import settings.SettingsManager; import settings.SettingsManager;
import settings.ColorSetting.ColorSettingName;
import alias.Alias; import alias.Alias;
import audio.AudioEvent.Type; import audio.AudioEvent.Type;
import audio.metadata.AudioMetadata; import audio.metadata.AudioMetadata;
import audio.metadata.Metadata; import audio.metadata.Metadata;
import audio.metadata.MetadataType; import audio.metadata.MetadataType;
import audio.output.AudioOutput;
public class AudioChannelPanel extends JPanel public class AudioChannelPanel extends JPanel
implements Listener<AudioEvent>, SettingChangeListener implements Listener<AudioEvent>, SettingChangeListener
@@ -44,85 +38,60 @@ public class AudioChannelPanel extends JPanel
private Color mDetailsColor; private Color mDetailsColor;
private SettingsManager mSettingsManager; private SettingsManager mSettingsManager;
private IAudioController mController; private AudioOutput mAudioOutput;
private String mChannel;
private JLabel mChannelName = new JLabel( " " );
private JLabel mToLabel = new JLabel( "TO:" ); private JLabel mToLabel = new JLabel( "TO:" );
private JLabel mTo = new JLabel( "" ); private JLabel mTo = new JLabel( "" );
private JLabel mToAlias = new JLabel( "" ); private JLabel mToAlias = new JLabel( "" );
private JLabel mMutedLabel = new JLabel( " " );
private JLabel mFromLabel = new JLabel( "FROM:" ); private JLabel mFromLabel = new JLabel( "FROM:" );
private JLabel mFrom = new JLabel( "" ); private JLabel mFrom = new JLabel( "" );
private JLabel mFromAlias = new JLabel( "" ); private JLabel mFromAlias = new JLabel( "" );
private JLabel mChannelLabel;
private JLabel mChannelName;
private JLabel mMutedLabel;
private boolean mConfigured = false; private boolean mConfigured = false;
private BooleanControl mMuteControl; public AudioChannelPanel( SettingsManager settingsManager, AudioOutput audioOutput )
private FloatControl mGainControl;
public AudioChannelPanel( SettingsManager settingsManager,
IAudioController controller,
String channel )
{ {
mSettingsManager = settingsManager; mSettingsManager = settingsManager;
mSettingsManager.addListener( this ); mSettingsManager.addListener( this );
mAudioOutput = audioOutput;
if( mAudioOutput != null )
{
mAudioOutput.addAudioEventListener( this );
mAudioOutput.setAudioMetadataListener( new AudioMetadataProcessor() );
}
mLabelColor = mSettingsManager.getColorSetting( mLabelColor = mSettingsManager.getColorSetting(
ColorSettingName.CHANNEL_STATE_LABEL_DECODER ).getColor(); ColorSettingName.CHANNEL_STATE_LABEL_DECODER ).getColor();
mDetailsColor = mSettingsManager.getColorSetting( mDetailsColor = mSettingsManager.getColorSetting(
ColorSettingName.CHANNEL_STATE_LABEL_DETAILS ).getColor(); 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(); init();
} }
private void 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 ); 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.setFont( mFont );
mToLabel.setForeground( mLabelColor ); if( mAudioOutput != null )
{
mToLabel.setForeground( mLabelColor );
}
else
{
mToLabel.setForeground( getBackground() );
}
add( mToLabel ); add( mToLabel );
mTo.setFont( mFont ); mTo.setFont( mFont );
@@ -133,8 +102,19 @@ public class AudioChannelPanel extends JPanel
mToAlias.setForeground( mLabelColor ); mToAlias.setForeground( mLabelColor );
add( mToAlias,"wrap" ); add( mToAlias,"wrap" );
mMutedLabel.setFont( mFont );
mMutedLabel.setForeground( Color.RED );
add( mMutedLabel );
mFromLabel.setFont( mFont ); mFromLabel.setFont( mFont );
mFromLabel.setForeground( mLabelColor ); if( mAudioOutput != null )
{
mFromLabel.setForeground( mLabelColor );
}
else
{
mFromLabel.setForeground( getBackground() );
}
add( mFromLabel ); add( mFromLabel );
mFrom.setFont( mFont ); mFrom.setFont( mFont );
@@ -144,54 +124,40 @@ public class AudioChannelPanel extends JPanel
mFromAlias.setFont( mFont ); mFromAlias.setFont( mFont );
mFromAlias.setForeground( mLabelColor ); mFromAlias.setForeground( mLabelColor );
add( mFromAlias,"wrap" ); add( mFromAlias,"wrap" );
mChannelLabel = new JLabel( "Channel:" );
mChannelLabel.setFont( mFont );
mChannelLabel.setForeground( mDetailsColor );
add( mChannelLabel ); JSeparator separator = new JSeparator( JSeparator.HORIZONTAL );
separator.setBackground( Color.DARK_GRAY );
mChannelName = new JLabel( mChannel ); add( separator, "span,growx" );
mChannelName.setFont( mFont );
mChannelName.setForeground( mDetailsColor );
add( mChannelName );
mMutedLabel = new JLabel( "" );
mMutedLabel.setFont( mFont );
mMutedLabel.setForeground( Color.RED );
add( mMutedLabel, "wrap" );
} }
private void updateMuteState()
{
if( mMuteControl != null )
{
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
mMutedLabel.setText( mMuteControl.getValue() ? "Muted" : "" );
}
} );
}
}
@Override @Override
public void receive( final AudioEvent audioEvent ) public void receive( final AudioEvent audioEvent )
{ {
if( audioEvent.getType() == Type.AUDIO_STOPPED ) switch( audioEvent.getType() )
{ {
EventQueue.invokeLater( new Runnable() case AUDIO_STOPPED:
{ EventQueue.invokeLater( new Runnable()
@Override
public void run()
{ {
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 * 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 public enum Type
{ {
AUDIO_MUTED,
AUDIO_UNMUTED,
AUDIO_SQUELCHED,
AUDIO_UNSQUELCHED,
AUDIO_STARTED, AUDIO_STARTED,
AUDIO_STOPPED, AUDIO_STOPPED,
AUDIO_CONFIGURATION_CHANGED; AUDIO_CONFIGURATION_CHANGE_STARTED,
AUDIO_CONFIGURATION_CHANGE_COMPLETE;
} }
} }

View File

@@ -1,6 +1,9 @@
package audio; package audio;
import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line;
import javax.sound.sampled.SourceDataLine;
public class AudioFormats public class AudioFormats
{ {
@@ -18,8 +21,10 @@ public class AudioFormats
public static final int IMBE_FRAME_SIZE_BYTES = 18; public static final int IMBE_FRAME_SIZE_BYTES = 18;
public static final int IMBE_SAMPLE_SIZE_BITS = 144; public static final int IMBE_SAMPLE_SIZE_BITS = 144;
public static final int ONE_CHANNEL = 1; 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_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 = public static AudioFormat IMBE_AUDIO_FORMAT =
new AudioFormat( IMBE_ENCODING, new AudioFormat( IMBE_ENCODING,
@@ -35,16 +40,37 @@ public class AudioFormats
PCM_8KHZ_RATE, PCM_8KHZ_RATE,
PCM_SAMPLE_SIZE_BITS, PCM_SAMPLE_SIZE_BITS,
ONE_CHANNEL, ONE_CHANNEL,
PCM_FRAME_SIZE_BYTES, PCM_FRAME_SIZE_BYTES_MONO,
PCM_8KHZ_RATE, PCM_8KHZ_RATE,
LITTLE_ENDIAN ); LITTLE_ENDIAN );
public static AudioFormat PCM_SIGNED_48KHZ_16BITS = public static AudioFormat PCM_SIGNED_48KHZ_16BITS_MONO =
new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
PCM_48KHZ_RATE, PCM_48KHZ_RATE,
PCM_SAMPLE_SIZE_BITS, PCM_SAMPLE_SIZE_BITS,
ONE_CHANNEL, ONE_CHANNEL,
PCM_FRAME_SIZE_BYTES, PCM_FRAME_SIZE_BYTES_MONO,
PCM_48KHZ_RATE, PCM_48KHZ_RATE,
LITTLE_ENDIAN ); 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; package audio;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@@ -10,16 +12,20 @@ import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.sound.sampled.BooleanControl; import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.FloatControl; import javax.sound.sampled.Mixer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sample.Broadcaster;
import sample.Listener; 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.AudioOutput;
import audio.output.MonoAudioOutput; import audio.output.MonoAudioOutput;
import audio.output.StereoAudioOutput;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType; 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 AUDIO_TIMEOUT = 2000; //2 seconds
public static final int NO_SOURCE = 0; 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 LinkedTransferQueue<AudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
private Map<Integer,AudioChannelAssignment> mChannelAssignments = new HashMap<>(); private Map<Integer,AudioChannelAssignment> mChannelAssignments = new HashMap<>();
private int mLowestPrioritySource; private int mLowestPrioritySource;
private List<AudioOutput> mAvailableAudioOutputs = new ArrayList<>(); 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 List<String> mChannelNames = new ArrayList<>();
private AudioOutput mAudioOutput;
private Listener<AudioEvent> mConfigurationChangeListener; private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
private ThreadPoolManager mThreadPoolManager; private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture<?> mProcessingTask; 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 * packets to any combination of outputs based on any alias audio routing
* options specified by the user. * options specified by the user.
*/ */
@@ -53,14 +66,24 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
{ {
mThreadPoolManager = manager; mThreadPoolManager = manager;
mAudioOutput = new MonoAudioOutput(); //TODO: save audio system preferences to the settings manager
mAvailableAudioOutputs.add( mAudioOutput );
mChannelNames.add( mAudioOutput.getChannelName() );
mChannelMap.put( mAudioOutput.getChannelName(), mAudioOutput );
mProcessingTask = mThreadPoolManager.scheduleFixedRate( /* Use the system default mixer and mono channel as default startup */
ThreadType.AUDIO_PROCESSING, new AudioPacketProcessor(), 15, Mixer mixer = AudioSystem.getMixer( null );
TimeUnit.MILLISECONDS );
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() public void dispose()
@@ -74,8 +97,6 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mThreadPoolManager = null; mThreadPoolManager = null;
mProcessingTask = null; mProcessingTask = null;
mAudioOutput.dispose();
} }
@Override @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> public class AudioChannelAssignment implements Listener<AudioEvent>
{ {
private static final long CALL_END_DELAY = 2000; //2 seconds private static final long CALL_END_DELAY = 2000; //2 seconds
@@ -280,102 +309,91 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
} }
} }
} }
@Override @Override
public List<String> getAudioChannels() public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException
{ {
return mChannelNames; if( entry != null &&
} ( entry.getMixerChannel() == MixerChannel.MONO ||
entry.getMixerChannel() == MixerChannel.STEREO ) )
@Override
public BooleanControl getMuteControl( String channel )
throws AudioException
{
if( mChannelMap.containsKey( channel ) )
{ {
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 switch( entry.getMixerChannel() )
public boolean hasMuteControl( String channel ) {
{ case MONO:
return mChannelMap.containsKey( channel ) && AudioOutput mono = new MonoAudioOutput( entry.getMixer() );
mChannelMap.get( channel ) != null && mAvailableAudioOutputs.add( mono );
mChannelMap.get( channel ).getMuteControl() != null; 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 mProcessingTask = mThreadPoolManager.scheduleFixedRate(
public FloatControl getGainControl( String channel ) throws AudioException ThreadType.AUDIO_PROCESSING, new AudioPacketProcessor(), 15,
{ TimeUnit.MILLISECONDS );
if( mChannelMap.containsKey( channel ) )
{
return mChannelMap.get( channel ).getGainControl();
}
throw new AudioException( "Channel [" + channel + "] is not a valid channel" ); mControllerBroadcaster.broadcast( CONFIGURATION_CHANGE_COMPLETE );
}
@Override
public boolean hasGainControl( String channel )
{
return mChannelMap.containsKey( channel ) &&
mChannelMap.get( channel ) != null &&
mChannelMap.get( channel ).getGainControl() != null;
}
/**
* Sets the listener to receive audio events, specifically, audio
* configuration change events
*/
@Override
public void setConfigurationChangeListener( Listener<AudioEvent> listener )
{
mConfigurationChangeListener = listener;
}
@Override
public void addAudioEventListener( String channel, Listener<AudioEvent> listener )
throws AudioException
{
if( mChannelMap.containsKey( channel ) )
{
mChannelMap.get( channel ).addAudioEventListener( listener );
}
else
{
throw new AudioException( "Channel [" + channel + "] is not a valid "
+ "channel. Audio event listener was not registered" );
} }
} }
@Override @Override
public void removeAudioEventListener( String channel, Listener<AudioEvent> listener ) public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException
{ {
if( mChannelMap.containsKey( channel ) ) return mMixerChannel;
{
mChannelMap.get( channel ).removeAudioEventListener( listener );
}
} }
@Override @Override
public void setAudioMetadataListener( String channel, public List<AudioOutput> getAudioOutputs()
Listener<AudioMetadata> listener )
{ {
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 @Override
public void removeAudioMetadataListener( String channel ) public void addControllerListener( Listener<AudioEvent> listener )
{ {
if( mChannelMap.containsKey( channel ) ) mControllerBroadcaster.addListener( listener );
{ }
mChannelMap.get( channel ).removeAudioMetadataListener();
} @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.EventQueue;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; 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.JButton;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel; 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; import net.miginfocom.swing.MigLayout;
@@ -16,68 +27,329 @@ import org.slf4j.LoggerFactory;
import sample.Listener; import sample.Listener;
import settings.SettingsManager; import settings.SettingsManager;
import source.mixer.MixerChannelConfiguration;
import source.mixer.MixerManager;
import audio.output.AudioOutput;
public class AudioPanel extends JPanel implements Listener<AudioEvent> public class AudioPanel extends JPanel implements Listener<AudioEvent>
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final Logger mLog = LoggerFactory.getLogger( AudioPanel.class ); 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 SettingsManager mSettingsManager;
private IAudioController mController; private IAudioController mController;
private MuteButton mMuteButton;
private JButton mMuteButton;
private AudioChannelsPanel mAudioChannelsPanel;
public AudioPanel( SettingsManager settingsManager, IAudioController controller ) public AudioPanel( SettingsManager settingsManager, IAudioController controller )
{ {
mSettingsManager = settingsManager; mSettingsManager = settingsManager;
mController = controller; mController = controller;
mMuteButton = new MuteButton( mController ); mController.addControllerListener( this );
init(); init();
} }
private void 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 ); setBackground( Color.BLACK );
add( mMuteButton, "span" ); mMuteButton = new MuteButton();
mMuteButton.setBackground( getBackground() );
add( mMuteButton );
for( String channel: mController.getAudioChannels() ) mAudioChannelsPanel = new AudioChannelsPanel( mSettingsManager, mController );
{
add( new AudioChannelPanel( mSettingsManager, mController, channel ), "span" ); add( mAudioChannelsPanel );
}
addMouseListener( new MouseSelectionListener() );
} }
public int getPreferredHeight()
{
return mMuteButton.getHeight() +
( mController.getAudioChannels().size() * 20 );
}
@Override @Override
public void receive( AudioEvent event ) 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 public class MuteButton extends JButton
{ {
private IAudioController mController; private static final long serialVersionUID = 1L;
private boolean mMuted = false; private boolean mMuted = false;
public MuteButton( IAudioController controller ) public MuteButton()
{ {
super( "Mute Audio" ); setIcon( UNMUTED_ICON );
setBorderPainted( false );
setBackground( Color.GREEN );
mController = controller;
addActionListener( new ActionListener() addActionListener( new ActionListener()
{ {
@@ -86,31 +358,17 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
{ {
mMuted = !mMuted; mMuted = !mMuted;
for( String channel: mController.getAudioChannels() ) for( AudioOutput output: mController.getAudioOutputs() )
{ {
try output.setMuted( mMuted );
{
BooleanControl control = mController.getMuteControl( channel );
if( control != null )
{
control.setValue( mMuted );
}
}
catch ( AudioException e1 )
{
mLog.error( "Couldn't obtain mute control for "
+ "channel [" + channel + "] from audio manager", e1 );
}
} }
EventQueue.invokeLater( new Runnable() EventQueue.invokeLater( new Runnable()
{ {
@Override @Override
public void run() public void run()
{ {
MuteButton.this.setText( ( mMuted ? "Unmute Audio" : "Mute Audio" ) ); setIcon( mMuted ? MUTED_ICON : UNMUTED_ICON );
MuteButton.this.setBackground( ( mMuted ? Color.RED : Color.GREEN ) );
} }
} ); } );
} }

View File

@@ -2,35 +2,23 @@ package audio;
import java.util.List; import java.util.List;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.FloatControl;
import sample.Listener; import sample.Listener;
import audio.metadata.AudioMetadata; import source.mixer.MixerChannelConfiguration;
import audio.output.AudioOutput;
/** /**
* Interface for controlling one or more audio channels * Interface for controlling one or more audio channels
*/ */
public interface IAudioController 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; /* Audio Output(s) */
public List<AudioOutput> getAudioOutputs();
public boolean hasMuteControl( String channel );
/* Controller Audio Event Listener */
public FloatControl getGainControl( String channel ) throws AudioException; public void addControllerListener( Listener<AudioEvent> listener );
public void removeControllerListener( Listener<AudioEvent> listener );
public boolean hasGainControl( String channel );
public void addAudioEventListener( String channel, Listener<AudioEvent> listener )
throws AudioException;
public void removeAudioEventListener( String channel, Listener<AudioEvent> listener );
public void setAudioMetadataListener( String channel, Listener<AudioMetadata> listener );
public void removeAudioMetadataListener( String channel );
public void setConfigurationChangeListener( Listener<AudioEvent> listener );
} }

View File

@@ -1,5 +1,7 @@
package audio.output; package audio.output;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.BooleanControl; import javax.sound.sampled.BooleanControl;
@@ -11,6 +13,7 @@ import org.slf4j.LoggerFactory;
import sample.Broadcaster; import sample.Broadcaster;
import sample.Listener; import sample.Listener;
import audio.AudioEvent; import audio.AudioEvent;
import audio.AudioEvent.Type;
import audio.AudioPacket; import audio.AudioPacket;
import audio.metadata.AudioMetadata; import audio.metadata.AudioMetadata;
@@ -20,10 +23,24 @@ public abstract class AudioOutput implements Listener<AudioPacket>
private AtomicBoolean mSquelched = new AtomicBoolean( false ); private AtomicBoolean mSquelched = new AtomicBoolean( false );
private Listener<AudioMetadata> mAudioMetadataListener; private Listener<AudioMetadata> mAudioMetadataListener;
protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>(); protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
protected LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
protected ScheduledExecutorService mExecutorService;
protected boolean mCanProcessAudio;
protected long mLastBufferReceived;
public void dispose() public void dispose()
{ {
mCanProcessAudio = false;
mBuffer.clear();
if( mExecutorService != null )
{
mExecutorService.shutdown();
}
mAudioEventBroadcaster.dispose(); mAudioEventBroadcaster.dispose();
mAudioEventBroadcaster = null; mAudioEventBroadcaster = null;
mAudioMetadataListener = null; mAudioMetadataListener = null;
@@ -31,8 +48,8 @@ public abstract class AudioOutput implements Listener<AudioPacket>
public abstract String getChannelName(); public abstract String getChannelName();
public abstract BooleanControl getMuteControl(); public abstract void setMuted( boolean muted );
public abstract boolean hasMuteControl(); public abstract boolean isMuted();
public abstract FloatControl getGainControl(); public abstract FloatControl getGainControl();
public abstract boolean hasGainControl(); 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 ) public void setSquelched( boolean squelched )
{ {
mSquelched.set( squelched ); mSquelched.set( squelched );
broadcast( new AudioEvent( squelched ? Type.AUDIO_SQUELCHED :
Type.AUDIO_UNSQUELCHED, getChannelName() ) );
} }
public boolean isSquelched() public boolean isSquelched()
@@ -81,4 +106,23 @@ public abstract class AudioOutput implements Listener<AudioPacket>
{ {
mAudioMetadataListener = null; 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.BooleanControl; import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.FloatControl; import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine; import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import source.mixer.MixerChannel;
import audio.AudioEvent; import audio.AudioEvent;
import audio.AudioFormats; import audio.AudioFormats;
import audio.AudioPacket; import audio.AudioPacket;
@@ -27,14 +26,14 @@ import audio.AudioPacket.Type;
import controller.NamingThreadFactory; 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. * audio data packets.
*/ */
public class MonoAudioOutput extends AudioOutput public class MonoAudioOutput extends AudioOutput
{ {
private final static Logger mLog = LoggerFactory.getLogger( MonoAudioOutput.class ); 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 */ * two bytes per sample */
private final static int BUFFER_SIZE = 48000; 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 * 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 * 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 */ * 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 START_THRESHOLD = SAMPLE_SIZE_40_MS;
private final static int STOP_THRESHOLD = BUFFER_SIZE - 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 SourceDataLine mOutput;
private FloatControl mGainControl; private FloatControl mGainControl;
private BooleanControl mMuteControl; private BooleanControl mMuteControl;
private boolean mCanProcessAudio;
private long mLastBufferReceived;
private AudioEvent mAudioStartEvent; private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent; private AudioEvent mAudioStopEvent;
public MonoAudioOutput() public MonoAudioOutput( Mixer mixer )
{ {
super(); super();
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED, getChannelName() ); mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED, getChannelName() ); getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
getChannelName() );
try try
{ {
mOutput = AudioSystem.getSourceDataLine( mOutput = (SourceDataLine)mixer.getLine(
AudioFormats.PCM_SIGNED_48KHZ_16BITS ); AudioFormats.MONO_SOURCE_DATALINE_INFO );
if( mOutput != null ) if( mOutput != null )
{ {
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS, BUFFER_SIZE ); mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE );
mCanProcessAudio = true; mCanProcessAudio = true;
@@ -103,47 +96,18 @@ public class MonoAudioOutput extends AudioOutput
@Override @Override
public String getChannelName() public String getChannelName()
{ {
return "Mono"; return MixerChannel.MONO.getLabel();
}
/**
* Timestamp when most recent buffer was received
*/
public long getLastBufferReceived()
{
return mLastBufferReceived;
} }
public void dispose() public void dispose()
{ {
super.dispose(); super.dispose();
mCanProcessAudio = false;
mBuffer.clear();
if( mExecutorService != null )
{
mExecutorService.shutdown();
}
if( mOutput != null ) if( mOutput != null )
{ {
mOutput.close(); mOutput.close();
} }
} }
@Override
public void receive( AudioPacket packet )
{
if( mCanProcessAudio )
{
mLastBufferReceived = System.currentTimeMillis();
mBuffer.add( packet );
}
}
public class QueueProcessor implements Runnable 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 @Override
public BooleanControl getMuteControl() public boolean isMuted()
{ {
return mMuteControl; if( mMuteControl != null )
} {
return mMuteControl.getValue();
@Override }
public boolean hasMuteControl()
{ return false;
return mMuteControl != null;
} }
@Override @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(), AudioPanel audioPanel = new AudioPanel( mResourceManager.getSettingsManager(),
mResourceManager.getAudioManager() ); mResourceManager.getAudioManager() );
audioPanel.setPreferredSize( new Dimension( 200, audioPanel.getPreferredHeight() ) );
JideSplitPane audioChannelListSplit = new JideSplitPane( JideSplitPane.VERTICAL_SPLIT ); JideSplitPane audioChannelListSplit = new JideSplitPane( JideSplitPane.VERTICAL_SPLIT );
audioChannelListSplit.setDividerSize( 5 ); audioChannelListSplit.setDividerSize( 5 );

View File

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

View File

@@ -6,6 +6,7 @@ import java.awt.event.ActionListener;
import javax.swing.JMenu; import javax.swing.JMenu;
import javax.swing.JMenuItem; import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JSeparator; import javax.swing.JSeparator;
import module.decode.event.ActivitySummaryFrame; import module.decode.event.ActivitySummaryFrame;
@@ -101,15 +102,22 @@ public class ChannelUtils
@Override @Override
public void actionPerformed( ActionEvent e ) public void actionPerformed( ActionEvent e )
{ {
/* Disable the channel */ int response = JOptionPane.showConfirmDialog( anchor,
channel.setEnabled( DISABLED, BROADCAST_CHANGE ); "Do you want to delete channel " + channel.getName() + "?",
"Are you sure?", JOptionPane.YES_NO_CANCEL_OPTION );
/* Broadcast channel deleted event */
channel.fireChannelEvent( Event.CHANNEL_DELETED ); if( response == JOptionPane.YES_OPTION )
if( playlistManager != null )
{ {
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, auxConfig,
aliasList, aliasList,
new System( "Instrumented" ), new System( "Instrumented" ),
new Site( "Instrumented" ) ); new Site( "Instrumented" ),
"Channel Name" );
mProcessingChain.addModules( modules ); mProcessingChain.addModules( modules );

View File

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

View File

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

View File

@@ -133,29 +133,37 @@ public class P25AudioModule extends Module implements Listener<Message>,
Class temp = Class.forName( "jmbe.JMBEAudioLibrary" ); Class temp = Class.forName( "jmbe.JMBEAudioLibrary" );
library = (AudioConversionLibrary)temp.newInstance(); 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" mCanConvertAudio = true;
+ " - P25 audio will be available" );
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 else
{ {
if( !mLibraryLoadStatusLogged ) mLog.debug( "JMBE library version 0.3.x or higher is required" );
{
mLog.info( "JMBE audio conversion library NOT FOUND" );
mLibraryLoadStatusLogged = true;
}
} }
} }
catch ( ClassNotFoundException e1 ) catch ( ClassNotFoundException e1 )

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem; import javax.sound.sampled.AudioSystem;
@@ -28,6 +29,8 @@ import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line; import javax.sound.sampled.Line;
import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer; import javax.sound.sampled.Mixer;
import javax.sound.sampled.Port;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.TargetDataLine; import javax.sound.sampled.TargetDataLine;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -39,6 +42,7 @@ import source.config.SourceConfigMixer;
import source.config.SourceConfiguration; import source.config.SourceConfiguration;
import source.tuner.MixerTunerDataLine; import source.tuner.MixerTunerDataLine;
import source.tuner.MixerTunerType; import source.tuner.MixerTunerType;
import audio.AudioFormats;
public class MixerManager public class MixerManager
{ {
@@ -47,23 +51,9 @@ public class MixerManager
private static MixerManager sInstance = null; private static MixerManager sInstance = null;
private ArrayList<DiscoveredMixer> mDiscoveredMixers = private List<InputMixerConfiguration> mInputMixers = new ArrayList<>();
new ArrayList<DiscoveredMixer>(); private List<MixerChannelConfiguration> mOutputMixers = new ArrayList<>();
private HashMap<String,MixerTunerDataLine> mMixerTuners = new HashMap<>();
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 MixerManager() private MixerManager()
{ {
@@ -92,7 +82,7 @@ public class MixerManager
if( mixerName != null ) if( mixerName != null )
{ {
DiscoveredMixer mixer = getDiscoveredMixer( mixerName ); InputMixerConfiguration mixer = getInputMixer( mixerName );
if( mixer != null ) if( mixer != null )
{ {
@@ -104,7 +94,8 @@ public class MixerManager
if( channel == MixerChannel.MONO ) if( channel == MixerChannel.MONO )
{ {
DataLine.Info info = DataLine.Info info =
new DataLine.Info(TargetDataLine.class, MONO ); new DataLine.Info(TargetDataLine.class,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO );
TargetDataLine dataLine; TargetDataLine dataLine;
@@ -116,7 +107,7 @@ public class MixerManager
if( dataLine != null ) if( dataLine != null )
{ {
return new RealMixerSource( dataLine, return new RealMixerSource( dataLine,
MONO, AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO,
mixerName, mixerName,
new ShortAdapter() ); new ShortAdapter() );
} }
@@ -132,7 +123,8 @@ public class MixerManager
else else
{ {
DataLine.Info info = DataLine.Info info =
new DataLine.Info(TargetDataLine.class, STEREO ); new DataLine.Info(TargetDataLine.class,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO );
TargetDataLine dataLine; TargetDataLine dataLine;
@@ -143,7 +135,8 @@ public class MixerManager
if( dataLine != null ) if( dataLine != null )
{ {
return new RealMixerSource( dataLine, STEREO, return new RealMixerSource( dataLine,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO,
mixerName, new ChannelShortAdapter( mixerName, new ChannelShortAdapter(
mixerConfig.getChannel() ) ); mixerConfig.getChannel() ) );
} }
@@ -163,15 +156,14 @@ public class MixerManager
return null; return null;
} }
public DiscoveredMixer[] getMixers() public InputMixerConfiguration[] getInputMixers()
{ {
return mDiscoveredMixers.toArray( return mInputMixers.toArray( new InputMixerConfiguration[ mInputMixers.size() ] );
new DiscoveredMixer[ mDiscoveredMixers.size() ] );
} }
public DiscoveredMixer getDiscoveredMixer( String name ) public InputMixerConfiguration getInputMixer( String name )
{ {
for( DiscoveredMixer mixer: mDiscoveredMixers ) for( InputMixerConfiguration mixer: mInputMixers )
{ {
if( mixer.getMixerName().contentEquals( name ) ) if( mixer.getMixerName().contentEquals( name ) )
{ {
@@ -181,6 +173,11 @@ public class MixerManager
return null; return null;
} }
public MixerChannelConfiguration[] getOutputMixers()
{
return mOutputMixers.toArray( new MixerChannelConfiguration[ mOutputMixers.size() ] );
}
public Collection<MixerTunerDataLine> getMixerTunerDataLines() public Collection<MixerTunerDataLine> getMixerTunerDataLines()
{ {
@@ -204,21 +201,28 @@ public class MixerManager
if( mixer != null ) if( mixer != null )
{ {
EnumSet<MixerChannel> channels = getSupportedChannels( mixer ); EnumSet<MixerChannel> inputChannels = getSupportedTargetChannels( mixer );
if( channels != null ) if( inputChannels != null )
{ {
mDiscoveredMixers.add( mInputMixers.add(
new DiscoveredMixer( mixer, channels ) ); new InputMixerConfiguration( mixer, inputChannels ) );
sb.append( "\t[LOADED] Mixer:" + mixerInfo.getName() + sb.append( "\t[LOADED] Input: " + mixerInfo.getName() +
" CHANNELS: " + channels + "\n" ); " CHANNELS: " + inputChannels + "\n" );
} }
else
EnumSet<MixerChannel> outputChannels = getSupportedSourceChannels( mixer );
if( outputChannels != null )
{ {
sb.append( "\t[NOT LOADED] Mixer:" + for( MixerChannel channel: outputChannels )
mixerInfo.getName() + {
" - audio format not supported\n" ); mOutputMixers.add( new MixerChannelConfiguration( mixer, channel ) );
}
sb.append( "\t[LOADED] Output: " + mixerInfo.getName() +
" CHANNELS: " + outputChannels + "\n" );
} }
} }
else else
@@ -249,10 +253,6 @@ public class MixerManager
break; 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; return retVal;
} }
private EnumSet<MixerChannel> getSupportedChannels( Mixer mixer ) private EnumSet<MixerChannel> getSupportedTargetChannels( Mixer mixer )
{ {
DataLine.Info stereoInfo= 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 ); boolean stereoSupported = mixer.isLineSupported( stereoInfo );
DataLine.Info monoInfo= 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 ); boolean monoSupported = mixer.isLineSupported( monoInfo );
@@ -317,6 +319,42 @@ public class MixerManager
return EnumSet.of( MixerChannel.MONO ); 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; return null;
} }
@@ -354,17 +392,24 @@ public class MixerManager
"\n lineclass:" + lineInfo.getLineClass() + "\n lineclass:" + lineInfo.getLineClass() +
"\n" ); "\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(); return sb.toString();
} }
public class DiscoveredMixer public class InputMixerConfiguration
{ {
private Mixer mMixer; private Mixer mMixer;
private EnumSet<MixerChannel> mChannels; private EnumSet<MixerChannel> mChannels;
public DiscoveredMixer( Mixer mixer, EnumSet<MixerChannel> channels ) public InputMixerConfiguration( Mixer mixer, EnumSet<MixerChannel> channels )
{ {
mMixer = mixer; mMixer = mixer;
mChannels = channels; mChannels = channels;
@@ -389,10 +434,14 @@ public class MixerManager
{ {
return mChannels.contains( channel ); return mChannels.contains( channel );
} }
}
public static void main( String[] args )
{
mLog.debug( "Starting ..." );
public String toString() MixerManager.getInstance();
{
return getMixerName(); mLog.debug( "Finished!" );
}
} }
} }

View File

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