diff --git a/build/build.xml b/build/build.xml index 2a9ff1dc..90dd15ac 100644 --- a/build/build.xml +++ b/build/build.xml @@ -5,8 +5,8 @@ - - + + diff --git a/images/audio_muted.png b/images/audio_muted.png new file mode 100644 index 00000000..a52b40ad Binary files /dev/null and b/images/audio_muted.png differ diff --git a/images/audio_unmuted.png b/images/audio_unmuted.png new file mode 100644 index 00000000..3a5d8cc5 Binary files /dev/null and b/images/audio_unmuted.png differ diff --git a/src/audio/AudioChannelPanel.java b/src/audio/AudioChannelPanel.java index 0f217211..ca793384 100644 --- a/src/audio/AudioChannelPanel.java +++ b/src/audio/AudioChannelPanel.java @@ -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, 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 diff --git a/src/audio/AudioChannelsPanel.java b/src/audio/AudioChannelsPanel.java new file mode 100644 index 00000000..07c7604c --- /dev/null +++ b/src/audio/AudioChannelsPanel.java @@ -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 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" ); + } + } +} diff --git a/src/audio/AudioEvent.java b/src/audio/AudioEvent.java index e82bbf14..146d31d2 100644 --- a/src/audio/AudioEvent.java +++ b/src/audio/AudioEvent.java @@ -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; } } diff --git a/src/audio/AudioFormats.java b/src/audio/AudioFormats.java index e9ab4add..f7c58198 100644 --- a/src/audio/AudioFormats.java +++ b/src/audio/AudioFormats.java @@ -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 ); } diff --git a/src/audio/AudioManager.java b/src/audio/AudioManager.java index 344d8a1a..4a26cecc 100644 --- a/src/audio/AudioManager.java +++ b/src/audio/AudioManager.java @@ -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, 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 mAudioPacketQueue = new LinkedTransferQueue<>(); private Map mChannelAssignments = new HashMap<>(); private int mLowestPrioritySource; private List mAvailableAudioOutputs = new ArrayList<>(); - private Map mChannelMap = new HashMap<>(); + private Map mAudioOutputMap = new HashMap<>(); private List mChannelNames = new ArrayList<>(); - private AudioOutput mAudioOutput; - private Listener mConfigurationChangeListener; + private Broadcaster 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, 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, IAudioController mThreadPoolManager = null; mProcessingTask = null; - - mAudioOutput.dispose(); } @Override @@ -221,6 +242,14 @@ public class AudioManager implements Listener, IAudioController } } + public void endAllAudioAssignments() + { + for( AudioOutput output: mAudioOutputMap.values() ) + { + output.receive( new AudioPacket( AudioPacket.Type.END, null ) ); + } + } + public class AudioChannelAssignment implements Listener { private static final long CALL_END_DELAY = 2000; //2 seconds @@ -280,102 +309,91 @@ public class AudioManager implements Listener, IAudioController } } } - + @Override - public List 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 listener ) - { - mConfigurationChangeListener = listener; - } - - @Override - public void addAudioEventListener( String channel, Listener 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 listener ) + public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException { - if( mChannelMap.containsKey( channel ) ) - { - mChannelMap.get( channel ).removeAudioEventListener( listener ); - } + return mMixerChannel; } @Override - public void setAudioMetadataListener( String channel, - Listener listener ) + public List getAudioOutputs() { - if( mChannelMap.containsKey( channel ) ) + List outputs = new ArrayList<>( mAudioOutputMap.values() ); + + Collections.sort( outputs, new Comparator() { - 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 listener ) { - if( mChannelMap.containsKey( channel ) ) - { - mChannelMap.get( channel ).removeAudioMetadataListener(); - } + mControllerBroadcaster.addListener( listener ); + } + + @Override + public void removeControllerListener( Listener listener ) + { + mControllerBroadcaster.removeListener( listener ); } } diff --git a/src/audio/AudioPanel.java b/src/audio/AudioPanel.java index f62b733b..844c3486 100644 --- a/src/audio/AudioPanel.java +++ b/src/audio/AudioPanel.java @@ -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 { 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 { 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 ); } } ); } diff --git a/src/audio/IAudioController.java b/src/audio/IAudioController.java index eeb324e6..4b12b6d0 100644 --- a/src/audio/IAudioController.java +++ b/src/audio/IAudioController.java @@ -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 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 listener ) - throws AudioException; - - public void removeAudioEventListener( String channel, Listener listener ); - - public void setAudioMetadataListener( String channel, Listener listener ); - - public void removeAudioMetadataListener( String channel ); - - public void setConfigurationChangeListener( Listener listener ); + /* Audio Output(s) */ + public List getAudioOutputs(); + + /* Controller Audio Event Listener */ + public void addControllerListener( Listener listener ); + public void removeControllerListener( Listener listener ); } diff --git a/src/audio/output/AudioOutput.java b/src/audio/output/AudioOutput.java index 9b81abf3..6c831b24 100644 --- a/src/audio/output/AudioOutput.java +++ b/src/audio/output/AudioOutput.java @@ -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 private AtomicBoolean mSquelched = new AtomicBoolean( false ); private Listener mAudioMetadataListener; + protected Broadcaster mAudioEventBroadcaster = new Broadcaster<>(); + protected LinkedTransferQueue 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 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 } } + 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 { 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 ); + } + } } diff --git a/src/audio/output/MonoAudioOutput.java b/src/audio/output/MonoAudioOutput.java index 532d4385..5715fa78 100644 --- a/src/audio/output/MonoAudioOutput.java +++ b/src/audio/output/MonoAudioOutput.java @@ -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 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 diff --git a/src/audio/output/StereoAudioOutput.java b/src/audio/output/StereoAudioOutput.java new file mode 100644 index 00000000..bd1d8338 --- /dev/null +++ b/src/audio/output/StereoAudioOutput.java @@ -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 packets = new ArrayList(); + + 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; + } +} diff --git a/src/controller/ControllerPanel.java b/src/controller/ControllerPanel.java index d5eb8de3..6b627649 100644 --- a/src/controller/ControllerPanel.java +++ b/src/controller/ControllerPanel.java @@ -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 ); diff --git a/src/controller/channel/Channel.java b/src/controller/channel/Channel.java index c7d946cc..9d50da47 100644 --- a/src/controller/channel/Channel.java +++ b/src/controller/channel/Channel.java @@ -824,7 +824,7 @@ public class Channel extends Configuration List modules = DecoderFactory.getModules( mChannelType, mResourceManager, mDecodeConfiguration, mRecordConfiguration, mAuxDecodeConfiguration, aliasList, mSystem, - mSite ); + mSite, mName ); mProcessingChain.addModules( modules ); diff --git a/src/controller/channel/ChannelUtils.java b/src/controller/channel/ChannelUtils.java index 1d49ca9e..5f50f6e6 100644 --- a/src/controller/channel/ChannelUtils.java +++ b/src/controller/channel/ChannelUtils.java @@ -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(); + } } } } ); diff --git a/src/instrument/gui/DecoderViewFrame.java b/src/instrument/gui/DecoderViewFrame.java index 3a11edc0..08738bdb 100644 --- a/src/instrument/gui/DecoderViewFrame.java +++ b/src/instrument/gui/DecoderViewFrame.java @@ -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 ); diff --git a/src/module/decode/DecoderFactory.java b/src/module/decode/DecoderFactory.java index 9bf12ffb..4eed26cd 100644 --- a/src/module/decode/DecoderFactory.java +++ b/src/module/decode/DecoderFactory.java @@ -135,10 +135,11 @@ public class DecoderFactory AuxDecodeConfiguration auxConfig, AliasList aliasList, System system, - Site site ) + Site site, + String channelName ) { List 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 modules = new ArrayList(); @@ -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: diff --git a/src/module/decode/mpt1327/MPT1327DecoderState.java b/src/module/decode/mpt1327/MPT1327DecoderState.java index 1f4dac33..ad40aa52 100644 --- a/src/module/decode/mpt1327/MPT1327DecoderState.java +++ b/src/module/decode/mpt1327/MPT1327DecoderState.java @@ -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(); diff --git a/src/module/decode/p25/audio/P25AudioModule.java b/src/module/decode/p25/audio/P25AudioModule.java index a3389f7e..bd68ff3f 100644 --- a/src/module/decode/p25/audio/P25AudioModule.java +++ b/src/module/decode/p25/audio/P25AudioModule.java @@ -133,29 +133,37 @@ public class P25AudioModule extends Module implements Listener, 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 ) diff --git a/src/module/decode/state/AlwaysUnsquelchedDecoderState.java b/src/module/decode/state/AlwaysUnsquelchedDecoderState.java index a93e1090..ad74e2f8 100644 --- a/src/module/decode/state/AlwaysUnsquelchedDecoderState.java +++ b/src/module/decode/state/AlwaysUnsquelchedDecoderState.java @@ -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 ) ); } diff --git a/src/settings/SettingsManager.java b/src/settings/SettingsManager.java index 838f3df1..8d8b31a6 100644 --- a/src/settings/SettingsManager.java +++ b/src/settings/SettingsManager.java @@ -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; diff --git a/src/source/mixer/MixerChannel.java b/src/source/mixer/MixerChannel.java index 7b45ae38..9c6ff309 100644 --- a/src/source/mixer/MixerChannel.java +++ b/src/source/mixer/MixerChannel.java @@ -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 getTargetChannels() + { + return EnumSet.of( LEFT,RIGHT,MONO ); + } + + /** + * Mixer Source Data Line (ie sound card output) channel options + */ + public static EnumSet getSourceChannels() + { + return EnumSet.of( MONO,STEREO ); + } } diff --git a/src/source/mixer/MixerChannelConfiguration.java b/src/source/mixer/MixerChannelConfiguration.java new file mode 100644 index 00000000..0be3b358 --- /dev/null +++ b/src/source/mixer/MixerChannelConfiguration.java @@ -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; + } +} diff --git a/src/source/mixer/MixerEditor.java b/src/source/mixer/MixerEditor.java index effa7f65..3782213b 100644 --- a/src/source/mixer/MixerEditor.java +++ b/src/source/mixer/MixerEditor.java @@ -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 mComboMixers; + private JComboBox mComboMixers; private JComboBoxmComboChannels; 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(); + mComboMixers = new JComboBox(); 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( mixers ) ); + new DefaultComboBoxModel( 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.values() ) ); + EnumSet channels = MixerChannel.getTargetChannels(); + + mComboChannels.setModel( new DefaultComboBoxModel( + channels.toArray( new MixerChannel[ channels.size() ] ) ) ); MixerChannel channel = ((SourceConfigMixer)mConfig).getChannel(); diff --git a/src/source/mixer/MixerManager.java b/src/source/mixer/MixerManager.java index 7217f7d5..7e05e07a 100644 --- a/src/source/mixer/MixerManager.java +++ b/src/source/mixer/MixerManager.java @@ -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 mDiscoveredMixers = - new ArrayList(); - - private HashMap mMixerTuners = - new HashMap(); - - 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 mInputMixers = new ArrayList<>(); + private List mOutputMixers = new ArrayList<>(); + private HashMap 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 getMixerTunerDataLines() { @@ -204,21 +201,28 @@ public class MixerManager if( mixer != null ) { - EnumSet channels = getSupportedChannels( mixer ); + EnumSet 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 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 getSupportedChannels( Mixer mixer ) + private EnumSet 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 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 mChannels; - public DiscoveredMixer( Mixer mixer, EnumSet channels ) + public InputMixerConfiguration( Mixer mixer, EnumSet 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!" ); } } diff --git a/src/util/TimeStamp.java b/src/util/TimeStamp.java index 0985f6cd..94314c12 100644 --- a/src/util/TimeStamp.java +++ b/src/util/TimeStamp.java @@ -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 ) ); } /**