From 3e05a9eb0321e711bae795e5aef8b409d4059f4c Mon Sep 17 00:00:00 2001 From: denny Date: Sat, 3 Oct 2015 17:50:03 -0400 Subject: [PATCH] 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. --- build/build.xml | 4 +- images/audio_muted.png | Bin 0 -> 2772 bytes images/audio_unmuted.png | Bin 0 -> 6987 bytes src/audio/AudioChannelPanel.java | 203 ++++------- src/audio/AudioChannelsPanel.java | 37 ++ src/audio/AudioEvent.java | 7 +- src/audio/AudioFormats.java | 34 +- src/audio/AudioManager.java | 202 +++++----- src/audio/AudioPanel.java | 344 +++++++++++++++--- src/audio/IAudioController.java | 34 +- src/audio/output/AudioOutput.java | 48 ++- src/audio/output/MonoAudioOutput.java | 94 ++--- src/audio/output/StereoAudioOutput.java | 259 +++++++++++++ src/controller/ControllerPanel.java | 1 - src/controller/channel/Channel.java | 2 +- src/controller/channel/ChannelUtils.java | 24 +- src/instrument/gui/DecoderViewFrame.java | 3 +- src/module/decode/DecoderFactory.java | 10 +- .../decode/mpt1327/MPT1327DecoderState.java | 2 +- .../decode/p25/audio/P25AudioModule.java | 36 +- .../state/AlwaysUnsquelchedDecoderState.java | 8 +- src/settings/SettingsManager.java | 2 +- src/source/mixer/MixerChannel.java | 35 +- .../mixer/MixerChannelConfiguration.java | 68 ++++ src/source/mixer/MixerEditor.java | 24 +- src/source/mixer/MixerManager.java | 149 +++++--- src/util/TimeStamp.java | 2 +- 27 files changed, 1174 insertions(+), 458 deletions(-) create mode 100644 images/audio_muted.png create mode 100644 images/audio_unmuted.png create mode 100644 src/audio/AudioChannelsPanel.java create mode 100644 src/audio/output/StereoAudioOutput.java create mode 100644 src/source/mixer/MixerChannelConfiguration.java 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 0000000000000000000000000000000000000000..a52b40adc63ededc791fe22cd6bf325fc8c856c9 GIT binary patch literal 2772 zcmd5;c~leE8lRaAFrgBNf5z3WY+{phYWqUO_-0j1NR4EK#b4Ey-vri?Xy< zN|i!zA;=aK5R}DHM6{wrS(O68$^*+5P)H;!osjp}JLjF#Ki@s)e)rz*{^s|6^L=+q zIpDEJN83mn0015LubBS?0FdZ{0HR4W0VfJW0YLkTJ9CE@_sv*uP_oz1D#7H@tRsHR z5WkSxWTIHzPX5Z5`N-GunB9)+zCRo+iLm-^t0eP0W6aGam6l(*^?S(~YqxbeH}(C_ ze7E1?7;?+Q`;tG3-*h>hRU+_~_wtad2JF{W<_TeJP?i&1%fe{`$I zsZhM-@08IX4J(S_sr4-?)wk9?5N5;B9c0hf@o1tGp zaqDq@tCsU{Q5Anvr$#`{vqt`hQ#R;B?}$0Nu7>mH8T0i<IW}ExSGA26X-t94?AC(pW1CF~dUQLr-A2*SAbd)w=MicDgZb7G)IaotqI?l;~ z3Ovj}X;j|g9ifUtP{%v^m z(J;-uysYjaIIbeYe`wMt=R>^-r^)|>h6G!UdxqCois2%sxgLyGIQ}VTF+$=U-6{qo z^FdBsOrfprA_YAcp7-q5Hj)JE2ZD;T{ZK|%kTL=SX8IgR3% zjN~JoJ(W(97x_k%Yy<4^MB6x>&I9#8>hQqA=w?>#Y6vyYWCzXIOJ$!4kYtALOfVmx z)-+zi6vmoTwhgN%(nk1&hm(Q;CIb#XBu`q+RcJ6^rtsV}uM*Qzt9^%>S0|{|52q8a z4ye%RYD@=SQ39d*;;=ZNB@rn#kX%fl)5-^uVC-L>TE4zn${fz*oG%sn<5zSZ|`5t(3fr0DEoXdNT z->C#bm_moSje@}R+c&h#RzNT3f6Bdxdo*{$W>jL3(6CDCvS(>fZM-m+E+#X~;j7A9 zzl#Ks>1C<`lm*yA{>J_*1WW|Jpr6FQxRwoNBG_A5EA&lz@e*V=_A{22UZjRYB1|X! z<`R!Wj=Aq?S6Kr8Xc`C_;$8;U?KDGI$+USWfv_RSX-WLF;A1L6!6gDR7L^UW+ld~X zbXv4H{Qx#Ig+BjQ479*zYoJib?C;mpZK$iu@Y}(PS}gv3L(1b}K;JakahE*r$OdQwkSH@WB^~toSmj0lAGpqIVd8@c z+s#o(BDwQwp7S*mw=xAz@-`J9*}&Ek&yEDXYEHOPE_ETkFpDKdX3+n;*Z`=0^}lPU z1Dt+dKxXvKqzcHycKcuAqdn9ZbrbMq!3}h~fh=voAQM#`I>cr*QwdaEPp9ERB1rGt zq}fE1EKmzXkRm|Tg3&fx?7t2&&!h=rqJAK@Hw)kmt|X?{S)-ezp5Q=fR!_x|wFgH3 z;NTHy{mqWlum=VVtp!R#bL7-s+!Cwv0nd~AaK3L(n?Bj0&IdNhUtdg&zf7YRdB@ z)_>5cGg*FaEaj1X*q5_&rHzr|k688fU>U(y)TYC-rIvTDDYulS63dau+tIUn+2d!`2)R~*6=5ls+D+y7 z5w?gtl{O<^%U{P$Ls*qZk+);JKV(B@1XHSbP35-%jL7VrjG2kxQ==H zSvGbwAhvjRV~-c!bXg2gK2FAUB(Ap;50UDi^1BNmN)6d6X{&wT*d7+SA5$jdS1b>ngT@%N)`=i}P6~vK>!J^;JzH z-Wt%x9OlU20kJk${OEoZTN!sL4c{h`#C_Rrnc7$@Iox+Kr2e;^!fEu-;xtt8W~yL=-3=UUk*4uKq%{-RH^QdGwVS%#(f!uATX8<+XU? PwFuCF6J=$jvP*XEO=e_NBJ=86 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 XM#rUxkV7^4kPNtd$=bNa5Eb)(neYC{ literal 0 HcmV?d00001 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 ) ); } /**