mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
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:
@@ -5,8 +5,8 @@
|
||||
|
||||
<!-- version properties -->
|
||||
<property name="major" value="0"/>
|
||||
<property name="minor" value="1"/>
|
||||
<property name="build" value="5"/>
|
||||
<property name="minor" value="2"/>
|
||||
<property name="build" value="0"/>
|
||||
|
||||
<!-- Build properties -->
|
||||
<property name="classes" location="../classes"/>
|
||||
|
||||
BIN
images/audio_muted.png
Normal file
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
BIN
images/audio_unmuted.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
@@ -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
|
||||
|
||||
37
src/audio/AudioChannelsPanel.java
Normal file
37
src/audio/AudioChannelsPanel.java
Normal 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" );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
259
src/audio/output/StereoAudioOutput.java
Normal file
259
src/audio/output/StereoAudioOutput.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -154,7 +154,6 @@ public class ControllerPanel extends JPanel
|
||||
|
||||
AudioPanel audioPanel = new AudioPanel( mResourceManager.getSettingsManager(),
|
||||
mResourceManager.getAudioManager() );
|
||||
audioPanel.setPreferredSize( new Dimension( 200, audioPanel.getPreferredHeight() ) );
|
||||
|
||||
JideSplitPane audioChannelListSplit = new JideSplitPane( JideSplitPane.VERTICAL_SPLIT );
|
||||
audioChannelListSplit.setDividerSize( 5 );
|
||||
|
||||
@@ -824,7 +824,7 @@ public class Channel extends Configuration
|
||||
List<Module> modules = DecoderFactory.getModules( mChannelType,
|
||||
mResourceManager, mDecodeConfiguration, mRecordConfiguration,
|
||||
mAuxDecodeConfiguration, aliasList, mSystem,
|
||||
mSite );
|
||||
mSite, mName );
|
||||
|
||||
mProcessingChain.addModules( modules );
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.awt.event.ActionListener;
|
||||
|
||||
import javax.swing.JMenu;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JSeparator;
|
||||
|
||||
import module.decode.event.ActivitySummaryFrame;
|
||||
@@ -101,15 +102,22 @@ public class ChannelUtils
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
/* Disable the channel */
|
||||
channel.setEnabled( DISABLED, BROADCAST_CHANGE );
|
||||
|
||||
/* Broadcast channel deleted event */
|
||||
channel.fireChannelEvent( Event.CHANNEL_DELETED );
|
||||
|
||||
if( playlistManager != null )
|
||||
int response = JOptionPane.showConfirmDialog( anchor,
|
||||
"Do you want to delete channel " + channel.getName() + "?",
|
||||
"Are you sure?", JOptionPane.YES_NO_CANCEL_OPTION );
|
||||
|
||||
if( response == JOptionPane.YES_OPTION )
|
||||
{
|
||||
playlistManager.save();
|
||||
/* Disable the channel */
|
||||
channel.setEnabled( DISABLED, BROADCAST_CHANGE );
|
||||
|
||||
/* Broadcast channel deleted event */
|
||||
channel.fireChannelEvent( Event.CHANNEL_DELETED );
|
||||
|
||||
if( playlistManager != null )
|
||||
{
|
||||
playlistManager.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
@@ -109,7 +109,8 @@ public class DecoderViewFrame extends JInternalFrame
|
||||
auxConfig,
|
||||
aliasList,
|
||||
new System( "Instrumented" ),
|
||||
new Site( "Instrumented" ) );
|
||||
new Site( "Instrumented" ),
|
||||
"Channel Name" );
|
||||
|
||||
mProcessingChain.addModules( modules );
|
||||
|
||||
|
||||
@@ -135,10 +135,11 @@ public class DecoderFactory
|
||||
AuxDecodeConfiguration auxConfig,
|
||||
AliasList aliasList,
|
||||
System system,
|
||||
Site site )
|
||||
Site site,
|
||||
String channelName )
|
||||
{
|
||||
List<Module> modules = getPrimaryModules( channelType, decodeConfig,
|
||||
recordConfig, aliasList, system, site, resourceManager );
|
||||
recordConfig, aliasList, system, site, channelName, resourceManager );
|
||||
|
||||
modules.addAll( getAuxiliaryDecoders( auxConfig, aliasList ) );
|
||||
|
||||
@@ -176,6 +177,7 @@ public class DecoderFactory
|
||||
AliasList aliasList,
|
||||
System system,
|
||||
Site site,
|
||||
String channelName,
|
||||
ResourceManager resourceManager )
|
||||
{
|
||||
List<Module> modules = new ArrayList<Module>();
|
||||
@@ -191,12 +193,12 @@ public class DecoderFactory
|
||||
{
|
||||
case AM:
|
||||
modules.add( new AMDecoder( decodeConfig ) );
|
||||
modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.AM ) );
|
||||
modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.AM, channelName ) );
|
||||
break;
|
||||
case NBFM:
|
||||
modules.add( new AudioModule( recordAudio, NO_REMOVE_DC ) );
|
||||
modules.add( new NBFMDecoder( decodeConfig ) );
|
||||
modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.NBFM ) );
|
||||
modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.NBFM, channelName ) );
|
||||
modules.add( getFMDemodulator( decodeConfig, pass, stop, REMOVE_DC ) );
|
||||
break;
|
||||
case LTR_STANDARD:
|
||||
|
||||
@@ -180,7 +180,7 @@ public class MPT1327DecoderState extends DecoderState
|
||||
}
|
||||
mSite = mpt.getSiteID();
|
||||
|
||||
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.CONTROL ) );
|
||||
broadcast( new DecoderStateEvent( this, Event.START, State.CONTROL ) );
|
||||
break;
|
||||
case GTC:
|
||||
String from = mpt.getFromID();
|
||||
|
||||
@@ -133,29 +133,37 @@ public class P25AudioModule extends Module implements Listener<Message>,
|
||||
Class temp = Class.forName( "jmbe.JMBEAudioLibrary" );
|
||||
|
||||
library = (AudioConversionLibrary)temp.newInstance();
|
||||
|
||||
mAudioConverter = library.getAudioConverter( IMBE_CODEC,
|
||||
AudioFormats.PCM_SIGNED_48KHZ_16BITS );
|
||||
|
||||
if( mAudioConverter != null )
|
||||
if( ( library.getMajorVersion() == 0 && library.getMinorVersion() >= 3 ) ||
|
||||
library.getMajorVersion() > 0 )
|
||||
{
|
||||
mCanConvertAudio = true;
|
||||
mAudioConverter = library.getAudioConverter( IMBE_CODEC,
|
||||
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO );
|
||||
|
||||
if( !mLibraryLoadStatusLogged )
|
||||
if( mAudioConverter != null )
|
||||
{
|
||||
mLog.info( "JMBE audio conversion library successfully loaded"
|
||||
+ " - P25 audio will be available" );
|
||||
mCanConvertAudio = true;
|
||||
|
||||
mLibraryLoadStatusLogged = true;
|
||||
if( !mLibraryLoadStatusLogged )
|
||||
{
|
||||
mLog.info( "JMBE audio conversion library successfully loaded"
|
||||
+ " - P25 audio will be available" );
|
||||
|
||||
mLibraryLoadStatusLogged = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( !mLibraryLoadStatusLogged )
|
||||
{
|
||||
mLog.info( "JMBE audio conversion library NOT FOUND" );
|
||||
mLibraryLoadStatusLogged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( !mLibraryLoadStatusLogged )
|
||||
{
|
||||
mLog.info( "JMBE audio conversion library NOT FOUND" );
|
||||
mLibraryLoadStatusLogged = true;
|
||||
}
|
||||
mLog.debug( "JMBE library version 0.3.x or higher is required" );
|
||||
}
|
||||
}
|
||||
catch ( ClassNotFoundException e1 )
|
||||
|
||||
@@ -24,6 +24,9 @@ import module.decode.state.DecoderStateEvent.Event;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import audio.metadata.Metadata;
|
||||
import audio.metadata.MetadataType;
|
||||
|
||||
/**
|
||||
* Basic decoder channel state - provides the minimum channel state functionality
|
||||
* to support an always un-squelched audio decoder.
|
||||
@@ -34,12 +37,14 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
LoggerFactory.getLogger( AlwaysUnsquelchedDecoderState.class );
|
||||
|
||||
private DecoderType mDecoderType;
|
||||
private String mChannelName;
|
||||
|
||||
public AlwaysUnsquelchedDecoderState( DecoderType decoderType )
|
||||
public AlwaysUnsquelchedDecoderState( DecoderType decoderType, String channelName )
|
||||
{
|
||||
super( null );
|
||||
|
||||
mDecoderType = decoderType;
|
||||
mChannelName = channelName;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,6 +92,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
broadcast( new Metadata( MetadataType.TO, mChannelName, false ) );
|
||||
broadcast( new DecoderStateEvent( this, Event.ALWAYS_UNSQUELCH, State.IDLE ) );
|
||||
}
|
||||
|
||||
|
||||
@@ -434,7 +434,7 @@ public class SettingsManager
|
||||
return null;
|
||||
}
|
||||
|
||||
private ImageIcon getScaledIcon( ImageIcon original, int height )
|
||||
public static ImageIcon getScaledIcon( ImageIcon original, int height )
|
||||
{
|
||||
double scale = (double)original.getIconHeight() / (double)height;
|
||||
|
||||
|
||||
@@ -17,7 +17,40 @@
|
||||
******************************************************************************/
|
||||
package source.mixer;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
public enum MixerChannel
|
||||
{
|
||||
LEFT,RIGHT,MONO;
|
||||
LEFT( "L" ),
|
||||
RIGHT( "R" ),
|
||||
MONO( "M" ),
|
||||
STEREO( "S" );
|
||||
|
||||
private String mLabel;
|
||||
|
||||
private MixerChannel( String label )
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
public String getLabel()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixer Target Data Line (ie sample source) channel options
|
||||
*/
|
||||
public static EnumSet<MixerChannel> getTargetChannels()
|
||||
{
|
||||
return EnumSet.of( LEFT,RIGHT,MONO );
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixer Source Data Line (ie sound card output) channel options
|
||||
*/
|
||||
public static EnumSet<MixerChannel> getSourceChannels()
|
||||
{
|
||||
return EnumSet.of( MONO,STEREO );
|
||||
}
|
||||
}
|
||||
|
||||
68
src/source/mixer/MixerChannelConfiguration.java
Normal file
68
src/source/mixer/MixerChannelConfiguration.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
******************************************************************************/
|
||||
package source.mixer;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
import javax.swing.DefaultComboBoxModel;
|
||||
import javax.swing.JComboBox;
|
||||
@@ -25,13 +27,13 @@ import javax.swing.JLabel;
|
||||
import source.SourceEditor;
|
||||
import source.config.SourceConfigMixer;
|
||||
import source.config.SourceConfiguration;
|
||||
import source.mixer.MixerManager.DiscoveredMixer;
|
||||
import source.mixer.MixerManager.InputMixerConfiguration;
|
||||
import controller.ResourceManager;
|
||||
|
||||
public class MixerEditor extends SourceEditor
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
private JComboBox<DiscoveredMixer> mComboMixers;
|
||||
private JComboBox<InputMixerConfiguration> mComboMixers;
|
||||
private JComboBox<MixerChannel>mComboChannels;
|
||||
protected Mixer.Info mSelectedMixer = null;
|
||||
|
||||
@@ -49,7 +51,7 @@ public class MixerEditor extends SourceEditor
|
||||
JLabel mixerLabel = new JLabel( "Mixer:" );
|
||||
add( mixerLabel, "align right" );
|
||||
|
||||
mComboMixers = new JComboBox<DiscoveredMixer>();
|
||||
mComboMixers = new JComboBox<InputMixerConfiguration>();
|
||||
|
||||
add( mComboMixers, "wrap" );
|
||||
|
||||
@@ -75,7 +77,7 @@ public class MixerEditor extends SourceEditor
|
||||
{
|
||||
SourceConfigMixer config = (SourceConfigMixer)mConfig;
|
||||
|
||||
DiscoveredMixer selectedMixer =
|
||||
InputMixerConfiguration selectedMixer =
|
||||
mComboMixers.getItemAt( mComboMixers.getSelectedIndex() );
|
||||
|
||||
if( selectedMixer != null )
|
||||
@@ -103,10 +105,10 @@ public class MixerEditor extends SourceEditor
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
DiscoveredMixer[] mixers = MixerManager.getInstance().getMixers();
|
||||
InputMixerConfiguration[] mixers = MixerManager.getInstance().getInputMixers();
|
||||
|
||||
mComboMixers.setModel(
|
||||
new DefaultComboBoxModel<DiscoveredMixer>( mixers ) );
|
||||
new DefaultComboBoxModel<InputMixerConfiguration>( mixers ) );
|
||||
|
||||
SourceConfigMixer config = (SourceConfigMixer)mConfig;
|
||||
|
||||
@@ -125,8 +127,8 @@ public class MixerEditor extends SourceEditor
|
||||
{
|
||||
mComboMixers.setSelectedIndex( 0 );
|
||||
|
||||
DiscoveredMixer selectedMixer =
|
||||
(DiscoveredMixer)mComboMixers.getSelectedItem();
|
||||
InputMixerConfiguration selectedMixer =
|
||||
(InputMixerConfiguration)mComboMixers.getSelectedItem();
|
||||
|
||||
config.setMixer( selectedMixer.getMixerName() );
|
||||
}
|
||||
@@ -142,8 +144,10 @@ public class MixerEditor extends SourceEditor
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
mComboChannels.setModel(
|
||||
new DefaultComboBoxModel<MixerChannel>( MixerChannel.values() ) );
|
||||
EnumSet<MixerChannel> channels = MixerChannel.getTargetChannels();
|
||||
|
||||
mComboChannels.setModel( new DefaultComboBoxModel<MixerChannel>(
|
||||
channels.toArray( new MixerChannel[ channels.size() ] ) ) );
|
||||
|
||||
MixerChannel channel = ((SourceConfigMixer)mConfig).getChannel();
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
@@ -28,6 +29,8 @@ import javax.sound.sampled.DataLine;
|
||||
import javax.sound.sampled.Line;
|
||||
import javax.sound.sampled.LineUnavailableException;
|
||||
import javax.sound.sampled.Mixer;
|
||||
import javax.sound.sampled.Port;
|
||||
import javax.sound.sampled.SourceDataLine;
|
||||
import javax.sound.sampled.TargetDataLine;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@@ -39,6 +42,7 @@ import source.config.SourceConfigMixer;
|
||||
import source.config.SourceConfiguration;
|
||||
import source.tuner.MixerTunerDataLine;
|
||||
import source.tuner.MixerTunerType;
|
||||
import audio.AudioFormats;
|
||||
|
||||
public class MixerManager
|
||||
{
|
||||
@@ -47,23 +51,9 @@ public class MixerManager
|
||||
|
||||
private static MixerManager sInstance = null;
|
||||
|
||||
private ArrayList<DiscoveredMixer> mDiscoveredMixers =
|
||||
new ArrayList<DiscoveredMixer>();
|
||||
|
||||
private HashMap<String,MixerTunerDataLine> mMixerTuners =
|
||||
new HashMap<String,MixerTunerDataLine>();
|
||||
|
||||
public static AudioFormat MONO = new AudioFormat( 48000, //SampleRate
|
||||
16, //Sample Size
|
||||
1, //Channels
|
||||
true, //Signed
|
||||
false ); //Little Endian
|
||||
|
||||
public static AudioFormat STEREO = new AudioFormat( 48000, //SampleRate
|
||||
16, //Sample Size
|
||||
2, //Channels
|
||||
true, //Signed
|
||||
false ); //Little Endian
|
||||
private List<InputMixerConfiguration> mInputMixers = new ArrayList<>();
|
||||
private List<MixerChannelConfiguration> mOutputMixers = new ArrayList<>();
|
||||
private HashMap<String,MixerTunerDataLine> mMixerTuners = new HashMap<>();
|
||||
|
||||
private MixerManager()
|
||||
{
|
||||
@@ -92,7 +82,7 @@ public class MixerManager
|
||||
|
||||
if( mixerName != null )
|
||||
{
|
||||
DiscoveredMixer mixer = getDiscoveredMixer( mixerName );
|
||||
InputMixerConfiguration mixer = getInputMixer( mixerName );
|
||||
|
||||
if( mixer != null )
|
||||
{
|
||||
@@ -104,7 +94,8 @@ public class MixerManager
|
||||
if( channel == MixerChannel.MONO )
|
||||
{
|
||||
DataLine.Info info =
|
||||
new DataLine.Info(TargetDataLine.class, MONO );
|
||||
new DataLine.Info(TargetDataLine.class,
|
||||
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO );
|
||||
|
||||
TargetDataLine dataLine;
|
||||
|
||||
@@ -116,7 +107,7 @@ public class MixerManager
|
||||
if( dataLine != null )
|
||||
{
|
||||
return new RealMixerSource( dataLine,
|
||||
MONO,
|
||||
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO,
|
||||
mixerName,
|
||||
new ShortAdapter() );
|
||||
}
|
||||
@@ -132,7 +123,8 @@ public class MixerManager
|
||||
else
|
||||
{
|
||||
DataLine.Info info =
|
||||
new DataLine.Info(TargetDataLine.class, STEREO );
|
||||
new DataLine.Info(TargetDataLine.class,
|
||||
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO );
|
||||
|
||||
TargetDataLine dataLine;
|
||||
|
||||
@@ -143,7 +135,8 @@ public class MixerManager
|
||||
|
||||
if( dataLine != null )
|
||||
{
|
||||
return new RealMixerSource( dataLine, STEREO,
|
||||
return new RealMixerSource( dataLine,
|
||||
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO,
|
||||
mixerName, new ChannelShortAdapter(
|
||||
mixerConfig.getChannel() ) );
|
||||
}
|
||||
@@ -163,15 +156,14 @@ public class MixerManager
|
||||
return null;
|
||||
}
|
||||
|
||||
public DiscoveredMixer[] getMixers()
|
||||
public InputMixerConfiguration[] getInputMixers()
|
||||
{
|
||||
return mDiscoveredMixers.toArray(
|
||||
new DiscoveredMixer[ mDiscoveredMixers.size() ] );
|
||||
return mInputMixers.toArray( new InputMixerConfiguration[ mInputMixers.size() ] );
|
||||
}
|
||||
|
||||
public DiscoveredMixer getDiscoveredMixer( String name )
|
||||
public InputMixerConfiguration getInputMixer( String name )
|
||||
{
|
||||
for( DiscoveredMixer mixer: mDiscoveredMixers )
|
||||
for( InputMixerConfiguration mixer: mInputMixers )
|
||||
{
|
||||
if( mixer.getMixerName().contentEquals( name ) )
|
||||
{
|
||||
@@ -181,6 +173,11 @@ public class MixerManager
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public MixerChannelConfiguration[] getOutputMixers()
|
||||
{
|
||||
return mOutputMixers.toArray( new MixerChannelConfiguration[ mOutputMixers.size() ] );
|
||||
}
|
||||
|
||||
public Collection<MixerTunerDataLine> getMixerTunerDataLines()
|
||||
{
|
||||
@@ -204,21 +201,28 @@ public class MixerManager
|
||||
|
||||
if( mixer != null )
|
||||
{
|
||||
EnumSet<MixerChannel> channels = getSupportedChannels( mixer );
|
||||
EnumSet<MixerChannel> inputChannels = getSupportedTargetChannels( mixer );
|
||||
|
||||
if( channels != null )
|
||||
if( inputChannels != null )
|
||||
{
|
||||
mDiscoveredMixers.add(
|
||||
new DiscoveredMixer( mixer, channels ) );
|
||||
mInputMixers.add(
|
||||
new InputMixerConfiguration( mixer, inputChannels ) );
|
||||
|
||||
sb.append( "\t[LOADED] Mixer:" + mixerInfo.getName() +
|
||||
" CHANNELS: " + channels + "\n" );
|
||||
sb.append( "\t[LOADED] Input: " + mixerInfo.getName() +
|
||||
" CHANNELS: " + inputChannels + "\n" );
|
||||
}
|
||||
else
|
||||
|
||||
EnumSet<MixerChannel> outputChannels = getSupportedSourceChannels( mixer );
|
||||
|
||||
if( outputChannels != null )
|
||||
{
|
||||
sb.append( "\t[NOT LOADED] Mixer:" +
|
||||
mixerInfo.getName() +
|
||||
" - audio format not supported\n" );
|
||||
for( MixerChannel channel: outputChannels )
|
||||
{
|
||||
mOutputMixers.add( new MixerChannelConfiguration( mixer, channel ) );
|
||||
}
|
||||
|
||||
sb.append( "\t[LOADED] Output: " + mixerInfo.getName() +
|
||||
" CHANNELS: " + outputChannels + "\n" );
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -249,10 +253,6 @@ public class MixerManager
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append( "\t[NOT LOADED] Tuner:" + mixerInfo.getName() + " - couldn't get target data line\n" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,15 +288,17 @@ public class MixerManager
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private EnumSet<MixerChannel> getSupportedChannels( Mixer mixer )
|
||||
private EnumSet<MixerChannel> getSupportedTargetChannels( Mixer mixer )
|
||||
{
|
||||
DataLine.Info stereoInfo=
|
||||
new DataLine.Info(TargetDataLine.class, STEREO );
|
||||
new DataLine.Info(TargetDataLine.class,
|
||||
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO );
|
||||
|
||||
boolean stereoSupported = mixer.isLineSupported( stereoInfo );
|
||||
|
||||
DataLine.Info monoInfo=
|
||||
new DataLine.Info(TargetDataLine.class, MONO );
|
||||
new DataLine.Info(TargetDataLine.class,
|
||||
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO );
|
||||
|
||||
boolean monoSupported = mixer.isLineSupported( monoInfo );
|
||||
|
||||
@@ -317,6 +319,42 @@ public class MixerManager
|
||||
return EnumSet.of( MixerChannel.MONO );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns enumset of SourceDataLine (audio output) channels
|
||||
* (MONO and/or STEREO) supported by the mixer, or null if the mixer doesn't
|
||||
* have any source data lines.
|
||||
*/
|
||||
private EnumSet<MixerChannel> getSupportedSourceChannels( Mixer mixer )
|
||||
{
|
||||
DataLine.Info stereoInfo=
|
||||
new DataLine.Info(SourceDataLine.class,
|
||||
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO );
|
||||
|
||||
boolean stereoSupported = mixer.isLineSupported( stereoInfo );
|
||||
|
||||
DataLine.Info monoInfo=
|
||||
new DataLine.Info(SourceDataLine.class,
|
||||
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO );
|
||||
|
||||
boolean monoSupported = mixer.isLineSupported( monoInfo );
|
||||
|
||||
if( stereoSupported && monoSupported )
|
||||
{
|
||||
return EnumSet.of( MixerChannel.MONO,
|
||||
MixerChannel.STEREO );
|
||||
}
|
||||
else if( stereoSupported )
|
||||
{
|
||||
return EnumSet.of( MixerChannel.STEREO );
|
||||
}
|
||||
else if( monoSupported )
|
||||
{
|
||||
return EnumSet.of( MixerChannel.MONO );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -354,17 +392,24 @@ public class MixerManager
|
||||
"\n lineclass:" + lineInfo.getLineClass() +
|
||||
"\n" );
|
||||
}
|
||||
|
||||
Line.Info portInfo = new Line.Info( Port.class );
|
||||
|
||||
if( mixer.isLineSupported( portInfo ) )
|
||||
{
|
||||
sb.append( "**PORT LINE IS SUPPORTED BY THIS MIXER***\n" );
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public class DiscoveredMixer
|
||||
public class InputMixerConfiguration
|
||||
{
|
||||
private Mixer mMixer;
|
||||
private EnumSet<MixerChannel> mChannels;
|
||||
|
||||
public DiscoveredMixer( Mixer mixer, EnumSet<MixerChannel> channels )
|
||||
public InputMixerConfiguration( Mixer mixer, EnumSet<MixerChannel> channels )
|
||||
{
|
||||
mMixer = mixer;
|
||||
mChannels = channels;
|
||||
@@ -389,10 +434,14 @@ public class MixerManager
|
||||
{
|
||||
return mChannels.contains( channel );
|
||||
}
|
||||
}
|
||||
|
||||
public static void main( String[] args )
|
||||
{
|
||||
mLog.debug( "Starting ..." );
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return getMixerName();
|
||||
}
|
||||
MixerManager.getInstance();
|
||||
|
||||
mLog.debug( "Finished!" );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public class TimeStamp
|
||||
*/
|
||||
public static String getFormattedTimeWithMilliseconds( long timestamp )
|
||||
{
|
||||
return mSDFTime.format( new Date( timestamp ) );
|
||||
return mSDFTimeWithMilliseconds.format( new Date( timestamp ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user