From 4be63d4b95fbc05f5272a96ca2f8871847032ed7 Mon Sep 17 00:00:00 2001 From: denny Date: Sun, 10 Jan 2016 18:02:04 -0500 Subject: [PATCH] Work in progress. -Started work on new channel editor channel viewer/controller -Removed a number of unused buffer package classes -Moved automatic frequency control to be a module and removed special handling -Added IUnfilteredRealBufferListener & Provider interfaces -Completely removed the ResourceManager class --- .classpath | 5 + images/control_play.png | Bin 0 -> 592 bytes images/control_stop.png | Bin 0 -> 403 bytes images/no_icon.png | Bin 688 -> 743 bytes imports/tablefilter-swing-5.2.1.jar | Bin 0 -> 181090 bytes src/alias/AliasDirectoryNode.java | 20 +- src/alias/AliasEditor.java | 30 +- src/alias/AliasIDNode.java | 7 +- src/alias/AliasListNode.java | 18 +- src/alias/AliasNode.java | 78 +-- src/alias/GroupNode.java | 17 +- src/alias/action/AliasActionNode.java | 11 +- src/alias/action/beep/BeepActionNode.java | 5 +- src/alias/action/clip/ClipActionNode.java | 5 +- src/alias/action/script/ScriptActionNode.java | 5 +- src/alias/id/esn/ESNNode.java | 5 +- src/alias/id/fleetsync/FleetsyncIDNode.java | 5 +- src/alias/id/fleetsync/StatusIDNode.java | 5 +- src/alias/id/lojack/LoJackIDNode.java | 5 +- src/alias/id/mdc/MDC1200IDNode.java | 5 +- src/alias/id/mobileID/MINNode.java | 5 +- src/alias/id/mpt1327/MPT1327IDNode.java | 5 +- src/alias/id/siteID/SiteIDNode.java | 5 +- src/alias/id/talkgroup/TalkgroupIDNode.java | 5 +- src/alias/id/uniqueID/UniqueIDNode.java | 5 +- src/alias/priority/PriorityNode.java | 5 +- src/alias/record/NonRecordableNode.java | 5 +- src/audio/AudioModule.java | 40 +- src/audio/AudioPanel.java | 9 +- src/buffer/BufferOverflowException.java | 33 - src/buffer/BufferOverflowListener.java | 23 - src/buffer/ByteArrayDelayBuffer.java | 60 -- src/buffer/ByteArrayListener.java | 23 - src/buffer/ByteBufferListener.java | 25 - src/buffer/ByteBufferProvider.java | 33 - src/buffer/ComplexByteBuffer.java | 91 --- src/buffer/ComplexSampleBufferAssembler.java | 73 --- src/buffer/DelayBuffer.java | 67 -- .../FloatArrayCircularAveragingBuffer.java | 97 --- src/buffer/FloatBufferAssembler.java | 70 --- src/buffer/FloatCircularAveragingBuffer.java | 110 ---- src/buffer/FloatCircularAveragingBuffer2.java | 68 -- src/buffer/FloatSampleBuffer.java | 66 -- src/buffer/FloatSampleBufferAssembler.java | 72 --- src/buffer/FloatTappedCircularBuffer.java | 86 --- src/buffer/FloatToByteBufferAssembler.java | 69 --- src/buffer/StereoByteBuffer.java | 59 -- src/buffer/StereoByteBufferListener.java | 24 - src/buffer/StereoByteBufferProvider.java | 32 - .../StereoToMonoByteBufferConverter.java | 58 -- src/controller/ConfigurableNode.java | 14 +- .../ConfigurationControllerModel.java | 63 +- src/controller/ControllerPanel.java | 43 +- ...ditor.java => OldConfigurationEditor.java} | 4 +- src/controller/ResourceManager.java | 177 ------ .../channel/AbstractChannelEditor.java | 13 +- src/controller/channel/Channel.java | 584 ++++-------------- .../ChannelConfigurationEditor.java} | 37 +- src/controller/channel/ChannelEditor.java | 284 ++++----- src/controller/channel/ChannelEvent.java | 2 + src/controller/channel/ChannelModel.java | 346 +++++++++++ src/controller/channel/ChannelNode.java | 33 +- .../channel/ChannelProcessingManager.java | 301 +++++++++ .../channel/ChannelSelectionManager.java | 71 +++ src/controller/channel/ChannelUtils.java | 45 +- .../channel/ChannelValidationException.java | 11 - .../ConfigurationValidationException.java | 23 + .../channel/IChannelEventBroadcaster.java} | 14 +- .../channel/NameConfigurationEditor.java | 221 +++++++ src/controller/channel/OldChannelEditor.java | 260 ++++++++ .../channel/map/ChannelMapListNode.java | 9 +- .../channel/map/ChannelMapNode.java | 5 +- src/controller/site/SiteNode.java | 31 +- src/controller/system/SystemListNode.java | 29 +- src/controller/system/SystemNode.java | 26 +- .../filter/dc/DCRemovalFilter_RealBuffer.java | 60 ++ src/dsp/psk/LSMDemodulator.java | 8 +- src/dsp/psk/LSMDemodulator2.java | 8 +- src/gui/SDRTrunk.java | 80 ++- src/gui/control/JFrequencyControl.java | 23 +- .../InstrumentableProcessingChain.java | 2 + src/instrument/gui/DecoderSelectionFrame.java | 15 +- src/instrument/gui/DecoderViewFrame.java | 38 +- src/map/IconManager.java | 26 +- src/map/MapPanel.java | 7 +- src/map/MapService.java | 10 +- src/module/ProcessingChain.java | 209 ++++--- .../decode/AuxDecodeComponentEditor.java | 25 +- src/module/decode/DecodeComponentEditor.java | 60 +- src/module/decode/DecodeEditor.java | 21 +- src/module/decode/DecoderFactory.java | 142 ++--- src/module/decode/event/CallEventPanel.java | 71 ++- .../decode/event/MessageActivityPanel.java | 125 ++-- .../decode/fleetsync2/Fleetsync2Decoder.java | 6 +- src/module/decode/lj1200/LJ1200Decoder.java | 6 +- src/module/decode/ltrnet/LTRNetDecoder.java | 11 +- .../ltrstandard/LTRStandardDecoder.java | 10 +- src/module/decode/mdc1200/MDCDecoder.java | 6 +- .../decode/mpt1327/MPT1327ConfigEditor.java | 73 ++- src/module/decode/mpt1327/MPT1327Decoder.java | 6 +- src/module/decode/p25/C4FMSymbolFilter.java | 110 +++- src/module/decode/p25/P25DecodeEditor.java | 64 +- src/module/decode/p25/P25Decoder.java | 4 +- src/module/decode/p25/P25_C4FMDecoder.java | 70 ++- src/module/decode/p25/P25_LSMDecoder.java | 13 - .../decode/passport/PassportDecoder.java | 13 +- .../decode/state/ChannelCollectionPanel.java | 14 +- src/module/decode/state/ChannelList.java | 14 +- .../decode/state/ChannelStatePanel.java | 49 +- .../decode/state/TrafficChannelManager.java | 92 ++- src/module/decode/tait/Tait1200Decoder.java | 6 +- .../demodulate/am/AMDemodulatorModule.java | 8 +- .../audio/DemodulatedAudioFilterModule.java | 101 +++ .../demodulate/fm/FMDemodulatorModule.java | 122 +--- src/module/log/EventLogComponentEditor.java | 28 +- src/playlist/PlaylistEditor.java | 88 +++ src/playlist/PlaylistManager.java | 74 ++- src/playlist/PlaylistNode.java | 39 +- src/record/RecordComponentEditor.java | 29 +- src/record/RecordEditor.java | 21 +- src/record/RecorderManager.java | 9 +- src/record/wave/RealBufferWaveRecorder.java | 6 +- .../real/IFilteredRealBufferListener.java | 8 + .../real/IFilteredRealBufferProvider.java | 9 + src/sample/real/IRealBufferListener.java | 8 - src/sample/real/IRealBufferProvider.java | 9 - .../real/IUnFilteredRealBufferListener.java | 8 + .../real/IUnFilteredRealBufferProvider.java | 9 + src/settings/SettingsManager.java | 8 +- src/source/ComplexSource.java | 4 +- src/source/EmptySourceEditor.java | 19 +- src/source/FloatSource.java | 12 - src/source/RealSource.java | 4 +- src/source/Source.java | 12 +- src/source/SourceComponentEditor.java | 44 +- src/source/SourceEditor.java | 17 +- src/source/SourceEditorFactory.java | 52 -- src/source/SourceManager.java | 79 ++- src/source/mixer/MixerEditor.java | 25 +- src/source/mixer/MixerManager.java | 17 +- src/source/mixer/RealMixerSource.java | 26 +- src/source/recording/Recording.java | 25 +- src/source/recording/RecordingEditor.java | 27 +- src/source/recording/RecordingGroupNode.java | 28 +- src/source/recording/RecordingNode.java | 7 +- .../recording/RecordingSourceManager.java | 53 +- .../tuner/ITunerChannelProvider.java} | 54 +- src/source/tuner/Tuner.java | 36 +- src/source/tuner/TunerChannelProvider.java | 28 - src/source/tuner/TunerChannelSource.java | 266 ++++---- src/source/tuner/TunerController.java | 15 +- src/source/tuner/TunerEditor.java | 22 +- src/source/tuner/TunerManager.java | 46 +- src/source/tuner/TunerNode.java | 36 +- src/source/tuner/airspy/AirspyTuner.java | 6 +- .../airspy/AirspyTunerConfigurationPanel.java | 30 +- .../tuner/airspy/AirspyTunerEditorPanel.java | 19 +- src/source/tuner/ettus/B100Tuner.java | 6 +- .../tuner/ettus/B100TunerEditorPanel.java | 8 +- src/source/tuner/fcd/FCDTuner.java | 9 +- src/source/tuner/fcd/FCDTunerController.java | 4 +- .../proV1/FCD1TunerConfigurationPanel.java | 26 +- .../tuner/fcd/proV1/FCD1TunerController.java | 6 +- .../tuner/fcd/proV1/FCD1TunerEditorPanel.java | 19 +- .../FCD2TunerConfigurationPanel.java | 26 +- .../fcd/proplusV2/FCD2TunerController.java | 6 +- .../fcd/proplusV2/FCD2TunerEditorPanel.java | 19 +- .../AutomaticFrequencyControl_R.java | 181 ------ .../AutomaticFrequencyControl_RB.java | 197 ------ .../frequency/DCTrackingFrequencyControl.java | 135 ++++ .../frequency/FrequencyChangeBroadcaster.java | 26 - .../tuner/frequency/FrequencyChangeEvent.java | 11 +- .../FrequencyChangeProcessorWrapper.java | 32 + .../tuner/frequency/FrequencyController.java | 24 +- .../frequency/FrequencyCorrectionControl.java | 161 ----- ...ner.java => IFrequencyChangeListener.java} | 6 +- .../frequency/IFrequencyChangeProcessor.java | 6 + .../frequency/IFrequencyChangeProvider.java | 9 + .../IFrequencyCorrectionController.java | 8 - src/source/tuner/hackrf/HackRFTuner.java | 6 +- .../hackrf/HackRFTunerConfigurationPanel.java | 25 +- .../tuner/hackrf/HackRFTunerEditorPanel.java | 20 +- src/source/tuner/rtl/RTL2832Tuner.java | 10 +- .../tuner/rtl/RTL2832TunerController.java | 6 +- .../rtl/e4k/E4KTunerConfigurationPanel.java | 28 +- .../tuner/rtl/e4k/E4KTunerController.java | 6 +- .../tuner/rtl/e4k/E4KTunerEditorPanel.java | 17 +- .../r820t/R820TTunerConfigurationPanel.java | 26 +- .../tuner/rtl/r820t/R820TTunerController.java | 6 +- .../rtl/r820t/R820TTunerEditorPanel.java | 17 +- src/source/wave/ComplexWaveSource.java | 35 +- src/source/wave/RealWaveSource.java | 39 +- src/spectrum/ChannelSpectrumPanel.java | 75 +-- src/spectrum/DFTProcessor.java | 6 +- src/spectrum/OverlayPanel.java | 18 +- src/spectrum/SpectralDisplayPanel.java | 58 +- src/spectrum/SpectrumFrame.java | 12 +- 197 files changed, 4181 insertions(+), 4439 deletions(-) create mode 100644 images/control_play.png create mode 100644 images/control_stop.png create mode 100644 imports/tablefilter-swing-5.2.1.jar delete mode 100644 src/buffer/BufferOverflowException.java delete mode 100644 src/buffer/BufferOverflowListener.java delete mode 100644 src/buffer/ByteArrayDelayBuffer.java delete mode 100644 src/buffer/ByteArrayListener.java delete mode 100644 src/buffer/ByteBufferListener.java delete mode 100644 src/buffer/ByteBufferProvider.java delete mode 100644 src/buffer/ComplexByteBuffer.java delete mode 100644 src/buffer/ComplexSampleBufferAssembler.java delete mode 100644 src/buffer/DelayBuffer.java delete mode 100644 src/buffer/FloatArrayCircularAveragingBuffer.java delete mode 100644 src/buffer/FloatBufferAssembler.java delete mode 100644 src/buffer/FloatCircularAveragingBuffer.java delete mode 100644 src/buffer/FloatCircularAveragingBuffer2.java delete mode 100644 src/buffer/FloatSampleBuffer.java delete mode 100644 src/buffer/FloatSampleBufferAssembler.java delete mode 100644 src/buffer/FloatTappedCircularBuffer.java delete mode 100644 src/buffer/FloatToByteBufferAssembler.java delete mode 100644 src/buffer/StereoByteBuffer.java delete mode 100644 src/buffer/StereoByteBufferListener.java delete mode 100644 src/buffer/StereoByteBufferProvider.java delete mode 100644 src/buffer/StereoToMonoByteBufferConverter.java rename src/controller/{ConfigurationEditor.java => OldConfigurationEditor.java} (95%) delete mode 100644 src/controller/ResourceManager.java rename src/controller/{Editor.java => channel/ChannelConfigurationEditor.java} (58%) create mode 100644 src/controller/channel/ChannelModel.java create mode 100644 src/controller/channel/ChannelProcessingManager.java create mode 100644 src/controller/channel/ChannelSelectionManager.java delete mode 100644 src/controller/channel/ChannelValidationException.java create mode 100644 src/controller/channel/ConfigurationValidationException.java rename src/{buffer/ComplexByteBufferListener.java => controller/channel/IChannelEventBroadcaster.java} (77%) create mode 100644 src/controller/channel/NameConfigurationEditor.java create mode 100644 src/controller/channel/OldChannelEditor.java create mode 100644 src/dsp/filter/dc/DCRemovalFilter_RealBuffer.java create mode 100644 src/module/demodulate/audio/DemodulatedAudioFilterModule.java create mode 100644 src/playlist/PlaylistEditor.java create mode 100644 src/sample/real/IFilteredRealBufferListener.java create mode 100644 src/sample/real/IFilteredRealBufferProvider.java delete mode 100644 src/sample/real/IRealBufferListener.java delete mode 100644 src/sample/real/IRealBufferProvider.java create mode 100644 src/sample/real/IUnFilteredRealBufferListener.java create mode 100644 src/sample/real/IUnFilteredRealBufferProvider.java delete mode 100644 src/source/FloatSource.java delete mode 100644 src/source/SourceEditorFactory.java rename src/{buffer/ShortComplexByteBuffer.java => source/tuner/ITunerChannelProvider.java} (54%) delete mode 100644 src/source/tuner/TunerChannelProvider.java delete mode 100644 src/source/tuner/frequency/AutomaticFrequencyControl_R.java delete mode 100644 src/source/tuner/frequency/AutomaticFrequencyControl_RB.java create mode 100644 src/source/tuner/frequency/DCTrackingFrequencyControl.java delete mode 100644 src/source/tuner/frequency/FrequencyChangeBroadcaster.java create mode 100644 src/source/tuner/frequency/FrequencyChangeProcessorWrapper.java delete mode 100644 src/source/tuner/frequency/FrequencyCorrectionControl.java rename src/source/tuner/frequency/{FrequencyChangeListener.java => IFrequencyChangeListener.java} (90%) create mode 100644 src/source/tuner/frequency/IFrequencyChangeProcessor.java create mode 100644 src/source/tuner/frequency/IFrequencyChangeProvider.java delete mode 100644 src/source/tuner/frequency/IFrequencyCorrectionController.java diff --git a/.classpath b/.classpath index a630c18d..2217d1f1 100644 --- a/.classpath +++ b/.classpath @@ -25,5 +25,10 @@ + + + + + diff --git a/images/control_play.png b/images/control_play.png new file mode 100644 index 0000000000000000000000000000000000000000..0846555d0ca84cb99d4c70dad80144a232604041 GIT binary patch literal 592 zcmV-W0k7R5;6} zlgp~&KoEw{L*wq6jM9+RMU)_iNO6K~b@$|K=nj zb2!5=4Mjq_zrU*f>U2#vSVnMZ9ja4cY`AdOM*t}k^goWqfa3Iq(>2kSH;P81hAqIyBm_{t1>+!KRdtb~{1AK7>C~ zD-Nov`UX!X6ET_La7f{B_|*cRuZGR%^C`gjd@jmW6h*+;8;{2{8ja|Fzf-YTq+l@k zGLg?!DijI~9$+CG0P)O;^z5vZ zY!uIB*x&E}vNJj4{?GTJBigE^o7UKdzE#&EBXnfjM2N9qUNJ=7T*(!I*v$dVF@wV! zPcbfCO)dpCHwm6#49koVc}1IZ;f0opGWdxBx;Rl@XzG}46S&UgQ6wI6lQE987w+r= zQ{sp)?}bM^P?P)rx$R<IWI`!e`47o+X|cM zhswMI)6H-!+|Ry*GtmT2KZBZTgf*vuh@Iv~E3-o`X2T^+DyW5TGfmLUU@4QG+c^0c z#zg&+`J#c@0((YV&ur`*Iawk7JMET#>EkfEJtz9cD4oNF6e!0Aw?I&|=qKwHAy9XoQ zA^YBJr@&gw)O7(`yrYQGx{Yirh2eMScnk9(q^&}_PQ*$`FTOgh0*()5P;Djfw78dp Z{0&Uoz&aZx@__&V002ovPDHLkV1iM?Q(XW6 literal 688 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

7X;Vp{~T^h)odJa)sXl-E=^62 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

Vi?P8EgPo`UsT2h^tGuEo;vUbNci+z`!vJ1_ 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 literal 0 HcmV?d00001 diff --git a/src/alias/AliasDirectoryNode.java b/src/alias/AliasDirectoryNode.java index a616b755..c44d32b9 100644 --- a/src/alias/AliasDirectoryNode.java +++ b/src/alias/AliasDirectoryNode.java @@ -23,15 +23,27 @@ import java.awt.event.ActionListener; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; +import settings.SettingsManager; import controller.ConfigurableNode; public class AliasDirectoryNode extends ConfigurableNode { private static final long serialVersionUID = 1L; + + private PlaylistManager mPlaylistManager; - public AliasDirectoryNode( AliasDirectory directory ) + private SettingsManager mSettingsManager; + + public AliasDirectoryNode( PlaylistManager playlistManager, + SettingsManager settingsManager, + AliasDirectory directory ) { - super( directory ); + super( playlistManager, directory ); + + mPlaylistManager = playlistManager; + + mSettingsManager = settingsManager; } public AliasDirectory getAliasDirectory() @@ -43,7 +55,7 @@ public class AliasDirectoryNode extends ConfigurableNode { for( AliasList list: getAliasDirectory().getAliasList() ) { - AliasListNode node = new AliasListNode( list ); + AliasListNode node = new AliasListNode( mPlaylistManager, mSettingsManager, list ); getModel().addNode( node, AliasDirectoryNode.this, getChildCount() ); @@ -72,7 +84,7 @@ public class AliasDirectoryNode extends ConfigurableNode getAliasDirectory().addAliasList( list ); - AliasListNode node = new AliasListNode( list ); + AliasListNode node = new AliasListNode( mPlaylistManager, mSettingsManager, list ); getModel().addNode( node, AliasDirectoryNode.this, diff --git a/src/alias/AliasEditor.java b/src/alias/AliasEditor.java index 77be321b..cb4f9682 100644 --- a/src/alias/AliasEditor.java +++ b/src/alias/AliasEditor.java @@ -19,6 +19,7 @@ package alias; import java.awt.Color; import java.awt.Dimension; +import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -32,19 +33,19 @@ import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.tree.TreePath; +import map.IconManager; import map.MapIcon; import map.MapIconListCellRenderer; import net.miginfocom.swing.MigLayout; import settings.SettingsManager; import controller.ConfigurableNode; -import controller.ResourceManager; public class AliasEditor extends JPanel implements ActionListener { private static final long serialVersionUID = 1L; private AliasNode mAliasNode; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; private JLabel mLabelName; private JTextField mTextAlias; @@ -54,10 +55,11 @@ public class AliasEditor extends JPanel implements ActionListener private JComboBox mComboMapIcon; private JButton mBtnIconManager; - public AliasEditor( AliasNode aliasNode, ResourceManager resourceManager ) + public AliasEditor( AliasNode aliasNode, + SettingsManager settingsManager ) { mAliasNode = aliasNode; - mResourceManager = resourceManager; + mSettingsManager = settingsManager; initGUI(); } @@ -98,8 +100,7 @@ public class AliasEditor extends JPanel implements ActionListener add( new JLabel( "Icon:" ) ); - mComboMapIcon = new JComboBox( - mResourceManager.getSettingsManager().getMapIcons() ); + mComboMapIcon = new JComboBox( mSettingsManager.getMapIcons() ); MapIconListCellRenderer renderer = new MapIconListCellRenderer(); renderer.setPreferredSize( new Dimension( 200, 30 ) ); @@ -112,8 +113,7 @@ public class AliasEditor extends JPanel implements ActionListener iconName = SettingsManager.DEFAULT_ICON; } - MapIcon savedIcon = mResourceManager.getSettingsManager() - .getMapIcon( iconName ); + MapIcon savedIcon = mSettingsManager.getMapIcon( iconName ); if( savedIcon != null ) { @@ -131,7 +131,17 @@ public class AliasEditor extends JPanel implements ActionListener @Override public void actionPerformed( ActionEvent arg0 ) { - mResourceManager.getController().showIconManager(); + final IconManager iconManager = + new IconManager( mSettingsManager, AliasEditor.this ); + + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + iconManager.setVisible( true ); + } + } ); } } ); add( mBtnIconManager, "span 2,wrap" ); @@ -194,7 +204,7 @@ public class AliasEditor extends JPanel implements ActionListener mButtonColor.setBackground( mAliasNode.getAlias().getMapColor() ); mButtonColor.setForeground( mAliasNode.getAlias().getMapColor() ); - MapIcon savedIcon = mResourceManager.getSettingsManager() + MapIcon savedIcon = mSettingsManager .getMapIcon( mAliasNode.getAlias().getIconName() ); mComboMapIcon.setSelectedItem( savedIcon ); diff --git a/src/alias/AliasIDNode.java b/src/alias/AliasIDNode.java index c861c56a..3852b44d 100644 --- a/src/alias/AliasIDNode.java +++ b/src/alias/AliasIDNode.java @@ -17,19 +17,20 @@ ******************************************************************************/ package alias; +import playlist.PlaylistManager; import controller.ConfigurableNode; public class AliasIDNode extends ConfigurableNode { private static final long serialVersionUID = 1L; - public AliasIDNode( AliasID id ) + public AliasIDNode( PlaylistManager playlistManager, AliasID id ) { - super( id ); + super( playlistManager, id ); } public void save() { - getModel().getResourceManager().getPlaylistManager().save(); + getPlaylistManager().save(); } } diff --git a/src/alias/AliasListNode.java b/src/alias/AliasListNode.java index 65632971..573708ce 100644 --- a/src/alias/AliasListNode.java +++ b/src/alias/AliasListNode.java @@ -26,15 +26,25 @@ import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JSeparator; +import playlist.PlaylistManager; +import settings.SettingsManager; import controller.ConfigurableNode; public class AliasListNode extends ConfigurableNode { private static final long serialVersionUID = 1L; + + private PlaylistManager mPlaylistManager; + private SettingsManager mSettingsManager; - public AliasListNode( AliasList list ) + public AliasListNode( PlaylistManager playlistManager, + SettingsManager settingsManager, + AliasList list ) { - super( list ); + super( playlistManager, list ); + + mPlaylistManager = playlistManager; + mSettingsManager = settingsManager; } @Override @@ -52,7 +62,7 @@ public class AliasListNode extends ConfigurableNode { for( Group group: getList().getGroup() ) { - GroupNode node = new GroupNode( group ); + GroupNode node = new GroupNode( mPlaylistManager, mSettingsManager, group ); getModel().addNode( node, AliasListNode.this, getChildCount() ); @@ -81,7 +91,7 @@ public class AliasListNode extends ConfigurableNode getList().addGroup( group ); - GroupNode node = new GroupNode( group ); + GroupNode node = new GroupNode( mPlaylistManager, mSettingsManager, group ); getModel().addNode( node, AliasListNode.this, diff --git a/src/alias/AliasNode.java b/src/alias/AliasNode.java index 43de7703..070e3b60 100644 --- a/src/alias/AliasNode.java +++ b/src/alias/AliasNode.java @@ -27,6 +27,8 @@ import javax.swing.JPanel; import javax.swing.JPopupMenu; import map.MapIcon; +import playlist.PlaylistManager; +import settings.SettingsManager; import alias.action.AliasAction; import alias.action.beep.BeepAction; import alias.action.beep.BeepActionNode; @@ -64,15 +66,22 @@ public class AliasNode extends ConfigurableNode { private static final long serialVersionUID = 1L; - public AliasNode( Alias alias ) + private SettingsManager mSettingsManager; + private PlaylistManager mPlaylistManager; + + public AliasNode( PlaylistManager playlistManager, + SettingsManager settingsManager, + Alias alias ) { - super( alias ); + super( playlistManager, alias ); + + mSettingsManager = settingsManager; } @Override public JPanel getEditor() { - return new AliasEditor( this, getModel().getResourceManager() ); + return new AliasEditor( this, mSettingsManager ); } public Alias getAlias() @@ -84,8 +93,7 @@ public class AliasNode extends ConfigurableNode { String icon = getAlias().getIconName(); - MapIcon mapIcon = getModel().getResourceManager() - .getSettingsManager().getMapIcon( icon ); + MapIcon mapIcon = mSettingsManager.getMapIcon( icon ); if( mapIcon != null ) { @@ -104,51 +112,51 @@ public class AliasNode extends ConfigurableNode switch( aliasID.getType() ) { case ESN: - getModel().addNode( new ESNNode( (Esn)aliasID ), + getModel().addNode( new ESNNode( mPlaylistManager, (Esn)aliasID ), AliasNode.this, getChildCount() ); break; case Fleetsync: - getModel().addNode( new FleetsyncIDNode( (FleetsyncID)aliasID ), + getModel().addNode( new FleetsyncIDNode( mPlaylistManager, (FleetsyncID)aliasID ), AliasNode.this, getChildCount() ); break; case LoJack: - getModel().addNode( new LoJackIDNode( (LoJackFunctionAndID)aliasID ), + getModel().addNode( new LoJackIDNode( mPlaylistManager, (LoJackFunctionAndID)aliasID ), AliasNode.this, getChildCount() ); break; case LTRNetUID: - getModel().addNode( new UniqueIDNode( (UniqueID)aliasID ), + getModel().addNode( new UniqueIDNode( mPlaylistManager, (UniqueID)aliasID ), AliasNode.this, getChildCount() ); break; case MDC1200: - getModel().addNode( new MDC1200IDNode( (MDC1200ID)aliasID ), + getModel().addNode( new MDC1200IDNode( mPlaylistManager, (MDC1200ID)aliasID ), AliasNode.this, getChildCount() ); break; case MIN: - getModel().addNode( new MINNode( (Min)aliasID ), + getModel().addNode( new MINNode( mPlaylistManager, (Min)aliasID ), AliasNode.this, getChildCount() ); break; case MPT1327: - getModel().addNode( new MPT1327IDNode( (MPT1327ID)aliasID ), + getModel().addNode( new MPT1327IDNode( mPlaylistManager, (MPT1327ID)aliasID ), AliasNode.this, getChildCount() ); break; case NonRecordable: - getModel().addNode( new NonRecordableNode( (NonRecordable)aliasID ), + getModel().addNode( new NonRecordableNode( mPlaylistManager, (NonRecordable)aliasID ), AliasNode.this, getChildCount() ); break; case Priority: - getModel().addNode( new PriorityNode( (Priority)aliasID ), + getModel().addNode( new PriorityNode( mPlaylistManager, (Priority)aliasID ), AliasNode.this, getChildCount() ); break; case Site: - getModel().addNode( new SiteIDNode( (SiteID)aliasID ), + getModel().addNode( new SiteIDNode( mPlaylistManager, (SiteID)aliasID ), AliasNode.this, getChildCount() ); break; case Status: - getModel().addNode( new StatusIDNode( (StatusID)aliasID ), + getModel().addNode( new StatusIDNode( mPlaylistManager, (StatusID)aliasID ), AliasNode.this, getChildCount() ); break; case Talkgroup: - getModel().addNode( new TalkgroupIDNode( (TalkgroupID)aliasID ), + getModel().addNode( new TalkgroupIDNode( mPlaylistManager, (TalkgroupID)aliasID ), AliasNode.this, getChildCount() ); break; default: @@ -161,17 +169,17 @@ public class AliasNode extends ConfigurableNode { if( action instanceof BeepAction ) { - getModel().addNode( new BeepActionNode( (BeepAction)action ), + getModel().addNode( new BeepActionNode( mPlaylistManager, (BeepAction)action ), AliasNode.this, getChildCount() ); } else if( action instanceof ClipAction ) { - getModel().addNode( new ClipActionNode( (ClipAction)action ), + getModel().addNode( new ClipActionNode( mPlaylistManager, (ClipAction)action ), AliasNode.this, getChildCount() ); } else if( action instanceof ScriptAction ) { - getModel().addNode( new ScriptActionNode( (ScriptAction)action ), + getModel().addNode( new ScriptActionNode( mPlaylistManager, (ScriptAction)action ), AliasNode.this, getChildCount() ); } } @@ -200,7 +208,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( esn ); - ESNNode node = new ESNNode( esn ); + ESNNode node = new ESNNode( mPlaylistManager, esn ); getModel().addNode( node, AliasNode.this, @@ -221,7 +229,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( fs ); - FleetsyncIDNode node = new FleetsyncIDNode( fs ); + FleetsyncIDNode node = new FleetsyncIDNode( mPlaylistManager, fs ); getModel().addNode( node, AliasNode.this, @@ -242,7 +250,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( lj ); - LoJackIDNode node = new LoJackIDNode( lj ); + LoJackIDNode node = new LoJackIDNode( mPlaylistManager, lj ); getModel().addNode( node, AliasNode.this, @@ -263,7 +271,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( mdc ); - MDC1200IDNode node = new MDC1200IDNode( mdc ); + MDC1200IDNode node = new MDC1200IDNode( mPlaylistManager, mdc ); getModel().addNode( node, AliasNode.this, @@ -284,7 +292,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( min ); - MINNode node = new MINNode( min ); + MINNode node = new MINNode( mPlaylistManager, min ); getModel().addNode( node, AliasNode.this, @@ -305,7 +313,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( mpt ); - MPT1327IDNode node = new MPT1327IDNode( mpt ); + MPT1327IDNode node = new MPT1327IDNode( mPlaylistManager, mpt ); getModel().addNode( node, AliasNode.this, @@ -326,7 +334,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( siteID ); - SiteIDNode node = new SiteIDNode( siteID ); + SiteIDNode node = new SiteIDNode( mPlaylistManager, siteID ); getModel().addNode( node, AliasNode.this, @@ -347,7 +355,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( statusID ); - StatusIDNode node = new StatusIDNode( statusID ); + StatusIDNode node = new StatusIDNode( mPlaylistManager, statusID ); getModel().addNode( node, AliasNode.this, @@ -369,7 +377,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( tg ); - TalkgroupIDNode node = new TalkgroupIDNode( tg ); + TalkgroupIDNode node = new TalkgroupIDNode( mPlaylistManager, tg ); getModel().addNode( node, AliasNode.this, @@ -390,7 +398,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( uid ); - UniqueIDNode node = new UniqueIDNode( uid ); + UniqueIDNode node = new UniqueIDNode( mPlaylistManager, uid ); getModel().addNode( node, AliasNode.this, @@ -416,7 +424,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasAction( clipAction ); - ClipActionNode node = new ClipActionNode( clipAction ); + ClipActionNode node = new ClipActionNode( mPlaylistManager, clipAction ); getModel().addNode( node, AliasNode.this, @@ -439,7 +447,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasAction( beepAction ); - BeepActionNode node = new BeepActionNode( beepAction ); + BeepActionNode node = new BeepActionNode( mPlaylistManager, beepAction ); getModel().addNode( node, AliasNode.this, @@ -462,7 +470,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasAction( scriptAction ); - ScriptActionNode node = new ScriptActionNode( scriptAction ); + ScriptActionNode node = new ScriptActionNode( mPlaylistManager, scriptAction ); getModel().addNode( node, AliasNode.this, @@ -491,7 +499,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( non ); - NonRecordableNode node = new NonRecordableNode( non ); + NonRecordableNode node = new NonRecordableNode( mPlaylistManager, non ); getModel().addNode( node, AliasNode.this, @@ -518,7 +526,7 @@ public class AliasNode extends ConfigurableNode getAlias().addAliasID( priority ); - PriorityNode node = new PriorityNode( priority ); + PriorityNode node = new PriorityNode( mPlaylistManager, priority ); getModel().addNode( node, AliasNode.this, diff --git a/src/alias/GroupNode.java b/src/alias/GroupNode.java index ac0ca894..f8d52904 100644 --- a/src/alias/GroupNode.java +++ b/src/alias/GroupNode.java @@ -26,15 +26,24 @@ import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JSeparator; +import playlist.PlaylistManager; +import settings.SettingsManager; import controller.ConfigurableNode; public class GroupNode extends ConfigurableNode { private static final long serialVersionUID = 1L; + + private PlaylistManager mPlaylistManager; + private SettingsManager mSettingsManager; - public GroupNode( Group group ) + public GroupNode( PlaylistManager playlistManager, + SettingsManager settingsManager, + Group group ) { - super( group ); + super( playlistManager, group ); + + mSettingsManager = settingsManager; } @Override @@ -52,7 +61,7 @@ public class GroupNode extends ConfigurableNode { for( Alias alias: getGroup().getAlias() ) { - AliasNode node = new AliasNode( alias ); + AliasNode node = new AliasNode( mPlaylistManager, mSettingsManager, alias ); getModel().addNode( node, GroupNode.this, getChildCount() ); @@ -81,7 +90,7 @@ public class GroupNode extends ConfigurableNode getGroup().addAlias( alias ); - AliasNode node = new AliasNode( alias ); + AliasNode node = new AliasNode( mPlaylistManager, mSettingsManager, alias ); getModel().addNode( node, GroupNode.this, diff --git a/src/alias/action/AliasActionNode.java b/src/alias/action/AliasActionNode.java index 79ee2484..173ecf07 100644 --- a/src/alias/action/AliasActionNode.java +++ b/src/alias/action/AliasActionNode.java @@ -17,20 +17,25 @@ ******************************************************************************/ package alias.action; +import playlist.PlaylistManager; import controller.ConfigurableNode; public class AliasActionNode extends ConfigurableNode { private static final long serialVersionUID = 1L; + + private PlaylistManager mPlaylistManager; - public AliasActionNode( AliasAction action ) + public AliasActionNode( PlaylistManager playlistManager, AliasAction action ) { - super( action ); + super( playlistManager, action ); + + mPlaylistManager = playlistManager; } public void save() { - getModel().getResourceManager().getPlaylistManager().save(); + mPlaylistManager.save(); } public String getIconPath() diff --git a/src/alias/action/beep/BeepActionNode.java b/src/alias/action/beep/BeepActionNode.java index 817e3949..ba5fccdf 100644 --- a/src/alias/action/beep/BeepActionNode.java +++ b/src/alias/action/beep/BeepActionNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasNode; import alias.action.AliasActionNode; @@ -32,9 +33,9 @@ public class BeepActionNode extends AliasActionNode { private static final long serialVersionUID = 1L; - public BeepActionNode( BeepAction beepAction ) + public BeepActionNode( PlaylistManager playlistManager, BeepAction beepAction ) { - super( beepAction ); + super( playlistManager, beepAction ); } @Override diff --git a/src/alias/action/clip/ClipActionNode.java b/src/alias/action/clip/ClipActionNode.java index e3255cd0..15f7005b 100644 --- a/src/alias/action/clip/ClipActionNode.java +++ b/src/alias/action/clip/ClipActionNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasNode; import alias.action.AliasActionNode; @@ -32,9 +33,9 @@ public class ClipActionNode extends AliasActionNode { private static final long serialVersionUID = 1L; - public ClipActionNode( ClipAction clipAction ) + public ClipActionNode( PlaylistManager playlistManager, ClipAction clipAction ) { - super( clipAction ); + super( playlistManager, clipAction ); } @Override diff --git a/src/alias/action/script/ScriptActionNode.java b/src/alias/action/script/ScriptActionNode.java index b0385e3f..cbe25b45 100644 --- a/src/alias/action/script/ScriptActionNode.java +++ b/src/alias/action/script/ScriptActionNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasNode; import alias.action.AliasActionNode; @@ -32,9 +33,9 @@ public class ScriptActionNode extends AliasActionNode { private static final long serialVersionUID = 1L; - public ScriptActionNode( ScriptAction scriptAction ) + public ScriptActionNode( PlaylistManager playlistManager, ScriptAction scriptAction ) { - super( scriptAction ); + super( playlistManager, scriptAction ); } @Override diff --git a/src/alias/id/esn/ESNNode.java b/src/alias/id/esn/ESNNode.java index 1b6234d0..ecdd33e8 100644 --- a/src/alias/id/esn/ESNNode.java +++ b/src/alias/id/esn/ESNNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasIDNode; import alias.AliasNode; @@ -32,9 +33,9 @@ public class ESNNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public ESNNode( Esn esn ) + public ESNNode( PlaylistManager playlistManager, Esn esn ) { - super( esn ); + super( playlistManager, esn ); } @Override diff --git a/src/alias/id/fleetsync/FleetsyncIDNode.java b/src/alias/id/fleetsync/FleetsyncIDNode.java index 9a16c1d9..4d0e09f1 100644 --- a/src/alias/id/fleetsync/FleetsyncIDNode.java +++ b/src/alias/id/fleetsync/FleetsyncIDNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasIDNode; import alias.AliasNode; @@ -32,9 +33,9 @@ public class FleetsyncIDNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public FleetsyncIDNode( FleetsyncID id ) + public FleetsyncIDNode( PlaylistManager playlistManager, FleetsyncID id ) { - super( id ); + super( playlistManager, id ); } @Override diff --git a/src/alias/id/fleetsync/StatusIDNode.java b/src/alias/id/fleetsync/StatusIDNode.java index 621626fb..49aaeb83 100644 --- a/src/alias/id/fleetsync/StatusIDNode.java +++ b/src/alias/id/fleetsync/StatusIDNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasIDNode; import alias.AliasNode; @@ -32,9 +33,9 @@ public class StatusIDNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public StatusIDNode( StatusID id ) + public StatusIDNode( PlaylistManager playlistManager, StatusID id ) { - super( id ); + super( playlistManager, id ); } @Override diff --git a/src/alias/id/lojack/LoJackIDNode.java b/src/alias/id/lojack/LoJackIDNode.java index 3b4f3ed0..fcda77bd 100644 --- a/src/alias/id/lojack/LoJackIDNode.java +++ b/src/alias/id/lojack/LoJackIDNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasIDNode; import alias.AliasNode; @@ -32,9 +33,9 @@ public class LoJackIDNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public LoJackIDNode( LoJackFunctionAndID id ) + public LoJackIDNode( PlaylistManager playlistManager, LoJackFunctionAndID id ) { - super( id ); + super( playlistManager,id ); } @Override diff --git a/src/alias/id/mdc/MDC1200IDNode.java b/src/alias/id/mdc/MDC1200IDNode.java index 5a5a837b..9c315a49 100644 --- a/src/alias/id/mdc/MDC1200IDNode.java +++ b/src/alias/id/mdc/MDC1200IDNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasIDNode; import alias.AliasNode; @@ -32,9 +33,9 @@ public class MDC1200IDNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public MDC1200IDNode( MDC1200ID id ) + public MDC1200IDNode( PlaylistManager playlistManager, MDC1200ID id ) { - super( id ); + super( playlistManager, id ); } @Override diff --git a/src/alias/id/mobileID/MINNode.java b/src/alias/id/mobileID/MINNode.java index cf832f91..be0b7e83 100644 --- a/src/alias/id/mobileID/MINNode.java +++ b/src/alias/id/mobileID/MINNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasIDNode; import alias.AliasNode; @@ -32,9 +33,9 @@ public class MINNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public MINNode( Min min ) + public MINNode( PlaylistManager playlistManager, Min min ) { - super( min ); + super( playlistManager, min ); } @Override diff --git a/src/alias/id/mpt1327/MPT1327IDNode.java b/src/alias/id/mpt1327/MPT1327IDNode.java index ab591365..a5937cee 100644 --- a/src/alias/id/mpt1327/MPT1327IDNode.java +++ b/src/alias/id/mpt1327/MPT1327IDNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasIDNode; import alias.AliasNode; @@ -32,9 +33,9 @@ public class MPT1327IDNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public MPT1327IDNode( MPT1327ID id ) + public MPT1327IDNode( PlaylistManager playlistManager, MPT1327ID id ) { - super( id ); + super( playlistManager, id ); } @Override diff --git a/src/alias/id/siteID/SiteIDNode.java b/src/alias/id/siteID/SiteIDNode.java index 26569699..e9e2a4b4 100644 --- a/src/alias/id/siteID/SiteIDNode.java +++ b/src/alias/id/siteID/SiteIDNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasIDNode; import alias.AliasNode; @@ -32,9 +33,9 @@ public class SiteIDNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public SiteIDNode( SiteID id ) + public SiteIDNode( PlaylistManager playlistManager, SiteID id ) { - super( id ); + super( playlistManager, id ); } @Override diff --git a/src/alias/id/talkgroup/TalkgroupIDNode.java b/src/alias/id/talkgroup/TalkgroupIDNode.java index 0a8e7e0a..261adc75 100644 --- a/src/alias/id/talkgroup/TalkgroupIDNode.java +++ b/src/alias/id/talkgroup/TalkgroupIDNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasIDNode; import alias.AliasNode; @@ -32,9 +33,9 @@ public class TalkgroupIDNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public TalkgroupIDNode( TalkgroupID id ) + public TalkgroupIDNode( PlaylistManager playlistManager, TalkgroupID id ) { - super( id ); + super( playlistManager,id ); } @Override diff --git a/src/alias/id/uniqueID/UniqueIDNode.java b/src/alias/id/uniqueID/UniqueIDNode.java index 5c850f15..ebb92f4f 100644 --- a/src/alias/id/uniqueID/UniqueIDNode.java +++ b/src/alias/id/uniqueID/UniqueIDNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasIDNode; import alias.AliasNode; @@ -32,9 +33,9 @@ public class UniqueIDNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public UniqueIDNode( UniqueID id ) + public UniqueIDNode( PlaylistManager playlistManager, UniqueID id ) { - super( id ); + super( playlistManager, id ); } @Override diff --git a/src/alias/priority/PriorityNode.java b/src/alias/priority/PriorityNode.java index 178860fe..707ae0f7 100644 --- a/src/alias/priority/PriorityNode.java +++ b/src/alias/priority/PriorityNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasIDNode; import alias.AliasNode; @@ -32,9 +33,9 @@ public class PriorityNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public PriorityNode( Priority priority ) + public PriorityNode( PlaylistManager playlistManager, Priority priority ) { - super( priority ); + super( playlistManager, priority ); } @Override diff --git a/src/alias/record/NonRecordableNode.java b/src/alias/record/NonRecordableNode.java index eb8ba572..082238df 100644 --- a/src/alias/record/NonRecordableNode.java +++ b/src/alias/record/NonRecordableNode.java @@ -25,6 +25,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import alias.AliasID; import alias.AliasIDNode; import alias.AliasNode; @@ -33,9 +34,9 @@ public class NonRecordableNode extends AliasIDNode { private static final long serialVersionUID = 1L; - public NonRecordableNode( NonRecordable id ) + public NonRecordableNode( PlaylistManager playlistManager, NonRecordable id ) { - super( id ); + super( playlistManager, id ); } @Override diff --git a/src/audio/AudioModule.java b/src/audio/AudioModule.java index 7e28b70d..1f87f38d 100644 --- a/src/audio/AudioModule.java +++ b/src/audio/AudioModule.java @@ -8,7 +8,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; -import sample.real.IRealBufferListener; +import sample.real.IFilteredRealBufferListener; import sample.real.RealBuffer; import audio.metadata.AudioMetadata; import audio.metadata.IMetadataListener; @@ -18,7 +18,6 @@ import audio.squelch.SquelchState; import controller.channel.ChannelEvent; import controller.channel.IChannelEventListener; import dsp.filter.Filters; -import dsp.filter.dc.DCRemovalFilter_RB; import dsp.filter.fir.real.RealFIRFilter_R_R; /** @@ -32,49 +31,31 @@ import dsp.filter.fir.real.RealFIRFilter_R_R; public class AudioModule extends Module implements IAudioPacketProvider, IChannelEventListener, IMetadataListener, - IRealBufferListener, + IFilteredRealBufferListener, ISquelchStateListener, Listener { protected static final Logger mLog = LoggerFactory.getLogger( AudioModule.class ); - /* Determines responsiveness of DC filter to frequency changes */ - private static final float DC_REMOVAL_RATIO = 0.000003f; - /* Provides a unique identifier for this audio module instance to use as a * source identifier for all audio packets */ private static int UNIQUE_ID = 0; private int mSourceID; private AudioMetadata mAudioMetadata; - private ChannelEventListener mChannelEventListener = new ChannelEventListener(); - private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); private SquelchState mSquelchState = SquelchState.SQUELCH; - - private RealFIRFilter_R_R mAudioFilter = new RealFIRFilter_R_R( - Filters.FIR_BANDPASS_AUDIO_48KHZ.getCoefficients(), 2.0f ); - - private DCRemovalFilter_RB mDCRemovalFilter; - private Listener mAudioPacketListener; + +//TODO: remove this -- is being replaced by the DemodulatedAudioFilterModule + private RealFIRFilter_R_R mAudioFilter = new RealFIRFilter_R_R( + Filters.FIR_BANDPASS_AUDIO_48KHZ.getCoefficients(), 2.0f ); - private boolean mRemoveDC = false; - - public AudioModule( boolean record, boolean removeDC ) + public AudioModule( boolean record ) { mSourceID = ++UNIQUE_ID; - mAudioMetadata = new AudioMetadata( mSourceID, record ); - - mRemoveDC = removeDC; - - if( mRemoveDC ) - { - mDCRemovalFilter = new DCRemovalFilter_RB( DC_REMOVAL_RATIO ); - mDCRemovalFilter.setListener( this ); - } } @Override @@ -132,13 +113,8 @@ public class AudioModule extends Module implements IAudioPacketProvider, } @Override - public Listener getRealBufferListener() + public Listener getFilteredRealBufferListener() { - if( mRemoveDC ) - { - return mDCRemovalFilter; - } - return this; } diff --git a/src/audio/AudioPanel.java b/src/audio/AudioPanel.java index 0d008f01..3e6ba613 100644 --- a/src/audio/AudioPanel.java +++ b/src/audio/AudioPanel.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import sample.Listener; import settings.SettingsManager; +import source.SourceManager; import source.mixer.MixerChannelConfiguration; import source.mixer.MixerManager; import audio.output.AudioOutput; @@ -43,14 +44,18 @@ public class AudioPanel extends JPanel implements Listener new ImageIcon( "images/audio_unmuted.png" ), 38 ); private SettingsManager mSettingsManager; + private SourceManager mSourceManager; private IAudioController mController; private JButton mMuteButton; private AudioChannelsPanel mAudioChannelsPanel; - public AudioPanel( SettingsManager settingsManager, IAudioController controller ) + public AudioPanel( SettingsManager settingsManager, + SourceManager sourceManager, + IAudioController controller ) { mSettingsManager = settingsManager; + mSourceManager = sourceManager; mController = controller; mController.addControllerListener( this ); @@ -148,7 +153,7 @@ public class AudioPanel extends JPanel implements Listener JMenu outputMenu = new JMenu( "Audio Output" ); MixerChannelConfiguration[] mixerConfigurations = - MixerManager.getInstance().getOutputMixers(); + mSourceManager.getMixerManager().getOutputMixers(); for( MixerChannelConfiguration mixerConfig: mixerConfigurations ) { diff --git a/src/buffer/BufferOverflowException.java b/src/buffer/BufferOverflowException.java deleted file mode 100644 index 4b6e1a57..00000000 --- a/src/buffer/BufferOverflowException.java +++ /dev/null @@ -1,33 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -public class BufferOverflowException extends Exception -{ - private static final long serialVersionUID = 1L; - - public BufferOverflowException( String text ) - { - super( text ); - } - - public BufferOverflowException( String text, Throwable exc ) - { - super( text, exc ); - } -} diff --git a/src/buffer/BufferOverflowListener.java b/src/buffer/BufferOverflowListener.java deleted file mode 100644 index 2f8acb03..00000000 --- a/src/buffer/BufferOverflowListener.java +++ /dev/null @@ -1,23 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -public interface BufferOverflowListener -{ - public void overflow( Object source, String message ); -} diff --git a/src/buffer/ByteArrayDelayBuffer.java b/src/buffer/ByteArrayDelayBuffer.java deleted file mode 100644 index fc60703a..00000000 --- a/src/buffer/ByteArrayDelayBuffer.java +++ /dev/null @@ -1,60 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -public class ByteArrayDelayBuffer -{ - private int mBufferSize; - private byte[][] mBuffer = null; - private int mBufferPointer = 0; - - /** - * Implements a delay buffer for byte arrays. As a new byte[] is placed - * into the buffer, the oldest byte[] is returned - * @param bufferSize - number of byte arrays to buffer - */ - public ByteArrayDelayBuffer( int bufferSize ) - { - mBufferSize = bufferSize; - } - - /** - * Inserts new bytes into the buffer - * - * @return - oldest byte[] from the buffer, or null if the buffer is not - * yet full - */ - public byte[] get( byte[] bytes ) - { - if( mBuffer == null ) - { - mBuffer = new byte[ mBufferSize ][ bytes.length ]; - } - - byte[] returnBytes = mBuffer[ mBufferPointer ]; - - mBuffer[ mBufferPointer++ ] = bytes; - - if( mBufferPointer >= mBufferSize ) - { - mBufferPointer = 0; - } - - return returnBytes; - } -} diff --git a/src/buffer/ByteArrayListener.java b/src/buffer/ByteArrayListener.java deleted file mode 100644 index 021b8e2a..00000000 --- a/src/buffer/ByteArrayListener.java +++ /dev/null @@ -1,23 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -public interface ByteArrayListener -{ - public void receive( byte[] data ); -} diff --git a/src/buffer/ByteBufferListener.java b/src/buffer/ByteBufferListener.java deleted file mode 100644 index 7df629a0..00000000 --- a/src/buffer/ByteBufferListener.java +++ /dev/null @@ -1,25 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.nio.ByteBuffer; - -public interface ByteBufferListener -{ - public void receive( ByteBuffer buffer ); -} diff --git a/src/buffer/ByteBufferProvider.java b/src/buffer/ByteBufferProvider.java deleted file mode 100644 index f85962c3..00000000 --- a/src/buffer/ByteBufferProvider.java +++ /dev/null @@ -1,33 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - - - -public interface ByteBufferProvider -{ - /** - * Adds a sample listener to receive samples - */ - public void addByteBufferListener( ByteBufferListener listener ); - - /** - * Removes the sample listener, if it is currently registered as a listener - */ - public void removeByteBufferListener( ByteBufferListener listener ); -} diff --git a/src/buffer/ComplexByteBuffer.java b/src/buffer/ComplexByteBuffer.java deleted file mode 100644 index 1abb25dc..00000000 --- a/src/buffer/ComplexByteBuffer.java +++ /dev/null @@ -1,91 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.nio.ByteBuffer; -import java.util.Enumeration; - -import sample.complex.Complex; - -/** - * Provides a complex sample enumeration wrapper around a byte array containing - * interleaved byte-length I & Q sample values in the range of 0 - 255, converting - * those samples into complex samples with value range of -128 to 0 to 127. - */ -public class ComplexByteBuffer implements Enumeration -{ - private static float[] mValues; - - /** - * Creates a lookup table that converts the 8-bit valued range from 0 - 255 - * into scaled float values of -128 to 0 to 127 - */ - static - { - mValues = new float[ 256 ]; - - float scale = (float)( 1.0f / 127.0f ); - - for( int x = 0; x < 256; x++ ) - { - mValues[ x ] = (float)( x - 128 ) * scale; - } - } - - private ByteBuffer mBytes; -// private byte[] mBytes; -// private int mPointer; - - public ComplexByteBuffer( byte[] bytes ) - { - mBytes = ByteBuffer.wrap( bytes ); -// mBytes = bytes; -// mPointer = 0; - } - - public byte[] toArray() - { -// return mBytes; - return mBytes.array(); - } - - /** - * Indicates if there are more complex samples remaining. - */ - @Override - public boolean hasMoreElements() - { -// return mPointer < mBytes.length; - return mBytes.hasRemaining(); - } - - /** - * Returns the next complex sample. - * - * Note: does not check for buffer overrun. Use the hasMoreElements() - * method to ensure you do not read past the end of the buffer. - */ - @Override - public Complex nextElement() - { -// return new ComplexSample( mValues[ ( mBytes[ mPointer++ ] & 0xFF ) ], -// mValues[ ( mBytes[ mPointer++ ] & 0xFF ) ] ); - return new Complex( mValues[ ( mBytes.get() & 0xFF ) ], - mValues[ ( mBytes.get() & 0xFF ) ] ); - } -} diff --git a/src/buffer/ComplexSampleBufferAssembler.java b/src/buffer/ComplexSampleBufferAssembler.java deleted file mode 100644 index 935696f1..00000000 --- a/src/buffer/ComplexSampleBufferAssembler.java +++ /dev/null @@ -1,73 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Arrays; - -import sample.complex.Complex; - -public class ComplexSampleBufferAssembler -{ - private ByteBuffer mByteBuffer; - private int mSizeInBytes; - - public ComplexSampleBufferAssembler( int size ) - { - mSizeInBytes = size * 4; - - //Allocate a byte buffer with 4 bytes per sample (ie 2 x 16-bit samples) - mByteBuffer = ByteBuffer.allocate( mSizeInBytes ); - mByteBuffer.order( ByteOrder.LITTLE_ENDIAN ); - } - - /** - * Load the sample's bytes, left then right, little endian, into the - * backing byte array - */ - public void put( Complex sample ) - { - mByteBuffer.putShort( (short)( sample.left() * Short.MAX_VALUE ) ); - mByteBuffer.putShort( (short)( sample.right() * Short.MAX_VALUE ) ); - } - - /** - * Indicates if there is room for more samples in the buffer, or if it is full - */ - public boolean hasRemaining() - { - return mByteBuffer.hasRemaining(); - } - - /** - * Returns a copy of the byte array backing this buffer - */ - public ByteBuffer get() - { - return ByteBuffer.wrap( Arrays.copyOf( mByteBuffer.array(), mSizeInBytes ) ); - } - - /** - * Clear the bytes from the underlying buffer and reset the pointer to 0. - */ - public void clear() - { - mByteBuffer.clear(); - } -} diff --git a/src/buffer/DelayBuffer.java b/src/buffer/DelayBuffer.java deleted file mode 100644 index 001058db..00000000 --- a/src/buffer/DelayBuffer.java +++ /dev/null @@ -1,67 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import sample.Listener; - -/** - * Delay Buffer - implements a circular delay buffer backed by an object array - */ -public class DelayBuffer implements Listener -{ - private Object[] mBuffer; - private int mBufferPointer = 0; - private Listener mListener; - - public DelayBuffer( int length ) - { - mBuffer = new Object[ length ]; - } - - @Override - @SuppressWarnings( "unchecked" ) - public void receive( T value ) - { - Object delayed = mBuffer[ mBufferPointer ]; - - mBuffer[ mBufferPointer ] = value; - - mBufferPointer++; - - /* Wrap the buffer pointer around to 0 when necessary */ - if( mBufferPointer >= mBuffer.length ) - { - mBufferPointer = 0; - } - - if( mListener != null && delayed != null ) - { - mListener.receive( (T)delayed ); - } - } - - public void setListener( Listener listener ) - { - mListener = listener; - } - - public void removeListener( Listener listener ) - { - mListener = null; - } -} diff --git a/src/buffer/FloatArrayCircularAveragingBuffer.java b/src/buffer/FloatArrayCircularAveragingBuffer.java deleted file mode 100644 index 5003d789..00000000 --- a/src/buffer/FloatArrayCircularAveragingBuffer.java +++ /dev/null @@ -1,97 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.util.ArrayList; -import java.util.Arrays; - -public class FloatArrayCircularAveragingBuffer -{ - private ArrayList mBuffer; - private int mBufferPointer; - private int mBufferSize; - private float[] mAverage; - - public FloatArrayCircularAveragingBuffer( int bufferSize ) - { - mBufferSize = bufferSize; - mBuffer = new ArrayList(); - } - - /** - * Replaces the oldest float array element in the buffer with this new - * float array element, and then returns the average of the buffer contents - * with the newElement values included in the average array - */ - public float[] get( float[] newElement ) - { - /** - * If we've never setup the average array, or the newElements length is - * different than what we've been processing, reset the buffer & averages - */ - if( mAverage == null || mAverage.length != newElement.length ) - { - reset( newElement.length ); - } - - float[] oldElement = mBuffer.get( mBufferPointer ); - - for( int x = 0; x < newElement.length; x++ ) - { - /** - * Change the current average value in each array element by the - * difference of the new element minus the old element, divided - * by the buffer size ... average it - */ - mAverage[ x ] += - ( ( newElement[ x ] - oldElement[ x ] ) / mBufferSize ); - } - - mBuffer.set( mBufferPointer, newElement ); - - mBufferPointer++; - - if( mBufferPointer >= mBufferSize ) - { - mBufferPointer = 0; - } - - return mAverage; - } - - /** - * Clears buffer and average and fills buffer with zero-valued float arrays - */ - private void reset( int arrayLength ) - { - mAverage = new float[ arrayLength ]; - Arrays.fill( mAverage, 0.0f ); - - mBuffer.clear(); - mBufferPointer = 0; - - float[] emptyArray = new float[ arrayLength ]; - Arrays.fill( emptyArray, 0.0f ); - - for( int x = 0; x < mBufferSize; x++ ) - { - mBuffer.add( emptyArray ); - } - } - -} diff --git a/src/buffer/FloatBufferAssembler.java b/src/buffer/FloatBufferAssembler.java deleted file mode 100644 index 651a1794..00000000 --- a/src/buffer/FloatBufferAssembler.java +++ /dev/null @@ -1,70 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.util.Arrays; - -import sample.Listener; - - -/** - * Assembles single samples into sample blocks (ie arrays) for broadcast to - * registered SampleBlockListeners - */ -public class FloatBufferAssembler implements Listener -{ - private Float[] mFloatBuffer; - private int mBufferSize; - private int mBufferPointer = 0; - private Listener mListener; - - public FloatBufferAssembler( int bufferSize ) - { - mBufferSize = bufferSize; - mFloatBuffer = new Float[ mBufferSize ]; - } - - /** - * Stores received samples in the sample block and manages the storage - * pointer. Sends sample blocks when full. - */ - @Override - public void receive( Float sample ) - { - mFloatBuffer[ mBufferPointer++ ] = sample; - - if( mBufferPointer == mBufferSize ) - { - if( mListener != null ) - { - mListener.receive( Arrays.copyOf( mFloatBuffer, mBufferSize ) ); - } - - mBufferPointer = 0; - } - } - - /** - * Adds a new sampleblock listener to receive sample blocks as they are - * assembled from the sample stream - */ - public void setListener( Listener listener ) - { - mListener = listener; - } -} diff --git a/src/buffer/FloatCircularAveragingBuffer.java b/src/buffer/FloatCircularAveragingBuffer.java deleted file mode 100644 index 6a05ea61..00000000 --- a/src/buffer/FloatCircularAveragingBuffer.java +++ /dev/null @@ -1,110 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.util.Arrays; - -/** - * Circular sample buffer - allocates a buffer and stores samples in a circular - * fashion, overwriting older samples with newly arrived samples. Initially - * fills buffer with 0-valued samples - * - * Produces no output. Obtain the average buffer value on-demand via the - * getAverage() method - */ -public class FloatCircularAveragingBuffer -{ - private float[] mBuffer; - private int mBufferPointer = 0; - private int mSampleCountForAveraging; - private int mCurrentSampleCount = 0; - private float mAverage = 0.0f; - - public FloatCircularAveragingBuffer( int size, int sampleCountForAveraging ) - { - mBuffer = new float[ size ]; - mSampleCountForAveraging = sampleCountForAveraging; - Arrays.fill( mBuffer, 0.0f ); - } - - /** - * Convenience wrapper to throw away to put the new sample and throwaway - * the oldest sample - */ - public void put( float newestSample ) - { - get( newestSample ); - } - - /** - * Loads the newest sample into the buffer and returns the oldest sample - */ - public float get( float newestSample ) - { - float oldestSample = mBuffer[ mBufferPointer ]; - - mBuffer[ mBufferPointer ] = newestSample; - - mBufferPointer++; - - if( mBufferPointer >= mBuffer.length ) - { - mBufferPointer = 0; - } - - mCurrentSampleCount++; - - if( mCurrentSampleCount >= mSampleCountForAveraging ) - { - calculateAverage(); - - mCurrentSampleCount = 0; - } - - return oldestSample; - } - - /** - * Calculates the average of the maximum and minimum values present in - * the buffer - */ - private void calculateAverage() - { - float min = mBuffer[ 0 ]; - float max = mBuffer[ 0 ]; - - for( int x = 0; x < mBuffer.length; x++ ) - { - if( mBuffer[ x ] < min ) - { - min = mBuffer[ x ]; - } - if( mBuffer[ x ] > max ) - { - max = mBuffer[ x ]; - } - } - - mAverage = (float)( ( min + max ) / 2.0f ); - } - - public float getAverage() - { - return mAverage; - } -} diff --git a/src/buffer/FloatCircularAveragingBuffer2.java b/src/buffer/FloatCircularAveragingBuffer2.java deleted file mode 100644 index da27d207..00000000 --- a/src/buffer/FloatCircularAveragingBuffer2.java +++ /dev/null @@ -1,68 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.util.Arrays; - -/** - * Circular sample buffer - stores samples in a circular buffer - * overwriting older samples with newly arrived samples. Initially - * fills buffer with 0-valued samples - * - * Continuously calculates the average of the samples in the buffer, with the - * average available from the average() method. - */ -public class FloatCircularAveragingBuffer2 -{ - private float[] mBuffer; - private int mBufferPointer = 0; - private float mAverage = 0.0f; - - public FloatCircularAveragingBuffer2( int size ) - { - mBuffer = new float[ size ]; - Arrays.fill( mBuffer, 0.0f ); - } - - /** - * Loads the newest sample into the buffer and returns the oldest sample - */ - public float get( float newestSample ) - { - float oldestSample = mBuffer[ mBufferPointer ]; - - mBuffer[ mBufferPointer ] = newestSample; - - mBufferPointer++; - - if( mBufferPointer >= mBuffer.length ) - { - mBufferPointer = 0; - } - - mAverage = mAverage - ( oldestSample / mBuffer.length ) + - ( newestSample / mBuffer.length ); - - return oldestSample; - } - - public float average() - { - return mAverage; - } -} diff --git a/src/buffer/FloatSampleBuffer.java b/src/buffer/FloatSampleBuffer.java deleted file mode 100644 index bca43189..00000000 --- a/src/buffer/FloatSampleBuffer.java +++ /dev/null @@ -1,66 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.nio.ByteBuffer; - - -public class FloatSampleBuffer -{ - private static final long serialVersionUID = 1L; - - private int mSize; - private float[] mBuffer; - private int mBufferPointer = 0; - - public FloatSampleBuffer( int size ) - { - super(); - mBuffer = new float[ size ]; - } - - public void put( float sample ) - { - if( !isFull() ) - { - mBuffer[ mBufferPointer++ ] = sample; - } - } - - public void reset() - { - mBufferPointer = 0; - } - - public boolean isFull() - { - return mBufferPointer >= mBuffer.length; - } - - public byte[] getBytes() - { - ByteBuffer bb = ByteBuffer.allocate( mBuffer.length * 2 ); - - for( int x = 0; x < mBuffer.length; x++ ) - { - bb.putShort( ((Float)mBuffer[ x ]).shortValue() ); - } - - return bb.array(); - } -} diff --git a/src/buffer/FloatSampleBufferAssembler.java b/src/buffer/FloatSampleBufferAssembler.java deleted file mode 100644 index de4d53c2..00000000 --- a/src/buffer/FloatSampleBufferAssembler.java +++ /dev/null @@ -1,72 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -public class FloatSampleBufferAssembler -{ - private ByteBuffer mByteBuffer; - private int mSizeInBytes; - - public FloatSampleBufferAssembler( int size ) - { - mSizeInBytes = size * 2; - - //Allocate a byte buffer with 2 bytes per sample (ie 1 x 16-bit sample) - mByteBuffer = ByteBuffer.allocate( mSizeInBytes ); - mByteBuffer.order( ByteOrder.LITTLE_ENDIAN ); - } - - /** - * Load the sample's bytes, left then right, little endian, into the - * backing byte array - */ - public void put( float sample ) - { - mByteBuffer.putShort( (short)( sample * 32768 ) ); - } - - /** - * Indicates if there is room for more samples in the buffer, or if it is full - */ - public boolean hasRemaining() - { - return mByteBuffer.hasRemaining(); - } - - /** - * Returns a copy of the byte array backing this buffer - */ - public ByteBuffer get() - { - return mByteBuffer; -// return ByteBuffer.wrap( Arrays.copyOf( mByteBuffer.array(), mSizeInBytes ) ); - } - - /** - * Clear the bytes from the underlying buffer and reset the pointer to 0. - */ - public void reset() - { -// mByteBuffer.clear(); - mByteBuffer = ByteBuffer.allocate( mSizeInBytes ); - mByteBuffer.order( ByteOrder.LITTLE_ENDIAN ); - } -} diff --git a/src/buffer/FloatTappedCircularBuffer.java b/src/buffer/FloatTappedCircularBuffer.java deleted file mode 100644 index 5d0c3e7f..00000000 --- a/src/buffer/FloatTappedCircularBuffer.java +++ /dev/null @@ -1,86 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - - -/** - * Implements a FIFO circular buffer for floats that provides access - * to each sample by index number, where index 0 is the newest/latest - * complex sample and index (length - 1) is the oldest sample in the buffer. - */ -public class FloatTappedCircularBuffer -{ - private Float[] mBuffer; - private int mBufferPointer = 0; - - public FloatTappedCircularBuffer( int length ) - { - mBuffer = new Float[ length ]; - - //Pre-load the buffer with 0-valued samples - for( int x = 0; x < length; x++ ) - { - mBuffer[ x ] = Float.valueOf( 0.0f ); - } - } - - /** - * Adds the sample to the buffer and removes the oldest sample - * @param sample - */ - public void add( Float sample ) - { - mBuffer[ mBufferPointer++ ] = sample; - - //Reset the buffer pointer once it points past the end of the buffer - if( mBufferPointer >= mBuffer.length ) - { - mBufferPointer = 0; - } - } - - /** - * Retrieves a sample currently in the buffer by index position. - * - * Index 0 is the newest/latest sample in the buffer and index (length - 1) - * is the oldest. - * - * Note: the buffer is preloaded with complex samples of 0,0 value. - * - * @param index where 0 is the newest/latest sample and length-1 is the oldest - * @return complex sample from the buffer. - * @throws ArrayIndexOutOfBoundsException if you attempt to access a sample - * outside of the buffer size - */ - public Float get( int index ) - { - /* Since the samples are stored in a forward fashion, we have to access - * the samples in reverse to comply with the access contract, where - * index 0 is the newest and index (length -1) is the oldest, all - * relative to the current buffer pointer */ - - int pointer = mBuffer.length - 1 + mBufferPointer - index; - - if( pointer >= mBuffer.length ) - { - pointer -= mBuffer.length; - } - - return mBuffer[ pointer ]; - } -} diff --git a/src/buffer/FloatToByteBufferAssembler.java b/src/buffer/FloatToByteBufferAssembler.java deleted file mode 100644 index 5f24ffce..00000000 --- a/src/buffer/FloatToByteBufferAssembler.java +++ /dev/null @@ -1,69 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import sample.Broadcaster; -import sample.Listener; - -public class FloatToByteBufferAssembler implements Listener -{ - private Broadcaster mBroadcaster = new Broadcaster(); - - private int mBufferSize = 0; - private ByteBuffer mBuffer; - - public FloatToByteBufferAssembler( int bufferSize ) - { - mBufferSize = bufferSize; - resetBuffer(); - } - - public void addByteBufferListener( Listener listener ) - { - mBroadcaster.addListener( listener ); - } - - public void removeByteBufferListener( Listener listener ) - { - mBroadcaster.removeListener( listener ); - } - - private void resetBuffer() - { - mBuffer = ByteBuffer.allocate( mBufferSize ); - mBuffer.order( ByteOrder.BIG_ENDIAN ); - } - - @Override - public void receive( Float sample ) - { - mBuffer.putShort( Float.valueOf( sample ).shortValue() ); - - if( mBuffer.position() >= mBufferSize ) - { - //Buffer is full ... send it - mBroadcaster.receive( mBuffer ); - - //Reset the buffer - resetBuffer(); - } - } -} diff --git a/src/buffer/StereoByteBuffer.java b/src/buffer/StereoByteBuffer.java deleted file mode 100644 index 6e4e6db4..00000000 --- a/src/buffer/StereoByteBuffer.java +++ /dev/null @@ -1,59 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Enumeration; - -import sample.complex.Complex; -import source.mixer.MixerChannel; - -public class StereoByteBuffer implements Enumeration -{ - private ByteBuffer mByteBuffer; - - public StereoByteBuffer( byte[] bytes, int frames ) - { - mByteBuffer = ByteBuffer.wrap( bytes ); - mByteBuffer.order( ByteOrder.LITTLE_ENDIAN ); - } - - public byte[] array() - { - return mByteBuffer.array(); - } - - public ByteBuffer getChannel( MixerChannel channel ) - { - return StereoToMonoByteBufferConverter.getChannel( mByteBuffer, channel ); - } - - @Override - public boolean hasMoreElements() - { - return mByteBuffer.hasRemaining(); - } - - @Override - public Complex nextElement() - { - return new Complex( (float)mByteBuffer.getShort(), - (float)mByteBuffer.getShort() ); - } -} diff --git a/src/buffer/StereoByteBufferListener.java b/src/buffer/StereoByteBufferListener.java deleted file mode 100644 index 641305c1..00000000 --- a/src/buffer/StereoByteBufferListener.java +++ /dev/null @@ -1,24 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - - -public interface StereoByteBufferListener -{ - public void receive( StereoByteBuffer buffer ); -} diff --git a/src/buffer/StereoByteBufferProvider.java b/src/buffer/StereoByteBufferProvider.java deleted file mode 100644 index 76f91930..00000000 --- a/src/buffer/StereoByteBufferProvider.java +++ /dev/null @@ -1,32 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - - -public interface StereoByteBufferProvider -{ - /** - * Adds a sample listener to receive samples - */ - public void addStereoByteBufferListener( StereoByteBufferListener listener ); - - /** - * Removes the sample listener, if it is currently registered as a listener - */ - public void removeStereoByteBufferListener( StereoByteBufferListener listener ); -} diff --git a/src/buffer/StereoToMonoByteBufferConverter.java b/src/buffer/StereoToMonoByteBufferConverter.java deleted file mode 100644 index 55e43aca..00000000 --- a/src/buffer/StereoToMonoByteBufferConverter.java +++ /dev/null @@ -1,58 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package buffer; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import source.mixer.MixerChannel; - -public class StereoToMonoByteBufferConverter -{ - public static ByteBuffer getChannel( ByteBuffer buffer, MixerChannel channel ) - { - byte[] channelBuffer = new byte[ buffer.capacity() / 2 ]; - - - int channelBufferPointer = 0; - - if( channel == MixerChannel.RIGHT ) - { - //throw away the first two bytes - buffer.get(); - buffer.get(); - } - - while( channelBufferPointer < channelBuffer.length ) - { - if( buffer.hasRemaining() ) - { - channelBuffer[ channelBufferPointer++ ] = buffer.get(); - channelBuffer[ channelBufferPointer++ ] = buffer.get(); - } - - if( buffer.hasRemaining() ) - { - buffer.get(); - buffer.get(); - } - } - - return ByteBuffer.wrap( channelBuffer ).order( ByteOrder.LITTLE_ENDIAN ); - } -} diff --git a/src/controller/ConfigurableNode.java b/src/controller/ConfigurableNode.java index f9a6e3be..77da7453 100644 --- a/src/controller/ConfigurableNode.java +++ b/src/controller/ConfigurableNode.java @@ -17,14 +17,24 @@ ******************************************************************************/ package controller; +import playlist.PlaylistManager; public abstract class ConfigurableNode extends BaseNode { private static final long serialVersionUID = 1L; + + private PlaylistManager mPlaylistManager; - public ConfigurableNode( Object object ) + public ConfigurableNode( PlaylistManager playlistManager, Object object ) { super( object ); + + mPlaylistManager = playlistManager; + } + + protected PlaylistManager getPlaylistManager() + { + return mPlaylistManager; } /** @@ -32,7 +42,7 @@ public abstract class ConfigurableNode extends BaseNode */ public void save() { - getModel().getResourceManager().getPlaylistManager().save(); + mPlaylistManager.save(); BaseNode parent = (BaseNode)getParent(); diff --git a/src/controller/ConfigurationControllerModel.java b/src/controller/ConfigurationControllerModel.java index 8bbd48c3..692ee7ee 100644 --- a/src/controller/ConfigurationControllerModel.java +++ b/src/controller/ConfigurationControllerModel.java @@ -28,9 +28,14 @@ import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.MutableTreeNode; import javax.swing.tree.TreePath; -import map.IconManager; import module.decode.DecoderType; + +import playlist.PlaylistManager; import playlist.PlaylistNode; + +import settings.SettingsManager; +import source.SourceManager; +import source.recording.RecordingGroupNode; import source.tuner.Tuner; import source.tuner.TunerGroupNode; import source.tuner.TunerNode; @@ -39,14 +44,14 @@ import controller.channel.Channel; import controller.channel.ChannelModel; import controller.channel.ChannelNode; +import controller.channel.ChannelProcessingManager; + public class ConfigurationControllerModel extends DefaultTreeModel { private static final long serialVersionUID = 1L; private JTree mTree; - private ResourceManager mResourceManager; - private ArrayList mTunerSelectionListeners = new ArrayList(); @@ -54,7 +59,11 @@ public class ConfigurationControllerModel extends DefaultTreeModel private TunerGroupNode mTunerGroupNode; // private RecordingGroupNode mRecordingGroupNode; - private IconManager mIconManagerFrame; + private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; + private PlaylistManager mPlaylistManager; + private SettingsManager mSettingsManager; + private SourceManager mSourceManager; private ChannelModel mChannelModel; /** @@ -63,14 +72,21 @@ public class ConfigurationControllerModel extends DefaultTreeModel * design perspective, the model and the control are necessarily merged to * work well with the JTree as the system view. */ - public ConfigurationControllerModel( ChannelModel channelModel, - ResourceManager resourceManager ) + public ConfigurationControllerModel( ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, + SettingsManager settingsManager, + SourceManager sourceManager ) { super( new BaseNode( null ) ); mChannelModel = channelModel; - - mResourceManager = resourceManager; + + mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; + mPlaylistManager = playlistManager; + mSettingsManager = settingsManager; + mSourceManager = sourceManager; /** * Give the root node a reference to this model, so that all nodes can @@ -84,23 +100,6 @@ public class ConfigurationControllerModel extends DefaultTreeModel return mTree; } - public void showIconManager() - { - if( mIconManagerFrame == null ) - { - mIconManagerFrame = new IconManager( mResourceManager, mTree ); - - mResourceManager.getSettingsManager().addListener( mIconManagerFrame ); - } - - if( !mIconManagerFrame.isVisible() ) - { - mIconManagerFrame.setVisible( true ); - } - - mIconManagerFrame.toFront(); - } - /** * init() gets invoked manually/externally, after all of the references * are established in the constructor @@ -124,7 +123,8 @@ public class ConfigurationControllerModel extends DefaultTreeModel /** * Add the playlist node */ - mPlaylistNode = new PlaylistNode(); + mPlaylistNode = new PlaylistNode( mChannelModel, mChannelProcessingManager, + mPlaylistManager, mSettingsManager, mSourceManager ); insertNodeInto( mPlaylistNode, (MutableTreeNode)root, 1 ); mPlaylistNode.loadPlaylist(); } @@ -137,11 +137,6 @@ public class ConfigurationControllerModel extends DefaultTreeModel } } - public ResourceManager getResourceManager() - { - return mResourceManager; - } - public void setTree( JTree tree ) { mTree = tree; @@ -149,11 +144,13 @@ public class ConfigurationControllerModel extends DefaultTreeModel private void addTuners( TunerGroupNode parent ) { - List tuners = mResourceManager.getTunerManager().getTuners(); + List tuners = mSourceManager.getTunerManager().getTuners(); for( Tuner tuner: tuners ) { - TunerNode child = new TunerNode( mChannelModel, tuner ); + TunerNode child = new TunerNode( mChannelModel, + mChannelProcessingManager, mPlaylistManager, mSettingsManager, + tuner ); insertNodeInto( child, parent, parent.getChildCount() ); diff --git a/src/controller/ControllerPanel.java b/src/controller/ControllerPanel.java index 64b40a6d..e8612478 100644 --- a/src/controller/ControllerPanel.java +++ b/src/controller/ControllerPanel.java @@ -33,8 +33,10 @@ import module.decode.event.CallEventPanel; import module.decode.event.MessageActivityPanel; import module.decode.state.ChannelList; import net.miginfocom.swing.MigLayout; +import playlist.PlaylistEditor; import playlist.PlaylistManager; import settings.SettingsManager; +import source.SourceManager; import spectrum.ChannelSpectrumPanel; import audio.AudioManager; import audio.AudioPanel; @@ -53,15 +55,16 @@ public class ControllerPanel extends JPanel private CallEventPanel mCallEventPanel; - private MessageActivityPanel mMessageActivityPanel = - new MessageActivityPanel(); + private MessageActivityPanel mMessageActivityPanel; private ChannelSpectrumPanel mChannelSpectrumPanel; - private JideTabbedPane mTabbedPane; + private PlaylistEditor mPlaylistEditor; + + private JideTabbedPane mTabbedPane; protected ConfigurationTreePanel mSystemControlViewPanel; - protected ConfigurationEditor mConfigurationEditor; + protected OldConfigurationEditor mConfigurationEditor; protected JideSplitPane mSystemControlSplitPane; protected JTable mChannelActivityTable = new JTable(); @@ -78,20 +81,30 @@ public class ControllerPanel extends JPanel ChannelProcessingManager channelProcessingManager, MapService mapService, PlaylistManager playlistManager, - SettingsManager settingsManager ) + SettingsManager settingsManager, + SourceManager sourceManager ) { mChannelModel = channelModel; mController = controller; mSettingsManager = settingsManager; - mAudioPanel = new AudioPanel( mSettingsManager, audioManager ); + mAudioPanel = new AudioPanel( mSettingsManager, sourceManager, audioManager ); - mMapPanel = new MapPanel( mapService, mSettingsManager, channelProcessingManager ); + mMapPanel = new MapPanel( mapService, mSettingsManager ); - mCallEventPanel = new CallEventPanel( mSettingsManager ); - - mChannelStateList = new ChannelList( playlistManager, mSettingsManager ); + mMessageActivityPanel = new MessageActivityPanel( channelProcessingManager ); + + mCallEventPanel = new CallEventPanel( mSettingsManager, + channelProcessingManager ); + mChannelSpectrumPanel = new ChannelSpectrumPanel( mSettingsManager, + channelProcessingManager ); + + mChannelStateList = new ChannelList( channelModel, channelProcessingManager, + playlistManager, mSettingsManager ); + + mPlaylistEditor = new PlaylistEditor( channelModel, playlistManager, sourceManager ); + init(); } @@ -102,7 +115,7 @@ public class ControllerPanel extends JPanel "[grow,fill]") ); //System Configuration View and Editor - mConfigurationEditor = new ConfigurationEditor(); + mConfigurationEditor = new OldConfigurationEditor(); mSystemControlViewPanel = new ConfigurationTreePanel( mController ); mSystemControlViewPanel.addTreeSelectionListener( mConfigurationEditor ); @@ -112,8 +125,6 @@ public class ControllerPanel extends JPanel mSystemControlSplitPane.add( mSystemControlViewPanel ); mSystemControlSplitPane.add( mConfigurationEditor ); - mChannelSpectrumPanel = new ChannelSpectrumPanel( mSettingsManager ); - //Tabbed View - configuration, calls, messages, map mTabbedPane = new JideTabbedPane(); mTabbedPane.setFont( this.getFont() ); @@ -122,10 +133,8 @@ public class ControllerPanel extends JPanel mTabbedPane.addTab( "Channel Spectrum", mChannelSpectrumPanel ); mTabbedPane.addTab( "Events", mCallEventPanel ); mTabbedPane.addTab( "Messages", mMessageActivityPanel ); - - JTable channelsTable = new JTable( mChannelModel ); - JScrollPane channelsScroller = new JScrollPane( channelsTable ); - mTabbedPane.addTab( "Channels", channelsScroller ); + + mTabbedPane.addTab( "Playlist", mPlaylistEditor ); /** * Change listener to enable/disable the channel spectrum display diff --git a/src/controller/ConfigurationEditor.java b/src/controller/OldConfigurationEditor.java similarity index 95% rename from src/controller/ConfigurationEditor.java rename to src/controller/OldConfigurationEditor.java index 84a99a30..29d504ee 100644 --- a/src/controller/ConfigurationEditor.java +++ b/src/controller/OldConfigurationEditor.java @@ -26,7 +26,7 @@ import javax.swing.JTree; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; -public class ConfigurationEditor extends JPanel +public class OldConfigurationEditor extends JPanel implements TreeSelectionListener { private static final long serialVersionUID = 1L; @@ -34,7 +34,7 @@ public class ConfigurationEditor extends JPanel /** * Host for configuration editor display panels */ - public ConfigurationEditor() + public OldConfigurationEditor() { init(); } diff --git a/src/controller/ResourceManager.java b/src/controller/ResourceManager.java deleted file mode 100644 index a71de851..00000000 --- a/src/controller/ResourceManager.java +++ /dev/null @@ -1,177 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package controller; - -import map.MapService; -import module.log.EventLogManager; -import playlist.PlaylistManager; -import record.RecorderManager; -import settings.SettingsManager; -import source.SourceManager; -import source.recording.RecordingSourceManager; -import source.tuner.TunerManager; -import audio.AudioManager; -import controller.channel.ChannelModel; - -/** - * Manager for system wide resources - */ -public class ResourceManager -{ - private AudioManager mAudioManager; - private EventLogManager mEventLogManager; - private PlaylistManager mPlaylistManager; - private RecorderManager mRecorderManager; - private RecordingSourceManager mRecordingSourceManager; - private SettingsManager mSettingsManager; - private SourceManager mSourceManager; - private TunerManager mTunerManager; - private ConfigurationControllerModel mController; - private MapService mMapService; - private ThreadPoolManager mThreadPoolManager; - - public ResourceManager( PlaylistManager playlistManager, - ThreadPoolManager threadPoolManager, - ChannelModel channelModel ) - { - this( threadPoolManager, - new SettingsManager(), - playlistManager, - new EventLogManager(), - channelModel ); - } - - public ResourceManager( ThreadPoolManager threadPoolManager, - SettingsManager settingsManager, - PlaylistManager playlistManager, - EventLogManager eventLogManager, - ChannelModel channelModel ) - { - mThreadPoolManager = threadPoolManager; - mSettingsManager = settingsManager; - mPlaylistManager = playlistManager; - mEventLogManager = eventLogManager; - - /** - * ChannelManager requires a reference to the ResourceManager so that - * channel processing chains can access system wide resources - */ - mAudioManager = new AudioManager( mThreadPoolManager ); - mRecorderManager = new RecorderManager( mThreadPoolManager ); - mSourceManager = new SourceManager( this ); - mRecordingSourceManager = new RecordingSourceManager( this ); - mTunerManager = new TunerManager( this ); - mController = new ConfigurationControllerModel( channelModel, this ); - mMapService = new MapService( this ); - } - - public AudioManager getAudioManager() - { - return mAudioManager; - } - - public void setAudioManager( AudioManager audioManager ) - { - mAudioManager = audioManager; - } - - public ConfigurationControllerModel getController() - { - return mController; - } - - public ThreadPoolManager getThreadPoolManager() - { - return mThreadPoolManager; - } - - public void setController( ConfigurationControllerModel controller ) - { - mController = controller; - } - - public TunerManager getTunerManager() - { - return mTunerManager; - } - - public MapService getMapService() - { - return mMapService; - } - - public SettingsManager getSettingsManager() - { - return mSettingsManager; - } - - public void setSettingsManager( SettingsManager manager ) - { - mSettingsManager = manager; - } - - public PlaylistManager getPlaylistManager() - { - return mPlaylistManager; - } - - public void setPlaylistManager( PlaylistManager aliasManager ) - { - mPlaylistManager = aliasManager; - } - - public EventLogManager getEventLogManager() - { - return mEventLogManager; - } - - public void setEventLogManager( EventLogManager eventLogManager ) - { - mEventLogManager = eventLogManager; - } - - public RecorderManager getRecorderManager() - { - return mRecorderManager; - } - - public void setRecorderManager( RecorderManager recorderManager ) - { - mRecorderManager = recorderManager; - } - - public SourceManager getSourceManager() - { - return mSourceManager; - } - - public void setSourceManager( SourceManager sourceManager ) - { - mSourceManager = sourceManager; - } - - public RecordingSourceManager getRecordingSourceManager() - { - return mRecordingSourceManager; - } - - public void setRecordingSourceManager( RecordingSourceManager manager ) - { - mRecordingSourceManager = manager; - } -} diff --git a/src/controller/channel/AbstractChannelEditor.java b/src/controller/channel/AbstractChannelEditor.java index 41c3b4aa..2ef02f60 100644 --- a/src/controller/channel/AbstractChannelEditor.java +++ b/src/controller/channel/AbstractChannelEditor.java @@ -17,23 +17,22 @@ ******************************************************************************/ package controller.channel; -import controller.Editor; -public abstract class AbstractChannelEditor extends Editor +public abstract class AbstractChannelEditor extends ChannelConfigurationEditor { private static final long serialVersionUID = 1L; - protected ChannelNode mChannelNode; + protected Channel mChannel; - public AbstractChannelEditor( ChannelNode channel ) + public AbstractChannelEditor( Channel channel ) { super(); - mChannelNode = channel; + mChannel = channel; } - public ChannelNode getChannelNode() + public Channel getChannel() { - return mChannelNode; + return mChannel; } public abstract void save(); diff --git a/src/controller/channel/Channel.java b/src/controller/channel/Channel.java index 3da95791..b7d290a8 100644 --- a/src/controller/channel/Channel.java +++ b/src/controller/channel/Channel.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * Copyright (C) 2014-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,35 +17,21 @@ ******************************************************************************/ package controller.channel; -import java.util.List; - import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlSeeAlso; import javax.xml.bind.annotation.XmlTransient; -import message.Message; -import module.Module; -import module.ProcessingChain; -import module.decode.DecoderFactory; import module.decode.config.AuxDecodeConfiguration; import module.decode.config.DecodeConfigFactory; import module.decode.config.DecodeConfiguration; -import module.decode.event.CallEventModel; -import module.decode.event.MessageActivityModel; -import module.decode.state.ChannelState; -import module.log.MessageEventLogger; import module.log.config.EventLogConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import record.RecorderManager; -import record.RecorderType; import record.config.RecordConfiguration; -import source.Source; -import source.SourceException; import source.SourceType; import source.config.SourceConfigFactory; import source.config.SourceConfigRecording; @@ -53,27 +39,25 @@ import source.config.SourceConfigTuner; import source.config.SourceConfiguration; import source.tuner.TunerChannel; import source.tuner.TunerChannel.Type; -import alias.AliasList; -import audio.metadata.Metadata; -import audio.metadata.MetadataType; -import controller.ResourceManager; + +import source.tuner.frequency.FrequencyChangeEvent; +import source.tuner.frequency.FrequencyChangeEvent.Event; +import source.tuner.frequency.IFrequencyChangeProcessor; import controller.config.Configuration; -import filter.FilterSet; @XmlSeeAlso( { Configuration.class } ) @XmlRootElement( name = "channel" ) -public class Channel extends Configuration +public class Channel extends Configuration implements IFrequencyChangeProcessor { private final static Logger mLog = LoggerFactory.getLogger( Channel.class ); - private static final boolean DISABLED = false; - - public enum ChannelType - { - STANDARD, - TRAFFIC - }; + // Standard channels are persisted and traffic channels are temporary + public enum ChannelType { STANDARD, TRAFFIC }; + + // Static unique channel identifier tracking + private static int UNIQUE_ID = 0; + private DecodeConfiguration mDecodeConfiguration = DecodeConfigFactory.getDefaultDecodeConfiguration(); private AuxDecodeConfiguration mAuxDecodeConfiguration = @@ -84,134 +68,67 @@ public class Channel extends Configuration new EventLogConfiguration(); private RecordConfiguration mRecordConfiguration = new RecordConfiguration(); - private ResourceManager mResourceManager; private String mAliasListName; - private String mSystem; - private String mSite; - private String mName; + private String mSystem = "System"; + private String mSite = "Site"; + private String mName = "Channel"; + private boolean mEnabled; private boolean mSelected; - private ChannelType mChannelType; - private ChannelState mChannelState; - private CallEventModel mCallEventModel; - private MessageActivityModel mMessageActivityModel; - private MessageEventLogger mEventLogger; - private ProcessingChain mProcessingChain; + private TunerChannel mTunerChannel = null; - private long mTrafficChannelTimeout = - DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS; + private ChannelType mChannelType = ChannelType.STANDARD; + + private int mChannelID; + private int mChannelFrequencyCorrection = 0; /** - * Constructs a new standard channel with a default name of "New Channel" - */ - public Channel() - { - this( "New Channel", ChannelType.STANDARD ); - } - - /** - * Constructs a new standard channel with the specified channel name - */ - public Channel( String channelName ) - { - this( channelName, ChannelType.STANDARD ); - } - - /** - * Constructs a new channel with the specified channel name and type. + * Channel represents a complete set of configurations needed to setup and + * manage a processing chain and/or manage as a set of business objects that + * can be displayed in graphical components. * * @param channelName * @param channelType */ public Channel( String channelName, ChannelType channelType ) { - mName = channelName; + this( channelName ); mChannelType = channelType; } - + /** - * Indicates if the channel has Automatic or Direct Frequency Control + * Constructs a new standard channel with the specified channel name */ - public boolean hasFrequencyControl() + public Channel( String channelName ) { - return mProcessingChain != null && - mProcessingChain.hasFrequencyCorrectionControl(); - } - - public long getFrequencyCorrection() - { - if( hasFrequencyControl() ) - { - return mProcessingChain.getFrequencyCorrectionControl().getErrorCorrection(); - } - - return 0; + this(); + mName = channelName; } /** - * Sets the channel state traffic channel call end timer to the specified - * value. - */ - public void setTrafficChannelTimeout( long milliseconds ) - { - mTrafficChannelTimeout = milliseconds; - - if( mChannelState != null ) - { - mChannelState.setTrafficChannelTimeout( milliseconds ); - } - } - - /** - * Sets the resource manager responsible for this channel and registers - * the channel manager as a listener to this channel. Fires channel add - * channel event to the newly added channel manager channel listener. - * - * Note: resource manager instance is applied after object construction - * since we construct the channel objects from the xml playlist + * Constructs a new standard channel with a default name of "Channel" */ //TODO: remove this - public void setResourceManager( ResourceManager resourceManager ) + public Channel() { - mResourceManager = resourceManager; - - /* If we're enabled, fire enable changed event to get processing started */ - if( mEnabled ) - { - enableChanged(); - } + mChannelID = UNIQUE_ID++; } //TODO: remove this - @XmlTransient - public ResourceManager getResourceManager() - { - return mResourceManager; - } - /** - * Returns the processing chain for this channel. If the processing chain - * is not currently constructed and running, this method will return null. + * Unique identifier for this channel. Value is transient (not persisted). */ @XmlTransient - public ProcessingChain getProcessingChain() + public int getChannelID() { - return mProcessingChain; + return mChannelID; } /** - * Indicates if this channel has a processing chain constructed and it - * is processing. - */ - public boolean isProcessing() - { - return mProcessingChain != null && mProcessingChain.processing(); - } - - /** - * Indicates if this channel has been selected for audio output + * Indicates if this channel has been selected for prioritized audio output + * and display of processing chain products */ @XmlTransient public boolean isSelected() @@ -224,100 +141,12 @@ public class Channel extends Configuration mSelected = selected; } - @XmlTransient - public CallEventModel getCallEventModel() - { - return mCallEventModel; - } - - /** - * Returns the MessageEventLogger for this processing chain - */ - @XmlTransient - public MessageEventLogger getEventLogger() - { - return mEventLogger; - } - - /** - * Sets the MessageEventLogger for this processing chain - */ - public void setEventLogger( MessageEventLogger eventLogger ) - { - mEventLogger = eventLogger; - } - - @XmlTransient - public MessageActivityModel getMessageActivityModel() - { - return mMessageActivityModel; - } - - @XmlTransient - public ChannelState getChannelState() - { - return mChannelState; - } - @XmlTransient public ChannelType getChannelType() { return mChannelType; } - - /** - * Orderly shutdown method when this channel is going to be deleted. - */ - public void dispose() - { - setEnabled( DISABLED ); - } - - /** - * Returns a full channel name containing the system, site and channel name. - */ - public String getChannelDisplayName() - { - StringBuilder sb = new StringBuilder(); - - sb.append( ( mSystem == null ? "-" : mSystem ) ); - sb.append( ( mSite == null ? "-" : mSite ) ); - sb.append( mName ); - - return sb.toString(); - } - - /** - * Returns the tuner channel requirement for this channel when the source - * configuration specifies a tuner and frequency as the source for this - * channel. - * - * If the source configuration is anything else, this method returns null. - */ - public TunerChannel getTunerChannel() - { - TunerChannel retVal = null; - - if( mSourceConfiguration.getSourceType() == SourceType.TUNER ) - { - SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration; - - retVal = new TunerChannel( Type.LOCKED, config.getFrequency(), - mDecodeConfiguration.getDecoderType().getChannelBandwidth() ); - } - else if( mSourceConfiguration.getSourceType() == SourceType.RECORDING ) - { - SourceConfigRecording config = - (SourceConfigRecording)mSourceConfiguration; - - retVal = new TunerChannel( Type.LOCKED, config.getFrequency(), - mDecodeConfiguration.getDecoderType().getChannelBandwidth() ); - } - - return retVal; - } - /** * Returns the owning system for this channel. */ @@ -343,50 +172,78 @@ public class Channel extends Configuration @XmlAttribute( name = "site" ) public String getSite() { - return mSite; + return mSite; } public void setSite( String site ) { mSite = site; } - + public boolean hasSite() { return mSite != null; } + /** + * Returns the name of this channel. + */ + @XmlAttribute( name = "name" ) + public String getName() + { + return mName; + } + + /** + * Sets the name of this channel. + */ + public void setName( String name ) + { + mName = name; + } + /** - * Default display string for this channel -- the channel name. + * Default display string for this channel: SYSTEM_SITE_NAME */ public String toString() { - return mName; + StringBuilder sb = new StringBuilder(); + sb.append( hasSystem() ? getSystem() : "SYSTEM" ); + sb.append( "_" ); + sb.append( hasSite() ? getSite() : "SITE" ); + sb.append( "_" ); + sb.append( getName() ); + + return sb.toString(); } /** * Indicates if this channel is enabled for processing. */ - @XmlAttribute + @XmlAttribute( name = "enabled" ) public boolean getEnabled() { return mEnabled; } /** - * Enables/disables this channel for processing. Automatically starts or - * stops channel processing in response to this change. + * Indicates if this channel will automatically start processing on + * application startup or if this channel is currently processing after + * application startup. + * + * Note: this method is package private and is intended to be managed + * by a co-package central processing manager + * + * @see controller.ChannelProcessingManager */ - public void setEnabled( boolean enabled ) + void setEnabled( boolean enabled ) { mEnabled = enabled; - - enableChanged(); } /** * Returns the alias list that is used by this channel for looking up alias - * values for the various identifiers produced by the decoder. + * values for the various identifiers produced by the decoder(s). */ @XmlElement( name = "alias_list_name" ) public String getAliasListName() @@ -404,7 +261,7 @@ public class Channel extends Configuration } /** - * Gets the decoder configuration used by this channel + * Gets the primary decoder configuration used by this channel */ @XmlElement( name = "decode_configuration" ) public DecodeConfiguration getDecodeConfiguration() @@ -413,7 +270,7 @@ public class Channel extends Configuration } /** - * Sets the decoder configuration used by this channel + * Sets the primary decoder configuration used by this channel */ public void setDecodeConfiguration( DecodeConfiguration config ) { @@ -421,7 +278,8 @@ public class Channel extends Configuration } /** - * Gets the aux decoder configuration used by this channel + * Gets the aux decoder configuration used by this channel. Aux decoders + * operate on demodulated audio */ @XmlElement( name = "aux_decode_configuration" ) public AuxDecodeConfiguration getAuxDecodeConfiguration() @@ -438,7 +296,8 @@ public class Channel extends Configuration } /** - * Returns the source configuration for this channel. + * Returns the source configuration for this channel. A channel source + * identifies where the processing chain will get source sample data from. */ @XmlElement( name = "source_configuration" ) public SourceConfiguration getSourceConfiguration() @@ -452,6 +311,10 @@ public class Channel extends Configuration public void setSourceConfiguration( SourceConfiguration config ) { mSourceConfiguration = config; + + //Clear the tune channel object so that it can be recreated if the + //source configuration changes + mTunerChannel = null; } /** @@ -489,252 +352,41 @@ public class Channel extends Configuration } /** - * Returns the name of this channel. - */ - @XmlAttribute - public String getName() - { - return mName; - } - - /** - * Sets the name of this channel. - */ - public void setName( String name ) - { - mName = name; - } - - /** - * Responds to changes in the enabled setting by starting the processing - * change if the channel is now enabled, or stopping the procesing chain - * if it was already started. - */ - private void enableChanged() - { - if( mEnabled ) - { - start(); - } - else - { - stop(); - } - } - - /** - * Starts the channel processing chain. - */ - private void start() - { - if( mEnabled && mResourceManager != null ) - { - setupChannelState(); - - try - { - setup(); - - mProcessingChain.start(); - } - catch( SourceException se ) - { - //Do nothing, we couldn't get a source for the channel - } - } - } - - private void setupChannelState() - { - if( mChannelState == null ) - { - mChannelState = new ChannelState( mResourceManager - .getThreadPoolManager(), mChannelType ); - - mChannelState.setTrafficChannelTimeout( mTrafficChannelTimeout ); - } - } - - /** - * Sets up the channel and prepares to start processing. Sets up the - * processing chain and requests a source. + * Convenience method to construct a tuner channel object representing a + * tuner or recording source frequency and bandwidth that can be used by + * application components for graphically representing this channel. * - * @throws SourceException - if no source is available or if there is an - * error obtaining a source + * If the source configuration is not a tuner or recording, this method + * returns null. */ - public void setup() throws SourceException - { - setupProcessingChain(); - setupSource(); - } + @XmlTransient + public TunerChannel getTunerChannel() + { + if( mTunerChannel == null ) + { + if( mSourceConfiguration.getSourceType() == SourceType.TUNER ) + { + SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration; + + mTunerChannel = new TunerChannel( Type.LOCKED, config.getFrequency(), + mDecodeConfiguration.getDecoderType().getChannelBandwidth() ); + } + else if( mSourceConfiguration.getSourceType() == SourceType.RECORDING ) + { + SourceConfigRecording config = + (SourceConfigRecording)mSourceConfiguration; + + mTunerChannel = new TunerChannel( Type.LOCKED, config.getFrequency(), + mDecodeConfiguration.getDecoderType().getChannelBandwidth() ); + } + } + + return mTunerChannel; + } /** - * Setup recording options. Adds RecorderManager as a audio packet listener - * to receive and record decoded audio. - */ - private void setupRecording() - { - List recorders = mRecordConfiguration.getRecorders(); - - if( !recorders.isEmpty() ) - { - if( recorders.contains( RecorderType.AUDIO ) ) - { - mProcessingChain.addAudioPacketListener( - mResourceManager.getRecorderManager() ); - } - - /* Add baseband recorder */ - if( ( recorders.contains( RecorderType.BASEBAND ) && - mChannelType == ChannelType.STANDARD ) ) - { - mProcessingChain.addModule( RecorderManager.getBasebandRecorder( - mResourceManager.getThreadPoolManager(), getChannelName() ) ); - } - - /* Add traffic channel baseband recorder */ - if( recorders.contains( RecorderType.TRAFFIC_BASEBAND ) && - mChannelType == ChannelType.TRAFFIC ) - { - mProcessingChain.addModule( RecorderManager.getBasebandRecorder( - mResourceManager.getThreadPoolManager(), getChannelName() ) ); - } - } - } - - private String getChannelName() - { - StringBuilder sb = new StringBuilder(); - sb.append( mSystem ); - sb.append( "_" ); - sb.append( mSite ); - sb.append( "_" ); - sb.append( mName ); - - return sb.toString(); - } - - private void setupLogging() - { - if( mProcessingChain != null ) - { - String channelName = getChannelName(); - - List loggers = mResourceManager.getEventLogManager() - .getLoggers( mEventLogConfiguration, channelName ); - - if( !loggers.isEmpty() ) - { - mProcessingChain.addModules( loggers ); - } - } - } - - private void setupProcessingChain() - { - if( mProcessingChain == null ) - { - mProcessingChain = new ProcessingChain(); - - /* Get the optional alias list for the decode modules to use */ - AliasList aliasList = mResourceManager.getPlaylistManager() - .getPlayist().getAliasDirectory().getAliasList( mAliasListName ); - - /* Add the channel state as a module */ - mProcessingChain.addModule( mChannelState ); - - /* Processing Modules */ - List modules = DecoderFactory.getModules( mChannelType, - mResourceManager, mDecodeConfiguration, mRecordConfiguration, - mAuxDecodeConfiguration, aliasList, mSystem, - mSite, mName ); - - mProcessingChain.addModules( modules ); - - /* Get message filters for the set of processing modules */ - FilterSet messageFilter = DecoderFactory.getMessageFilters( modules ); - - /* Setup the message activity model and add message listeners */ - mMessageActivityModel = new MessageActivityModel( messageFilter ); - mProcessingChain.addMessageListener( mMessageActivityModel ); - - /* Add audio manager as listener for audio packets */ - mProcessingChain.addAudioPacketListener( - mResourceManager.getAudioManager() ); - - /* Create the call event model to receive call events */ - mCallEventModel = new CallEventModel(); - mProcessingChain.addCallEventListener( mCallEventModel ); - - setupLogging(); - - setupRecording(); - - /* Inject channel metadata that will be inserted into audio packets - * for the recorder manager and streaming */ - mProcessingChain.broadcast( - new Metadata( MetadataType.SYSTEM, mSystem ) ); - mProcessingChain.broadcast( - new Metadata( MetadataType.SITE_ID, mSite ) ); - mProcessingChain.broadcast( - new Metadata( MetadataType.CHANNEL_NAME, mName ) ); - } - } - - /** - * Attempts to obtain a source and apply it to the processing chain. - * - * @throws SourceException - if the processing chain is not setup or if - * there is an error produced while obtaining the source - */ - private void setupSource() throws SourceException - { - if( mProcessingChain != null ) - { - Source source = mResourceManager.getSourceManager() - .getSource( getSourceConfiguration(), getDecodeConfiguration() - .getDecoderType().getChannelBandwidth() ); - - if( source != null ) - { - mProcessingChain.setSource( source ); - } - else - { - throw new SourceException( "Couldn't obtain tuner channel source" ); - } - } - else - { - throw new SourceException( "Cannot obtain source for a null " - + "processing chain - setup the processing chain first" ); - } - } - - /** - * Stops the channel processing chain - */ - private void stop() - { - if( !mEnabled && mProcessingChain != null ) - { - fireChannelEvent( Event.CHANNEL_PROCESSING_STOPPED ); - - mProcessingChain.stop(); - - if( mChannelType == ChannelType.STANDARD ) - { - mProcessingChain.dispose(); - mProcessingChain = null; - } - - mSelected = false; - } - } - - /** - * Indicates if any part of this channel is contained within the - * minimum and maximum frequency values. + * Convenience method to indicate if any part of this channel is contained + * within the minimum and maximum frequency values. */ public boolean isWithin( long minimum, long maximum ) { diff --git a/src/controller/Editor.java b/src/controller/channel/ChannelConfigurationEditor.java similarity index 58% rename from src/controller/Editor.java rename to src/controller/channel/ChannelConfigurationEditor.java index 56411ce1..7b3e1427 100644 --- a/src/controller/Editor.java +++ b/src/controller/channel/ChannelConfigurationEditor.java @@ -1,6 +1,7 @@ /******************************************************************************* + * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * Copyright (C) 2014-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,37 +16,29 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package controller; +package controller.channel; import javax.swing.JPanel; -import net.miginfocom.swing.MigLayout; -import controller.channel.ChannelValidationException; - -public abstract class Editor extends JPanel +public abstract class ChannelConfigurationEditor extends JPanel { private static final long serialVersionUID = 1L; - public Editor() - { - setLayout( new MigLayout() ); - } + /** + * Sets or resets the configuration editor components + */ + public abstract void setConfiguration( Channel channel ); + /** + * Saves the contents of the editor components to the configuration + */ public abstract void save(); - public abstract void reset(); - /** - * Override this method to allow the editor to inspect the argument for - * valid configuration + * Validates the contents of the editor components prior to saving the + * content to the configuration. * - * @param editor - any editor - * - * @throws ChannelValidationException if the configuration is not compatible - * with this editor's configuration. Supplies a validation text value for - * display to the user + * @throws ConfigurationValidationException if the content is invalid */ - public void validate( Editor editor ) throws ChannelValidationException - { - } + public abstract void validateConfiguration() throws ConfigurationValidationException; } diff --git a/src/controller/channel/ChannelEditor.java b/src/controller/channel/ChannelEditor.java index c9935088..a76902d7 100644 --- a/src/controller/channel/ChannelEditor.java +++ b/src/controller/channel/ChannelEditor.java @@ -19,21 +19,17 @@ package controller.channel; import java.awt.Color; -import java.awt.ComponentOrientation; +import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.util.Collections; -import java.util.List; -import javax.swing.DefaultComboBoxModel; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JOptionPane; +import javax.swing.JPanel; import javax.swing.JTextField; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.TreePath; import module.decode.AuxDecodeComponentEditor; import module.decode.DecodeComponentEditor; @@ -46,20 +42,21 @@ import org.slf4j.LoggerFactory; import playlist.PlaylistManager; import record.RecordComponentEditor; import source.SourceComponentEditor; +import source.SourceManager; import alias.AliasList; import com.jidesoft.swing.JideTabbedPane; -import controller.Editor; +import controller.channel.ChannelEvent.Event; -public class ChannelEditor extends Editor implements ActionListener +public class ChannelEditor extends JPanel + implements ActionListener, ChannelEventListener { + private static final long serialVersionUID = 1L; + private final static Logger mLog = LoggerFactory.getLogger( ChannelEditor.class ); - private static final long serialVersionUID = 1L; - private DefaultTreeModel mTreeModel; - private ChannelNode mChannelNode; - + private ChannelConfigurationEditor mNameConfigurationEditor; private DecodeComponentEditor mDecodeEditor; private AuxDecodeComponentEditor mAuxDecodeEditor; private EventLogComponentEditor mEventLogEditor; @@ -70,18 +67,33 @@ public class ChannelEditor extends Editor implements ActionListener private JCheckBox mChannelEnabled = new JCheckBox(); private JComboBox mComboAliasLists; + private Channel mChannel; + private ChannelModel mChannelModel; private PlaylistManager mPlaylistManager; + private SourceManager mSourceManager; - public ChannelEditor( ChannelNode channel, PlaylistManager playlistManager ) + public ChannelEditor( ChannelModel channelModel, + PlaylistManager playlistManager, + SourceManager sourceManager ) { super(); - mChannelNode = channel; - + mChannelModel = channelModel; mPlaylistManager = playlistManager; + mSourceManager = sourceManager; + + mNameConfigurationEditor = new NameConfigurationEditor( + mChannelModel, mPlaylistManager ); initGUI(); } + + public void setChannel( Channel channel ) + { + mNameConfigurationEditor.setConfiguration( channel ); + + mChannel = channel; + } private void initGUI() { @@ -89,65 +101,29 @@ public class ChannelEditor extends Editor implements ActionListener add( new JLabel( "Channel Configuration" ), "span, align center" ); - add( new JLabel( "Enabled:" ) ); + add( new JLabel( "Enabled:" ), "wrap" ); - mChannelEnabled.setSelected( - mChannelNode.getChannel().getEnabled() ); - mChannelEnabled.setComponentOrientation( ComponentOrientation.RIGHT_TO_LEFT ); - add( mChannelEnabled ); - - add( new JLabel( "Name:" ) ); - - mChannelName = new JTextField( mChannelNode.getChannel().getName() ); - add( mChannelName, "growx,push" ); - - /** - * ComboBox: Alias Lists - */ - add( new JLabel( "Alias List:" ) ); - - mComboAliasLists = new JComboBox(); - - List lists = mChannelNode.getModel().getResourceManager() - .getPlaylistManager().getPlayist().getAliasDirectory().getAliasList(); - - Collections.sort( lists ); - - mComboAliasLists.setModel( new DefaultComboBoxModel( - lists.toArray( new AliasList[ lists.size() ] ) ) ); - - String aliasListName = - mChannelNode.getChannel().getAliasListName(); - - if( aliasListName != null ) - { - AliasList selected = mChannelNode.getModel().getResourceManager() - .getPlaylistManager().getPlayist().getAliasDirectory() - .getAliasList( aliasListName ); - - mComboAliasLists.setSelectedItem( selected ); - } - - add( mComboAliasLists, "growx,push" ); + add( mNameConfigurationEditor ); JideTabbedPane tabs = new JideTabbedPane(); tabs.setFont( this.getFont() ); tabs.setForeground( Color.BLACK ); - mSourceEditor = new SourceComponentEditor( mChannelNode ); - tabs.addTab( "Source", mSourceEditor ); - - mDecodeEditor = new DecodeComponentEditor( mChannelNode, mPlaylistManager ); - tabs.addTab( "Decoder", mDecodeEditor ); - - mAuxDecodeEditor = new AuxDecodeComponentEditor( mChannelNode ); - tabs.addTab( "Aux", mAuxDecodeEditor ); - - mEventLogEditor = new EventLogComponentEditor( mChannelNode ); - tabs.addTab( "Event Log", mEventLogEditor ); - - mRecordEditor = new RecordComponentEditor( mChannelNode ); - tabs.addTab( "Record", mRecordEditor ); + tabs.addTab( "Name/Alias", mNameConfigurationEditor ); +// mSourceEditor = new SourceComponentEditor( mSourceManager, mChannel ); +// tabs.addTab( "Source", mSourceEditor ); +// +// mDecodeEditor = new DecodeComponentEditor( mPlaylistManager, mChannel ); +// tabs.addTab( "Decoder", mDecodeEditor ); +// +// mAuxDecodeEditor = new AuxDecodeComponentEditor( mChannel ); +// tabs.addTab( "Aux", mAuxDecodeEditor ); +// +// mEventLogEditor = new EventLogComponentEditor( mChannel ); +// tabs.addTab( "Event Log", mEventLogEditor ); +// +// mRecordEditor = new RecordComponentEditor( mChannel ); +// tabs.addTab( "Record", mRecordEditor ); add( tabs, "span,grow,push" ); @@ -159,6 +135,12 @@ public class ChannelEditor extends Editor implements ActionListener btnReset.addActionListener( ChannelEditor.this ); add( btnReset, "growx,push" ); } + + @Override + public void channelChanged( ChannelEvent event ) + { + + } @Override public void actionPerformed( ActionEvent e ) @@ -171,109 +153,105 @@ public class ChannelEditor extends Editor implements ActionListener } else if( command.contentEquals( "Reset" ) ) { - reset(); - } - - if( mTreeModel != null ) - { - mTreeModel.nodeChanged( mChannelNode ); + setChannel( mChannel ); } } - @Override public void save() { try { - /** - * Validate the source configuration against the other component - * configuration editors - */ - validate( mSourceEditor.getSourceEditor() ); - - boolean expanded = mChannelNode.getModel().getTree() - .isExpanded( new TreePath( mChannelNode ) ); - - mSourceEditor.save(); - mDecodeEditor.save(); - mAuxDecodeEditor.save(); - mEventLogEditor.save(); - mRecordEditor.save(); - - mChannelNode.getChannel().setName( mChannelName.getText() ); - mChannelNode.getChannel().setEnabled( mChannelEnabled.isSelected() ); - - AliasList selected = mComboAliasLists.getItemAt( - mComboAliasLists.getSelectedIndex() ); - if( selected != null ) - { - mChannelNode.getChannel().setAliasListName( selected.getName() ); - } - - - mChannelNode.save(); - mChannelNode.show(); - - if( expanded ) - { - mChannelNode.getModel().getTree().expandPath( new TreePath( mChannelNode ) ); - } + //Validate each of the configuration editors to check for errors + mNameConfigurationEditor.validateConfiguration(); +// /** +// * Validate the source configuration against the other component +// * configuration editors +// */ +// validate( mSourceEditor.getSourceEditor() ); +// +// mSourceEditor.save(); +// mDecodeEditor.save(); +// mAuxDecodeEditor.save(); +// mEventLogEditor.save(); +// mRecordEditor.save(); +// +// mChannel.setName( mChannelName.getText() ); +// mChannel.setEnabled( mChannelEnabled.isSelected() ); +// +// AliasList selected = mComboAliasLists.getItemAt( +// mComboAliasLists.getSelectedIndex() ); +// if( selected != null ) +// { +// mChannel.setAliasListName( selected.getName() ); +// } } - catch ( ChannelValidationException validationException ) + catch ( ConfigurationValidationException validationException ) { /* ChannelValidationException can be thrown by any of the component * editors. Show the validation error text in a popup menu to the - * user */ + * user and set the focus on the error component */ JOptionPane.showMessageDialog( ChannelEditor.this, validationException.getMessage(), "Channel Configuration Error", JOptionPane.ERROR_MESSAGE ); + + Component component = validationException.getComponent(); + + if( component != null ) + { + component.requestFocus(); + } + + return; } - } - @Override - public void reset() - { - mChannelName.setText( - mChannelNode.getChannel().getName() ); - mChannelName.requestFocus(); - mChannelName.requestFocusInWindow(); + //Save the contents of each editor to the channel configuration + mNameConfigurationEditor.save(); - mChannelEnabled.setSelected( - mChannelNode.getChannel().getEnabled() ); - mChannelEnabled.requestFocus(); - mChannelEnabled.requestFocusInWindow(); - - String aliasListName = - mChannelNode.getChannel().getAliasListName(); - - if( aliasListName != null ) - { - AliasList selected = mChannelNode.getModel().getResourceManager() - .getPlaylistManager().getPlayist().getAliasDirectory() - .getAliasList( aliasListName ); - - mComboAliasLists.setSelectedItem( selected ); - } - - - mSourceEditor.reset(); - mDecodeEditor.reset(); - mAuxDecodeEditor.reset(); - mEventLogEditor.reset(); - mRecordEditor.reset(); + //Broadcast a channel configuration change event + mChannelModel.broadcast( new ChannelEvent( mChannel, + Event.NOTIFICATION_CONFIGURATION_CHANGE ) ); } - /** - * Validate the specified editor against each of the component editors - */ - @Override - public void validate( Editor editor ) throws ChannelValidationException - { - mSourceEditor.validate( editor ); - mDecodeEditor.validate( editor ); - mAuxDecodeEditor.validate( editor ); - mEventLogEditor.validate( editor ); - mRecordEditor.validate( editor ); - } +// @Override +// public void reset() +// { +// mChannelName.setText( mChannel.getName() ); +// mChannelName.requestFocus(); +// mChannelName.requestFocusInWindow(); +// +// mChannelEnabled.setSelected( mChannel.getEnabled() ); +// mChannelEnabled.requestFocus(); +// mChannelEnabled.requestFocusInWindow(); +// +// String aliasListName = mChannel.getAliasListName(); +// +// if( aliasListName != null ) +// { +// AliasList selected = mPlaylistManager.getPlayist().getAliasDirectory() +// .getAliasList( aliasListName ); +// +// mComboAliasLists.setSelectedItem( selected ); +// } +// +// +// mSourceEditor.reset(); +// mDecodeEditor.reset(); +// mAuxDecodeEditor.reset(); +// mEventLogEditor.reset(); +// mRecordEditor.reset(); +// } + +// /** +// * Validate the specified editor against each of the component editors +// */ +// @Override +// public void validate( ChannelConfigurationEditor editor ) throws ConfigurationValidationException +// { +// mSourceEditor.validate( editor ); +// mDecodeEditor.validate( editor ); +// mAuxDecodeEditor.validate( editor ); +// mEventLogEditor.validate( editor ); +// mRecordEditor.validate( editor ); +// } } diff --git a/src/controller/channel/ChannelEvent.java b/src/controller/channel/ChannelEvent.java index 93bb94fc..a4166a2d 100644 --- a/src/controller/channel/ChannelEvent.java +++ b/src/controller/channel/ChannelEvent.java @@ -64,6 +64,8 @@ public class ChannelEvent NOTIFICATION_SELECTION_CHANGE, //Channel's channel-state has been reset NOTIFICATION_STATE_RESET, + //Request to delete the channel + REQUEST_DELETE, //Request to deselect the channel REQUEST_DESELECT, //Request to disable a channel - response will be a PROCESSING_STOP_NOTIFICATION diff --git a/src/controller/channel/ChannelModel.java b/src/controller/channel/ChannelModel.java new file mode 100644 index 00000000..1f0d6821 --- /dev/null +++ b/src/controller/channel/ChannelModel.java @@ -0,0 +1,346 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2015 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.swing.event.TableModelEvent; +import javax.swing.event.TableModelListener; +import javax.swing.table.TableModel; + +import module.decode.DecoderType; +import controller.channel.Channel.ChannelType; +import controller.channel.ChannelEvent.Event; + +/** + * Channel Model + */ +public class ChannelModel implements IChannelEventBroadcaster, TableModel +{ + private ChannelSelectionManager mChannelSelectionManager; + private List mChannels = new ArrayList<>(); + private List mTrafficChannels = new ArrayList<>(); + private List mListeners = new CopyOnWriteArrayList<>(); + private List mTableModelListeners = new CopyOnWriteArrayList<>(); + + public ChannelModel() + { + mChannelSelectionManager = new ChannelSelectionManager( this ); + addListener( mChannelSelectionManager ); + } + + /** + * Unmodifiable list of non-traffic channels currently in the model + */ + public List getChannels() + { + return Collections.unmodifiableList( mChannels ); + } + + /** + * Returns a list of unique system values from across the channel set + */ + public List getSystems() + { + List systems = new ArrayList<>(); + + for( Channel channel: mChannels ) + { + if( channel.hasSystem() && !systems.contains( channel.getSystem() ) ) + { + systems.add( channel.getSystem() ); + } + } + + Collections.sort( systems ); + + return systems; + } + + /** + * Returns a list of unique site values for all channels that have a matching + * system value + */ + public List getSites( String system ) + { + List sites = new ArrayList<>(); + + if( system != null ) + { + for( Channel channel: mChannels ) + { + if( channel.hasSystem() && + system.equals( channel.getSystem() ) && + channel.hasSite() && + !sites.contains( channel.getSite() ) ) + { + sites.add( channel.getSite() ); + } + } + } + + Collections.sort( sites ); + + return sites; + } + + /** + * Broadcasts the channel event to all registered listeners + */ + @Override + public void broadcast( ChannelEvent event ) + { + for( ChannelEventListener listener: mListeners ) + { + listener.channelChanged( event ); + } + + if( event.getChannel().getChannelType() == ChannelType.STANDARD ) + { + switch( event.getEvent() ) + { + case NOTIFICATION_CONFIGURATION_CHANGE: + case NOTIFICATION_PROCESSING_START: + case NOTIFICATION_PROCESSING_STOP: + case NOTIFICATION_SELECTION_CHANGE: + int index = mChannels.indexOf( event.getChannel() ); + broadcast( new TableModelEvent( this, index ) ); + break; + default: + break; + } + } + + if( event.getEvent() == Event.REQUEST_DELETE ) + { + removeChannel( event.getChannel() ); + } + } + + /** + * Bulk loading of channel list. Each channel is added and a channel add + * event is broadcast. + */ + public void addChannels( List channels ) + { + for( Channel channel: channels ) + { + addChannel( channel ); + } + } + + /** + * Adds the channel to the model and broadcasts a channel add event + */ + public void addChannel( Channel channel ) + { + switch( channel.getChannelType() ) + { + case STANDARD: + mChannels.add( channel ); + + int index = mChannels.size() - 1; + + broadcast( new TableModelEvent( this, index, index, + TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT ) ); + break; + case TRAFFIC: + mTrafficChannels.add( channel ); + break; + default: + break; + } + + broadcast( new ChannelEvent( channel, Event.NOTIFICATION_ADD ) ); + } + + /** + * Removes the channel from the model and broadcasts a channel remove event + */ + public void removeChannel( Channel channel ) + { + switch( channel.getChannelType() ) + { + case STANDARD: + int index = mChannels.indexOf( channel ); + + mChannels.remove( channel ); + + if( index >= 0 ) + { + broadcast( new TableModelEvent( this, index, index, + TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE ) ); + } + break; + case TRAFFIC: + mTrafficChannels.remove( channel ); + break; + default: + break; + } + + broadcast( new ChannelEvent( channel, Event.NOTIFICATION_DELETE ) ); + } + + /** + * Returns a list of channels that fall within the frequency range + * + * @param start frequency of the range + * @param stop frequency of the range + * + * @return list of channels or an empty list if none fall within the range + */ + public List getChannelsInFrequencyRange( long start, long stop ) + { + List channels = new ArrayList<>(); + + for( Channel channel: mChannels ) + { + if( channel.isWithin( start, stop ) ) + { + channels.add( channel ); + } + } + + for( Channel channel: mTrafficChannels ) + { + if( channel.isWithin( start, stop ) ) + { + channels.add( channel ); + } + } + + return channels; + } + + public void addListener( ChannelEventListener listener ) + { + mListeners.add( listener ); + } + + public void removeListener( ChannelEventListener listener ) + { + mListeners.remove( listener ); + } + + //Table Model Interface Methods - standard channels only + @Override + public int getRowCount() + { + return mChannels.size(); + } + + @Override + public int getColumnCount() + { + return 6; + } + + @Override + public String getColumnName( int columnIndex ) + { + switch( columnIndex ) + { + case 0: + return "System"; + case 1: + return "Site"; + case 2: + return "Name"; + case 3: + return "Alias List"; + case 4: + return "Source"; + case 5: + return "Decoder"; + } + + return null; + } + + @Override + public Class getColumnClass( int columnIndex ) + { + return String.class; + } + + @Override + public boolean isCellEditable( int rowIndex, int columnIndex ) + { + return false; + } + + @Override + public Object getValueAt( int rowIndex, int columnIndex ) + { + Channel channel = mChannels.get( rowIndex ); + + switch( columnIndex ) + { + case 0: + return channel.getSystem(); + case 1: + return channel.getSite(); + case 2: + return channel.getName(); + case 3: + return channel.getAliasListName(); + case 4: + return channel.getSourceConfiguration().getDescription(); + case 5: + return channel.getDecodeConfiguration() + .getDecoderType().getShortDisplayString(); + } + + return null; + } + + @Override + public void setValueAt( Object aValue, int rowIndex, int columnIndex ) + { + throw new IllegalArgumentException( "Not yet implemented" ); + } + + private void broadcast( TableModelEvent event ) + { + for( TableModelListener listener: mTableModelListeners ) + { + listener.tableChanged( event ); + } + } + + @Override + public void addTableModelListener( TableModelListener listener ) + { + mTableModelListeners.add( listener ); + } + + @Override + public void removeTableModelListener( TableModelListener listener ) + { + mTableModelListeners.remove( listener ); + } + + public void createChannel( DecoderType decoderType, long frequency ) + { + throw new UnsupportedOperationException( "Not yet implemented" ); + } +} diff --git a/src/controller/channel/ChannelNode.java b/src/controller/channel/ChannelNode.java index acaa0222..ca8e5b37 100644 --- a/src/controller/channel/ChannelNode.java +++ b/src/controller/channel/ChannelNode.java @@ -27,6 +27,8 @@ import javax.swing.JPopupMenu; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import playlist.PlaylistManager; +import source.SourceManager; import source.tuner.TunerChannel; import controller.ConfigurableNode; import controller.site.SiteNode; @@ -43,10 +45,22 @@ public class ChannelNode extends ConfigurableNode implements ChannelEventListene private final static Logger mLog = LoggerFactory.getLogger( ChannelNode.class ); private static DecimalFormat sFORMAT = new DecimalFormat( "0.0000" ); + + private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; + private SourceManager mSourceManager; - public ChannelNode( Channel channel ) + public ChannelNode( Channel channel, + ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, + SourceManager sourceManager ) { - super( channel ); + super( playlistManager, channel ); + + mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; + mSourceManager = sourceManager; } public void init() @@ -69,9 +83,6 @@ public class ChannelNode extends ConfigurableNode implements ChannelEventListene /* Add this node as listener to receive changes from underlying channel */ // getChannel().addListener( this ); - /* Add the resource manager to the channel so that the channel - * can provide channel change events to all system resources */ - getChannel().setResourceManager( getModel().getResourceManager() ); } /** @@ -109,7 +120,7 @@ public class ChannelNode extends ConfigurableNode implements ChannelEventListene { if( getChannel().getEnabled() ) { - if( getChannel().isProcessing() ) + if( getChannel().getEnabled() ) { return Color.GREEN; } @@ -125,9 +136,7 @@ public class ChannelNode extends ConfigurableNode implements ChannelEventListene @Override public JPanel getEditor() { - return new ChannelEditor( this, - getChannel().getResourceManager() == null ? null : - getChannel().getResourceManager().getPlaylistManager() ); + return new OldChannelEditor( getChannel(), mChannelModel, getPlaylistManager(), mSourceManager ); } public Channel getChannel() @@ -158,9 +167,9 @@ public class ChannelNode extends ConfigurableNode implements ChannelEventListene { JPopupMenu popupMenu = new JPopupMenu(); - JMenu channelMenu = ChannelUtils.getContextMenu( - getModel().getResourceManager().getPlaylistManager(), - getChannel(), getModel().getTree() ); + JMenu channelMenu = ChannelUtils.getContextMenu( mChannelModel, + mChannelProcessingManager, getPlaylistManager(), getChannel(), + getModel().getTree() ); if( channelMenu != null ) { diff --git a/src/controller/channel/ChannelProcessingManager.java b/src/controller/channel/ChannelProcessingManager.java new file mode 100644 index 00000000..9bdb727e --- /dev/null +++ b/src/controller/channel/ChannelProcessingManager.java @@ -0,0 +1,301 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ + +package controller.channel; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +import message.Message; +import module.Module; +import module.ProcessingChain; +import module.decode.DecoderFactory; +import module.decode.event.MessageActivityModel; +import module.log.EventLogManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import playlist.PlaylistManager; +import record.RecorderManager; +import record.RecorderType; +import sample.Listener; +import source.Source; +import source.SourceException; +import source.SourceManager; +import audio.AudioPacket; +import audio.metadata.Metadata; +import audio.metadata.MetadataType; +import controller.ThreadPoolManager; +import controller.channel.Channel.ChannelType; +import controller.channel.ChannelEvent.Event; +import filter.FilterSet; + +public class ChannelProcessingManager implements ChannelEventListener +{ + private final static Logger mLog = LoggerFactory.getLogger( ChannelProcessingManager.class ); + + private Map mProcessingChains = new HashMap<>(); + + private List> mAudioPacketListeners = new CopyOnWriteArrayList<>(); + private List> mMessageListeners = new CopyOnWriteArrayList<>(); + + private ChannelModel mChannelModel; + private EventLogManager mEventLogManager; + private PlaylistManager mPlaylistManager; + private RecorderManager mRecorderManager; + private SourceManager mSourceManager; + private ThreadPoolManager mThreadPoolManager; + + public ChannelProcessingManager( ChannelModel channelModel, + EventLogManager eventLogManager, + PlaylistManager playlistManager, + RecorderManager recorderManager, + SourceManager sourceManager, + ThreadPoolManager threadPoolManager ) + { + mChannelModel = channelModel; + mEventLogManager = eventLogManager; + mPlaylistManager = playlistManager; + mRecorderManager = recorderManager; + mSourceManager = sourceManager; + mThreadPoolManager = threadPoolManager; + + mChannelModel.addListener( this ); + } + + /** + * Indicates if a processing chain is constructed for the channel and that + * the processing chain is currently processing. + */ + public boolean isProcessing( Channel channel ) + { + if( mProcessingChains.containsKey( channel.getChannelID() ) ) + { + return mProcessingChains.get( channel.getChannelID() ).isProcessing(); + } + + return false; + } + + /** + * Returns the current processing chain associated with the channel, or + * null if a processing chain is not currently setup for the channel + */ + public ProcessingChain getProcessingChain( Channel channel ) + { + return mProcessingChains.get( channel.getChannelID() ); + } + + @Override + public void channelChanged( ChannelEvent event ) + { + Channel channel = event.getChannel(); + + switch( event.getEvent() ) + { + case REQUEST_ENABLE: + if( !mProcessingChains.containsKey( channel.getChannelID() ) || + !mProcessingChains.get( channel.getChannelID() ).isProcessing() ) + { + startProcessing( channel ); + } + break; + case REQUEST_DISABLE: + if( channel.getEnabled() ) + { + switch( channel.getChannelType() ) + { + case STANDARD: + stopProcessing( channel, true ); + break; + case TRAFFIC: + //Don't remove traffic channel processing chains + //until explicitly deleted, so that we can reuse them + stopProcessing( channel, false ); + default: + break; + } + } + case NOTIFICATION_DELETE: + if( channel.getEnabled() ) + { + stopProcessing( channel, true ); + } + break; + default: + break; + } + } + + private void startProcessing( Channel channel ) + { + ProcessingChain processingChain = mProcessingChains.get( channel.getChannelID() ); + + //If we're already processing, ignore the request + if( processingChain != null && processingChain.isProcessing() ) + { + return; + } + + //Ensure that we can get a source before we construct a new processing chain + Source source = null; + + try + { + source = mSourceManager.getSource( channel.getSourceConfiguration(), + channel.getDecodeConfiguration().getDecoderType().getChannelBandwidth() ); + } + catch( SourceException se ) + { + mLog.debug( "Error obtaining source for channel [" + channel.getName() + "]", se ); + } + + if( source == null ) + { + channel.setEnabled( false ); + + mChannelModel.broadcast( + new ChannelEvent( channel, Event.NOTIFICATION_ENABLE_REJECTED ) ); + + return; + } + + if( processingChain == null ) + { + processingChain = new ProcessingChain( mThreadPoolManager, + channel.getChannelType() ); + + /* Register global listeners */ + for( Listener listener: mAudioPacketListeners ) + { + processingChain.addAudioPacketListener( listener ); + } + + for( Listener listener: mMessageListeners ) + { + processingChain.addMessageListener( listener ); + } + + /* Processing Modules */ + List modules = DecoderFactory.getModules( mChannelModel, + this, mPlaylistManager, channel ); + processingChain.addModules( modules ); + + /* Setup message activity model with filtering */ + FilterSet messageFilter = DecoderFactory.getMessageFilters( modules ); + MessageActivityModel messageModel = new MessageActivityModel( messageFilter ); + processingChain.setMessageActivityModel( messageModel ); + + /* Setup event logging */ + List loggers = mEventLogManager.getLoggers( + channel.getEventLogConfiguration(), channel.getName() ); + + if( !loggers.isEmpty() ) + { + processingChain.addModules( loggers ); + } + + /* Setup recorders */ + List recorders = channel.getRecordConfiguration().getRecorders(); + + if( !recorders.isEmpty() ) + { + if( recorders.contains( RecorderType.AUDIO ) ) + { + processingChain.addAudioPacketListener( mRecorderManager ); + } + + /* Add baseband recorder */ + if( ( recorders.contains( RecorderType.BASEBAND ) && + channel.getChannelType() == ChannelType.STANDARD ) ) + { + processingChain.addModule( mRecorderManager.getBasebandRecorder( + channel.toString() ) ); + } + + /* Add traffic channel baseband recorder */ + if( recorders.contains( RecorderType.TRAFFIC_BASEBAND ) && + channel.getChannelType() == ChannelType.TRAFFIC ) + { + processingChain.addModule( mRecorderManager + .getBasebandRecorder( channel.toString() ) ); + } + } + + /* Inject channel metadata that will be inserted into audio packets + * for the recorder manager and streaming */ + processingChain.broadcast( + new Metadata( MetadataType.SYSTEM, channel.getSystem() ) ); + processingChain.broadcast( + new Metadata( MetadataType.SITE_ID, channel.getSite() ) ); + processingChain.broadcast( + new Metadata( MetadataType.CHANNEL_NAME, channel.getName() ) ); + } + + processingChain.setSource( source ); + processingChain.start(); + + channel.setEnabled( true ); + + mChannelModel.broadcast( + new ChannelEvent( channel, Event.NOTIFICATION_PROCESSING_START ) ); + } + + private void stopProcessing( Channel channel, boolean remove ) + { + mChannelModel.broadcast( + new ChannelEvent( channel, Event.NOTIFICATION_PROCESSING_STOP ) ); + } + + /** + * Adds a message listener that will be added to all channels to receive + * any messages. + */ + public void addAudioPacketListener( Listener listener ) + { + mAudioPacketListeners.add( listener ); + } + + /** + * Removes a message listener. + */ + public void removeAudioPacketListener( Listener listener ) + { + mAudioPacketListeners.remove( listener ); + } + + /** + * Adds a message listener that will be added to all channels to receive + * any messages. + */ + public void addMessageListener( Listener listener ) + { + mMessageListeners.add( listener ); + } + + /** + * Removes a message listener. + */ + public void removeMessageListener( Listener listener ) + { + mMessageListeners.remove( listener ); + } +} diff --git a/src/controller/channel/ChannelSelectionManager.java b/src/controller/channel/ChannelSelectionManager.java new file mode 100644 index 00000000..65ac9b91 --- /dev/null +++ b/src/controller/channel/ChannelSelectionManager.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2015 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + +import controller.channel.ChannelEvent.Event; + +public class ChannelSelectionManager implements ChannelEventListener +{ + private IChannelEventBroadcaster mChannelEventBroadcaster; + private Channel mSelectedChannel = null; + + /** + * Manages channel selection state to ensure that only one channel is ever + * in a selected state. + */ + public ChannelSelectionManager( IChannelEventBroadcaster broadcaster ) + { + mChannelEventBroadcaster = broadcaster; + } + + @Override + public void channelChanged( ChannelEvent event ) + { + switch( event.getEvent() ) + { + case REQUEST_SELECT: + if( mSelectedChannel != null ) + { + mSelectedChannel.setSelected( false ); + mChannelEventBroadcaster.broadcast( new ChannelEvent( mSelectedChannel, + Event.NOTIFICATION_SELECTION_CHANGE ) ); + mSelectedChannel = null; + } + + mSelectedChannel = event.getChannel(); + mSelectedChannel.setSelected( true ); + mChannelEventBroadcaster.broadcast( new ChannelEvent( event.getChannel(), + Event.NOTIFICATION_SELECTION_CHANGE ) ); + break; + case NOTIFICATION_PROCESSING_STOP: + case REQUEST_DESELECT: + if( mSelectedChannel != null && + mSelectedChannel == event.getChannel() ) + { + mSelectedChannel.setSelected( false ); + mSelectedChannel = null; + mChannelEventBroadcaster.broadcast( new ChannelEvent( event.getChannel(), + Event.NOTIFICATION_SELECTION_CHANGE ) ); + } + break; + default: + break; + } + } + +} diff --git a/src/controller/channel/ChannelUtils.java b/src/controller/channel/ChannelUtils.java index f4fd2bf1..9a716ea3 100644 --- a/src/controller/channel/ChannelUtils.java +++ b/src/controller/channel/ChannelUtils.java @@ -9,10 +9,11 @@ import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JSeparator; +import controller.channel.ChannelEvent.Event; +import module.ProcessingChain; import module.decode.event.ActivitySummaryFrame; import module.decode.state.DecoderState; import playlist.PlaylistManager; -import controller.channel.ChannelEvent.Event; public class ChannelUtils { @@ -23,7 +24,9 @@ public class ChannelUtils /** * Creates a context menu for the channel argument */ - public static JMenu getContextMenu( final PlaylistManager playlistManager, + public static JMenu getContextMenu( final ChannelModel channelModel, + final ChannelProcessingManager channelProcessingManager, + final PlaylistManager playlistManager, final Channel channel, final Component anchor ) { @@ -39,12 +42,8 @@ public class ChannelUtils @Override public void actionPerformed( ActionEvent e ) { - channel.setEnabled( DISABLED ); - - if( playlistManager != null ) - { - playlistManager.save(); - } + channelModel.broadcast( + new ChannelEvent( channel, Event.REQUEST_DISABLE ) ); } } ); @@ -62,10 +61,14 @@ public class ChannelUtils { StringBuilder sb = new StringBuilder(); - for( DecoderState decoderState: channel - .getProcessingChain().getDecoderStates() ) + ProcessingChain chain = channelProcessingManager.getProcessingChain( channel ); + + if( chain != null ) { - sb.append( decoderState.getActivitySummary() ); + for( DecoderState decoderState: chain.getDecoderStates() ) + { + sb.append( decoderState.getActivitySummary() ); + } } new ActivitySummaryFrame( sb.toString(), anchor ); @@ -82,12 +85,8 @@ public class ChannelUtils @Override public void actionPerformed( ActionEvent e ) { - channel.setEnabled( ENABLED ); - - if( playlistManager != null ) - { - playlistManager.save(); - } + channelModel.broadcast( + new ChannelEvent( channel, Event.REQUEST_ENABLE ) ); } } ); @@ -108,16 +107,8 @@ public class ChannelUtils if( response == JOptionPane.YES_OPTION ) { - /* Disable the channel */ - channel.setEnabled( DISABLED ); - - /* Broadcast channel deleted event */ -// channel.fireChannelEvent( Event.NOTIFICATION_DELETE ); - - if( playlistManager != null ) - { - playlistManager.save(); - } + channelModel.broadcast( + new ChannelEvent( channel, Event.REQUEST_DELETE ) ); } } } ); diff --git a/src/controller/channel/ChannelValidationException.java b/src/controller/channel/ChannelValidationException.java deleted file mode 100644 index 53422a11..00000000 --- a/src/controller/channel/ChannelValidationException.java +++ /dev/null @@ -1,11 +0,0 @@ -package controller.channel; - -public class ChannelValidationException extends Exception -{ - private static final long serialVersionUID = 1L; - - public ChannelValidationException( String validationMessage ) - { - super( validationMessage ); - } -} diff --git a/src/controller/channel/ConfigurationValidationException.java b/src/controller/channel/ConfigurationValidationException.java new file mode 100644 index 00000000..a61862c5 --- /dev/null +++ b/src/controller/channel/ConfigurationValidationException.java @@ -0,0 +1,23 @@ +package controller.channel; + +import java.awt.Component; + +public class ConfigurationValidationException extends Exception +{ + private static final long serialVersionUID = 1L; + + private Component mComponent; + + public ConfigurationValidationException( Component component, + String validationMessage ) + { + super( validationMessage ); + + mComponent = component; + } + + public Component getComponent() + { + return mComponent; + } +} diff --git a/src/buffer/ComplexByteBufferListener.java b/src/controller/channel/IChannelEventBroadcaster.java similarity index 77% rename from src/buffer/ComplexByteBufferListener.java rename to src/controller/channel/IChannelEventBroadcaster.java index efdb3a1a..ef7c89fd 100644 --- a/src/buffer/ComplexByteBufferListener.java +++ b/src/controller/channel/IChannelEventBroadcaster.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * Copyright (C) 2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,9 +15,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package buffer; +package controller.channel; -public interface ComplexByteBufferListener +/** + * Provides channel event broadcasting capability. + */ +public interface IChannelEventBroadcaster { - public void receive( ComplexByteBuffer buffer ); + /** + * Broadcasts the channel event + */ + public void broadcast( ChannelEvent event ); } diff --git a/src/controller/channel/NameConfigurationEditor.java b/src/controller/channel/NameConfigurationEditor.java new file mode 100644 index 00000000..c7c171ec --- /dev/null +++ b/src/controller/channel/NameConfigurationEditor.java @@ -0,0 +1,221 @@ +package controller.channel; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collections; +import java.util.List; + +import javax.swing.ComboBoxModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JTextField; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.miginfocom.swing.MigLayout; +import playlist.PlaylistManager; +import alias.AliasList; + +public class NameConfigurationEditor extends ChannelConfigurationEditor +{ + private static final long serialVersionUID = 1L; + + private final static Logger mLog = LoggerFactory.getLogger( NameConfigurationEditor.class ); + + private static ComboBoxModel EMPTY_MODEL = new DefaultComboBoxModel<>(); + + private ChannelModel mChannelModel; + private PlaylistManager mPlaylistManager; + + private JTextField mChannelName = new JTextField( "Please select a channel to edit" ); + private JComboBox mSystemNameCombo = new JComboBox<>( EMPTY_MODEL ); + private JComboBox mSiteNameCombo = new JComboBox<>( EMPTY_MODEL ); + private JComboBox mAliasListCombo; + + private Channel mChannel; + + public NameConfigurationEditor( ChannelModel model, + PlaylistManager playlistManager ) + { + mChannelModel = model; + mPlaylistManager = playlistManager; + + setLayout( new MigLayout( "fill,wrap 2", "[right][]", "[][][][grow]" ) ); + + add( new JLabel( "Name:" ) ); + add( mChannelName, "growx" ); + + mSystemNameCombo.setEditable( true ); + mSystemNameCombo.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + Object selected = mSystemNameCombo.getSelectedItem(); + + if( selected != null ) + { + String system = (String)selected; + + List sites = mChannelModel.getSites( system ); + + if( !sites.contains( mChannel.getSite() ) ) + { + sites.add( mChannel.getSite() ); + } + + if( sites.isEmpty() ) + { + mSiteNameCombo.setModel( EMPTY_MODEL ); + } + else + { + mSiteNameCombo.setModel( new DefaultComboBoxModel( + sites.toArray( new String[ sites.size() ] ) ) );; + } + + mSiteNameCombo.setSelectedItem( mChannel.getSite() ); + } + } + } ); + + add( new JLabel( "System:" ) ); + add( mSystemNameCombo, "growx" ); + + mSiteNameCombo.setEditable( true ); + + add( new JLabel( "Site:" ) ); + add( mSiteNameCombo, "growx" ); + + /** + * ComboBox: Alias Lists + */ + add( new JLabel( "Alias List:" ) ); + + mAliasListCombo = new JComboBox(); + + List lists = mPlaylistManager.getPlayist() + .getAliasDirectory().getAliasList(); + + Collections.sort( lists ); + + mAliasListCombo.setModel( new DefaultComboBoxModel( + lists.toArray( new AliasList[ lists.size() ] ) ) ); + + add( mAliasListCombo, "growx" ); + + add( new JLabel() ); + + final JButton aliasListButton = new JButton( "New Alias List ..." ); + + aliasListButton.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + String name = JOptionPane.showInputDialog( aliasListButton, + "Please enter an Alias List name:" ); + + if( name != null && !name.isEmpty() ) + { + AliasList aliasList = new AliasList( name ); + + mPlaylistManager.getPlayist().getAliasDirectory() + .addAliasList( aliasList ); + + mPlaylistManager.save(); + + List lists = mPlaylistManager.getPlayist() + .getAliasDirectory().getAliasList(); + + Collections.sort( lists ); + + mAliasListCombo.setModel( new DefaultComboBoxModel( + lists.toArray( new AliasList[ lists.size() ] ) ) ); + + mAliasListCombo.setSelectedItem( aliasList ); + } + } + } ); + + add( aliasListButton, "growx" ); + } + + @Override + public void save() + { + if( mChannel != null ) + { + mChannel.setName( mChannelName.getText() ); + + Object system = mSystemNameCombo.getSelectedItem(); + mChannel.setSystem( ( system == null ? null : (String)system ) ); + + Object site = mSiteNameCombo.getSelectedItem(); + mChannel.setSystem( ( site == null ? null : (String)site ) ); + } + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + //System and site can be null, but we must (should?) have a channel name + String name = mChannelName.getText(); + + if( name == null || name.isEmpty() ) + { + throw new ConfigurationValidationException( + mChannelName, "Channel name cannot be empty" ); + } + } + + @Override + public void setConfiguration( Channel channel ) + { + mChannel = channel; + + mChannelName.setText( channel.getName() ); + + List systems = mChannelModel.getSystems(); + + if( systems.isEmpty() ) + { + mSystemNameCombo.setModel( EMPTY_MODEL ); + } + else + { + mSystemNameCombo.setModel( new DefaultComboBoxModel( + systems.toArray( new String[ systems.size() ] ) ) );; + } + + mSystemNameCombo.setSelectedItem( channel.getSystem() ); + + List sites = mChannelModel.getSites( channel.getSystem() ); + + if( sites.isEmpty() ) + { + mSiteNameCombo.setModel( EMPTY_MODEL ); + } + else + { + mSiteNameCombo.setModel( new DefaultComboBoxModel( + sites.toArray( new String[ sites.size() ] ) ) );; + } + + mSiteNameCombo.setSelectedItem( channel.getSite() ); + + String aliasListName = mChannel.getAliasListName(); + + if( aliasListName != null ) + { + AliasList selected = mPlaylistManager.getPlayist().getAliasDirectory() + .getAliasList( aliasListName ); + + mAliasListCombo.setSelectedItem( selected ); + } + } +} diff --git a/src/controller/channel/OldChannelEditor.java b/src/controller/channel/OldChannelEditor.java new file mode 100644 index 00000000..f4f317f8 --- /dev/null +++ b/src/controller/channel/OldChannelEditor.java @@ -0,0 +1,260 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package controller.channel; + + +import java.awt.Color; +import java.awt.ComponentOrientation; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collections; +import java.util.List; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JTextField; + +import module.decode.AuxDecodeComponentEditor; +import module.decode.DecodeComponentEditor; +import module.log.EventLogComponentEditor; +import net.miginfocom.swing.MigLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import playlist.PlaylistManager; +import record.RecordComponentEditor; +import source.SourceComponentEditor; +import source.SourceManager; +import alias.AliasList; + +import com.jidesoft.swing.JideTabbedPane; + +public class OldChannelEditor extends ChannelConfigurationEditor implements ActionListener +{ + private final static Logger mLog = LoggerFactory.getLogger( OldChannelEditor.class ); + + private static final long serialVersionUID = 1L; + + private DecodeComponentEditor mDecodeEditor; + private AuxDecodeComponentEditor mAuxDecodeEditor; + private EventLogComponentEditor mEventLogEditor; + private RecordComponentEditor mRecordEditor; + private SourceComponentEditor mSourceEditor; + + private JTextField mChannelName; + private JCheckBox mChannelEnabled = new JCheckBox(); + private JComboBox mComboAliasLists; + + private Channel mChannel; + private ChannelModel mChannelModel; + private PlaylistManager mPlaylistManager; + private SourceManager mSourceManager; + + public OldChannelEditor( Channel channel, + ChannelModel channelModel, + PlaylistManager playlistManager, + SourceManager sourceManager ) + { + super(); + + mChannel = channel; + mChannelModel = channelModel; + mPlaylistManager = playlistManager; + mSourceManager = sourceManager; + + initGUI(); + } + + private void initGUI() + { + setLayout( new MigLayout( "fill,wrap 2", "[right,grow][grow]", "[][][][][grow][]" ) ); + + add( new JLabel( "Channel Configuration" ), "span, align center" ); + + add( new JLabel( "Enabled:" ) ); + + mChannelEnabled.setSelected( mChannel.getEnabled() ); + mChannelEnabled.setComponentOrientation( ComponentOrientation.RIGHT_TO_LEFT ); + add( mChannelEnabled ); + + add( new JLabel( "Name:" ) ); + + mChannelName = new JTextField( mChannel.getName() ); + add( mChannelName, "growx,push" ); + + /** + * ComboBox: Alias Lists + */ + add( new JLabel( "Alias List:" ) ); + + mComboAliasLists = new JComboBox(); + + List lists = mPlaylistManager.getPlayist() + .getAliasDirectory().getAliasList(); + + Collections.sort( lists ); + + mComboAliasLists.setModel( new DefaultComboBoxModel( + lists.toArray( new AliasList[ lists.size() ] ) ) ); + + String aliasListName = mChannel.getAliasListName(); + + if( aliasListName != null ) + { + AliasList selected = mPlaylistManager.getPlayist().getAliasDirectory() + .getAliasList( aliasListName ); + + mComboAliasLists.setSelectedItem( selected ); + } + + add( mComboAliasLists, "growx,push" ); + + JideTabbedPane tabs = new JideTabbedPane(); + tabs.setFont( this.getFont() ); + tabs.setForeground( Color.BLACK ); + + mSourceEditor = new SourceComponentEditor( mSourceManager, mChannel ); + tabs.addTab( "Source", mSourceEditor ); + + mDecodeEditor = new DecodeComponentEditor( mPlaylistManager, mChannel ); + tabs.addTab( "Decoder", mDecodeEditor ); + + mAuxDecodeEditor = new AuxDecodeComponentEditor( mChannel ); + tabs.addTab( "Aux", mAuxDecodeEditor ); + + mEventLogEditor = new EventLogComponentEditor( mChannel ); + tabs.addTab( "Event Log", mEventLogEditor ); + + mRecordEditor = new RecordComponentEditor( mChannel ); + tabs.addTab( "Record", mRecordEditor ); + + add( tabs, "span,grow,push" ); + + JButton btnSave = new JButton( "Save" ); + btnSave.addActionListener( OldChannelEditor.this ); + add( btnSave, "growx,push" ); + + JButton btnReset = new JButton( "Reset" ); + btnReset.addActionListener( OldChannelEditor.this ); + add( btnReset, "growx,push" ); + } + + @Override + public void actionPerformed( ActionEvent e ) + { + String command = e.getActionCommand(); + + if( command.contentEquals( "Save" ) ) + { + save(); + } + else if( command.contentEquals( "Reset" ) ) + { +// reset(); + } + } + + @Override + public void save() + { +// try +// { +// /** +// * Validate the source configuration against the other component +// * configuration editors +// */ +// validate( mSourceEditor.getSourceEditor() ); +// +// mSourceEditor.save(); +// mDecodeEditor.save(); +// mAuxDecodeEditor.save(); +// mEventLogEditor.save(); +// mRecordEditor.save(); +// +// mChannel.setName( mChannelName.getText() ); +// mChannel.setEnabled( mChannelEnabled.isSelected() ); +// +// AliasList selected = mComboAliasLists.getItemAt( +// mComboAliasLists.getSelectedIndex() ); +// if( selected != null ) +// { +// mChannel.setAliasListName( selected.getName() ); +// } +// } +// catch ( ConfigurationValidationException validationException ) +// { +// /* ChannelValidationException can be thrown by any of the component +// * editors. Show the validation error text in a popup menu to the +// * user */ +// JOptionPane.showMessageDialog( OldChannelEditor.this, +// validationException.getMessage(), +// "Channel Configuration Error", +// JOptionPane.ERROR_MESSAGE ); +// } + } + +// @Override +// public void reset() +// { +// mChannelName.setText( mChannel.getName() ); +// mChannelName.requestFocus(); +// mChannelName.requestFocusInWindow(); +// +// mChannelEnabled.setSelected( mChannel.getEnabled() ); +// mChannelEnabled.requestFocus(); +// mChannelEnabled.requestFocusInWindow(); +// +// String aliasListName = mChannel.getAliasListName(); +// +// if( aliasListName != null ) +// { +// AliasList selected = mPlaylistManager.getPlayist().getAliasDirectory() +// .getAliasList( aliasListName ); +// +// mComboAliasLists.setSelectedItem( selected ); +// } +// +// +// mSourceEditor.reset(); +// mDecodeEditor.reset(); +// mAuxDecodeEditor.reset(); +// mEventLogEditor.reset(); +// mRecordEditor.reset(); +// } + + @Override + public void setConfiguration( Channel channel ) + { + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + mSourceEditor.validate(); + mDecodeEditor.validate(); + mAuxDecodeEditor.validate(); + mEventLogEditor.validate(); + mRecordEditor.validate(); + } + +} diff --git a/src/controller/channel/map/ChannelMapListNode.java b/src/controller/channel/map/ChannelMapListNode.java index 208e22a4..9259bba3 100644 --- a/src/controller/channel/map/ChannelMapListNode.java +++ b/src/controller/channel/map/ChannelMapListNode.java @@ -23,15 +23,16 @@ import java.awt.event.ActionListener; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import controller.ConfigurableNode; public class ChannelMapListNode extends ConfigurableNode { private static final long serialVersionUID = 1L; - public ChannelMapListNode( ChannelMapList list ) + public ChannelMapListNode( PlaylistManager playlistManager, ChannelMapList list ) { - super( list ); + super( playlistManager, list ); } public ChannelMapList getChannelMapList() @@ -43,7 +44,7 @@ public class ChannelMapListNode extends ConfigurableNode { for( ChannelMap channelMap: getChannelMapList().getChannelMap() ) { - ChannelMapNode node = new ChannelMapNode( channelMap ); + ChannelMapNode node = new ChannelMapNode( getPlaylistManager(), channelMap ); getModel().addNode( node, ChannelMapListNode.this, getChildCount() ); @@ -72,7 +73,7 @@ public class ChannelMapListNode extends ConfigurableNode getChannelMapList().addChannelMap( channelMap ); - getModel().addNode( new ChannelMapNode( channelMap ), + getModel().addNode( new ChannelMapNode( getPlaylistManager(), channelMap ), ChannelMapListNode.this, ChannelMapListNode.this.getChildCount() ); diff --git a/src/controller/channel/map/ChannelMapNode.java b/src/controller/channel/map/ChannelMapNode.java index 7a0712e0..ef6c6bfe 100644 --- a/src/controller/channel/map/ChannelMapNode.java +++ b/src/controller/channel/map/ChannelMapNode.java @@ -25,15 +25,16 @@ import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; import controller.ConfigurableNode; public class ChannelMapNode extends ConfigurableNode { private static final long serialVersionUID = 1L; - public ChannelMapNode( ChannelMap ChannelMap ) + public ChannelMapNode( PlaylistManager playlistManager, ChannelMap ChannelMap ) { - super( ChannelMap ); + super( playlistManager, ChannelMap ); } public void init() diff --git a/src/controller/site/SiteNode.java b/src/controller/site/SiteNode.java index b1184ab3..65ad140f 100644 --- a/src/controller/site/SiteNode.java +++ b/src/controller/site/SiteNode.java @@ -29,26 +29,43 @@ import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.tree.DefaultMutableTreeNode; +import playlist.PlaylistManager; +import source.SourceManager; import controller.ConfigurableNode; import controller.channel.Channel; +import controller.channel.ChannelModel; import controller.channel.ChannelNode; +import controller.channel.ChannelProcessingManager; import controller.system.System; import controller.system.SystemNode; public class SiteNode extends ConfigurableNode { private static final long serialVersionUID = 1L; + + private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; + private SourceManager mSourceManager; - public SiteNode( Site site ) + public SiteNode( Site site, + ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, + SourceManager sourceManager ) { - super( site ); + super( playlistManager, site ); + + mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; + mSourceManager = sourceManager; } public void init() { for( Channel channel: getSite().getChannel() ) { - ChannelNode node = new ChannelNode( channel ); + ChannelNode node = new ChannelNode( channel, mChannelModel, + mChannelProcessingManager, getPlaylistManager(), mSourceManager ); getModel().insertNodeInto( node, SiteNode.this, getChildCount() ); @@ -130,7 +147,8 @@ public class SiteNode extends ConfigurableNode getSite().addChannel( channel ); - ChannelNode node = new ChannelNode( channel ); + ChannelNode node = new ChannelNode( channel, mChannelModel, + mChannelProcessingManager, getPlaylistManager(), mSourceManager ); getModel().addNode( node, SiteNode.this, @@ -139,11 +157,6 @@ public class SiteNode extends ConfigurableNode sort(); save(); node.show(); - - /* Give channel reference to the resource manager and the - * channel will register the listeners and send a channel - * add event */ - channel.setResourceManager( getModel().getResourceManager() ); } } ); diff --git a/src/controller/system/SystemListNode.java b/src/controller/system/SystemListNode.java index 4629133d..d0e5b3a9 100644 --- a/src/controller/system/SystemListNode.java +++ b/src/controller/system/SystemListNode.java @@ -23,15 +23,31 @@ import java.awt.event.ActionListener; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; +import playlist.PlaylistManager; +import source.SourceManager; import controller.ConfigurableNode; +import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; public class SystemListNode extends ConfigurableNode { private static final long serialVersionUID = 1L; - public SystemListNode( SystemList list ) + private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; + private SourceManager mSourceManager; + + public SystemListNode( SystemList list, + ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, + SourceManager sourceManager ) { - super( list ); + super( playlistManager, list ); + + mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; + mSourceManager = sourceManager; } public SystemList getSystemList() @@ -43,7 +59,8 @@ public class SystemListNode extends ConfigurableNode { for( System system: getSystemList().getSystem() ) { - SystemNode node = new SystemNode( system ); + SystemNode node = new SystemNode( system, mChannelModel, + mChannelProcessingManager, getPlaylistManager(), mSourceManager ); getModel().addNode( node, SystemListNode.this, getChildCount() ); @@ -72,9 +89,9 @@ public class SystemListNode extends ConfigurableNode getSystemList().addSystem( system ); - getModel().addNode( new SystemNode( system ), - SystemListNode.this, - SystemListNode.this.getChildCount() ); + getModel().addNode( new SystemNode( system, mChannelModel, + mChannelProcessingManager, getPlaylistManager(), mSourceManager ), + SystemListNode.this, SystemListNode.this.getChildCount() ); } } ); diff --git a/src/controller/system/SystemNode.java b/src/controller/system/SystemNode.java index a9c4e3b9..39f28214 100644 --- a/src/controller/system/SystemNode.java +++ b/src/controller/system/SystemNode.java @@ -29,7 +29,11 @@ import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.tree.DefaultMutableTreeNode; +import playlist.PlaylistManager; +import source.SourceManager; import controller.ConfigurableNode; +import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; import controller.site.Site; import controller.site.SiteNode; @@ -37,16 +41,29 @@ public class SystemNode extends ConfigurableNode { private static final long serialVersionUID = 1L; - public SystemNode( System system ) + private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; + private SourceManager mSourceManager; + + public SystemNode( System system, + ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, + SourceManager sourceManager ) { - super( system ); + super( playlistManager, system ); + + mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; + mSourceManager = sourceManager; } public void init() { for( Site site: getSystem().getSite() ) { - SiteNode node = new SiteNode( site ); + SiteNode node = new SiteNode( site, mChannelModel, + mChannelProcessingManager, getPlaylistManager(), mSourceManager ); getModel().insertNodeInto( node, SystemNode.this, getChildCount() ); @@ -133,7 +150,8 @@ public class SystemNode extends ConfigurableNode Site site = new Site(); getSystem().addSite( site ); - SiteNode node = new SiteNode( site ); + SiteNode node = new SiteNode( site, mChannelModel, + mChannelProcessingManager, getPlaylistManager(), mSourceManager ); getModel().addNode( node, SystemNode.this, diff --git a/src/dsp/filter/dc/DCRemovalFilter_RealBuffer.java b/src/dsp/filter/dc/DCRemovalFilter_RealBuffer.java new file mode 100644 index 00000000..308b4ed7 --- /dev/null +++ b/src/dsp/filter/dc/DCRemovalFilter_RealBuffer.java @@ -0,0 +1,60 @@ +package dsp.filter.dc; + +import sample.Listener; +import sample.real.RealBuffer; + +public class DCRemovalFilter_RealBuffer implements Listener +{ + private Listener mListener; + /** + * Removes DC bias present across the samples in the buffer by calculating + * the mean DC component and subtracting that component from each of the + * samples. + */ + public DCRemovalFilter_RealBuffer() + { + } + + public void dispose() + { + mListener = null; + } + + public RealBuffer filter( RealBuffer buffer ) + { + double accumulator = 0.0d; + + for( float sample: buffer.getSamples() ) + { + accumulator += sample; + } + + float mean = (float)( accumulator / (double)buffer.getSamples().length ); + + for( float sample: buffer.getSamples() ) + { + sample -= mean; + } + + return buffer; + } + + @Override + public void receive( RealBuffer buffer ) + { + if( mListener != null ) + { + mListener.receive( filter( buffer ) ); + } + } + + public void setListener( Listener listener ) + { + mListener = listener; + } + + public void removeListener() + { + mListener = null; + } +} diff --git a/src/dsp/psk/LSMDemodulator.java b/src/dsp/psk/LSMDemodulator.java index dd44a5a1..d3b40479 100644 --- a/src/dsp/psk/LSMDemodulator.java +++ b/src/dsp/psk/LSMDemodulator.java @@ -46,7 +46,7 @@ import org.slf4j.LoggerFactory; import sample.Listener; import sample.complex.Complex; import sample.complex.ComplexSampleListener; -import source.tuner.frequency.FrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeListener; import buffer.FloatAveragingBuffer; import dsp.filter.interpolator.RealInterpolator; import dsp.symbol.Dibit; @@ -79,7 +79,7 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener private CostasLoop mCostasLoop = new CostasLoop(); - private FrequencyChangeListener mFrequencyChangeListener; + private IFrequencyChangeListener mFrequencyChangeListener; private EyeDiagramDataTap mEyeDiagramDataTap; @@ -153,12 +153,12 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener } } - public void addListener( FrequencyChangeListener listener ) + public void addListener( IFrequencyChangeListener listener ) { mFrequencyChangeListener = listener; } - public void removeListener( FrequencyChangeListener listener ) + public void removeListener( IFrequencyChangeListener listener ) { mFrequencyChangeListener = null; } diff --git a/src/dsp/psk/LSMDemodulator2.java b/src/dsp/psk/LSMDemodulator2.java index 1df263f1..819af376 100644 --- a/src/dsp/psk/LSMDemodulator2.java +++ b/src/dsp/psk/LSMDemodulator2.java @@ -48,7 +48,7 @@ import sample.Listener; import sample.Provider; import sample.complex.Complex; import sample.complex.ComplexSampleListener; -import source.tuner.frequency.FrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeListener; import buffer.FloatAveragingBuffer; import dsp.filter.interpolator.RealInterpolator; import dsp.symbol.Dibit; @@ -84,7 +84,7 @@ public class LSMDemodulator2 implements Instrumentable, ComplexSampleListener, private CostasLoop mCostasLoop = new CostasLoop(); - private FrequencyChangeListener mFrequencyChangeListener; + private IFrequencyChangeListener mFrequencyChangeListener; private EyeDiagramDataTap mEyeDiagramDataTap; @@ -143,12 +143,12 @@ public class LSMDemodulator2 implements Instrumentable, ComplexSampleListener, } } - public void addListener( FrequencyChangeListener listener ) + public void addListener( IFrequencyChangeListener listener ) { mFrequencyChangeListener = listener; } - public void removeListener( FrequencyChangeListener listener ) + public void removeListener( IFrequencyChangeListener listener ) { mFrequencyChangeListener = null; } diff --git a/src/gui/SDRTrunk.java b/src/gui/SDRTrunk.java index 79345b07..6de05d02 100644 --- a/src/gui/SDRTrunk.java +++ b/src/gui/SDRTrunk.java @@ -43,7 +43,9 @@ import javax.swing.JOptionPane; import javax.swing.JSeparator; import javax.swing.KeyStroke; +import map.IconManager; import map.MapService; +import module.log.EventLogManager; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; @@ -51,7 +53,9 @@ import org.slf4j.LoggerFactory; import playlist.PlaylistManager; import properties.SystemProperties; +import record.RecorderManager; import settings.SettingsManager; +import source.SourceManager; import source.tuner.Tuner; import source.tuner.TunerSelectionListener; import spectrum.SpectralDisplayPanel; @@ -63,7 +67,6 @@ import com.jidesoft.swing.JideSplitPane; import controller.ConfigurationControllerModel; import controller.ControllerPanel; -import controller.ResourceManager; import controller.ThreadPoolManager; import controller.ThreadPoolManager.ThreadType; import controller.channel.ChannelEventListener; @@ -76,6 +79,7 @@ public class SDRTrunk private final static Logger mLog = LoggerFactory.getLogger( SDRTrunk.class ); private ControllerPanel mControllerPanel; + private SettingsManager mSettingsManager; private SpectralDisplayPanel mSpectralPanel; private JFrame mMainGui = new JFrame(); @@ -107,49 +111,54 @@ public class SDRTrunk //Log current properties setting SystemProperties.getInstance().logCurrentSettings(); + + mSettingsManager = new SettingsManager(); + + ThreadPoolManager threadPoolManager = new ThreadPoolManager(); + + SourceManager sourceManager = new SourceManager( mSettingsManager, threadPoolManager ); ChannelModel channelModel = new ChannelModel(); + PlaylistManager playlistManager = new PlaylistManager( channelModel ); + + RecorderManager recorderManager = new RecorderManager( threadPoolManager ); + + EventLogManager eventLogManager = new EventLogManager(); + ChannelProcessingManager channelProcessingManager = - new ChannelProcessingManager( channelModel ); + new ChannelProcessingManager( channelModel, eventLogManager, + playlistManager, recorderManager, sourceManager, threadPoolManager ); channelModel.addListener( channelProcessingManager ); - ThreadPoolManager threadPoolManager = new ThreadPoolManager(); + channelProcessingManager.addAudioPacketListener( recorderManager ); AliasActionManager aliasActionManager = new AliasActionManager( threadPoolManager ); - channelProcessingManager.addListener( aliasActionManager ); + channelProcessingManager.addMessageListener( aliasActionManager ); + + AudioManager audioManager = new AudioManager( threadPoolManager ); + channelProcessingManager.addAudioPacketListener( audioManager ); + + MapService mapService = new MapService( mSettingsManager ); + channelProcessingManager.addMessageListener( mapService ); ChannelSelectionManager channelSelectionManager = new ChannelSelectionManager( channelModel ); channelModel.addListener( channelSelectionManager ); - - /** - * Construct the resource manager now, so that it can use the system - * properties that were just loaded - */ - //TODO: get rid of resource manager ... below is start of breaking out - //the encapsulated pieces from the resource manager - PlaylistManager playlistManager = new PlaylistManager( channelModel ); - ResourceManager resource = - new ResourceManager( playlistManager, threadPoolManager, channelModel ); - - AudioManager audio = resource.getAudioManager(); - ConfigurationControllerModel configurationControllerModel = resource.getController(); - MapService map = resource.getMapService(); - SettingsManager settings = resource.getSettingsManager(); + //Initialize the playlist manager to load the saved playlist + playlistManager.init(); - mControllerPanel = - new ControllerPanel( audio, - configurationControllerModel, - channelModel, - channelProcessingManager, - map, - playlistManager, - settings ); + ConfigurationControllerModel configurationControllerModel = + new ConfigurationControllerModel( channelModel, channelProcessingManager, + playlistManager, mSettingsManager, sourceManager ); - mSpectralPanel = new SpectralDisplayPanel( channelModel, - configurationControllerModel, playlistManager, settings ); + mControllerPanel = new ControllerPanel( audioManager, configurationControllerModel, + channelModel, channelProcessingManager, mapService, playlistManager, + mSettingsManager, sourceManager ); + + mSpectralPanel = new SpectralDisplayPanel( configurationControllerModel, + channelModel, channelProcessingManager, playlistManager, mSettingsManager ); mTitle = getTitle(); @@ -162,7 +171,7 @@ public class SDRTrunk { Runnable cml = new ChannelMemoryLogger(); channelModel.addListener( (ChannelEventListener)cml ); - resource.getThreadPoolManager().scheduleFixedRate( + threadPoolManager.scheduleFixedRate( ThreadType.DECODER, cml, 5, TimeUnit.SECONDS ); } @@ -277,7 +286,16 @@ public class SDRTrunk @Override public void actionPerformed( ActionEvent arg0 ) { - mControllerPanel.getController().showIconManager(); + final IconManager iconManager = new IconManager( mSettingsManager, mMainGui ); + + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + iconManager.setVisible( true ); + } + } ); } } ); fileMenu.add( settingsMenu ); diff --git a/src/gui/control/JFrequencyControl.java b/src/gui/control/JFrequencyControl.java index a413e11f..95b88249 100644 --- a/src/gui/control/JFrequencyControl.java +++ b/src/gui/control/JFrequencyControl.java @@ -33,6 +33,7 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import javax.swing.JLabel; import javax.swing.JPanel; @@ -44,17 +45,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeListener; import source.tuner.frequency.FrequencyChangeEvent.Event; +import source.tuner.frequency.IFrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeProcessor; -public class JFrequencyControl extends JPanel implements FrequencyChangeListener +public class JFrequencyControl extends JPanel implements IFrequencyChangeProcessor { private static final long serialVersionUID = 1L; private final static Logger mLog = LoggerFactory.getLogger( JFrequencyControl.class ); - private ArrayList mListeners = - new ArrayList(); + private List mProcessors = new ArrayList<>(); private Color mHighlightColor = Color.YELLOW; @@ -136,7 +137,7 @@ public class JFrequencyControl extends JPanel implements FrequencyChangeListener @Override public void frequencyChanged( FrequencyChangeEvent event ) { - if( event.getEvent() == Event.FREQUENCY_CHANGE_NOTIFICATION ) + if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE ) { setFrequency( event.getValue().longValue(), false ); } @@ -177,10 +178,10 @@ public class JFrequencyControl extends JPanel implements FrequencyChangeListener { updateFrequency(); - Iterator it = mListeners.iterator(); + Iterator it = mProcessors.iterator(); FrequencyChangeEvent event = - new FrequencyChangeEvent( Event.FREQUENCY_CHANGE_NOTIFICATION, mFrequency ); + new FrequencyChangeEvent( Event.NOTIFICATION_FREQUENCY_CHANGE, mFrequency ); while( it.hasNext() ) { @@ -188,14 +189,14 @@ public class JFrequencyControl extends JPanel implements FrequencyChangeListener } } - public void addListener( FrequencyChangeListener listener ) + public void addListener( IFrequencyChangeProcessor processor ) { - mListeners.add( listener ); + mProcessors.add( processor ); } - public void removeListener( FrequencyChangeListener listener ) + public void removeListener( IFrequencyChangeProcessor processor ) { - mListeners.remove( listener ); + mProcessors.remove( processor ); } public class Digit extends JTextField diff --git a/src/instrument/InstrumentableProcessingChain.java b/src/instrument/InstrumentableProcessingChain.java index 3efef0ae..94181b18 100644 --- a/src/instrument/InstrumentableProcessingChain.java +++ b/src/instrument/InstrumentableProcessingChain.java @@ -9,11 +9,13 @@ import java.util.List; import module.Module; import module.ProcessingChain; import source.Source; +import controller.channel.Channel.ChannelType; public class InstrumentableProcessingChain extends ProcessingChain implements Instrumentable { public InstrumentableProcessingChain() { + super( ChannelType.STANDARD ); } public void setSource( Source source ) throws IllegalStateException diff --git a/src/instrument/gui/DecoderSelectionFrame.java b/src/instrument/gui/DecoderSelectionFrame.java index b2b6a32d..998191b1 100644 --- a/src/instrument/gui/DecoderSelectionFrame.java +++ b/src/instrument/gui/DecoderSelectionFrame.java @@ -13,8 +13,6 @@ import module.decode.config.DecodeConfiguration; import net.miginfocom.swing.MigLayout; import playlist.PlaylistManager; import source.IControllableFileSource; -import controller.ResourceManager; -import controller.ThreadPoolManager; import controller.channel.ChannelModel; public class DecoderSelectionFrame extends JInternalFrame @@ -22,7 +20,7 @@ public class DecoderSelectionFrame extends JInternalFrame private static final long serialVersionUID = 1L; private DecodeComponentEditor mDecodeEditor = new DecodeComponentEditor( null ); - private ResourceManager mResourceManager; + private PlaylistManager mPlaylistManager; private IControllableFileSource mSource; private JDesktopPane mDesktop; @@ -31,9 +29,7 @@ public class DecoderSelectionFrame extends JInternalFrame IControllableFileSource source ) { ChannelModel channelModel = new ChannelModel(); - - mResourceManager = new ResourceManager( new PlaylistManager( channelModel ), - new ThreadPoolManager(), channelModel ); + mPlaylistManager = new PlaylistManager( channelModel ); mDesktop = desktop; mSource = source; @@ -78,12 +74,7 @@ public class DecoderSelectionFrame extends JInternalFrame if( config != null ) { DecoderViewFrame decoderFrame = new DecoderViewFrame( - mResourceManager, - config, - null, - null, - null, - mSource ); + mPlaylistManager, null, mSource ); decoderFrame.setVisible( true ); diff --git a/src/instrument/gui/DecoderViewFrame.java b/src/instrument/gui/DecoderViewFrame.java index 9cdfe1c5..a2afb439 100644 --- a/src/instrument/gui/DecoderViewFrame.java +++ b/src/instrument/gui/DecoderViewFrame.java @@ -57,22 +57,20 @@ import javax.swing.JSeparator; import message.Message; import module.Module; import module.decode.DecoderFactory; -import module.decode.config.AuxDecodeConfiguration; import module.decode.config.DecodeConfiguration; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import record.config.RecordConfiguration; +import playlist.PlaylistManager; import sample.Listener; import source.IControllableFileSource; import source.Source; -import alias.AliasList; -import controller.ResourceManager; -import controller.channel.Channel.ChannelType; -import controller.site.Site; -import controller.system.System; +import controller.ThreadPoolManager; +import controller.channel.Channel; +import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; public class DecoderViewFrame extends JInternalFrame implements Listener @@ -88,29 +86,23 @@ public class DecoderViewFrame extends JInternalFrame private HashMap mPanelMap = new HashMap(); - public DecoderViewFrame( ResourceManager resourceManager, - DecodeConfiguration decodeConfig, - RecordConfiguration recordConfig, - AuxDecodeConfiguration auxConfig, - AliasList aliasList, + public DecoderViewFrame( PlaylistManager playlistManager, + Channel channel, IControllableFileSource source ) { mSource = source; - mDecodeConfig = decodeConfig; + mDecodeConfig = channel.getDecodeConfiguration(); mProcessingChain = new InstrumentableProcessingChain(); + + ChannelModel channelModel = new ChannelModel(); + ChannelProcessingManager channelProcessingManager = + new ChannelProcessingManager( channelModel, null, + playlistManager, null, null, new ThreadPoolManager() ); - List modules = DecoderFactory - .getModules( ChannelType.STANDARD, - resourceManager, - decodeConfig, - recordConfig, - auxConfig, - aliasList, - "Instrumented", - "Instrumented", - "Channel Name" ); + List modules = DecoderFactory.getModules( channelModel, + channelProcessingManager, playlistManager, channel ); mProcessingChain.addModules( modules ); diff --git a/src/map/IconManager.java b/src/map/IconManager.java index 220ca034..9cc07f3b 100644 --- a/src/map/IconManager.java +++ b/src/map/IconManager.java @@ -50,7 +50,7 @@ import org.slf4j.LoggerFactory; import settings.Setting; import settings.SettingChangeListener; -import controller.ResourceManager; +import settings.SettingsManager; public class IconManager extends JFrame implements ActionListener, SettingChangeListener @@ -62,8 +62,6 @@ public class IconManager extends JFrame private final static Logger mLog = LoggerFactory.getLogger( IconManager.class ); - private ResourceManager mResourceManager; - private static final String sMSG_NEW = "Type a name for the new icon, " + "select the file, and click Save,\nor Cancel to reset"; private static final String sMSG_EDIT = "Change the name for the selected \n" @@ -81,9 +79,11 @@ public class IconManager extends JFrame private JFileChooser mFileChooser; private Mode mMode = Mode.NORMAL; - public IconManager( ResourceManager resourceManager, Component displayOver ) + private SettingsManager mSettingsManager; + + public IconManager( SettingsManager settingsManager, Component displayOver ) { - mResourceManager = resourceManager; + mSettingsManager = settingsManager; init( displayOver ); } @@ -128,7 +128,7 @@ public class IconManager extends JFrame /** * MapIcon list */ - MapIcon[] icons = mResourceManager.getSettingsManager().getMapIcons(); + MapIcon[] icons = mSettingsManager.getMapIcons(); Arrays.sort( icons ); mIconList = new JList( icons ); @@ -176,8 +176,7 @@ public class IconManager extends JFrame @Override public void actionPerformed( ActionEvent e ) { - mResourceManager.getSettingsManager() - .setDefaultIcon( icon ); + mSettingsManager.setDefaultIcon( icon ); refreshList(); } @@ -193,8 +192,7 @@ public class IconManager extends JFrame @Override public void actionPerformed( ActionEvent arg0 ) { - mResourceManager.getSettingsManager() - .deleteMapIcon( icon ); + mSettingsManager.deleteMapIcon( icon ); } } ); @@ -350,7 +348,7 @@ public class IconManager extends JFrame private void refreshList() { - MapIcon[] icons = mResourceManager.getSettingsManager().getMapIcons(); + MapIcon[] icons = mSettingsManager.getMapIcons(); Arrays.sort( icons ); @@ -371,8 +369,7 @@ public class IconManager extends JFrame { if( path != null && !path.isEmpty() ) { - mResourceManager.getSettingsManager() - .addMapIcon( new MapIcon( name, path, true ) ); + mSettingsManager.addMapIcon( new MapIcon( name, path, true ) ); } else { @@ -400,8 +397,7 @@ public class IconManager extends JFrame { selected.setName( name ); selected.setPath( path ); - mResourceManager.getSettingsManager() - .updatMapIcon( selected, name, path ); + mSettingsManager.updatMapIcon( selected, name, path ); } else { diff --git a/src/map/MapPanel.java b/src/map/MapPanel.java index e9b66045..4a61b33f 100644 --- a/src/map/MapPanel.java +++ b/src/map/MapPanel.java @@ -46,17 +46,12 @@ public class MapPanel extends JPanel implements PlottableUpdateListener private JXMapViewer mMapViewer = new JXMapViewer(); private PlottableEntityPainter mMapPainter; - public MapPanel( MapService mapService, - SettingsManager settingsManager, - ChannelProcessingManager channelProcessingManager ) + public MapPanel( MapService mapService, SettingsManager settingsManager ) { mSettingsManager = settingsManager; mMapService = mapService; mMapPainter = new PlottableEntityPainter( mSettingsManager ); - /* Add Map Service as message listener to receive all messages */ - channelProcessingManager.addListener( (Listener)mMapService ); - init(); } diff --git a/src/map/MapService.java b/src/map/MapService.java index 0b6c9f87..0c251277 100644 --- a/src/map/MapService.java +++ b/src/map/MapService.java @@ -30,7 +30,6 @@ import sample.Listener; import settings.SettingsManager; import alias.Alias; import controller.NamingThreadFactory; -import controller.ResourceManager; public class MapService implements Listener { @@ -49,11 +48,11 @@ public class MapService implements Listener private HashMap mEntities = new HashMap(); - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; - public MapService( ResourceManager resourceManager ) + public MapService( SettingsManager resourceManager ) { - mResourceManager = resourceManager; + mSettingsManager = resourceManager; // scheduler.scheduleAtFixedRate( new CullThread(), 5, 5, TimeUnit.MINUTES ); } @@ -111,8 +110,7 @@ public class MapService implements Listener iconName = SettingsManager.DEFAULT_ICON; } - icon = mResourceManager.getSettingsManager() - .getMapIcon( iconName ); + icon = mSettingsManager.getMapIcon( iconName ); } if( color == null ) diff --git a/src/module/ProcessingChain.java b/src/module/ProcessingChain.java index c8e3da4f..68c94256 100644 --- a/src/module/ProcessingChain.java +++ b/src/module/ProcessingChain.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2015 Dennis Sheirer + * Copyright (C) 2015-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,8 +26,11 @@ import message.IMessageListener; import message.IMessageProvider; import message.Message; import module.decode.event.CallEvent; +import module.decode.event.CallEventModel; import module.decode.event.ICallEventListener; import module.decode.event.ICallEventProvider; +import module.decode.event.MessageActivityModel; +import module.decode.state.ChannelState; import module.decode.state.DecoderState; import module.decode.state.DecoderStateEvent; import module.decode.state.DecoderStateEvent.Event; @@ -42,16 +45,19 @@ import sample.Broadcaster; import sample.Listener; import sample.complex.ComplexBuffer; import sample.complex.IComplexBufferListener; -import sample.real.IRealBufferListener; -import sample.real.IRealBufferProvider; +import sample.real.IFilteredRealBufferListener; +import sample.real.IFilteredRealBufferProvider; +import sample.real.IUnFilteredRealBufferListener; +import sample.real.IUnFilteredRealBufferProvider; import sample.real.RealBuffer; import source.ComplexSource; import source.RealSource; import source.Source; import source.SourceException; import source.tuner.TunerChannelSource; -import source.tuner.frequency.FrequencyCorrectionControl; -import source.tuner.frequency.IFrequencyCorrectionController; +import source.tuner.frequency.FrequencyChangeEvent; +import source.tuner.frequency.IFrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeProvider; import audio.AudioPacket; import audio.IAudioPacketListener; import audio.IAudioPacketProvider; @@ -62,6 +68,7 @@ import audio.squelch.ISquelchStateListener; import audio.squelch.ISquelchStateProvider; import audio.squelch.SquelchState; import controller.ThreadPoolManager; +import controller.channel.Channel.ChannelType; import controller.channel.ChannelEvent; import controller.channel.IChannelEventListener; import controller.channel.IChannelEventProvider; @@ -95,8 +102,10 @@ public class ProcessingChain implements IChannelEventListener private Broadcaster mChannelEventBroadcaster = new Broadcaster<>(); private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); private Broadcaster mDecoderStateEventBroadcaster = new Broadcaster<>(); + private Broadcaster mFrequencyChangeEventBroadcaster = new Broadcaster<>(); private Broadcaster mMessageBroadcaster = new Broadcaster<>(); - private Broadcaster mRealBufferBroadcaster = new Broadcaster<>(); + private Broadcaster mFilteredRealBufferBroadcaster = new Broadcaster<>(); + private Broadcaster mUnFilteredRealBufferBroadcaster = new Broadcaster<>(); private Broadcaster mSquelchStateBroadcaster = new Broadcaster<>(); // private ThreadPoolManager mThreadPoolManager; @@ -104,12 +113,50 @@ public class ProcessingChain implements IChannelEventListener protected Source mSource; private List mModules = new ArrayList<>(); - private IFrequencyCorrectionController mFrequencyCorrectionController; + private CallEventModel mCallEventModel; + private ChannelState mChannelState; + private MessageActivityModel mMessageActivityModel; - public ProcessingChain() + public ProcessingChain( ChannelType channelType ) { + this( new ThreadPoolManager(), channelType ); } + + public ProcessingChain( ThreadPoolManager threadManager, + ChannelType channelType ) + { + mThreadPoolManager = threadManager; + + mChannelState = new ChannelState( mThreadPoolManager, channelType ); + addModule( mChannelState ); + + mCallEventModel = new CallEventModel(); + addCallEventListener( mCallEventModel ); + } + + public MessageActivityModel getMessageActivityModel() + { + return mMessageActivityModel; + } + + public CallEventModel getCallEventModel() + { + return mCallEventModel; + } + + public ChannelState getChannelState() + { + return mChannelState; + } + + public void setMessageActivityModel( MessageActivityModel model ) + { + mMessageActivityModel = model; + + addMessageListener( mMessageActivityModel ); + } + public void dispose() { stop(); @@ -121,14 +168,16 @@ public class ProcessingChain implements IChannelEventListener mModules.clear(); - mFrequencyCorrectionController = null; + + mThreadPoolManager = null; + mBufferProcessingTask = null; mAudioPacketBroadcaster.dispose(); mCallEventBroadcaster.dispose(); mChannelEventBroadcaster.dispose(); mComplexBufferBroadcaster.dispose(); mMessageBroadcaster.dispose(); - mRealBufferBroadcaster.dispose(); + mFilteredRealBufferBroadcaster.dispose(); mSquelchStateBroadcaster.dispose(); } @@ -147,7 +196,7 @@ public class ProcessingChain implements IChannelEventListener * Indicates if this processing chain is currently receiving samples from * a source and sending those samples to the decoders. */ - public boolean processing() + public boolean isProcessing() { return mRunning.get(); } @@ -160,28 +209,6 @@ public class ProcessingChain implements IChannelEventListener return mSource != null; } - /** - * Indicates if one of the modules provides frequency error control. - */ - public boolean hasFrequencyCorrectionControl() - { - return mFrequencyCorrectionController != null && - mFrequencyCorrectionController.hasFrequencyCorrectionControl(); - } - - /** - * Frequency correction controller. Can be null. - */ - public FrequencyCorrectionControl getFrequencyCorrectionControl() - { - if( mFrequencyCorrectionController != null ) - { - return mFrequencyCorrectionController.getFrequencyCorrectionControl(); - } - - return null; - } - /** * Applies a sample source to this processing chain. Processing won't * start until the start() method is invoked. @@ -194,7 +221,7 @@ public class ProcessingChain implements IChannelEventListener */ public void setSource( Source source ) throws IllegalStateException { - if( processing() ) + if( isProcessing() ) { throw new IllegalStateException( "Processing chain is currently " + "processing. Invoke stop() on the processing chain before " @@ -202,23 +229,6 @@ public class ProcessingChain implements IChannelEventListener } mSource = source; - - /* Establish two-way communication between the tuner channel source and - * the decoder for fine tuning frequency error correction feedback from - * the decoder. The tuner channel source will also notify the frequency - * controller during frequency changes to allow the controller to reset - * frequency error tracking */ - if( mSource != null && - mSource instanceof TunerChannelSource && - hasFrequencyCorrectionControl() ) - { - TunerChannelSource channel = (TunerChannelSource)mSource; - - FrequencyCorrectionControl control = getFrequencyCorrectionControl(); - - channel.setFrequencyChangeListener( control ); - control.setFrequencyChangeListener( channel ); - } } /** @@ -247,24 +257,45 @@ public class ProcessingChain implements IChannelEventListener return decoderStates; } + /** + * Adds the list of modules to this processing chain + */ + public void addModules( List modules ) + { + for( Module module: modules ) + { + addModule( module ); + } + } + /** * Adds a module to the processing chain. Each module is tested for the * interfaces that it supports and is registered or receives a listener * to consume or produce the supported interface data type. All elements - * that are produced by any component are automatically routed to all other - * components that support the corresponding listener interface. + * and events that are produced by any module are automatically routed to + * all other components that support the corresponding listener interface. * * At least one module should consume complex samples and either produce * decoded messages and/or audio, or produce decoded real sample buffers * for all other modules to consume. * - * @param module - processing module, demodulator or decoder + * @param module - processing module, demodulator, decoder, source, state + * machine, etc. */ public void addModule( Module module ) { mModules.add( module ); + + registerListeners( module ); + registerProviders( module ); + } - /* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Listeners <<<<<<<<<<<<<<<<<<<<<<<<<<< */ + /** + * Registers the module as a listener to each of the broadcasters that + * provide the data interface(s) supported by the module. + */ + private void registerListeners( Module module ) + { if( module instanceof IAudioPacketListener ) { mAudioPacketBroadcaster.addListener( @@ -295,6 +326,12 @@ public class ProcessingChain implements IChannelEventListener ((IDecoderStateEventListener)module).getDecoderStateListener() ); } + if( module instanceof IFrequencyChangeListener ) + { + mFrequencyChangeEventBroadcaster.addListener( + ((IFrequencyChangeListener)module).getFrequencyChangeListener()); + } + if( module instanceof IMessageListener ) { mMessageBroadcaster.addListener( @@ -307,10 +344,10 @@ public class ProcessingChain implements IChannelEventListener ((IMetadataListener)module).getMetadataListener() ); } - if( module instanceof IRealBufferListener ) + if( module instanceof IFilteredRealBufferListener ) { - mRealBufferBroadcaster.addListener( - ((IRealBufferListener)module).getRealBufferListener() ); + mFilteredRealBufferBroadcaster.addListener( + ((IFilteredRealBufferListener)module).getFilteredRealBufferListener() ); } if( module instanceof ISquelchStateListener ) @@ -318,8 +355,20 @@ public class ProcessingChain implements IChannelEventListener mSquelchStateBroadcaster.addListener( ((ISquelchStateListener)module).getSquelchStateListener() ); } - - /* >>>>>>>>>>>>>>>>>>> Providers <<<<<<<<<<<<<<<<<<<<<<<<< */ + + if( module instanceof IUnFilteredRealBufferListener ) + { + mFilteredRealBufferBroadcaster.addListener( + ((IUnFilteredRealBufferListener)module).getUnFilteredRealBufferListener() ); + } + } + + /** + * Registers the broadcaster(s) as listeners to the module for each + * provider interface that is supported by the module. + */ + private void registerProviders( Module module ) + { if( module instanceof IAudioPacketProvider ) { ((IAudioPacketProvider)module).setAudioPacketListener( @@ -342,12 +391,13 @@ public class ProcessingChain implements IChannelEventListener ((IDecoderStateEventProvider)module).setDecoderStateListener( mDecoderStateEventBroadcaster ); } - - if( module instanceof IFrequencyCorrectionController ) - { - mFrequencyCorrectionController = (IFrequencyCorrectionController)module; - } + if( module instanceof IFrequencyChangeProvider ) + { + ((IFrequencyChangeProvider)module).setFrequencyChangeListener( + mFrequencyChangeEventBroadcaster ); + } + if( module instanceof IMessageProvider ) { ((IMessageProvider)module).addMessageListener( mMessageBroadcaster ); @@ -359,9 +409,10 @@ public class ProcessingChain implements IChannelEventListener mMetadataBroadcaster ); } - if( module instanceof IRealBufferProvider ) + if( module instanceof IFilteredRealBufferProvider ) { - ((IRealBufferProvider)module).setRealBufferListener( mRealBufferBroadcaster ); + ((IFilteredRealBufferProvider)module) + .setFilteredRealBufferListener( mFilteredRealBufferBroadcaster ); } if( module instanceof ISquelchStateProvider ) @@ -369,19 +420,13 @@ public class ProcessingChain implements IChannelEventListener ((ISquelchStateProvider)module).setSquelchStateListener( mSquelchStateBroadcaster ); } - } - - /** - * Adds the list of modules to this processing chain - */ - public void addModules( List modules ) - { - for( Module module: modules ) + + if( module instanceof IUnFilteredRealBufferProvider ) { - addModule( module ); + ((IUnFilteredRealBufferProvider)module) + .setUnFilteredRealBufferListener( mUnFilteredRealBufferBroadcaster ); } } - /** * Starts processing if the chain has a valid source. Invocations on an @@ -421,7 +466,7 @@ public class ProcessingChain implements IChannelEventListener ((ComplexSource)mSource).setListener( mComplexBufferBroadcaster ); break; case REAL: - ((RealSource)mSource).setListener( mRealBufferBroadcaster ); + ((RealSource)mSource).setListener( mFilteredRealBufferBroadcaster ); break; default: throw new IllegalArgumentException( "Unrecognized source " @@ -448,7 +493,7 @@ public class ProcessingChain implements IChannelEventListener } else { - mLog.debug( "Source is null on start()" ); + mLog.error( "Source is null on start()" ); } } } @@ -469,7 +514,7 @@ public class ProcessingChain implements IChannelEventListener ((ComplexSource)mSource).removeListener( mComplexBufferBroadcaster ); break; case REAL: - ((RealSource)mSource).removeListener( mRealBufferBroadcaster ); + ((RealSource)mSource).removeListener( mFilteredRealBufferBroadcaster ); break; default: throw new IllegalArgumentException( "Unrecognized source " @@ -585,12 +630,12 @@ public class ProcessingChain implements IChannelEventListener public void addRealBufferListener( Listener listener ) { - mRealBufferBroadcaster.addListener( listener ); + mFilteredRealBufferBroadcaster.addListener( listener ); } public void removeRealBufferListener( Listener listener ) { - mRealBufferBroadcaster.removeListener( listener ); + mFilteredRealBufferBroadcaster.removeListener( listener ); } @Override diff --git a/src/module/decode/AuxDecodeComponentEditor.java b/src/module/decode/AuxDecodeComponentEditor.java index dd55ee59..8e3dc8cb 100644 --- a/src/module/decode/AuxDecodeComponentEditor.java +++ b/src/module/decode/AuxDecodeComponentEditor.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * Copyright (C) 2014-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,7 +26,8 @@ import javax.swing.SwingUtilities; import module.decode.config.AuxDecodeConfiguration; import controller.channel.AbstractChannelEditor; -import controller.channel.ChannelNode; +import controller.channel.Channel; +import controller.channel.ConfigurationValidationException; public class AuxDecodeComponentEditor extends AbstractChannelEditor { @@ -35,9 +36,9 @@ public class AuxDecodeComponentEditor extends AbstractChannelEditor private HashMap mControls = new HashMap(); - public AuxDecodeComponentEditor( ChannelNode channelNode ) + public AuxDecodeComponentEditor( Channel channel ) { - super( channelNode ); + super( channel ); List decoders = DecoderType.getAuxDecoders(); @@ -60,7 +61,7 @@ public class AuxDecodeComponentEditor extends AbstractChannelEditor public AuxDecodeConfiguration getConfig() { - return getChannelNode().getChannel().getAuxDecodeConfiguration(); + return getChannel().getAuxDecodeConfiguration(); } @Override @@ -107,4 +108,18 @@ public class AuxDecodeComponentEditor extends AbstractChannelEditor mDecoderType = decoder; } } + + @Override + public void setConfiguration( Channel channel ) + { + // TODO Auto-generated method stub + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + // TODO Auto-generated method stub + + } } diff --git a/src/module/decode/DecodeComponentEditor.java b/src/module/decode/DecodeComponentEditor.java index bec8de84..bdc48706 100644 --- a/src/module/decode/DecodeComponentEditor.java +++ b/src/module/decode/DecodeComponentEditor.java @@ -1,6 +1,7 @@ /******************************************************************************* + * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * Copyright (C) 2014-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,7 +18,6 @@ ******************************************************************************/ package module.decode; - import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -30,11 +30,10 @@ import module.decode.config.DecodeConfigFactory; import module.decode.config.DecodeConfiguration; import net.miginfocom.swing.MigLayout; import playlist.PlaylistManager; -import controller.Editor; import controller.channel.AbstractChannelEditor; import controller.channel.Channel; -import controller.channel.ChannelNode; -import controller.channel.ChannelValidationException; +import controller.channel.ChannelConfigurationEditor; +import controller.channel.ConfigurationValidationException; public class DecodeComponentEditor extends AbstractChannelEditor { @@ -48,13 +47,12 @@ public class DecodeComponentEditor extends AbstractChannelEditor public DecodeComponentEditor( PlaylistManager playlistManager ) { - this( new ChannelNode( new Channel() ), playlistManager ); + this( playlistManager, new Channel() ); } - public DecodeComponentEditor( ChannelNode channelNode, - PlaylistManager playlistMangager ) + public DecodeComponentEditor( PlaylistManager playlistMangager, Channel channel ) { - super( channelNode ); + super( channel ); mPlaylistManager = playlistMangager; @@ -87,17 +85,13 @@ public class DecodeComponentEditor extends AbstractChannelEditor { DecodeConfiguration config; - if( mChannelNode.getChannel() - .getDecodeConfiguration().getDecoderType() == - selected ) + if( mChannel.getDecodeConfiguration().getDecoderType() == selected ) { - config = mChannelNode.getChannel() - .getDecodeConfiguration(); + config = mChannel.getDecodeConfiguration(); } else { - config = DecodeConfigFactory - .getDecodeConfiguration( selected ); + config = DecodeConfigFactory.getDecodeConfiguration( selected ); } //Remove the existing one @@ -108,7 +102,7 @@ public class DecodeComponentEditor extends AbstractChannelEditor //Change to the new one mEditor = DecoderFactory.getEditorPanel( config, - mChannelNode, mPlaylistManager ); + mChannel, mPlaylistManager ); add( mEditor, "span 2,center" ); @@ -125,8 +119,7 @@ public class DecodeComponentEditor extends AbstractChannelEditor public void reset() { - final DecoderType decoder = mChannelNode.getChannel() - .getDecodeConfiguration().getDecoderType(); + final DecoderType decoder = mChannel.getDecodeConfiguration().getDecoderType(); SwingUtilities.invokeLater(new Runnable() { @@ -155,18 +148,31 @@ public class DecodeComponentEditor extends AbstractChannelEditor } //The component that calls save will invoke the change broadcast - mChannelNode.getChannel() - .setDecodeConfiguration( mEditor.getConfig() ); + mChannel.setDecodeConfiguration( mEditor.getConfig() ); } - /** - * Validates the editor against the current decode editor - */ +// /** +// * Validates the editor against the current decode editor +// */ +// @Override +// public void validate( ChannelConfigurationEditor editor ) throws ConfigurationValidationException +// { +// super.validate( editor ); +// +// mEditor.validate( editor ); +// } + @Override - public void validate( Editor editor ) throws ChannelValidationException + public void setConfiguration( Channel channel ) { - super.validate( editor ); + // TODO Auto-generated method stub + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + // TODO Auto-generated method stub - mEditor.validate( editor ); } } diff --git a/src/module/decode/DecodeEditor.java b/src/module/decode/DecodeEditor.java index dcebf156..cddc6082 100644 --- a/src/module/decode/DecodeEditor.java +++ b/src/module/decode/DecodeEditor.java @@ -31,9 +31,11 @@ import javax.swing.event.ChangeListener; import module.decode.config.DecodeConfiguration; import net.miginfocom.swing.MigLayout; -import controller.Editor; +import controller.channel.Channel; +import controller.channel.ChannelConfigurationEditor; +import controller.channel.ConfigurationValidationException; -public class DecodeEditor extends Editor +public class DecodeEditor extends ChannelConfigurationEditor { private static final long serialVersionUID = 1L; @@ -110,7 +112,16 @@ public class DecodeEditor extends Editor } @Override - public void reset() - { - } + public void setConfiguration( Channel channel ) + { + // TODO Auto-generated method stub + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + // TODO Auto-generated method stub + + } } diff --git a/src/module/decode/DecoderFactory.java b/src/module/decode/DecoderFactory.java index 2acc06b1..109c0017 100644 --- a/src/module/decode/DecoderFactory.java +++ b/src/module/decode/DecoderFactory.java @@ -63,10 +63,10 @@ import module.decode.nbfm.NBFMDecoder; import module.decode.nbfm.NBFMDecoderPanel; import module.decode.nbfm.NBFMEditor; import module.decode.p25.DecodeConfigP25Phase1; +import module.decode.p25.P25DecodeEditor; import module.decode.p25.P25Decoder.Modulation; import module.decode.p25.P25DecoderPanel; import module.decode.p25.P25DecoderState; -import module.decode.p25.P25DecodeEditor; import module.decode.p25.P25_C4FMDecoder; import module.decode.p25.P25_LSMDecoder; import module.decode.p25.audio.P25AudioModule; @@ -84,6 +84,7 @@ import module.decode.tait.Tait1200Decoder; import module.decode.tait.Tait1200DecoderPanel; import module.decode.tait.Tait1200DecoderState; import module.demodulate.am.AMDemodulatorModule; +import module.demodulate.audio.DemodulatedAudioFilterModule; import module.demodulate.fm.FMDemodulatorModule; import org.slf4j.Logger; @@ -97,12 +98,11 @@ import settings.SettingsManager; import util.TimeStamp; import alias.AliasList; import audio.AudioModule; -import controller.ResourceManager; +import controller.channel.Channel; import controller.channel.Channel.ChannelType; -import controller.channel.ChannelNode; +import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; import controller.channel.map.ChannelMap; -import controller.site.Site; -import controller.system.System; import filter.AllPassFilter; import filter.FilterSet; import filter.IFilter; @@ -120,72 +120,46 @@ public class DecoderFactory * Returns a list of one primary decoder and any auxiliary decoders, as * specified in the configurations. * - * @param channelType - standard or traffic channel type - * @param resourceManager - * @param decodeConfig - primary decoder configuration - * @param recordConfig - recording configuration used by traffic channel manager - * @param auxConfig - auxiliary decoder(s) configuration - * @param optional alias list - (null is ok) - * * @return list of configured decoders */ - public static List getModules( ChannelType channelType, - ResourceManager resourceManager, - DecodeConfiguration decodeConfig, - RecordConfiguration recordConfig, - AuxDecodeConfiguration auxConfig, - AliasList aliasList, - String system, - String site, - String channelName ) + public static List getModules( ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, + Channel channel ) { - List modules = getPrimaryModules( channelType, decodeConfig, - recordConfig, aliasList, system, site, channelName, resourceManager ); - modules.addAll( getAuxiliaryDecoders( auxConfig, aliasList ) ); + /* Get the optional alias list for the decode modules to use */ + AliasList aliasList = playlistManager.getPlayist() + .getAliasDirectory().getAliasList( channel.getAliasListName() ); + + List modules = getPrimaryModules( channelModel, channelProcessingManager, + playlistManager, aliasList, channel ); + + modules.addAll( getAuxiliaryDecoders( channel.getAuxDecodeConfiguration(), aliasList ) ); return modules; } - public static FMDemodulatorModule getFMDemodulator( - DecodeConfiguration config, int pass, int stop, boolean removeDC ) - { - if( config.isAFCEnabled() ) - { - return new FMDemodulatorModule( pass, stop, - config.getAFCMaximumCorrection(), removeDC ); - } - else - { - return new FMDemodulatorModule( pass, stop, - NO_FREQUENCY_CORRECTION, removeDC ); - } - } - /** * Constructs a primary decoder as specified in the decode configuration - * - * @param channelType - traffic or standard decode channel - * @param decodeConfig - primary decoder configuration - * @param recordConfig - recording options - * @param aliasList - optional alias list - * @param resourceManager - shared resource manager - * @return configured decoder or null */ - public static List getPrimaryModules( ChannelType channelType, - DecodeConfiguration decodeConfig, - RecordConfiguration recordConfig, + public static List getPrimaryModules( ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, AliasList aliasList, - String system, - String site, - String channelName, - ResourceManager resourceManager ) + Channel channel ) { List modules = new ArrayList(); + ChannelType channelType = channel.getChannelType(); + /* Baseband low-pass filter pass and stop frequencies */ - int pass = decodeConfig.getDecoderType().getChannelBandwidth() / 2; - int stop = pass + 1250; + DecodeConfiguration decodeConfig = channel.getDecodeConfiguration(); + + int iqPass = decodeConfig.getDecoderType().getChannelBandwidth() / 2; + int iqStop = iqPass + 1250; + + RecordConfiguration recordConfig = channel.getRecordConfiguration(); boolean recordAudio = recordConfig == null ? false : recordConfig.getRecorders().contains( RecorderType.AUDIO ); @@ -193,41 +167,44 @@ public class DecoderFactory switch( decodeConfig.getDecoderType() ) { case AM: - modules.add( new AMDemodulatorModule() ); modules.add( new AMDecoder( decodeConfig ) ); - modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.AM, channelName ) ); - modules.add( new AudioModule( recordAudio, REMOVE_DC ) ); + modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.AM, channel.getName() ) ); + modules.add( new AMDemodulatorModule() ); + modules.add( new DemodulatedAudioFilterModule( 4000, 6000 )); + modules.add( new AudioModule( recordAudio ) ); break; case NBFM: - modules.add( new AudioModule( recordAudio, NO_REMOVE_DC ) ); modules.add( new NBFMDecoder( decodeConfig ) ); - modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.NBFM, channelName ) ); - modules.add( getFMDemodulator( decodeConfig, pass, stop, REMOVE_DC ) ); + modules.add( new AlwaysUnsquelchedDecoderState( DecoderType.NBFM, channel.getName() ) ); + modules.add( new FMDemodulatorModule( iqPass, iqStop ) ); + modules.add( new DemodulatedAudioFilterModule( 4000, 6000 )); + modules.add( new AudioModule( recordAudio ) ); break; case LTR_STANDARD: - modules.add( new AudioModule( recordAudio, REMOVE_DC ) ); MessageDirection direction = ((DecodeConfigLTRStandard)decodeConfig).getMessageDirection(); modules.add( new LTRStandardDecoder( aliasList, direction ) ); modules.add( new LTRStandardDecoderState( aliasList ) ); - modules.add( getFMDemodulator( decodeConfig, pass, stop, NO_REMOVE_DC ) ); + modules.add( new FMDemodulatorModule( iqPass, iqStop ) ); + modules.add( new DemodulatedAudioFilterModule( 4000, 6000 )); + modules.add( new AudioModule( recordAudio ) ); break; case LTR_NET: - modules.add( new AudioModule( recordAudio, REMOVE_DC ) ); modules.add( new LTRNetDecoder( (DecodeConfigLTRNet)decodeConfig, aliasList ) ); modules.add( new LTRNetDecoderState( aliasList ) ); - modules.add( getFMDemodulator( decodeConfig, pass, stop, NO_REMOVE_DC ) ); + modules.add( new FMDemodulatorModule( iqPass, iqStop ) ); + modules.add( new DemodulatedAudioFilterModule( 4000, 6000 )); + modules.add( new AudioModule( recordAudio ) ); break; case MPT1327: DecodeConfigMPT1327 mptConfig = (DecodeConfigMPT1327)decodeConfig; - modules.add( new AudioModule( recordAudio, NO_REMOVE_DC ) ); - - ChannelMap channelMap = resourceManager.getPlaylistManager().getPlayist() + ChannelMap channelMap = playlistManager.getPlayist() .getChannelMapList().getChannelMap( mptConfig.getChannelMapName() ); Sync sync = mptConfig.getSync(); modules.add( new MPT1327Decoder( aliasList, sync ) ); + modules.add( new MPT1327DecoderState( aliasList, channelMap, channelType, mptConfig.getCallTimeout() * 1000 ) ); @@ -235,30 +212,34 @@ public class DecoderFactory { long timeout = mptConfig.getCallTimeout() * 1000; //convert to milliseconds - modules.add( new TrafficChannelManager( resourceManager, - decodeConfig, recordConfig, system, site, - ( aliasList != null ? aliasList.getName() : null ), timeout, + modules.add( new TrafficChannelManager( channelModel, channelProcessingManager, + decodeConfig, recordConfig, channel.getSystem(), + channel.getSite(), ( aliasList != null ? aliasList.getName() : null ), timeout, mptConfig.getTrafficChannelPoolSize() ) ); } - modules.add( getFMDemodulator( decodeConfig, pass, stop, REMOVE_DC ) ); + modules.add( new FMDemodulatorModule( iqPass, iqStop ) ); + modules.add( new DemodulatedAudioFilterModule( 4000, 6000 )); + modules.add( new AudioModule( recordAudio ) ); break; case PASSPORT: - modules.add( new AudioModule( recordAudio, REMOVE_DC ) ); modules.add( new PassportDecoder( decodeConfig, aliasList ) ); modules.add( new PassportDecoderState( aliasList ) ); - modules.add( getFMDemodulator( decodeConfig, pass, stop, NO_REMOVE_DC ) ); + modules.add( new FMDemodulatorModule( iqPass, iqStop ) ); + modules.add( new DemodulatedAudioFilterModule( 4000, 6000 )); + modules.add( new AudioModule( recordAudio ) ); break; case P25_PHASE1: DecodeConfigP25Phase1 p25Config = (DecodeConfigP25Phase1)decodeConfig; Modulation modulation = p25Config.getModulation(); + switch( modulation ) { case C4FM: - modules.add( getFMDemodulator( decodeConfig, 6750, 7500, NO_REMOVE_DC ) ); - modules.add( new P25_C4FMDecoder( aliasList ) ); + modules.add( new FMDemodulatorModule( 6750, 7500 ) ); + modules.add( new P25_C4FMDecoder( aliasList, decodeConfig.getAFCMaximumCorrection() ) ); modules.add( new P25DecoderState( aliasList, channelType, Modulation.C4FM, p25Config.getIgnoreDataCalls() ) ); break; @@ -277,8 +258,9 @@ public class DecoderFactory //Set call timeout to 3 seconds for P25 traffic channels long timeout = 3000; - modules.add( new TrafficChannelManager( resourceManager, - decodeConfig, recordConfig, system, site, + modules.add( new TrafficChannelManager( channelModel, + channelProcessingManager, decodeConfig, + recordConfig, channel.getSystem(), channel.getSite(), ( aliasList != null ? aliasList.getName() : null ), timeout, p25Config.getTrafficChannelPoolSize() ) ); } @@ -449,7 +431,7 @@ public class DecoderFactory } public static DecodeEditor getEditorPanel( DecodeConfiguration config, - ChannelNode channelNode, PlaylistManager playlistManager ) + Channel channel, PlaylistManager playlistManager ) { DecodeEditor configuredPanel; @@ -466,7 +448,7 @@ public class DecoderFactory break; case MPT1327: configuredPanel = new MPT1327ConfigEditor( config, - channelNode, playlistManager ); + channel, playlistManager ); break; case PASSPORT: configuredPanel = new PassportEditor( config ); diff --git a/src/module/decode/event/CallEventPanel.java b/src/module/decode/event/CallEventPanel.java index c9ac7ac0..48d8ce34 100644 --- a/src/module/decode/event/CallEventPanel.java +++ b/src/module/decode/event/CallEventPanel.java @@ -23,12 +23,14 @@ import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; +import module.ProcessingChain; import net.miginfocom.swing.MigLayout; import settings.SettingsManager; import controller.channel.Channel; import controller.channel.ChannelEvent; import controller.channel.ChannelEvent.Event; import controller.channel.ChannelEventListener; +import controller.channel.ChannelProcessingManager; public class CallEventPanel extends JPanel implements ChannelEventListener { @@ -37,9 +39,13 @@ public class CallEventPanel extends JPanel implements ChannelEventListener private JScrollPane mEmptyScroller; private Channel mDisplayedChannel; private CallEventAliasCellRenderer mRenderer; + private ChannelProcessingManager mChannelProcessingManager; - public CallEventPanel( SettingsManager settingsManager ) + public CallEventPanel( SettingsManager settingsManager, + ChannelProcessingManager channelProcessingManager ) { + mChannelProcessingManager = channelProcessingManager; + setLayout( new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]") ); JTable table = new JTable( new CallEventModel() ); @@ -69,34 +75,43 @@ public class CallEventPanel extends JPanel implements ChannelEventListener ( mDisplayedChannel != null && mDisplayedChannel != event.getChannel() ) ) { - EventQueue.invokeLater( new Runnable() - { - @Override - public void run() - { - removeAll(); - - JTable table = new JTable( - event.getChannel().getCallEventModel() ); - - table.setAutoCreateRowSorter( true ); - table.setAutoResizeMode( JTable.AUTO_RESIZE_LAST_COLUMN ); - - table.getColumnModel().getColumn( CallEventModel.FROM_ALIAS ) - .setCellRenderer( mRenderer ); - - table.getColumnModel().getColumn( CallEventModel.TO_ALIAS ) - .setCellRenderer( mRenderer ); - - add( new JScrollPane( table ) ); - - mDisplayedChannel = event.getChannel(); - - revalidate(); - repaint(); - } - } ); + ProcessingChain chain = mChannelProcessingManager + .getProcessingChain( event.getChannel() ); + if( chain != null ) + { + final CallEventModel model = chain.getCallEventModel(); + + if( model != null ) + { + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + removeAll(); + + JTable table = new JTable( model ); + + table.setAutoCreateRowSorter( true ); + table.setAutoResizeMode( JTable.AUTO_RESIZE_LAST_COLUMN ); + + table.getColumnModel().getColumn( CallEventModel.FROM_ALIAS ) + .setCellRenderer( mRenderer ); + + table.getColumnModel().getColumn( CallEventModel.TO_ALIAS ) + .setCellRenderer( mRenderer ); + + add( new JScrollPane( table ) ); + + mDisplayedChannel = event.getChannel(); + + revalidate(); + repaint(); + } + } ); + } + } } } else if( event.getEvent() == Event.NOTIFICATION_PROCESSING_STOP || diff --git a/src/module/decode/event/MessageActivityPanel.java b/src/module/decode/event/MessageActivityPanel.java index 9f84d619..6cc4ac71 100644 --- a/src/module/decode/event/MessageActivityPanel.java +++ b/src/module/decode/event/MessageActivityPanel.java @@ -36,6 +36,7 @@ import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import message.Message; +import module.ProcessingChain; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; @@ -48,6 +49,7 @@ import controller.channel.Channel; import controller.channel.ChannelEvent; import controller.channel.ChannelEvent.Event; import controller.channel.ChannelEventListener; +import controller.channel.ChannelProcessingManager; import filter.FilterEditorPanel; import filter.FilterSet; @@ -63,16 +65,21 @@ public class MessageActivityPanel extends JPanel implements ChannelEventListener private JTable mMessageTable; private Channel mDisplayedChannel; + private MessageActivityModel mDisplayedModel = EMPTY_MODEL; private MessageManagementPanel mManagementPanel = new MessageManagementPanel(); + + private ChannelProcessingManager mChannelProcessingManager; - public MessageActivityPanel() + public MessageActivityPanel( ChannelProcessingManager channelProcessingManager ) { + mChannelProcessingManager = channelProcessingManager; + setLayout( new MigLayout("insets 0 0 0 0", "[][grow,fill]", "[]0[grow,fill]") ); add( mManagementPanel, "span,growx" ); - mMessageTable = new JTable( EMPTY_MODEL ); + mMessageTable = new JTable( mDisplayedModel ); add( new JScrollPane( mMessageTable ), "span,grow" ); } @@ -91,7 +98,19 @@ public class MessageActivityPanel extends JPanel implements ChannelEventListener { mDisplayedChannel = event.getChannel(); - mMessageTable.setModel( event.getChannel().getMessageActivityModel() ); + ProcessingChain chain = mChannelProcessingManager + .getProcessingChain( mDisplayedChannel ); + + if( chain != null ) + { + mDisplayedModel = chain.getMessageActivityModel(); + } + else + { + mDisplayedModel = EMPTY_MODEL; + } + + mMessageTable.setModel( mDisplayedModel ); if( mDisplayedChannel != null ) { @@ -195,10 +214,9 @@ public class MessageActivityPanel extends JPanel implements ChannelEventListener public void mouseReleased( MouseEvent arg0 ) {} } ); - if( mDisplayedChannel != null ) + if( mDisplayedModel != null ) { - slider.setValue( mDisplayedChannel.getMessageActivityModel() - .getMaxMessageCount() ); + slider.setValue( mDisplayedModel.getMaxMessageCount() ); } else { @@ -210,10 +228,9 @@ public class MessageActivityPanel extends JPanel implements ChannelEventListener @Override public void stateChanged( ChangeEvent arg0 ) { - if( mDisplayedChannel != null ) + if( mDisplayedModel != null ) { - mDisplayedChannel.getMessageActivityModel() - .setMaxMessageCount( slider.getValue() ); + mDisplayedModel.setMaxMessageCount( slider.getValue() ); } } }); @@ -228,9 +245,9 @@ public class MessageActivityPanel extends JPanel implements ChannelEventListener @Override public void actionPerformed( ActionEvent e ) { - if( mDisplayedChannel != null ) + if( mDisplayedModel != null ) { - mDisplayedChannel.getMessageActivityModel().clear(); + mDisplayedModel.clear(); } } } ); @@ -258,50 +275,52 @@ public class MessageActivityPanel extends JPanel implements ChannelEventListener } else { - final JFrame editor = new JFrame(); - - editor.setTitle( "Message Filter Editor" ); - editor.setSize( 600, 400 ); - editor.setLocationRelativeTo( MessageFilterButton.this ); - editor.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ); - editor.setLayout( new MigLayout( "", "[grow,fill]", - "[grow,fill][][]" ) ); - - @SuppressWarnings( "unchecked" ) - FilterSet filter = (FilterSet) - mDisplayedChannel.getMessageActivityModel() - .getMessageFilter(); - - FilterEditorPanel panel = - new FilterEditorPanel( filter ); - - JScrollPane scroller = new JScrollPane( panel ); - scroller.setViewportView( panel ); - - editor.add( scroller, "wrap" ); - - editor.add( new JLabel( "Right-click to select/deselect " - + "all nodes" ), "wrap" ); - - JButton close = new JButton( "Close" ); - close.addActionListener( new ActionListener() + if( mDisplayedModel != null ) { - @Override - public void actionPerformed( ActionEvent e ) - { - editor.dispose(); - } - } ); - - editor.add( close ); + final JFrame editor = new JFrame(); + + editor.setTitle( "Message Filter Editor" ); + editor.setSize( 600, 400 ); + editor.setLocationRelativeTo( MessageFilterButton.this ); + editor.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ); + editor.setLayout( new MigLayout( "", "[grow,fill]", + "[grow,fill][][]" ) ); - EventQueue.invokeLater( new Runnable() - { - public void run() - { - editor.setVisible( true ); - } - } ); + @SuppressWarnings( "unchecked" ) + FilterSet filter = (FilterSet) + mDisplayedModel.getMessageFilter(); + + FilterEditorPanel panel = + new FilterEditorPanel( filter ); + + JScrollPane scroller = new JScrollPane( panel ); + scroller.setViewportView( panel ); + + editor.add( scroller, "wrap" ); + + editor.add( new JLabel( "Right-click to select/deselect " + + "all nodes" ), "wrap" ); + + JButton close = new JButton( "Close" ); + close.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + editor.dispose(); + } + } ); + + editor.add( close ); + + EventQueue.invokeLater( new Runnable() + { + public void run() + { + editor.setVisible( true ); + } + } ); + } } } } ); diff --git a/src/module/decode/fleetsync2/Fleetsync2Decoder.java b/src/module/decode/fleetsync2/Fleetsync2Decoder.java index 1425983a..f8092472 100644 --- a/src/module/decode/fleetsync2/Fleetsync2Decoder.java +++ b/src/module/decode/fleetsync2/Fleetsync2Decoder.java @@ -29,7 +29,7 @@ import java.util.List; import module.decode.Decoder; import module.decode.DecoderType; import sample.Listener; -import sample.real.IRealBufferListener; +import sample.real.IFilteredRealBufferListener; import sample.real.RealBuffer; import alias.AliasList; import bits.MessageFramer; @@ -44,7 +44,7 @@ import dsp.fsk.FSK2Decoder.Output; * Fleetsync II Decoder - 1200 baud 2FSK decoder that can process 48k sample rate * floating point samples and output fully framed Fleetsync II messages */ -public class Fleetsync2Decoder extends Decoder implements IRealBufferListener, +public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferListener, Instrumentable { /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ @@ -169,7 +169,7 @@ public class Fleetsync2Decoder extends Decoder implements IRealBufferListener, } @Override - public Listener getRealBufferListener() + public Listener getFilteredRealBufferListener() { return mDecimationFilter; } diff --git a/src/module/decode/lj1200/LJ1200Decoder.java b/src/module/decode/lj1200/LJ1200Decoder.java index 57297d57..1e20eeda 100644 --- a/src/module/decode/lj1200/LJ1200Decoder.java +++ b/src/module/decode/lj1200/LJ1200Decoder.java @@ -31,7 +31,7 @@ import module.decode.Decoder; import module.decode.DecoderType; import sample.Broadcaster; import sample.Listener; -import sample.real.IRealBufferListener; +import sample.real.IFilteredRealBufferListener; import sample.real.RealBuffer; import sample.real.RealSampleListener; import alias.AliasList; @@ -46,7 +46,7 @@ import dsp.fsk.FSK2Decoder.Output; /** * LJ1200 - 1200 baud 2FSK decoder */ -public class LJ1200Decoder extends Decoder implements IRealBufferListener, +public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListener, Instrumentable { /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ @@ -187,7 +187,7 @@ public class LJ1200Decoder extends Decoder implements IRealBufferListener, } @Override - public Listener getRealBufferListener() + public Listener getFilteredRealBufferListener() { return mDecimationFilter; } diff --git a/src/module/decode/ltrnet/LTRNetDecoder.java b/src/module/decode/ltrnet/LTRNetDecoder.java index 095dffe4..f5c1d3ca 100644 --- a/src/module/decode/ltrnet/LTRNetDecoder.java +++ b/src/module/decode/ltrnet/LTRNetDecoder.java @@ -28,14 +28,15 @@ import message.MessageDirection; import module.decode.Decoder; import module.decode.DecoderType; import sample.Listener; -import sample.real.IRealBufferListener; +import sample.real.IUnFilteredRealBufferListener; import sample.real.RealBuffer; import alias.AliasList; import bits.MessageFramer; import bits.SyncPattern; import dsp.fsk.LTRFSKDecoder; -public class LTRNetDecoder extends Decoder implements IRealBufferListener, Instrumentable +public class LTRNetDecoder extends Decoder + implements IUnFilteredRealBufferListener, Instrumentable { public static final int LTR_STANDARD_MESSAGE_LENGTH = 40; private LTRFSKDecoder mLTRFSKDecoder; @@ -44,6 +45,10 @@ public class LTRNetDecoder extends Decoder implements IRealBufferListener, Instr private List mAvailableTaps; + /** + * LTR-Net Decoder. Decodes unfiltered (e.g. demodulated but with no DC or + * audio filtering) samples and produces LTR-Net messages. + */ public LTRNetDecoder( DecodeConfigLTRNet config, AliasList aliasList ) { mLTRFSKDecoder = new LTRFSKDecoder(); @@ -79,7 +84,7 @@ public class LTRNetDecoder extends Decoder implements IRealBufferListener, Instr } @Override - public Listener getRealBufferListener() + public Listener getUnFilteredRealBufferListener() { return mLTRFSKDecoder; } diff --git a/src/module/decode/ltrstandard/LTRStandardDecoder.java b/src/module/decode/ltrstandard/LTRStandardDecoder.java index a39d659c..228d68f5 100644 --- a/src/module/decode/ltrstandard/LTRStandardDecoder.java +++ b/src/module/decode/ltrstandard/LTRStandardDecoder.java @@ -21,14 +21,14 @@ import message.MessageDirection; import module.decode.Decoder; import module.decode.DecoderType; import sample.Listener; -import sample.real.IRealBufferListener; +import sample.real.IUnFilteredRealBufferListener; import sample.real.RealBuffer; import alias.AliasList; import bits.MessageFramer; import bits.SyncPattern; import dsp.fsk.LTRFSKDecoder; -public class LTRStandardDecoder extends Decoder implements IRealBufferListener +public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBufferListener { public static final int LTR_STANDARD_MESSAGE_LENGTH = 40; @@ -36,6 +36,10 @@ public class LTRStandardDecoder extends Decoder implements IRealBufferListener private MessageFramer mLTRMessageFramer; private LTRStandardMessageProcessor mLTRMessageProcessor; + /** + * LTR Decoder. Decodes unfiltered (e.g. demodulated but with no DC or + * audio filtering) samples and produces LTR Standard messages. + */ public LTRStandardDecoder( AliasList aliasList, MessageDirection direction ) { @@ -68,7 +72,7 @@ public class LTRStandardDecoder extends Decoder implements IRealBufferListener } @Override - public Listener getRealBufferListener() + public Listener getUnFilteredRealBufferListener() { return mLTRFSKDecoder; } diff --git a/src/module/decode/mdc1200/MDCDecoder.java b/src/module/decode/mdc1200/MDCDecoder.java index 86289146..7e973af2 100644 --- a/src/module/decode/mdc1200/MDCDecoder.java +++ b/src/module/decode/mdc1200/MDCDecoder.java @@ -30,7 +30,7 @@ import java.util.List; import module.decode.Decoder; import module.decode.DecoderType; import sample.Listener; -import sample.real.IRealBufferListener; +import sample.real.IFilteredRealBufferListener; import sample.real.RealBuffer; import alias.AliasList; import bits.MessageFramer; @@ -46,7 +46,7 @@ import dsp.fsk.FSK2Decoder.Output; * MDC1200 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate * complex or floating point samples and output fully framed MDC1200 messages */ -public class MDCDecoder extends Decoder implements IRealBufferListener, +public class MDCDecoder extends Decoder implements IFilteredRealBufferListener, Instrumentable { /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ @@ -197,7 +197,7 @@ public class MDCDecoder extends Decoder implements IRealBufferListener, } @Override - public Listener getRealBufferListener() + public Listener getFilteredRealBufferListener() { return mDecimationFilter; } diff --git a/src/module/decode/mpt1327/MPT1327ConfigEditor.java b/src/module/decode/mpt1327/MPT1327ConfigEditor.java index 01c2af35..fa036c96 100644 --- a/src/module/decode/mpt1327/MPT1327ConfigEditor.java +++ b/src/module/decode/mpt1327/MPT1327ConfigEditor.java @@ -31,7 +31,7 @@ import module.decode.DecodeEditor; import module.decode.config.DecodeConfiguration; import module.decode.mpt1327.MPT1327Decoder.Sync; import playlist.PlaylistManager; -import controller.channel.ChannelNode; +import controller.channel.Channel; import controller.channel.map.ChannelMap; public class MPT1327ConfigEditor extends DecodeEditor @@ -44,16 +44,16 @@ public class MPT1327ConfigEditor extends DecodeEditor private JSlider mCallTimeout; private JLabel mTrafficChannelPoolSizeLabel; private JSlider mTrafficChannelPoolSize; - private ChannelNode mChannelNode; + private Channel mChannel; private PlaylistManager mPlaylistManager; public MPT1327ConfigEditor( DecodeConfiguration config, - ChannelNode channelNode, + Channel channel, PlaylistManager playlistManager ) { super( config ); - mChannelNode = channelNode; + mChannel = channel; mPlaylistManager = playlistManager; initGUI(); @@ -140,7 +140,7 @@ public class MPT1327ConfigEditor extends DecodeEditor add( mTrafficChannelPoolSizeLabel ); add( mTrafficChannelPoolSize, "wrap,grow" ); - reset(); +// reset(); } @Override @@ -167,36 +167,35 @@ public class MPT1327ConfigEditor extends DecodeEditor config.setTrafficChannelPoolSize( mTrafficChannelPoolSize.getValue() ); } - @Override - public void reset() - { - DecodeConfigMPT1327 config = (DecodeConfigMPT1327)mConfig; - - String name = config.getChannelMapName(); - - ChannelMap selected = null; - - if( name != null ) - { - for( ChannelMap map: mChannelNode.getModel().getResourceManager() - .getPlaylistManager().getPlayist().getChannelMapList() - .getChannelMap() ) - { - if( map.getName().equalsIgnoreCase( name ) ) - { - selected = map; - } - } - } - - mComboChannelMaps.setSelectedItem( selected ); - mComboChannelMaps.requestFocus(); - mComboChannelMaps.requestFocusInWindow(); - - mComboSync.setSelectedItem( config.getSync() ); - - mCallTimeout.setValue( config.getCallTimeout() ); - - mTrafficChannelPoolSize.setValue( config.getTrafficChannelPoolSize() ); - } +// @Override +// public void reset() +// { +// DecodeConfigMPT1327 config = (DecodeConfigMPT1327)mConfig; +// +// String name = config.getChannelMapName(); +// +// ChannelMap selected = null; +// +// if( name != null ) +// { +// for( ChannelMap map: mPlaylistManager.getPlayist().getChannelMapList() +// .getChannelMap() ) +// { +// if( map.getName().equalsIgnoreCase( name ) ) +// { +// selected = map; +// } +// } +// } +// +// mComboChannelMaps.setSelectedItem( selected ); +// mComboChannelMaps.requestFocus(); +// mComboChannelMaps.requestFocusInWindow(); +// +// mComboSync.setSelectedItem( config.getSync() ); +// +// mCallTimeout.setValue( config.getCallTimeout() ); +// +// mTrafficChannelPoolSize.setValue( config.getTrafficChannelPoolSize() ); +// } } diff --git a/src/module/decode/mpt1327/MPT1327Decoder.java b/src/module/decode/mpt1327/MPT1327Decoder.java index 679da857..799fd5a0 100644 --- a/src/module/decode/mpt1327/MPT1327Decoder.java +++ b/src/module/decode/mpt1327/MPT1327Decoder.java @@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory; import sample.Broadcaster; import sample.Listener; -import sample.real.IRealBufferListener; +import sample.real.IFilteredRealBufferListener; import sample.real.RealBuffer; import alias.AliasList; import bits.MessageFramer; @@ -55,7 +55,7 @@ import dsp.fsk.FSK2Decoder.Output; * traffic messages. */ public class MPT1327Decoder extends Decoder - implements IRealBufferListener, Instrumentable + implements IFilteredRealBufferListener, Instrumentable { private final static Logger mLog = LoggerFactory.getLogger( MPT1327Decoder.class ); @@ -146,7 +146,7 @@ public class MPT1327Decoder extends Decoder } @Override - public Listener getRealBufferListener() + public Listener getFilteredRealBufferListener() { return mDecimationFilter; } diff --git a/src/module/decode/p25/C4FMSymbolFilter.java b/src/module/decode/p25/C4FMSymbolFilter.java index 0c88b18a..651f2c60 100644 --- a/src/module/decode/p25/C4FMSymbolFilter.java +++ b/src/module/decode/p25/C4FMSymbolFilter.java @@ -16,12 +16,13 @@ import sample.Listener; import sample.real.RealBuffer; import sample.real.RealSampleListener; import sample.real.RealSampleProvider; -import source.tuner.frequency.FrequencyCorrectionControl; -import source.tuner.frequency.FrequencyCorrectionControl.FrequencyCorrectionResetListener; +import source.tuner.frequency.FrequencyChangeEvent; +import source.tuner.frequency.FrequencyChangeEvent.Event; +import source.tuner.frequency.IFrequencyChangeListener; import dsp.gain.DirectGainControl; -public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, - Listener, +public class C4FMSymbolFilter implements Listener, + IFrequencyChangeListener, RealSampleProvider, Instrumentable { @@ -201,15 +202,17 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, private float mHistory[] = new float[ NUMBER_FILTER_TAPS ]; private int mHistoryLast = 0; - private boolean mResetFrequencyTracker = false; - private RealSampleListener mListener; private DirectGainControl mGainController = new DirectGainControl( 15.0f, 0.1f, 35.0f, 0.3f ); - private FrequencyCorrectionControl mFrequencyCorrectionControl; private int mFrequencyAdjustmentRequested = 0; + private int mFrequencyCorrection = 0; + private int mFrequencyCorrectionMaximum = 3000; + private boolean mResetFrequencyTracker = false; + private FrequencyCorrectionProcessor mFrequencyCorrectionProcessor; + private Listener mFrequencyChangeListener; /** * C4FM Symbol Filter @@ -224,18 +227,15 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, * setting for this filter is 15.4 and that yields a symbol spread that * is centered on 2.0, ranging 1.92 to 2.08. */ - public C4FMSymbolFilter( FrequencyCorrectionControl frequencyControl ) + public C4FMSymbolFilter( int frequencyCorrectionMaximum ) { - mFrequencyCorrectionControl = frequencyControl; - - /* Register for reset events, so we can reset internal frequency tracking */ - mFrequencyCorrectionControl.setListener( this ); + mFrequencyCorrectionMaximum = frequencyCorrectionMaximum; } public void dispose() { mGainController = null; - mFrequencyCorrectionControl = null; +// mFrequencyCorrectionControl = null; mListener = null; } @@ -253,13 +253,21 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, * the internal frequency correction tracker */ if( mFrequencyAdjustmentRequested != 0 ) { - mFrequencyCorrectionControl.adjust( mFrequencyAdjustmentRequested ); + int correction = mFrequencyCorrection + mFrequencyAdjustmentRequested; + + if( correction > mFrequencyCorrectionMaximum ) + { + correction = mFrequencyCorrectionMaximum; + } + else if( correction < -mFrequencyCorrectionMaximum ) + { + correction = -mFrequencyCorrectionMaximum; + } + + broadcast( new FrequencyChangeEvent( + Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, correction ) ); mFrequencyAdjustmentRequested = 0; - - //Reset internal frequency tracking - mCoarseFrequencyCorrection = 0.0f; - mFineFrequencyCorrection = 0.0f; } } @@ -272,7 +280,8 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, mCoarseFrequencyCorrection = 0.0f; mFineFrequencyCorrection = 0.0f; - mFrequencyCorrectionControl.setFrequencyCorrection( 0 ); + broadcast( new FrequencyChangeEvent( + Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, 0 )); mResetFrequencyTracker = false; } @@ -397,17 +406,6 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, } } - /** - * Listener interface for frequency correction reset events. We reset the - * coarse and fine frequency control values when the tuner channel source - * has changed frequency (PPM) or sample rate - */ - @Override - public void resetFrequencyCorrection() - { - mResetFrequencyTracker = true; - } - @Override public void setListener( RealSampleListener listener ) { @@ -459,4 +457,56 @@ public class C4FMSymbolFilter implements FrequencyCorrectionResetListener, mSymbolSpreadTap = null; } } + + public void broadcast( FrequencyChangeEvent event ) + { + if( mFrequencyChangeListener != null ) + { + mFrequencyChangeListener.receive( event ); + } + } + + public void setFrequencyChangeListener( Listener listener ) + { + mFrequencyChangeListener = listener; + } + + @Override + public Listener getFrequencyChangeListener() + { + if( mFrequencyCorrectionProcessor == null ) + { + mFrequencyCorrectionProcessor = new FrequencyCorrectionProcessor(); + } + + return mFrequencyCorrectionProcessor; + } + + /** + * Receives notifications that the channel frequency correction has been + * applied and updates internal tracking value. + */ + public class FrequencyCorrectionProcessor implements Listener + { + @Override + public void receive( FrequencyChangeEvent event ) + { + switch( event.getEvent() ) + { + case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + mFrequencyCorrection = event.getValue().intValue(); + + //Reset internal frequency tracking + mCoarseFrequencyCorrection = 0.0f; + mFineFrequencyCorrection = 0.0f; + break; + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mResetFrequencyTracker = true; + break; + default: + break; + } + } + } } diff --git a/src/module/decode/p25/P25DecodeEditor.java b/src/module/decode/p25/P25DecodeEditor.java index e3c7052d..c87d1e7c 100644 --- a/src/module/decode/p25/P25DecodeEditor.java +++ b/src/module/decode/p25/P25DecodeEditor.java @@ -34,8 +34,8 @@ import org.slf4j.LoggerFactory; import source.SourceEditor; import source.tuner.TunerEditor; -import controller.Editor; -import controller.channel.ChannelValidationException; +import controller.channel.ChannelConfigurationEditor; +import controller.channel.ConfigurationValidationException; public class P25DecodeEditor extends DecodeEditor { @@ -101,29 +101,29 @@ public class P25DecodeEditor extends DecodeEditor add( new JLabel( "Ignore Data Calls" ) ); add( mIgnoreDataCalls, "wrap,grow" ); - reset(); +// reset(); } - /** - * Enforce source=tuner for CQPSK modulation - */ - @Override - public void validate( Editor editor ) throws ChannelValidationException - { - if( editor instanceof SourceEditor && - ((DecodeConfigP25Phase1)mConfig).getModulation() == Modulation.CQPSK ) - { - if( !( editor instanceof TunerEditor ) ) - { - throw new ChannelValidationException( - "

LSM Simulcast

" - + "

P25 LSM Simulcast decoder can only be used with " - + "a tuner source. Please change the Source to use a tuner" - + " or change the P25 Decoder to C4FM modulation" ); - - } - } - } +// /** +// * Enforce source=tuner for CQPSK modulation +// */ +// @Override +// public void validate( ChannelConfigurationEditor editor ) throws ConfigurationValidationException +// { +// if( editor instanceof SourceEditor && +// ((DecodeConfigP25Phase1)mConfig).getModulation() == Modulation.CQPSK ) +// { +// if( !( editor instanceof TunerEditor ) ) +// { +// throw new ConfigurationValidationException( +// "

LSM Simulcast

" +// + "

P25 LSM Simulcast decoder can only be used with " +// + "a tuner source. Please change the Source to use a tuner" +// + " or change the P25 Decoder to C4FM modulation" ); +// +// } +// } +// } @Override public void save() @@ -135,13 +135,13 @@ public class P25DecodeEditor extends DecodeEditor config.setTrafficChannelPoolSize( mTrafficChannelPoolSize.getValue() ); } - @Override - public void reset() - { - DecodeConfigP25Phase1 config = (DecodeConfigP25Phase1)mConfig; - - mComboModulation.setSelectedItem( config.getModulation() ); - mIgnoreDataCalls.setSelected( config.getIgnoreDataCalls() ); - mTrafficChannelPoolSize.setValue( config.getTrafficChannelPoolSize() ); - } +// @Override +// public void reset() +// { +// DecodeConfigP25Phase1 config = (DecodeConfigP25Phase1)mConfig; +// +// mComboModulation.setSelectedItem( config.getModulation() ); +// mIgnoreDataCalls.setSelected( config.getIgnoreDataCalls() ); +// mTrafficChannelPoolSize.setValue( config.getTrafficChannelPoolSize() ); +// } } diff --git a/src/module/decode/p25/P25Decoder.java b/src/module/decode/p25/P25Decoder.java index 674b00b9..ca46e81b 100644 --- a/src/module/decode/p25/P25Decoder.java +++ b/src/module/decode/p25/P25Decoder.java @@ -3,11 +3,9 @@ package module.decode.p25; import instrument.Instrumentable; import module.decode.Decoder; import module.decode.DecoderType; -import source.tuner.frequency.IFrequencyCorrectionController; import alias.AliasList; -public abstract class P25Decoder extends Decoder - implements IFrequencyCorrectionController, Instrumentable +public abstract class P25Decoder extends Decoder implements Instrumentable { private P25MessageProcessor mMessageProcessor; diff --git a/src/module/decode/p25/P25_C4FMDecoder.java b/src/module/decode/p25/P25_C4FMDecoder.java index c4fabf0a..3bae443d 100644 --- a/src/module/decode/p25/P25_C4FMDecoder.java +++ b/src/module/decode/p25/P25_C4FMDecoder.java @@ -24,22 +24,24 @@ import instrument.tap.stream.FloatBufferTap; import instrument.tap.stream.FloatTap; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sample.Listener; -import sample.real.IRealBufferListener; +import sample.real.IFilteredRealBufferListener; import sample.real.RealBuffer; -import source.tuner.frequency.FrequencyCorrectionControl; +import source.tuner.frequency.FrequencyChangeEvent; +import source.tuner.frequency.IFrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeProvider; import alias.AliasList; import dsp.filter.FilterFactory; import dsp.filter.Window.WindowType; import dsp.filter.fir.real.RealFIRFilter_RB_RB; -public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener +public class P25_C4FMDecoder extends P25Decoder + implements IFrequencyChangeListener, IFrequencyChangeProvider,IFilteredRealBufferListener { private final static Logger mLog = LoggerFactory.getLogger( P25_C4FMDecoder.class ); @@ -48,11 +50,8 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener private static final String INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT = "Tap Point: Symbol Filter Output"; private static final String INSTRUMENT_C4FM_SLICER_OUTPUT = "Tap Point: C4FM Slicer Output"; - private final static int MAXIMUM_FREQUENCY_CORRECTION = 3000; //Hertz, +/- - private List mAvailableTaps; private RealFIRFilter_RB_RB mC4FMPreFilter; - private FrequencyCorrectionControl mFrequencyCorrectionControl; private C4FMSymbolFilter mSymbolFilter; private C4FMSlicer mC4FMSlicer; private P25MessageFramer mMessageFramer; @@ -70,23 +69,18 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener * infrastructure and subscriber identities that will be included in each * decoded message */ - public P25_C4FMDecoder( AliasList aliasList ) + public P25_C4FMDecoder( AliasList aliasList, int frequencyCorrectionMaximum ) { super( aliasList ); /* Filter demodulated sample buffers */ float[] filter = FilterFactory.getLowPass( 48000, 2500, 4000, 80, WindowType.HANNING, true ); - mLog.debug( "Demod Filter tap count:" + filter.length + " coefficients:" + Arrays.toString( filter ) ); +// mLog.debug( "Demod Filter tap count:" + filter.length + " coefficients:" + Arrays.toString( filter ) ); mC4FMPreFilter = new RealFIRFilter_RB_RB( filter, 1.0f ); - /* Issue tuned frequency correction commands, remotely controlled by the - * downstream symbol filter */ - mFrequencyCorrectionControl = new FrequencyCorrectionControl( - MAXIMUM_FREQUENCY_CORRECTION ); - /* Shape gain and frequency offsets to optimize sample stream */ - mSymbolFilter = new C4FMSymbolFilter( mFrequencyCorrectionControl ); + mSymbolFilter = new C4FMSymbolFilter( frequencyCorrectionMaximum ); mC4FMPreFilter.setListener( mSymbolFilter ); /* Convert samples to symbols */ @@ -109,9 +103,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener mC4FMPreFilter.dispose(); mC4FMPreFilter = null; - mFrequencyCorrectionControl.dispose(); - mFrequencyCorrectionControl = null; - mSymbolFilter.dispose(); mSymbolFilter = null; @@ -128,19 +119,7 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener } @Override - public boolean hasFrequencyCorrectionControl() - { - return mFrequencyCorrectionControl != null; - } - - @Override - public FrequencyCorrectionControl getFrequencyCorrectionControl() - { - return mFrequencyCorrectionControl; - } - - @Override - public Listener getRealBufferListener() + public Listener getFilteredRealBufferListener() { return mC4FMPreFilter; } @@ -262,4 +241,33 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener // TODO Auto-generated method stub } + + @Override + public void setFrequencyChangeListener( Listener listener ) + { + if( mSymbolFilter != null ) + { + mSymbolFilter.setFrequencyChangeListener( listener ); + } + } + + @Override + public void removeFrequencyChangeListener() + { + if( mSymbolFilter != null ) + { + mSymbolFilter.setFrequencyChangeListener( null ); + } + } + + @Override + public Listener getFrequencyChangeListener() + { + if( mSymbolFilter != null ) + { + return mSymbolFilter.getFrequencyChangeListener(); + } + + return null; + } } diff --git a/src/module/decode/p25/P25_LSMDecoder.java b/src/module/decode/p25/P25_LSMDecoder.java index 21555ae9..0e72cba4 100644 --- a/src/module/decode/p25/P25_LSMDecoder.java +++ b/src/module/decode/p25/P25_LSMDecoder.java @@ -34,7 +34,6 @@ import sample.Listener; import sample.complex.ComplexBuffer; import sample.complex.ComplexBufferToStreamConverter; import sample.complex.IComplexBufferListener; -import source.tuner.frequency.FrequencyCorrectionControl; import alias.AliasList; import dsp.filter.FilterFactory; import dsp.filter.Window.WindowType; @@ -119,18 +118,6 @@ public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener return Modulation.CQPSK; } - @Override - public boolean hasFrequencyCorrectionControl() - { - return false; - } - - @Override - public FrequencyCorrectionControl getFrequencyCorrectionControl() - { - return null; - } - /** * Provides a list of instrumentation taps for monitoring internal processing */ diff --git a/src/module/decode/passport/PassportDecoder.java b/src/module/decode/passport/PassportDecoder.java index b6d46a2b..e125541d 100644 --- a/src/module/decode/passport/PassportDecoder.java +++ b/src/module/decode/passport/PassportDecoder.java @@ -28,7 +28,7 @@ import module.decode.Decoder; import module.decode.DecoderType; import module.decode.config.DecodeConfiguration; import sample.Listener; -import sample.real.IRealBufferListener; +import sample.real.IUnFilteredRealBufferListener; import sample.real.RealBuffer; import alias.AliasList; import bits.MessageFramer; @@ -36,7 +36,7 @@ import bits.SyncPattern; import dsp.fsk.LTRFSKDecoder; public class PassportDecoder extends Decoder - implements IRealBufferListener, Instrumentable + implements IUnFilteredRealBufferListener, Instrumentable { public static final int PASSPORT_MESSAGE_LENGTH = 68; public static final int PASSPORT_SYNC_LENGTH = 9; @@ -47,8 +47,11 @@ public class PassportDecoder extends Decoder private List mAvailableTaps; - public PassportDecoder( DecodeConfiguration config, - AliasList aliasList ) + /** + * Passport Decoder. Decodes unfiltered (e.g. demodulated but with no DC or + * audio filtering) samples and produces Passport messages. + */ + public PassportDecoder( DecodeConfiguration config, AliasList aliasList ) { mPassportFSKDecoder = new LTRFSKDecoder(); @@ -64,7 +67,7 @@ public class PassportDecoder extends Decoder } @Override - public Listener getRealBufferListener() + public Listener getUnFilteredRealBufferListener() { return mPassportFSKDecoder; } diff --git a/src/module/decode/state/ChannelCollectionPanel.java b/src/module/decode/state/ChannelCollectionPanel.java index 685511d1..eae3db84 100644 --- a/src/module/decode/state/ChannelCollectionPanel.java +++ b/src/module/decode/state/ChannelCollectionPanel.java @@ -16,6 +16,8 @@ import org.slf4j.LoggerFactory; import playlist.PlaylistManager; import settings.SettingsManager; import controller.channel.Channel; +import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; /** * Channel Collection assembles a primary channel panel and any related decoder @@ -28,15 +30,21 @@ public class ChannelCollectionPanel extends JPanel private final static Logger mLog = LoggerFactory.getLogger( ChannelCollectionPanel.class ); + private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; private PlaylistManager mPlaylistManager; private SettingsManager mSettingsManager; private List mChannelPanels = new ArrayList(); - public ChannelCollectionPanel( PlaylistManager playlistManager, + public ChannelCollectionPanel( ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, SettingsManager settingsManager, Channel channel ) { + mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; mPlaylistManager = playlistManager; mSettingsManager = settingsManager; @@ -49,8 +57,8 @@ public class ChannelCollectionPanel extends JPanel setOpaque( false ); - ChannelStatePanel csp = new ChannelStatePanel( mPlaylistManager, - mSettingsManager, channel ); + ChannelStatePanel csp = new ChannelStatePanel( mChannelModel, + mChannelProcessingManager, mPlaylistManager, mSettingsManager, channel ); mChannelPanels.add( csp ); diff --git a/src/module/decode/state/ChannelList.java b/src/module/decode/state/ChannelList.java index aff58241..5cd8f9b8 100644 --- a/src/module/decode/state/ChannelList.java +++ b/src/module/decode/state/ChannelList.java @@ -43,6 +43,8 @@ import settings.SettingsManager; import controller.channel.Channel; import controller.channel.ChannelEvent; import controller.channel.ChannelEventListener; +import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; /** * Gui wrapper for the list of currently processing primary channels @@ -56,12 +58,18 @@ public class ChannelList extends JPanel implements ChannelEventListener private HashMap mDisplayedPanels = new HashMap(); + private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; private PlaylistManager mPlaylistManager; private SettingsManager mSettingsManager; - public ChannelList( PlaylistManager playlistManager, + public ChannelList( ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, SettingsManager settingsManager ) { + mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; mPlaylistManager = playlistManager; mSettingsManager = settingsManager; @@ -96,8 +104,8 @@ public class ChannelList extends JPanel implements ChannelEventListener { if( !mDisplayedPanels.containsKey( channel ) ) { - ChannelCollectionPanel panel = new ChannelCollectionPanel( - mPlaylistManager, mSettingsManager, channel ); + ChannelCollectionPanel panel = new ChannelCollectionPanel( mChannelModel, + mChannelProcessingManager, mPlaylistManager, mSettingsManager, channel ); add( panel, "wrap" ); diff --git a/src/module/decode/state/ChannelStatePanel.java b/src/module/decode/state/ChannelStatePanel.java index 0559da06..4e626964 100644 --- a/src/module/decode/state/ChannelStatePanel.java +++ b/src/module/decode/state/ChannelStatePanel.java @@ -14,6 +14,7 @@ import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JPanel; +import module.ProcessingChain; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,6 +32,11 @@ import settings.Setting; import settings.SettingChangeListener; import settings.SettingsManager; +import controller.channel.Channel; +import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; +import controller.channel.ChannelUtils; + public class ChannelStatePanel extends JPanel implements Listener, SettingChangeListener { @@ -38,10 +44,6 @@ public class ChannelStatePanel extends JPanel private final static Logger mLog = LoggerFactory.getLogger( ChannelStatePanel.class ); - private static final boolean ENABLED = true; - private static final boolean DISABLED = false; - private static final boolean BROADCAST_CHANGE = true; - private Font mFontDetails = new Font( Font.MONOSPACED, Font.PLAIN, 12 ); private Font mFontDecoder = new Font( Font.MONOSPACED, Font.PLAIN, 12 ); @@ -75,7 +77,10 @@ public class ChannelStatePanel extends JPanel protected SettingsManager mSettingsManager; private Channel mChannel; + private ChannelState mChannelState; private List mDecoderPanels = new ArrayList<>(); + private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; /** * Gui component to a channel state. Provides System, Site and channel @@ -83,21 +88,31 @@ public class ChannelStatePanel extends JPanel * * Updates color background according to current channel state. */ - public ChannelStatePanel( PlaylistManager playlistManager, + public ChannelStatePanel( ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, SettingsManager settingsManager, Channel channel ) { + mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; mPlaylistManager = playlistManager; mSettingsManager = settingsManager; mSettingsManager.addListener( this ); mChannel = channel; - mChannel.getChannelState().setChangedAttributeListener( this ); + + ProcessingChain processingChain = + channelProcessingManager.getProcessingChain( mChannel ); + + mChannelState = processingChain.getChannelState(); + + mChannelState.setChangedAttributeListener( this ); init(); - for( DecoderState decoderState: getChannel().getProcessingChain().getDecoderStates() ) + for( DecoderState decoderState: processingChain.getDecoderStates() ) { DecoderPanel panel = DecoderFactory.getDecoderPanel( mSettingsManager, decoderState ); mDecoderPanels.add( panel ); @@ -126,8 +141,7 @@ public class ChannelStatePanel extends JPanel mChannelLabel.setForeground( mColorLabelDetails ); add( mChannelLabel, "wrap" ); - mStateLabel = new JLabel( mChannel.getChannelState().getState() - .getDisplayValue() ); + mStateLabel = new JLabel( mChannelState.getState().getDisplayValue() ); mStateLabel.setFont( mFontDecoder ); mStateLabel.setForeground( mColorLabelDecoder ); add( mStateLabel ); @@ -161,8 +175,6 @@ public class ChannelStatePanel extends JPanel mOptionsLabel.setFont( mFontDetails ); mOptionsLabel.setForeground( mColorLabelAuxDecoder ); add( mOptionsLabel, "wrap" ); - -// add( new JSeparator( JSeparator.HORIZONTAL ), "span,growx" ); } public Channel getChannel() @@ -177,7 +189,8 @@ public class ChannelStatePanel extends JPanel mSettingsManager.removeListener( this ); mSettingsManager = null; - mChannel.getChannelState().removeChangedAttributeListener(); + mChannelState.removeChangedAttributeListener(); + mChannelState = null; mChannel = null; for( DecoderPanel panel: mDecoderPanels ) @@ -192,8 +205,6 @@ public class ChannelStatePanel extends JPanel @Override public void receive( final ChangedAttribute changedAttribute ) { - final ChannelState state = mChannel.getChannelState(); - EventQueue.invokeLater( new Runnable() { @Override @@ -202,9 +213,9 @@ public class ChannelStatePanel extends JPanel switch( changedAttribute ) { case CHANNEL_STATE: - if( state.getState() != null ) + if( mChannelState.getState() != null ) { - mStateLabel.setText( state.getState().getDisplayValue() ); + mStateLabel.setText( mChannelState.getState().getDisplayValue() ); } break; case SOURCE: @@ -214,7 +225,7 @@ public class ChannelStatePanel extends JPanel case CHANNEL_NAME: case SITE_NAME: case SYSTEM_NAME: - mChannelLabel.setText( mChannel.getChannelDisplayName() ); + mChannelLabel.setText( mChannel.toString() ); break; default: break; @@ -419,7 +430,7 @@ public class ChannelStatePanel extends JPanel public JMenu getContextMenu() { - return ChannelUtils.getContextMenu( mPlaylistManager, mChannel, - ChannelStatePanel.this ); + return ChannelUtils.getContextMenu( mChannelModel, mChannelProcessingManager, + mPlaylistManager, mChannel, ChannelStatePanel.this ); } } diff --git a/src/module/decode/state/TrafficChannelManager.java b/src/module/decode/state/TrafficChannelManager.java index 18d90d74..ec64ab84 100644 --- a/src/module/decode/state/TrafficChannelManager.java +++ b/src/module/decode/state/TrafficChannelManager.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import module.Module; +import module.ProcessingChain; import module.decode.config.DecodeConfiguration; import module.decode.event.CallEvent; import module.decode.event.CallEvent.CallEventType; @@ -21,11 +22,12 @@ import source.tuner.TunerChannel; import source.tuner.TunerChannel.Type; import alias.Alias; import alias.priority.Priority; -import controller.ResourceManager; import controller.channel.Channel; import controller.channel.Channel.ChannelType; -import controller.site.Site; -import controller.system.System; +import controller.channel.ChannelEvent; +import controller.channel.ChannelEvent.Event; +import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; public class TrafficChannelManager extends Module implements ICallEventProvider, IDecoderStateEventListener @@ -40,15 +42,14 @@ public class TrafficChannelManager extends Module private DecoderStateEventListener mEventListener = new DecoderStateEventListener(); private Listener mCallEventListener; + private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; private DecodeConfiguration mDecodeConfiguration; private RecordConfiguration mRecordConfiguration; - private ResourceManager mResourceManager; private String mSystem; private String mSite; private String mAliasListName; - private long mTrafficChannelTimeout; private CallEvent mPreviousDoNotMonitorCallEvent; - private boolean mShuttingDown = false; /** * Monitors call events and allocates traffic decoder channels in response @@ -68,7 +69,8 @@ public class TrafficChannelManager extends Module * @param trafficChannelPoolSize - maximum number of allocated traffic channels * in the pool */ - public TrafficChannelManager( ResourceManager resourceManager, + public TrafficChannelManager( ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, DecodeConfiguration decodeConfiguration, RecordConfiguration recordConfiguration, String system, @@ -77,13 +79,14 @@ public class TrafficChannelManager extends Module long trafficChannelTimeout, int trafficChannelPoolSize ) { - mResourceManager = resourceManager; + mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; mDecodeConfiguration = decodeConfiguration; mRecordConfiguration = recordConfiguration; mSystem = system; mSite = site; mAliasListName = aliasListName; - mTrafficChannelTimeout = trafficChannelTimeout; +// mTrafficChannelTimeout = trafficChannelTimeout; mTrafficChannelPoolMaximumSize = trafficChannelPoolSize; } @@ -91,7 +94,6 @@ public class TrafficChannelManager extends Module public void dispose() { mCallEventListener = null; - mResourceManager = null; mDecodeConfiguration = null; mPreviousDoNotMonitorCallEvent = null; } @@ -115,7 +117,7 @@ public class TrafficChannelManager extends Module { for( Channel configuredChannel: mTrafficChannelPool ) { - if( !configuredChannel.isProcessing() ) + if( !configuredChannel.getEnabled() ) { channel = configuredChannel; break; @@ -130,12 +132,10 @@ public class TrafficChannelManager extends Module channel.setRecordConfiguration( mRecordConfiguration ); - channel.setResourceManager( mResourceManager ); - - channel.setTrafficChannelTimeout( mTrafficChannelTimeout ); - channel.setAliasListName( mAliasListName ); + mChannelModel.addChannel( channel ); + mTrafficChannelPool.add( channel ); } @@ -143,26 +143,15 @@ public class TrafficChannelManager extends Module if( channel != null ) { channel.setSourceConfiguration( new SourceConfigTuner( tunerChannel ) ); - channel.setSystem( mSystem ); channel.setSite( mSite ); channel.setName( channelNumber ); - channel.setEnabled( true ); + mChannelModel.broadcast( new ChannelEvent( channel, + Event.NOTIFICATION_CONFIGURATION_CHANGE ) ); - /* Check the channel obtained a source and is currently processing */ - if( channel.isProcessing() ) - { - mTrafficChannelsInUse.put( channelNumber, channel ); - } - else - { - channel.setEnabled( false ); - - channel.setSystem( mSystem ); - channel.setSite( mSite ); - channel.setName( "Traffic" ); - } + mChannelModel.broadcast( new ChannelEvent( channel, + Event.REQUEST_ENABLE ) ); } } @@ -210,26 +199,29 @@ public class TrafficChannelManager extends Module if( channel != null ) { - if( channel.isProcessing() ) + if( channel.getEnabled() ) { - callEvent.setDetails( "TRAFFIC CHANNEL ALLOCATED" ); - - ChannelState state = channel.getChannelState(); - - /* Register as a listener to be notified when the call - * is completed */ - state.configureAsTrafficChannel( - new TrafficChannelStatusListener( this, - callEvent.getChannel() ), event ); - - /* Set state to call so that audio squelch state is correct */ - state.setState( State.CALL ); + ProcessingChain chain = mChannelProcessingManager + .getProcessingChain( channel ); + + if( chain != null ) + { + ChannelState state = chain.getChannelState(); + + /* Register as a listener to be notified when the call + * is completed */ + state.configureAsTrafficChannel( + new TrafficChannelStatusListener( this, + callEvent.getChannel() ), event ); + + /* Set state to call so that audio squelch state is correct */ + state.setState( State.CALL ); + } } else { callEvent.setCallEventType( CallEventType.CALL_DETECT ); - callEvent.setDetails( "CHANNEL PROCESSING ERROR" ); - } + } } else { @@ -390,15 +382,13 @@ public class TrafficChannelManager extends Module { synchronized( mTrafficChannelsInUse ) { - if( channelNumber != null && - mTrafficChannelsInUse.containsKey( channelNumber ) ) + if( channelNumber != null && mTrafficChannelsInUse.containsKey( channelNumber ) ) { Channel channel = mTrafficChannelsInUse.get( channelNumber ); - + + mChannelModel.broadcast( new ChannelEvent( channel, Event.REQUEST_DISABLE ) ); + mTrafficChannelsInUse.remove( channelNumber ); - - /* Disable the channel and broadcast a notification */ - channel.setEnabled( false ); } } } diff --git a/src/module/decode/tait/Tait1200Decoder.java b/src/module/decode/tait/Tait1200Decoder.java index 83ccf2aa..8f21e80d 100644 --- a/src/module/decode/tait/Tait1200Decoder.java +++ b/src/module/decode/tait/Tait1200Decoder.java @@ -31,7 +31,7 @@ import module.decode.Decoder; import module.decode.DecoderType; import sample.Broadcaster; import sample.Listener; -import sample.real.IRealBufferListener; +import sample.real.IFilteredRealBufferListener; import sample.real.RealBuffer; import alias.AliasList; import bits.MessageFramer; @@ -45,7 +45,7 @@ import dsp.fsk.FSK2Decoder.Output; /** * TAIT 1200 - 1200 baud 2FSK decoder */ -public class Tait1200Decoder extends Decoder implements IRealBufferListener, +public class Tait1200Decoder extends Decoder implements IFilteredRealBufferListener, Instrumentable { /* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */ @@ -179,7 +179,7 @@ public class Tait1200Decoder extends Decoder implements IRealBufferListener, } @Override - public Listener getRealBufferListener() + public Listener getFilteredRealBufferListener() { return mDecimationFilter; } diff --git a/src/module/demodulate/am/AMDemodulatorModule.java b/src/module/demodulate/am/AMDemodulatorModule.java index 716c6677..8e311102 100644 --- a/src/module/demodulate/am/AMDemodulatorModule.java +++ b/src/module/demodulate/am/AMDemodulatorModule.java @@ -21,7 +21,7 @@ import module.Module; import sample.Listener; import sample.complex.ComplexBuffer; import sample.complex.IComplexBufferListener; -import sample.real.IRealBufferProvider; +import sample.real.IFilteredRealBufferProvider; import sample.real.RealBuffer; import dsp.am.AMDemodulator_CB; import dsp.filter.FilterFactory; @@ -31,7 +31,7 @@ import dsp.filter.fir.real.RealFIRFilter_RB_RB; import dsp.gain.AutomaticGainControl_RB; public class AMDemodulatorModule extends Module - implements IComplexBufferListener, IRealBufferProvider + implements IComplexBufferListener, IFilteredRealBufferProvider { private static final int SAMPLE_RATE = 48000; @@ -99,13 +99,13 @@ public class AMDemodulatorModule extends Module } @Override - public void setRealBufferListener( Listener listener ) + public void setFilteredRealBufferListener( Listener listener ) { mAGC.setListener( listener ); } @Override - public void removeRealBufferListener() + public void removeFilteredRealBufferListener() { mAGC.removeListener(); } diff --git a/src/module/demodulate/audio/DemodulatedAudioFilterModule.java b/src/module/demodulate/audio/DemodulatedAudioFilterModule.java new file mode 100644 index 00000000..c6535268 --- /dev/null +++ b/src/module/demodulate/audio/DemodulatedAudioFilterModule.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2014-2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ +package module.demodulate.audio; + +import module.Module; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sample.Listener; +import sample.real.IFilteredRealBufferProvider; +import sample.real.IUnFilteredRealBufferListener; +import sample.real.RealBuffer; +import dsp.filter.FilterFactory; +import dsp.filter.Window.WindowType; +import dsp.filter.dc.DCRemovalFilter_RealBuffer; +import dsp.filter.fir.real.RealFIRFilter_RB_RB; + +public class DemodulatedAudioFilterModule extends Module + implements IUnFilteredRealBufferListener, IFilteredRealBufferProvider +{ + private final static Logger mLog = LoggerFactory.getLogger( DemodulatedAudioFilterModule.class ); + + private DCRemovalFilter_RealBuffer mDCFilter = new DCRemovalFilter_RealBuffer(); + private RealFIRFilter_RB_RB mBandPassFilter; + /** + * Filters demodulated audio and removes any DC component from the buffer + * + * @param pass - audio pass band pass frequency + * @param stop - audio pass band stop frequency + */ + public DemodulatedAudioFilterModule( int pass, int stop ) + { + assert( stop > pass ); + +//TODO: change this to a band pass filter + mBandPassFilter = new RealFIRFilter_RB_RB( + FilterFactory.getLowPass( 48000, pass, stop, 60, + WindowType.HANNING, true ), 1.0f ); + + mDCFilter.setListener( mBandPassFilter ); + } + + @Override + public void dispose() + { + mBandPassFilter.dispose(); + mBandPassFilter = null; + + mDCFilter.dispose(); + mDCFilter = null; + } + + @Override + public void reset() + { + } + + @Override + public void setFilteredRealBufferListener( Listener listener ) + { + mBandPassFilter.setListener( listener ); + } + + @Override + public void removeFilteredRealBufferListener() + { + mBandPassFilter.removeListener(); + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } + + @Override + public Listener getUnFilteredRealBufferListener() + { + return mDCFilter; + } +} diff --git a/src/module/demodulate/fm/FMDemodulatorModule.java b/src/module/demodulate/fm/FMDemodulatorModule.java index 2d2afe16..da39a197 100644 --- a/src/module/demodulate/fm/FMDemodulatorModule.java +++ b/src/module/demodulate/fm/FMDemodulatorModule.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014,2015 Dennis Sheirer + * Copyright (C) 2014-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,43 +22,27 @@ import module.Module; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sample.Broadcaster; import sample.Listener; import sample.complex.ComplexBuffer; import sample.complex.IComplexBufferListener; -import sample.real.IRealBufferProvider; +import sample.real.IUnFilteredRealBufferProvider; import sample.real.RealBuffer; -import source.tuner.frequency.AutomaticFrequencyControl_RB; -import source.tuner.frequency.FrequencyCorrectionControl; -import source.tuner.frequency.IFrequencyCorrectionController; import dsp.filter.FilterFactory; import dsp.filter.Window.WindowType; -import dsp.filter.dc.DCRemovalFilter_RB; import dsp.filter.fir.complex.ComplexFIRFilter_CB_CB; import dsp.fm.FMDemodulator_CB; -public class FMDemodulatorModule extends Module implements IComplexBufferListener, - IFrequencyCorrectionController, IRealBufferProvider +public class FMDemodulatorModule extends Module + implements IComplexBufferListener, IUnFilteredRealBufferProvider { - private final static Logger mLog = LoggerFactory.getLogger( FMDemodulatorModule.class ); - - /* Determines responsiveness of DC filter to frequency changes */ - private static final float DC_REMOVAL_RATIO = 0.000003f; - private static final int SAMPLE_RATE = 48000; private ComplexFIRFilter_CB_CB mIQFilter; private FMDemodulator_CB mDemodulator; - private Broadcaster mUnfilteredBufferBroadcaster = new Broadcaster(); - private DCRemovalFilter_RB mDCRemovalFilter; - private AutomaticFrequencyControl_RB mAutomaticFrequencyControl; - private boolean mRemoveDC = true; - - private Listener mListener; /** - * FM Demodulator with integrated DC removal and automatic frequency - * correction control. + * FM Demodulator with I/Q filter. Demodulated output is unfiltered and + * may contain a DC component. * * @param pass - pass frequency for IQ filtering prior to demodulation. This * frequency should be half of the signal bandwidth since the filter will @@ -66,56 +50,16 @@ public class FMDemodulatorModule extends Module implements IComplexBufferListene * combined pass bandwidth will be twice this value. * * @param stop - stop frequency for IQ filtering prior to demodulation. - * - * @param maxFrequencyCorrection - defines the maximum frequency +/- that - * the correction controller can adjust from the center tuned frequency. - * Set this value to 0 for no frequency correction control. */ - public FMDemodulatorModule( int pass, int stop, int maxFrequencyCorrection ) - { - this( pass, stop, maxFrequencyCorrection, true ); - } - - public FMDemodulatorModule( int pass, int stop, int maxFrequencyCorrection, boolean removeDC ) + public FMDemodulatorModule( int pass, int stop ) { assert( stop > pass ); - mRemoveDC = removeDC; - mIQFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass( SAMPLE_RATE, pass, stop, 60, WindowType.HAMMING, true ), 1.0f ); mDemodulator = new FMDemodulator_CB( 1.0f ); mIQFilter.setListener( mDemodulator ); - - mUnfilteredBufferBroadcaster = new Broadcaster(); - mDemodulator.setListener( mUnfilteredBufferBroadcaster ); - - if( maxFrequencyCorrection > 0 ) - { - /* Frequency correction requires unfiltered samples in order to detect - * the DC component present in a mistuned FM demodulated sample stream */ - mAutomaticFrequencyControl = new AutomaticFrequencyControl_RB( maxFrequencyCorrection ); - mUnfilteredBufferBroadcaster.addListener( mAutomaticFrequencyControl ); - } - - if( mRemoveDC ) - { - mDCRemovalFilter = new DCRemovalFilter_RB( DC_REMOVAL_RATIO ); - mUnfilteredBufferBroadcaster.addListener( mDCRemovalFilter ); - } - } - - @Override - public boolean hasFrequencyCorrectionControl() - { - return mAutomaticFrequencyControl != null; - } - - @Override - public FrequencyCorrectionControl getFrequencyCorrectionControl() - { - return mAutomaticFrequencyControl; } @Override @@ -132,68 +76,24 @@ public class FMDemodulatorModule extends Module implements IComplexBufferListene mDemodulator.dispose(); mDemodulator = null; - - if( mDCRemovalFilter != null ) - { - mDCRemovalFilter.dispose(); - } - mDCRemovalFilter = null; - - if( mAutomaticFrequencyControl != null ) - { - mAutomaticFrequencyControl.dispose(); - mAutomaticFrequencyControl = null; - } - - if( mUnfilteredBufferBroadcaster != null ) - { - mUnfilteredBufferBroadcaster.dispose(); - mUnfilteredBufferBroadcaster = null; - } } @Override public void reset() { mDemodulator.reset(); - - if( mDCRemovalFilter != null ) - { - mDCRemovalFilter.reset(); - } - - if( mAutomaticFrequencyControl != null ) - { - mAutomaticFrequencyControl.reset(); - } } @Override - public void setRealBufferListener( Listener listener ) + public void setUnFilteredRealBufferListener( Listener listener ) { - mListener = listener; - - if( mRemoveDC ) - { - mDCRemovalFilter.setListener( listener ); - } - else - { - mUnfilteredBufferBroadcaster.addListener( listener ); - } + mDemodulator.setListener( listener ); } @Override - public void removeRealBufferListener() + public void removeUnFilteredRealBufferListener() { - if( mRemoveDC ) - { - mDCRemovalFilter.removeListener(); - } - else - { - mUnfilteredBufferBroadcaster.removeListener( mListener ); - } + mDemodulator.removeListener(); } @Override diff --git a/src/module/log/EventLogComponentEditor.java b/src/module/log/EventLogComponentEditor.java index e8fefbee..2a9e081a 100644 --- a/src/module/log/EventLogComponentEditor.java +++ b/src/module/log/EventLogComponentEditor.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * Copyright (C) 2014-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,7 +24,8 @@ import javax.swing.SwingUtilities; import module.log.config.EventLogConfiguration; import controller.channel.AbstractChannelEditor; -import controller.channel.ChannelNode; +import controller.channel.Channel; +import controller.channel.ConfigurationValidationException; public class EventLogComponentEditor extends AbstractChannelEditor { @@ -34,9 +35,9 @@ public class EventLogComponentEditor extends AbstractChannelEditor private JCheckBox mDecodedLogger = new JCheckBox( "Decoded Messages" ); private JCheckBox mCallEventLogger = new JCheckBox( "Call Events" ); - public EventLogComponentEditor( ChannelNode channelNode ) + public EventLogComponentEditor( Channel channel ) { - super( channelNode ); + super( channel ); add( mBinaryLogger, "span" ); add( mDecodedLogger, "span" ); @@ -53,7 +54,7 @@ public class EventLogComponentEditor extends AbstractChannelEditor { SwingUtilities.invokeLater(new Runnable() { - final List mLoggers = mChannelNode.getChannel() + final List mLoggers = mChannel .getEventLogConfiguration().getLoggers(); @Override @@ -75,8 +76,7 @@ public class EventLogComponentEditor extends AbstractChannelEditor @Override public void save() { - EventLogConfiguration config = mChannelNode.getChannel() - .getEventLogConfiguration(); + EventLogConfiguration config = mChannel.getEventLogConfiguration(); config.clear(); @@ -94,4 +94,18 @@ public class EventLogComponentEditor extends AbstractChannelEditor config.addLogger( EventLogType.CALL_EVENT ); } } + + @Override + public void setConfiguration( Channel channel ) + { + // TODO Auto-generated method stub + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + // TODO Auto-generated method stub + + } } diff --git a/src/playlist/PlaylistEditor.java b/src/playlist/PlaylistEditor.java new file mode 100644 index 00000000..5d5d92a2 --- /dev/null +++ b/src/playlist/PlaylistEditor.java @@ -0,0 +1,88 @@ +package playlist; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import net.coderazzi.filters.gui.AutoChoices; +import net.coderazzi.filters.gui.TableFilterHeader; +import net.miginfocom.swing.MigLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import source.SourceManager; + +import com.jidesoft.swing.JideSplitPane; + +import controller.channel.Channel; +import controller.channel.ChannelEditor; +import controller.channel.ChannelModel; + +public class PlaylistEditor extends JPanel implements ListSelectionListener +{ + private static final long serialVersionUID = 1L; + private final static Logger mLog = LoggerFactory.getLogger( PlaylistEditor.class ); + + private ChannelModel mChannelModel; + private JTable mChannelTable; + private ChannelEditor mEditor; + + public PlaylistEditor( ChannelModel channelModel, + PlaylistManager playlistManager, + SourceManager sourceManager ) + { + mChannelModel = channelModel; + + mEditor = new ChannelEditor( channelModel, playlistManager, sourceManager ); + + init(); + } + + private void init() + { + setLayout( new MigLayout( "insets 0 0 0 0", + "[grow,fill]", + "[grow,fill]") ); + + //System Configuration View and Editor + mChannelTable = new JTable( mChannelModel ); + mChannelTable.setSelectionMode( ListSelectionModel.SINGLE_SELECTION ); + mChannelTable.getSelectionModel().addListSelectionListener( this ); + mChannelTable.setAutoCreateRowSorter( true ); + + TableFilterHeader channelHeaderFilter = new TableFilterHeader( mChannelTable ); + channelHeaderFilter.setAutoChoices( AutoChoices.ENABLED ); + + JScrollPane channelScroller = new JScrollPane( mChannelTable ); + + JideSplitPane splitPane = new JideSplitPane( JideSplitPane.HORIZONTAL_SPLIT ); + splitPane.setDividerSize( 5 ); + splitPane.add( channelScroller ); + splitPane.add( mEditor ); + + add( splitPane ); + } + + @Override + public void valueChanged( ListSelectionEvent event ) + { + //This limits event firing to only when selection is complete + if( !event.getValueIsAdjusting() ) + { + int index = mChannelTable.convertRowIndexToModel( mChannelTable.getSelectedRow() ); + + mLog.debug( "Index:" + index ); + + Channel channel = mChannelModel.getChannels().get( index ); + + if( channel != null ) + { + mEditor.setChannel( channel ); + } + } + } +} diff --git a/src/playlist/PlaylistManager.java b/src/playlist/PlaylistManager.java index f23f7075..9d43c496 100644 --- a/src/playlist/PlaylistManager.java +++ b/src/playlist/PlaylistManager.java @@ -52,7 +52,6 @@ public class PlaylistManager public PlaylistManager( ChannelModel channelModel ) { mChannelModel = channelModel; - init(); } public Playlist getPlayist() @@ -73,7 +72,7 @@ public class PlaylistManager * Loads playlist from the current playlist file, or the default playlist file, * as specified in the current SDRTRunk system settings */ - private void init() + public void init() { SystemProperties props = SystemProperties.getInstance(); @@ -163,6 +162,75 @@ public class PlaylistManager } } + private void createBackupPlaylist() + { + SystemProperties props = SystemProperties.getInstance(); + + Path playlistPath = props.getApplicationFolder( "playlist" ); + + String playlistDefault = props.get( "playlist.defaultfilename", + "playlist.xml" ); + + String playlistCurrent = props.get( "playlist.currentfilename", + playlistDefault ); + + Path current = playlistPath.resolve( playlistCurrent ); + + if( Files.exists( current ) ) + { + int revision = 1; + + String playlistBackup = playlistCurrent.replace( ".xml", "_backup_" + revision + ".xml" ); + + Path filePath = playlistPath.resolve( playlistBackup ); + + while( Files.exists( filePath ) ) + { + revision++; + + playlistBackup = playlistCurrent.replace( ".xml", "_backup_" + revision + ".xml" ); + + filePath = playlistPath.resolve( playlistBackup ); + + if( revision > 10 ) + { + mLog.error( "Couldn't create playlist backup - maximum revisions exceeded" ); + return; + } + } + + File outputFile = new File( filePath.toString() ); + + try + { + if( !outputFile.exists() ) + { + outputFile.createNewFile(); + } + } + catch( Exception e ) + { + mLog.error( "PlaylistManager - couldn't create file to save " + + "playlist backup [" + filePath.toString() + "]" ); + } + + try( OutputStream out = new FileOutputStream( outputFile ) ) + { + Files.copy( current, out ); + + mLog.info( "Playlist backed up to:" + filePath ); + } + catch( IOException ioe ) + { + mLog.error( "Error copying playlist to backup file [" + filePath + "]" ); + } + } + else + { + mLog.error( "Couldn't create backup playlist - original playlist does not exist" ); + } + } + /** * Erases current playlist and loads playlist from the playlistPath filename, * if it exists. @@ -238,6 +306,8 @@ public class PlaylistManager { mLog.info( "Legacy playlist format detected - converting ..." ); + createBackupPlaylist(); + //Playlist version 1 to version 2 format conversion. In order to keep //backwards compatibility, transfer all of the System-Site-Channel //objects over to the new channel list format diff --git a/src/playlist/PlaylistNode.java b/src/playlist/PlaylistNode.java index 93fdc2a8..5d8e5302 100644 --- a/src/playlist/PlaylistNode.java +++ b/src/playlist/PlaylistNode.java @@ -19,10 +19,14 @@ package playlist; import javax.swing.JPanel; +import settings.SettingsManager; +import source.SourceManager; import alias.AliasDirectory; import alias.AliasDirectoryNode; import controller.BaseNode; import controller.EmptyEditor; +import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; import controller.channel.map.ChannelMapList; import controller.channel.map.ChannelMapListNode; import controller.system.SystemList; @@ -35,11 +39,27 @@ public class PlaylistNode extends BaseNode private ChannelMapListNode mChannelMapListNode; private SystemListNode mSystemListNode; - public PlaylistNode() - { + private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; + private PlaylistManager mPlaylistManager; + private SettingsManager mSettingsManager; + private SourceManager mSourceManager; + + public PlaylistNode( ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, + SettingsManager settingsManager, + SourceManager sourceManager ) + { super( null ); - } - + + mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; + mPlaylistManager = playlistManager; + mSettingsManager = settingsManager; + mSourceManager = sourceManager; + } + private void init() { /** @@ -58,7 +78,8 @@ public class PlaylistNode extends BaseNode getPlaylist().setAliasDirectory( directory ); } - mAliasDirectoryNode = new AliasDirectoryNode( directory ); + mAliasDirectoryNode = new AliasDirectoryNode( mPlaylistManager, + mSettingsManager, directory ); getModel().insertNodeInto( mAliasDirectoryNode, this, 0 ); @@ -74,7 +95,7 @@ public class PlaylistNode extends BaseNode channelMapList = new ChannelMapList(); } - mChannelMapListNode = new ChannelMapListNode( channelMapList ); + mChannelMapListNode = new ChannelMapListNode( mPlaylistManager, channelMapList ); getModel().insertNodeInto( mChannelMapListNode, this, 1 ); @@ -91,7 +112,8 @@ public class PlaylistNode extends BaseNode getPlaylist().setSystemList( list ); } - mSystemListNode = new SystemListNode( list ); + mSystemListNode = new SystemListNode( list, mChannelModel, + mChannelProcessingManager, mPlaylistManager, mSourceManager ); getModel().insertNodeInto( mSystemListNode, this, 2 ); @@ -112,8 +134,7 @@ public class PlaylistNode extends BaseNode public void loadPlaylist() { - Playlist playlist = - getModel().getResourceManager().getPlaylistManager().getPlayist(); + Playlist playlist = mPlaylistManager.getPlayist(); setUserObject( playlist ); diff --git a/src/record/RecordComponentEditor.java b/src/record/RecordComponentEditor.java index 435a6ccb..7c23a360 100644 --- a/src/record/RecordComponentEditor.java +++ b/src/record/RecordComponentEditor.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * Copyright (C) 2014-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,7 +22,8 @@ import javax.swing.JCheckBox; import record.config.RecordConfiguration; import controller.channel.AbstractChannelEditor; -import controller.channel.ChannelNode; +import controller.channel.Channel; +import controller.channel.ConfigurationValidationException; public class RecordComponentEditor extends AbstractChannelEditor { @@ -32,9 +33,9 @@ public class RecordComponentEditor extends AbstractChannelEditor private JCheckBox mBasebandRecorder = new JCheckBox( "Baseband I/Q" ); private JCheckBox mTrafficBasebandRecorder = new JCheckBox( "Traffic Channel Baseband I/Q" ); - public RecordComponentEditor( ChannelNode channelNode ) + public RecordComponentEditor( Channel channel ) { - super( channelNode ); + super( channel ); add( mAudioRecorder, "span" ); add( mBasebandRecorder, "span" ); @@ -46,8 +47,7 @@ public class RecordComponentEditor extends AbstractChannelEditor @Override public void save() { - RecordConfiguration config = - getChannelNode().getChannel().getRecordConfiguration(); + RecordConfiguration config = getChannel().getRecordConfiguration(); config.clearRecorders(); @@ -76,8 +76,7 @@ public class RecordComponentEditor extends AbstractChannelEditor @Override public void run() { - RecordConfiguration config = - getChannelNode().getChannel().getRecordConfiguration(); + RecordConfiguration config = getChannel().getRecordConfiguration(); mAudioRecorder.setSelected( config.getRecorders() .contains( RecorderType.AUDIO ) ); @@ -90,4 +89,18 @@ public class RecordComponentEditor extends AbstractChannelEditor } }); } + + @Override + public void setConfiguration( Channel channel ) + { + // TODO Auto-generated method stub + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + // TODO Auto-generated method stub + + } } diff --git a/src/record/RecordEditor.java b/src/record/RecordEditor.java index 6d25832e..9913bf9e 100644 --- a/src/record/RecordEditor.java +++ b/src/record/RecordEditor.java @@ -18,9 +18,11 @@ package record; import record.config.RecordConfiguration; -import controller.Editor; +import controller.channel.Channel; +import controller.channel.ChannelConfigurationEditor; +import controller.channel.ConfigurationValidationException; -public class RecordEditor extends Editor +public class RecordEditor extends ChannelConfigurationEditor { private static final long serialVersionUID = 1L; @@ -42,7 +44,16 @@ public class RecordEditor extends Editor } @Override - public void reset() - { - } + public void setConfiguration( Channel channel ) + { + // TODO Auto-generated method stub + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + // TODO Auto-generated method stub + + } } diff --git a/src/record/RecorderManager.java b/src/record/RecorderManager.java index 949ac241..b4bf4aad 100644 --- a/src/record/RecorderManager.java +++ b/src/record/RecorderManager.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014,2015 Dennis Sheirer + * Copyright (C) 2014-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,7 +20,6 @@ package record; import java.io.File; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +32,6 @@ import audio.AudioPacket; import audio.metadata.Metadata; import audio.metadata.MetadataType; import controller.ThreadPoolManager; -import controller.ThreadPoolManager.ThreadType; public class RecorderManager implements Listener { @@ -170,8 +168,7 @@ public class RecorderManager implements Listener /** * Constructs a baseband recorder for use in a processing chain. */ - public static ComplexBufferWaveRecorder getBasebandRecorder( - ThreadPoolManager threadPoolManager, String channelName ) + public ComplexBufferWaveRecorder getBasebandRecorder( String channelName ) { StringBuilder sb = new StringBuilder(); sb.append( SystemProperties.getInstance() @@ -180,7 +177,7 @@ public class RecorderManager implements Listener sb.append( channelName ); sb.append( "_baseband" ); - return new ComplexBufferWaveRecorder( threadPoolManager, + return new ComplexBufferWaveRecorder( mThreadPoolManager, AUDIO_SAMPLE_RATE, sb.toString() ); } } diff --git a/src/record/wave/RealBufferWaveRecorder.java b/src/record/wave/RealBufferWaveRecorder.java index 49b0f3b6..0ad0d4f7 100644 --- a/src/record/wave/RealBufferWaveRecorder.java +++ b/src/record/wave/RealBufferWaveRecorder.java @@ -34,7 +34,7 @@ import org.slf4j.LoggerFactory; import sample.ConversionUtils; import sample.Listener; -import sample.real.IRealBufferListener; +import sample.real.IFilteredRealBufferListener; import sample.real.RealBuffer; import util.TimeStamp; import controller.ThreadPoolManager; @@ -44,7 +44,7 @@ import controller.ThreadPoolManager.ThreadType; * WAVE audio recorder module for recording real sample buffers to a wave file */ public class RealBufferWaveRecorder extends Module - implements IRealBufferListener, Listener + implements IFilteredRealBufferListener, Listener { private final static Logger mLog = LoggerFactory.getLogger( RealBufferWaveRecorder.class ); @@ -151,7 +151,7 @@ public class RealBufferWaveRecorder extends Module } @Override - public Listener getRealBufferListener() + public Listener getFilteredRealBufferListener() { return this; } diff --git a/src/sample/real/IFilteredRealBufferListener.java b/src/sample/real/IFilteredRealBufferListener.java new file mode 100644 index 00000000..06928fda --- /dev/null +++ b/src/sample/real/IFilteredRealBufferListener.java @@ -0,0 +1,8 @@ +package sample.real; + +import sample.Listener; + +public interface IFilteredRealBufferListener +{ + public Listener getFilteredRealBufferListener(); +} diff --git a/src/sample/real/IFilteredRealBufferProvider.java b/src/sample/real/IFilteredRealBufferProvider.java new file mode 100644 index 00000000..e600d368 --- /dev/null +++ b/src/sample/real/IFilteredRealBufferProvider.java @@ -0,0 +1,9 @@ +package sample.real; + +import sample.Listener; + +public interface IFilteredRealBufferProvider +{ + public void setFilteredRealBufferListener( Listener listener ); + public void removeFilteredRealBufferListener(); +} diff --git a/src/sample/real/IRealBufferListener.java b/src/sample/real/IRealBufferListener.java deleted file mode 100644 index 6ef33052..00000000 --- a/src/sample/real/IRealBufferListener.java +++ /dev/null @@ -1,8 +0,0 @@ -package sample.real; - -import sample.Listener; - -public interface IRealBufferListener -{ - public Listener getRealBufferListener(); -} diff --git a/src/sample/real/IRealBufferProvider.java b/src/sample/real/IRealBufferProvider.java deleted file mode 100644 index 0b11debd..00000000 --- a/src/sample/real/IRealBufferProvider.java +++ /dev/null @@ -1,9 +0,0 @@ -package sample.real; - -import sample.Listener; - -public interface IRealBufferProvider -{ - public void setRealBufferListener( Listener listener ); - public void removeRealBufferListener(); -} diff --git a/src/sample/real/IUnFilteredRealBufferListener.java b/src/sample/real/IUnFilteredRealBufferListener.java new file mode 100644 index 00000000..bce0f168 --- /dev/null +++ b/src/sample/real/IUnFilteredRealBufferListener.java @@ -0,0 +1,8 @@ +package sample.real; + +import sample.Listener; + +public interface IUnFilteredRealBufferListener +{ + public Listener getUnFilteredRealBufferListener(); +} diff --git a/src/sample/real/IUnFilteredRealBufferProvider.java b/src/sample/real/IUnFilteredRealBufferProvider.java new file mode 100644 index 00000000..764a5aeb --- /dev/null +++ b/src/sample/real/IUnFilteredRealBufferProvider.java @@ -0,0 +1,9 @@ +package sample.real; + +import sample.Listener; + +public interface IUnFilteredRealBufferProvider +{ + public void setUnFilteredRealBufferListener( Listener listener ); + public void removeUnFilteredRealBufferListener(); +} diff --git a/src/settings/SettingsManager.java b/src/settings/SettingsManager.java index ddcc4fa9..df430aa6 100644 --- a/src/settings/SettingsManager.java +++ b/src/settings/SettingsManager.java @@ -386,6 +386,12 @@ public class SettingsManager } MapIcon mapIcon = mIcons.get( name ); + + if( mapIcon == null ) + { + mapIcon = new MapIcon( name, name ); + mIcons.put( name, mapIcon ); + } if( mapIcon != null ) { @@ -399,7 +405,7 @@ public class SettingsManager } else { - mResizedIcons.put( mergedName, mapIcon.getImageIcon() ); + return mapIcon.getImageIcon(); } } diff --git a/src/source/ComplexSource.java b/src/source/ComplexSource.java index eeb4084e..e7e1939c 100644 --- a/src/source/ComplexSource.java +++ b/src/source/ComplexSource.java @@ -6,8 +6,8 @@ import sample.complex.ComplexBuffer; public abstract class ComplexSource extends Source implements Provider { - public ComplexSource( String name ) + public ComplexSource() { - super( name, SampleType.COMPLEX ); + super( SampleType.COMPLEX ); } } diff --git a/src/source/EmptySourceEditor.java b/src/source/EmptySourceEditor.java index ad1c1669..04af856d 100644 --- a/src/source/EmptySourceEditor.java +++ b/src/source/EmptySourceEditor.java @@ -19,14 +19,15 @@ package source; import javax.swing.JLabel; +import controller.channel.Channel; +import controller.channel.ConfigurationValidationException; import source.config.SourceConfiguration; -import controller.ResourceManager; public class EmptySourceEditor extends SourceEditor { private static final long serialVersionUID = 1L; - public EmptySourceEditor( ResourceManager resourceManager, + public EmptySourceEditor( SourceManager resourceManager, SourceConfiguration config ) { super( resourceManager, config ); @@ -43,4 +44,18 @@ public class EmptySourceEditor extends SourceEditor JLabel selectLabel = new JLabel( "Please select a source" ); add( selectLabel ); } + + @Override + public void setConfiguration( Channel channel ) + { + // TODO Auto-generated method stub + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + // TODO Auto-generated method stub + + } } diff --git a/src/source/FloatSource.java b/src/source/FloatSource.java deleted file mode 100644 index c8161a9c..00000000 --- a/src/source/FloatSource.java +++ /dev/null @@ -1,12 +0,0 @@ -package source; - -import sample.Provider; -import sample.SampleType; - -public abstract class FloatSource extends Source implements Provider -{ - public FloatSource( String name ) - { - super( name, SampleType.REAL ); - } -} diff --git a/src/source/RealSource.java b/src/source/RealSource.java index 6b09ccd5..b512bfbc 100644 --- a/src/source/RealSource.java +++ b/src/source/RealSource.java @@ -7,8 +7,8 @@ import sample.real.RealBuffer; public abstract class RealSource extends Source implements Provider { - public RealSource( String name ) + public RealSource() { - super( name, SampleType.REAL ); + super( SampleType.REAL ); } } diff --git a/src/source/Source.java b/src/source/Source.java index 064cc6ce..217def00 100644 --- a/src/source/Source.java +++ b/src/source/Source.java @@ -17,27 +17,21 @@ ******************************************************************************/ package source; +import module.Module; import sample.SampleType; /** * Abstract class to define the minimum functionality of a sample data provider. */ -public abstract class Source +public abstract class Source extends Module { - protected String mName; protected SampleType mSampleType; - public Source( String name, SampleType sampleType ) + public Source( SampleType sampleType ) { - mName = name; mSampleType = sampleType; } - public String getName() - { - return mName; - } - public abstract int getSampleRate() throws SourceException; public abstract long getFrequency() throws SourceException; diff --git a/src/source/SourceComponentEditor.java b/src/source/SourceComponentEditor.java index c4d4e4a2..b4f3673a 100644 --- a/src/source/SourceComponentEditor.java +++ b/src/source/SourceComponentEditor.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * Copyright (C) 2014-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,7 +29,8 @@ import net.miginfocom.swing.MigLayout; import source.config.SourceConfigFactory; import source.config.SourceConfiguration; import controller.channel.AbstractChannelEditor; -import controller.channel.ChannelNode; +import controller.channel.Channel; +import controller.channel.ConfigurationValidationException; public class SourceComponentEditor extends AbstractChannelEditor { @@ -37,10 +38,13 @@ public class SourceComponentEditor extends AbstractChannelEditor private JComboBox mComboSources; private SourceEditor mEditor; + private SourceManager mSourceManager; - public SourceComponentEditor( ChannelNode channelNode ) + public SourceComponentEditor( SourceManager sourceManager, Channel channel ) { - super( channelNode ); + super( channel ); + + mSourceManager = sourceManager; setLayout( new MigLayout( "fill,wrap 2", "[right,grow][grow]", "[][][grow]" ) ); @@ -57,11 +61,9 @@ public class SourceComponentEditor extends AbstractChannelEditor { SourceConfiguration config; - if( selected == mChannelNode.getChannel() - .getSourceConfiguration().getSourceType() ) + if( selected == mChannel.getSourceConfiguration().getSourceType() ) { - config = mChannelNode.getChannel() - .getSourceConfiguration(); + config = mChannel.getSourceConfiguration(); } else { @@ -76,8 +78,7 @@ public class SourceComponentEditor extends AbstractChannelEditor } //Change to the new one - mEditor = SourceEditorFactory.getPanel( - mChannelNode.getModel().getResourceManager(), config ); + mEditor = mSourceManager.getPanel( config ); //Add it to the jpanel add( mEditor, "span 2" ); @@ -105,10 +106,10 @@ public class SourceComponentEditor extends AbstractChannelEditor { @Override - public void run() { + public void run() + { mComboSources.setSelectedItem( - mChannelNode.getChannel() - .getSourceConfiguration().getSourceType() ); + mChannel.getSourceConfiguration().getSourceType() ); mComboSources.requestFocus(); @@ -123,7 +124,20 @@ public class SourceComponentEditor extends AbstractChannelEditor mEditor.save(); //Let the calling save method send the config change event - mChannelNode.getChannel() - .setSourceConfiguration( mEditor.getConfig() ); + mChannel.setSourceConfiguration( mEditor.getConfig() ); } + + @Override + public void setConfiguration( Channel channel ) + { + // TODO Auto-generated method stub + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + // TODO Auto-generated method stub + + } } diff --git a/src/source/SourceEditor.java b/src/source/SourceEditor.java index dd5ad11e..1cb15a64 100644 --- a/src/source/SourceEditor.java +++ b/src/source/SourceEditor.java @@ -18,19 +18,18 @@ package source; import source.config.SourceConfiguration; -import controller.Editor; -import controller.ResourceManager; +import controller.channel.ChannelConfigurationEditor; -public abstract class SourceEditor extends Editor +public abstract class SourceEditor extends ChannelConfigurationEditor { private static final long serialVersionUID = 1L; - protected ResourceManager mResourceManager; + protected SourceManager mSourceManager; protected SourceConfiguration mConfig; - public SourceEditor( ResourceManager resourceManager, SourceConfiguration config ) + public SourceEditor( SourceManager sourceManager, SourceConfiguration config ) { - mResourceManager = resourceManager; + mSourceManager = sourceManager; mConfig = config; } @@ -38,9 +37,9 @@ public abstract class SourceEditor extends Editor { return mConfig; } - - public ResourceManager getResourceManager() + + protected SourceManager getSourceManager() { - return mResourceManager; + return mSourceManager; } } diff --git a/src/source/SourceEditorFactory.java b/src/source/SourceEditorFactory.java deleted file mode 100644 index b918f79f..00000000 --- a/src/source/SourceEditorFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package source; - -import source.config.SourceConfiguration; -import source.mixer.MixerEditor; -import source.recording.RecordingEditor; -import source.tuner.TunerEditor; -import controller.ResourceManager; - -public class SourceEditorFactory -{ - public static SourceEditor getPanel( ResourceManager resourceManager, - SourceConfiguration config ) - { - SourceEditor configuredPanel; - - switch( config.getSourceType() ) - { - case MIXER: - configuredPanel = new MixerEditor( resourceManager, config ); - break; - case TUNER: - configuredPanel = new TunerEditor( resourceManager, config ); - break; - case RECORDING: - configuredPanel = new RecordingEditor( resourceManager, config ); - break; - case NONE: - default: - configuredPanel = new EmptySourceEditor( resourceManager, config ); - break; - } - - return configuredPanel; - } -} diff --git a/src/source/SourceManager.java b/src/source/SourceManager.java index 8c783ab9..9fab09f4 100644 --- a/src/source/SourceManager.java +++ b/src/source/SourceManager.java @@ -17,27 +17,43 @@ ******************************************************************************/ package source; +import settings.SettingsManager; import source.config.SourceConfiguration; +import source.mixer.MixerEditor; import source.mixer.MixerManager; -import controller.ResourceManager; +import source.recording.RecordingEditor; +import source.recording.RecordingSourceManager; +import source.tuner.TunerEditor; +import source.tuner.TunerManager; +import controller.ThreadPoolManager; -public class SourceManager { - /** - * NOTE: each source will auto-start once a listener registers on it to - * receive samples and should auto-stop once a listener de-registers - * - * Each channel object will handle the starting and stopping of each channel - * processing chain. - * - * Source Manager will reach out to the MixerManager or the TunerManager - * to get source objects to pass off to the channel object, as requested. - */ - - private ResourceManager mResourceManager; +public class SourceManager +{ + private MixerManager mMixerManager; + private RecordingSourceManager mRecordingSourceManager; + private TunerManager mTunerManager; - public SourceManager( ResourceManager resourceManager ) + public SourceManager( SettingsManager settingsManager, + ThreadPoolManager threadPoolManager ) { - mResourceManager = resourceManager; + mMixerManager = new MixerManager(); + mRecordingSourceManager = new RecordingSourceManager( settingsManager, threadPoolManager ); + mTunerManager = new TunerManager( mMixerManager, settingsManager, threadPoolManager ); + } + + public MixerManager getMixerManager() + { + return mMixerManager; + } + + public RecordingSourceManager getRecordingSourceManager() + { + return mRecordingSourceManager; + } + + public TunerManager getTunerManager() + { + return mTunerManager; } public Source getSource( SourceConfiguration config, int bandwidth ) @@ -48,14 +64,13 @@ public class SourceManager { switch( config.getSourceType() ) { case MIXER: - retVal = MixerManager.getInstance().getSource( config ); + retVal = mMixerManager.getSource( config ); break; case TUNER: - retVal = mResourceManager.getTunerManager().getSource( config, bandwidth ); + retVal = mTunerManager.getSource( config, bandwidth ); break; case RECORDING: - retVal = mResourceManager.getRecordingSourceManager() - .getSource( config, bandwidth ); + retVal = mRecordingSourceManager.getSource( config, bandwidth ); case NONE: default: break; @@ -63,4 +78,28 @@ public class SourceManager { return retVal; } + + public SourceEditor getPanel( SourceConfiguration config ) + { + SourceEditor configuredPanel; + + switch( config.getSourceType() ) + { + case MIXER: + configuredPanel = new MixerEditor( this, config ); + break; + case TUNER: + configuredPanel = new TunerEditor( this, config ); + break; + case RECORDING: + configuredPanel = new RecordingEditor( this, config ); + break; + case NONE: + default: + configuredPanel = new EmptySourceEditor( this, config ); + break; + } + + return configuredPanel; + } } diff --git a/src/source/mixer/MixerEditor.java b/src/source/mixer/MixerEditor.java index 3782213b..075068b6 100644 --- a/src/source/mixer/MixerEditor.java +++ b/src/source/mixer/MixerEditor.java @@ -24,11 +24,13 @@ import javax.swing.DefaultComboBoxModel; import javax.swing.JComboBox; import javax.swing.JLabel; +import controller.channel.Channel; +import controller.channel.ConfigurationValidationException; import source.SourceEditor; +import source.SourceManager; import source.config.SourceConfigMixer; import source.config.SourceConfiguration; import source.mixer.MixerManager.InputMixerConfiguration; -import controller.ResourceManager; public class MixerEditor extends SourceEditor { @@ -38,10 +40,10 @@ public class MixerEditor extends SourceEditor protected Mixer.Info mSelectedMixer = null; - public MixerEditor( ResourceManager resourceManager, + public MixerEditor( SourceManager sourceManager, SourceConfiguration config ) { - super( resourceManager, config ); + super( sourceManager, config ); initGUI(); } @@ -105,7 +107,8 @@ public class MixerEditor extends SourceEditor @Override public void run() { - InputMixerConfiguration[] mixers = MixerManager.getInstance().getInputMixers(); + InputMixerConfiguration[] mixers = mSourceManager + .getMixerManager().getInputMixers(); mComboMixers.setModel( new DefaultComboBoxModel( mixers ) ); @@ -155,4 +158,18 @@ public class MixerEditor extends SourceEditor } }); } + + @Override + public void setConfiguration( Channel channel ) + { + // TODO Auto-generated method stub + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + // TODO Auto-generated method stub + + } } diff --git a/src/source/mixer/MixerManager.java b/src/source/mixer/MixerManager.java index 298e760d..d7af001a 100644 --- a/src/source/mixer/MixerManager.java +++ b/src/source/mixer/MixerManager.java @@ -49,27 +49,15 @@ public class MixerManager private final static Logger mLog = LoggerFactory.getLogger( MixerManager.class ); - private static MixerManager sInstance = null; - private List mInputMixers = new ArrayList<>(); private List mOutputMixers = new ArrayList<>(); private HashMap mMixerTuners = new HashMap<>(); - private MixerManager() + public MixerManager() { loadMixers(); } - public static MixerManager getInstance() - { - if( sInstance == null ) - { - sInstance = new MixerManager(); - } - - return sInstance; - } - public RealMixerSource getSource( SourceConfiguration config ) { RealMixerSource retVal = null; @@ -108,7 +96,6 @@ public class MixerManager { return new RealMixerSource( dataLine, AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, - mixerName, new ShortAdapter() ); } } @@ -137,7 +124,7 @@ public class MixerManager { return new RealMixerSource( dataLine, AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, - mixerName, new ChannelShortAdapter( + new ChannelShortAdapter( mixerConfig.getChannel() ) ); } } diff --git a/src/source/mixer/RealMixerSource.java b/src/source/mixer/RealMixerSource.java index 58b8d953..240e55bb 100644 --- a/src/source/mixer/RealMixerSource.java +++ b/src/source/mixer/RealMixerSource.java @@ -72,18 +72,30 @@ public class RealMixerSource extends RealSource * the left/right stereo channels are inverted. */ public RealMixerSource( TargetDataLine targetDataLine, - AudioFormat format, - String name, - ISampleAdapter sampleAdapter ) + AudioFormat format, + ISampleAdapter sampleAdapter ) { - super( name ); - mTargetDataLine = targetDataLine; mAudioFormat = format; mSampleAdapter = sampleAdapter; } - public void setListener( Listener listener ) + @Override + public void reset() + { + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } + + public void setListener( Listener listener ) { mSampleListeners.add( listener ); @@ -92,7 +104,7 @@ public class RealMixerSource extends RealSource { Thread thread = new Thread( mBufferReader ); thread.setDaemon( true ); - thread.setName( getName() + " Sample Reader" ); + thread.setName( "Sample Reader" ); thread.start(); } } diff --git a/src/source/recording/Recording.java b/src/source/recording/Recording.java index 90fad405..a3b1aba7 100644 --- a/src/source/recording/Recording.java +++ b/src/source/recording/Recording.java @@ -4,21 +4,20 @@ import java.io.File; import java.util.ArrayList; import java.util.concurrent.RejectedExecutionException; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerChannel; -import source.tuner.TunerChannelProvider; +import source.tuner.ITunerChannelProvider; import source.tuner.TunerChannelSource; import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeListener; import source.tuner.frequency.FrequencyChangeEvent.Event; -import controller.ResourceManager; +import source.tuner.frequency.IFrequencyChangeProcessor; import controller.ThreadPoolManager; public class Recording implements Comparable, - FrequencyChangeListener, - TunerChannelProvider + IFrequencyChangeProcessor, + ITunerChannelProvider { - private ResourceManager mResourceManager; private RecordingConfiguration mConfiguration; private ArrayList mTunerChannels = @@ -26,10 +25,12 @@ public class Recording implements Comparable, private long mCenterFrequency; - public Recording( ResourceManager resourceManager, + private SettingsManager mSettingsManager; + + public Recording( SettingsManager settingsManager, RecordingConfiguration configuration ) { - mResourceManager = resourceManager; + mSettingsManager = settingsManager; mConfiguration = configuration; mCenterFrequency = mConfiguration.getCenterFrequency(); } @@ -37,7 +38,7 @@ public class Recording implements Comparable, public void setAlias( String alias ) { mConfiguration.setAlias( alias ); - mResourceManager.getSettingsManager().save(); + mSettingsManager.save(); } public void setRecordingFile( File file ) throws SourceException @@ -50,7 +51,7 @@ public class Recording implements Comparable, } mConfiguration.setFilePath( file.getAbsolutePath() ); - mResourceManager.getSettingsManager().save(); + mSettingsManager.save(); } /** @@ -90,13 +91,13 @@ public class Recording implements Comparable, @Override public void frequencyChanged( FrequencyChangeEvent event ) { - if( event.getEvent() == Event.FREQUENCY_CHANGE_NOTIFICATION ) + if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE ) { long frequency = event.getValue().longValue(); mConfiguration.setCenterFrequency( frequency ); - mResourceManager.getSettingsManager().save(); + mSettingsManager.save(); mCenterFrequency = frequency; diff --git a/src/source/recording/RecordingEditor.java b/src/source/recording/RecordingEditor.java index aabec88f..a6be5cbe 100644 --- a/src/source/recording/RecordingEditor.java +++ b/src/source/recording/RecordingEditor.java @@ -26,11 +26,13 @@ import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.SwingUtilities; +import controller.channel.Channel; +import controller.channel.ConfigurationValidationException; import source.SourceEditor; +import source.SourceManager; import source.config.SourceConfigRecording; import source.config.SourceConfigTuner; import source.config.SourceConfiguration; -import controller.ResourceManager; public class RecordingEditor extends SourceEditor { @@ -38,10 +40,9 @@ public class RecordingEditor extends SourceEditor private JComboBox mComboRecordings; private JFrequencyControl mFrequencyControl; - public RecordingEditor( ResourceManager resourceManager, - SourceConfiguration config ) + public RecordingEditor( SourceManager sourceManager, SourceConfiguration config ) { - super( resourceManager, config ); + super( sourceManager, config ); initGUI(); } @@ -92,7 +93,7 @@ public class RecordingEditor extends SourceEditor @Override public void run() { - List recordings = getResourceManager() + List recordings = getSourceManager() .getRecordingSourceManager().getRecordings(); mComboRecordings.setModel( @@ -105,7 +106,7 @@ public class RecordingEditor extends SourceEditor if( recordingAlias != null ) { - Recording selected = mResourceManager + Recording selected = mSourceManager .getRecordingSourceManager() .getRecordingFromAlias( recordingAlias ); @@ -117,4 +118,18 @@ public class RecordingEditor extends SourceEditor } }); } + + @Override + public void setConfiguration( Channel channel ) + { + // TODO Auto-generated method stub + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + // TODO Auto-generated method stub + + } } diff --git a/src/source/recording/RecordingGroupNode.java b/src/source/recording/RecordingGroupNode.java index 8eb2029a..3e9d4631 100644 --- a/src/source/recording/RecordingGroupNode.java +++ b/src/source/recording/RecordingGroupNode.java @@ -24,17 +24,28 @@ import java.util.List; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; +import settings.SettingsManager; +import source.SourceManager; import controller.BaseNode; public class RecordingGroupNode extends BaseNode { private static final long serialVersionUID = 1L; - - public RecordingGroupNode() + + private SettingsManager mSettingsManager; + + public RecordingGroupNode( SourceManager sourceManager, SettingsManager settingsManager ) { - super( null ); + super( sourceManager ); + + mSettingsManager = settingsManager; } + private SourceManager getSourceManager() + { + return (SourceManager)getUserObject(); + } + public String toString() { return "Recordings [" + this.getChildCount() + "]"; @@ -53,13 +64,12 @@ public class RecordingGroupNode extends BaseNode { RecordingConfiguration config = new RecordingConfiguration(); - getModel().getResourceManager().getSettingsManager() - .addRecordingConfiguration( config ); + mSettingsManager.addRecordingConfiguration( config ); - Recording recording = getModel().getResourceManager() + Recording recording = getSourceManager() .getRecordingSourceManager().addRecording( config ); - RecordingNode node = new RecordingNode( recording ); + RecordingNode node = new RecordingNode( getSourceManager(), recording ); getModel().addNode( node, RecordingGroupNode.this, 0 ); @@ -76,12 +86,12 @@ public class RecordingGroupNode extends BaseNode public void loadRecordings() { - List recordings = getModel().getResourceManager() + List recordings = getSourceManager() .getRecordingSourceManager().getRecordings(); for( Recording recording: recordings ) { - RecordingNode node = new RecordingNode( recording ); + RecordingNode node = new RecordingNode( getSourceManager(), recording ); getModel().addNode( node, this, 0 ); } diff --git a/src/source/recording/RecordingNode.java b/src/source/recording/RecordingNode.java index 54ec12fd..920602e3 100644 --- a/src/source/recording/RecordingNode.java +++ b/src/source/recording/RecordingNode.java @@ -8,6 +8,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import source.SourceManager; import controller.BaseNode; public class RecordingNode extends BaseNode @@ -15,8 +16,10 @@ public class RecordingNode extends BaseNode private static final long serialVersionUID = 1L; private JPanel mEditor; + + private SourceManager mSourceManager; - public RecordingNode( Recording recording ) + public RecordingNode( SourceManager sourceManager, Recording recording ) { super( recording ); } @@ -64,7 +67,7 @@ public class RecordingNode extends BaseNode } else { - getModel().getResourceManager().getRecordingSourceManager() + mSourceManager.getRecordingSourceManager() .removeRecording( getRecording() ); getModel().removeNodeFromParent( RecordingNode.this ); diff --git a/src/source/recording/RecordingSourceManager.java b/src/source/recording/RecordingSourceManager.java index 0fcabaef..8c2f444f 100644 --- a/src/source/recording/RecordingSourceManager.java +++ b/src/source/recording/RecordingSourceManager.java @@ -31,21 +31,23 @@ import source.config.SourceConfigRecording; import source.config.SourceConfiguration; import source.tuner.TunerChannel; import source.tuner.TunerChannelSource; -import controller.ResourceManager; +import controller.ThreadPoolManager; public class RecordingSourceManager { private final static Logger mLog = LoggerFactory.getLogger( RecordingSourceManager.class ); - private ResourceManager mResourceManager; - private ArrayList mRecordings = new ArrayList(); - public RecordingSourceManager( ResourceManager resourceManager ) + private SettingsManager mSettingsManager; + private ThreadPoolManager mThreadPoolManager; + + public RecordingSourceManager( SettingsManager settingsManager, + ThreadPoolManager threadPoolManager ) { - mResourceManager = resourceManager; - + mSettingsManager = settingsManager; + mThreadPoolManager = threadPoolManager; loadRecordings(); } @@ -71,9 +73,7 @@ public class RecordingSourceManager if( recording != null ) { - retVal = recording.getChannel( - mResourceManager.getThreadPoolManager(), - tunerChannel ); + retVal = recording.getChannel( mThreadPoolManager, tunerChannel ); } else { @@ -116,7 +116,7 @@ public class RecordingSourceManager public Recording addRecording( RecordingConfiguration config ) { - Recording recording = new Recording( mResourceManager, config ); + Recording recording = new Recording( mSettingsManager, config ); mRecordings.add( recording ); @@ -126,7 +126,7 @@ public class RecordingSourceManager public void removeRecording( Recording recording ) { /* Remove the config from the settings manager */ - mResourceManager.getSettingsManager().removeRecordingConfiguration( + mSettingsManager.removeRecordingConfiguration( recording.getRecordingConfiguration() ); mRecordings.remove( recording ); @@ -134,24 +134,17 @@ public class RecordingSourceManager private void loadRecordings() { - if( mResourceManager != null ) - { - SettingsManager settingsManager = mResourceManager.getSettingsManager(); - - if( settingsManager != null ) - { - ArrayList recordingConfigurations = - settingsManager.getRecordingConfigurations(); + ArrayList recordingConfigurations = + mSettingsManager.getRecordingConfigurations(); - mLog.info( "discovered [" + recordingConfigurations.size() + "] recording configurations" ); - - for( RecordingConfiguration config: recordingConfigurations ) - { - Recording recording = new Recording( mResourceManager, config ); - - mRecordings.add( recording ); - } - } - } - } + mLog.info( "RecordingSourceManager - discovered [" + + recordingConfigurations.size() + "] recording configurations" ); + + for( RecordingConfiguration config: recordingConfigurations ) + { + Recording recording = new Recording( mSettingsManager, config ); + + mRecordings.add( recording ); + } + } } diff --git a/src/buffer/ShortComplexByteBuffer.java b/src/source/tuner/ITunerChannelProvider.java similarity index 54% rename from src/buffer/ShortComplexByteBuffer.java rename to src/source/tuner/ITunerChannelProvider.java index 2aff0558..06f7b2fa 100644 --- a/src/buffer/ShortComplexByteBuffer.java +++ b/src/source/tuner/ITunerChannelProvider.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer + * Copyright (C) 2014-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,39 +15,31 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see ******************************************************************************/ -package buffer; +package source.tuner; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Enumeration; +import java.util.concurrent.RejectedExecutionException; -import sample.complex.Complex; +import source.SourceException; +import controller.ThreadPoolManager; -public class ShortComplexByteBuffer implements Enumeration +public interface ITunerChannelProvider { - private ByteBuffer mByteBuffer; - - public ShortComplexByteBuffer( byte[] bytes, ByteOrder endianness ) - { - mByteBuffer = ByteBuffer.wrap( bytes ); - mByteBuffer.order( endianness ); - } - - public byte[] array() - { - return mByteBuffer.array(); - } + /** + * Returns a tuner frequency channel source, tuned to the correct frequency + * + * @param frequency - desired frequency + * + * @return - source for 48k sample rate + */ + public abstract TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, + TunerChannel channel ) throws RejectedExecutionException, SourceException; - @Override - public boolean hasMoreElements() - { - return mByteBuffer.hasRemaining(); - } - - @Override - public Complex nextElement() - { - return new Complex( (float)mByteBuffer.getShort(), - (float)mByteBuffer.getShort() ); - } + /** + * Releases the tuned channel resources + * + * @param channel - previously obtained tuner channel + */ + public abstract void releaseChannel( TunerChannelSource source ); + + } diff --git a/src/source/tuner/Tuner.java b/src/source/tuner/Tuner.java index e274c97f..805a3cb5 100644 --- a/src/source/tuner/Tuner.java +++ b/src/source/tuner/Tuner.java @@ -27,20 +27,18 @@ import org.slf4j.LoggerFactory; import sample.Listener; import sample.complex.ComplexBuffer; +import settings.SettingsManager; import source.SourceException; -import source.tuner.frequency.FrequencyChangeBroadcaster; import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeListener; import source.tuner.frequency.FrequencyChangeEvent.Event; -import controller.ResourceManager; +import source.tuner.frequency.IFrequencyChangeProcessor; import controller.ThreadPoolManager; /** * Tuner - provides tuner channel sources, representing a channel frequency */ -public abstract class Tuner implements FrequencyChangeBroadcaster, - FrequencyChangeListener, - TunerChannelProvider +public abstract class Tuner implements IFrequencyChangeProcessor, + ITunerChannelProvider { private final static Logger mLog = LoggerFactory.getLogger( Tuner.class ); @@ -53,8 +51,8 @@ public abstract class Tuner implements FrequencyChangeBroadcaster, protected CopyOnWriteArrayList> mSampleListeners = new CopyOnWriteArrayList>(); - protected CopyOnWriteArrayList - mFrequencyChangeListeners = new CopyOnWriteArrayList(); + protected CopyOnWriteArrayList + mFrequencyChangeProcessors = new CopyOnWriteArrayList(); public Tuner( String name ) { @@ -69,7 +67,7 @@ public abstract class Tuner implements FrequencyChangeBroadcaster, public void dispose() { mSampleListeners.clear(); - mFrequencyChangeListeners.clear(); + mFrequencyChangeProcessors.clear(); } public void setName( String name ) @@ -80,7 +78,7 @@ public abstract class Tuner implements FrequencyChangeBroadcaster, /** * Return an editor panel for the tuner */ - public abstract JPanel getEditor( ResourceManager resourceManager ); + public abstract JPanel getEditor( SettingsManager settingsManager ); /** @@ -191,17 +189,17 @@ public abstract class Tuner implements FrequencyChangeBroadcaster, * Adds the frequency change listener to receive frequency and/or bandwidth * change events */ - public void addListener( FrequencyChangeListener listener ) + public void addFrequencyChangeProcessor( IFrequencyChangeProcessor processor ) { - mFrequencyChangeListeners.add( listener ); + mFrequencyChangeProcessors.add( processor ); } /** * Removes the frequency change listener */ - public void removeListener( FrequencyChangeListener listener ) + public void removeFrequencyChangeProcessor( IFrequencyChangeProcessor processor ) { - mFrequencyChangeListeners.remove( listener ); + mFrequencyChangeProcessors.remove( processor ); } /** @@ -210,7 +208,7 @@ public abstract class Tuner implements FrequencyChangeBroadcaster, public void broadcastFrequencyChange( long frequency ) { broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.FREQUENCY_CHANGE_NOTIFICATION, frequency ) ); + new FrequencyChangeEvent( Event.NOTIFICATION_FREQUENCY_CHANGE, frequency ) ); } /** @@ -219,7 +217,7 @@ public abstract class Tuner implements FrequencyChangeBroadcaster, public void broadcastSampleRateChange( int sampleRate ) { broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.SAMPLE_RATE_CHANGE_NOTIFICATION, sampleRate ) ); + new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, sampleRate ) ); } /** @@ -228,7 +226,7 @@ public abstract class Tuner implements FrequencyChangeBroadcaster, public void broadcastActualSampleRateChange( int actualSampleRate ) { broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.SAMPLE_RATE_CHANGE_NOTIFICATION, actualSampleRate ) ); + new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, actualSampleRate ) ); } /** @@ -236,9 +234,9 @@ public abstract class Tuner implements FrequencyChangeBroadcaster, */ public void broadcastFrequencyChangeEvent( FrequencyChangeEvent event ) { - for( FrequencyChangeListener listener: mFrequencyChangeListeners ) + for( IFrequencyChangeProcessor processor: mFrequencyChangeProcessors ) { - listener.frequencyChanged( event ); + processor.frequencyChanged( event ); } } diff --git a/src/source/tuner/TunerChannelProvider.java b/src/source/tuner/TunerChannelProvider.java deleted file mode 100644 index f8fabe48..00000000 --- a/src/source/tuner/TunerChannelProvider.java +++ /dev/null @@ -1,28 +0,0 @@ -package source.tuner; - -import java.util.concurrent.RejectedExecutionException; - -import source.SourceException; -import controller.ThreadPoolManager; - -public interface TunerChannelProvider -{ - /** - * Returns a tuner frequency channel source, tuned to the correct frequency - * - * @param frequency - desired frequency - * - * @return - source for 48k sample rate - */ - public abstract TunerChannelSource getChannel( ThreadPoolManager threadPoolManager, - TunerChannel channel ) throws RejectedExecutionException, SourceException; - - /** - * Releases the tuned channel resources - * - * @param channel - previously obtained tuner channel - */ - public abstract void releaseChannel( TunerChannelSource source ); - - -} diff --git a/src/source/tuner/TunerChannelSource.java b/src/source/tuner/TunerChannelSource.java index 0c61dc60..010a6f7b 100644 --- a/src/source/tuner/TunerChannelSource.java +++ b/src/source/tuner/TunerChannelSource.java @@ -1,6 +1,6 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014,2015 Dennis Sheirer + * Copyright (C) 2014-2016 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,7 +36,7 @@ import source.ComplexSource; import source.SourceException; import source.tuner.frequency.FrequencyChangeEvent; import source.tuner.frequency.FrequencyChangeEvent.Event; -import source.tuner.frequency.FrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeProcessor; import controller.ThreadPoolManager; import controller.ThreadPoolManager.ThreadType; import dsp.filter.FilterFactory; @@ -45,8 +45,7 @@ import dsp.filter.cic.ComplexPrimeCICDecimate; import dsp.mixer.Oscillator; public class TunerChannelSource extends ComplexSource - implements FrequencyChangeListener, - Listener + implements IFrequencyChangeProcessor, Listener { private final static Logger mLog = LoggerFactory.getLogger( TunerChannelSource.class ); @@ -61,7 +60,7 @@ public class TunerChannelSource extends ComplexSource private Oscillator mMixer; private ComplexPrimeCICDecimate mDecimationFilter; private Listener mListener; - private FrequencyChangeListener mFrequencyChangeListener; + private IFrequencyChangeProcessor mFrequencyChangeProcessor; private ThreadPoolManager mThreadPoolManager; private ScheduledFuture mTaskHandle; @@ -71,17 +70,35 @@ public class TunerChannelSource extends ComplexSource private DecimationProcessor mDecimationProcessor = new DecimationProcessor(); - public TunerChannelSource( ThreadPoolManager threadPoolManager, - Tuner tuner, + private AtomicBoolean mRunning = new AtomicBoolean(); + private boolean mExpended = false; + + /** + * Provides a Digital Drop Channel (DDC) to decimate the IQ output from a + * tuner down to a 48 kHz IQ channel rate. + * + * Note: this class can only be used once (started and stopped) and a new + * tuner channel source must be requested from the tuner once this object + * has been stopped. This is because channels are managed dynamically and + * center tuned frequency may have changed since this source was obtained + * and thus the tuner might no longer be able to source this channel once it + * has been stopped. + * + * @param threadPoolManager to use for decimation processing task + * @param tuner to obtain wideband IQ samples from + * @param tunerChannel specifying the center frequency for the DDC + * @throws RejectedExecutionException if the thread pool manager cannot + * accept the decimation processing task + * @throws SourceException if the tuner has an issue providing IQ samples + */ + public TunerChannelSource( ThreadPoolManager threadPoolManager, Tuner tuner, TunerChannel tunerChannel ) throws RejectedExecutionException, SourceException { - super( "Tuner Channel Source" ); - mThreadPoolManager = threadPoolManager; mTuner = tuner; - mTuner.addListener( (FrequencyChangeListener)this ); + mTuner.addListener( this ); mTunerChannel = tunerChannel; @@ -94,59 +111,87 @@ public class TunerChannelSource extends ComplexSource /* Fire a sample rate change event to setup the decimation chain */ frequencyChanged( new FrequencyChangeEvent( - Event.SAMPLE_RATE_CHANGE_NOTIFICATION, mTuner.getSampleRate() ) ); - - /* Schedule the decimation task to run every 20 ms (50 iterations/second) */ - mTaskHandle = mThreadPoolManager.scheduleFixedRate( ThreadType.DECIMATION, - mDecimationProcessor, 20, TimeUnit.MILLISECONDS ); - - /* Finally, register to receive samples from the tuner */ - mTuner.addListener( (Listener)this ); + Event.NOTIFICATION_SAMPLE_RATE_CHANGE, mTuner.getSampleRate() ) ); } + @Override + public void reset() + { + } + + + @Override + public void start() + { + if( mExpended ) + { + throw new IllegalStateException( "Attempt to re-start an expended " + + "tuner channel source. TunerChannelSource objects can only " + + "be used once. " ); + } + + if( mRunning.compareAndSet( false, true )) + { + /* Schedule the decimation task to run every 20 ms (50 iterations/second) */ + mTaskHandle = mThreadPoolManager.scheduleFixedRate( ThreadType.DECIMATION, + mDecimationProcessor, 20, TimeUnit.MILLISECONDS ); + + /* Finally, register to receive samples from the tuner */ + mTuner.addListener( (Listener)this ); + } + else + { + mLog.warn( "Attempt to start() and already running tuner channel " + + "source was ignored" ); + } + } + + + @Override + public void stop() + { + if( mRunning.compareAndSet( true, false )) + { + mTuner.releaseChannel( this ); + mDecimationProcessor.shutdown(); + + if( mThreadPoolManager != null && mTaskHandle != null ) + { + mThreadPoolManager.cancel( mTaskHandle ); + mTaskHandle = null; + } + + mBuffer.clear(); + + mExpended = true; + } + else + { + mLog.warn( "Attempt to stop() and already stopped tuner channel " + + "source was ignored" ); + } + } + + + @Override public void dispose() { - /* Tell the tuner to release/unregister our resources */ - mTuner.removeListener( (FrequencyChangeListener)this ); - mTuner.releaseChannel( this ); - mTuner = null; - mTunerChannel = null; - - /* Flag the decimation processor to shutdown */ - if( mDecimationProcessor != null ) - { - mDecimationProcessor.dispose(); - } - + if( !mRunning.get() ) + { + /* Tell the tuner to release/unregister our resources */ + mTuner.removeFrequencyChangeProcessor( this ); + mTuner = null; + mTunerChannel = null; + mBuffer = null; + mFrequencyChangeProcessor = null; + mListener = null; + mMixer = null; + mDecimationFilter.dispose(); + mDecimationFilter = null; + } } - /** - * Cleanup method to allow the decimation processor to perform final cleanup - * once it gracefully shuts itself down - */ - private void cleanup() - { - if( mTaskHandle != null ) - { - mThreadPoolManager.cancel( mTaskHandle ); - mThreadPoolManager = null; - mTaskHandle = null; - } - - - mBuffer.clear(); - mBuffer = null; - - mFrequencyChangeListener = null; - mListener = null; - - mMixer = null; - - mDecimationFilter.dispose(); - mDecimationFilter = null; - } - public Tuner getTuner() { return mTuner; @@ -160,18 +205,21 @@ public class TunerChannelSource extends ComplexSource @Override public void receive( ComplexBuffer buffer ) { - mBuffer.add( buffer ); + if( mRunning.get() ) + { + mBuffer.add( buffer ); + } } - public void setFrequencyChangeListener( FrequencyChangeListener listener ) + public void setFrequencyChangeListener( IFrequencyChangeProcessor processor ) { - mFrequencyChangeListener = listener; + mFrequencyChangeProcessor = processor; } @Override public void setListener( Listener listener ) { - /* Get a pointer to the listener so that if we have to change the + /* Save a pointer to the listener so that if we have to change the * decimation filter, we can re-add the listener */ mListener = listener; @@ -192,31 +240,31 @@ public class TunerChannelSource extends ComplexSource public void frequencyChanged( FrequencyChangeEvent event ) { // Echo the event to the registered event listener - if( mFrequencyChangeListener != null ) + if( mFrequencyChangeProcessor != null ) { - mFrequencyChangeListener.frequencyChanged( event ); + mFrequencyChangeProcessor.frequencyChanged( event ); } switch( event.getEvent() ) { - case FREQUENCY_CHANGE_NOTIFICATION: + case NOTIFICATION_FREQUENCY_CHANGE: mTunerFrequency = event.getValue().longValue(); updateMixerFrequencyOffset(); break; - case CHANNEL_FREQUENCY_CORRECTION_CHANGE_REQUEST: + case REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE: mChannelFrequencyCorrection = event.getValue().intValue(); updateMixerFrequencyOffset(); - if( mFrequencyChangeListener != null ) + if( mFrequencyChangeProcessor != null ) { - mFrequencyChangeListener.frequencyChanged( + mFrequencyChangeProcessor.frequencyChanged( new FrequencyChangeEvent( - Event.CHANNEL_FREQUENCY_CORRECTION_CHANGE_NOTIFICATION, + Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, mChannelFrequencyCorrection ) ); } break; - case SAMPLE_RATE_CHANGE_NOTIFICATION: + case NOTIFICATION_SAMPLE_RATE_CHANGE: int sampleRate = event.getValue().intValue(); if( mTunerSampleRate != sampleRate ) @@ -272,12 +320,12 @@ public class TunerChannelSource extends ComplexSource */ public class DecimationProcessor implements Runnable { - private AtomicBoolean mDispose = new AtomicBoolean( false ); + private boolean mProcessing = true; private List mSampleBuffers = new ArrayList(); - public void dispose() + public void shutdown() { - mDispose.set( true ); + mProcessing = false; } @Override @@ -288,64 +336,66 @@ public class TunerChannelSource extends ComplexSource * run the program out of memory */ try { - if( mBuffer != null ) + if( mProcessing ) { - mBuffer.drainTo( mSampleBuffers, 4 ); - - for( Buffer buffer: mSampleBuffers ) + if( mBuffer != null ) { - /* Check to see if we've been shutdown */ - if( mDispose.get() ) + mBuffer.drainTo( mSampleBuffers, 4 ); + + for( Buffer buffer: mSampleBuffers ) { - cleanup(); - return; - } - else - { - float[] samples = buffer.getSamples(); - - /* We make a copy of the buffer so that we don't affect - * anyone else that is using the same buffer, like other - * channels or the spectral display */ - float[] translated = new float[ samples.length ]; - - /* Perform frequency translation */ - for( int x = 0; x < samples.length; x += 2 ) + /* Check to see if we've been shutdown */ + if( !mProcessing ) { - mMixer.rotate(); + mBuffer.clear(); + return; + } + else + { + float[] samples = buffer.getSamples(); + + /* We make a copy of the buffer so that we don't affect + * anyone else that is using the same buffer, like other + * channels or the spectral display */ + float[] translated = new float[ samples.length ]; - translated[ x ] = Complex.multiplyInphase( - samples[ x ], samples[ x + 1 ], mMixer.inphase(), mMixer.quadrature() ); - - translated[ x + 1 ] = Complex.multiplyQuadrature( + /* Perform frequency translation */ + for( int x = 0; x < samples.length; x += 2 ) + { + mMixer.rotate(); + + translated[ x ] = Complex.multiplyInphase( samples[ x ], samples[ x + 1 ], mMixer.inphase(), mMixer.quadrature() ); - } - - if( mDecimationFilter != null ) - { - mDecimationFilter.receive( new ComplexBuffer( translated ) ); + + translated[ x + 1 ] = Complex.multiplyQuadrature( + samples[ x ], samples[ x + 1 ], mMixer.inphase(), mMixer.quadrature() ); + } + + if( mProcessing && mDecimationFilter != null ) + { + mDecimationFilter.receive( new ComplexBuffer( translated ) ); + } } } + + mSampleBuffers.clear(); } - - mSampleBuffers.clear(); } } catch( Exception e ) { - /* Only log the stack trace if we've haven't been shutdown and - * this is a true error */ - if( !mDispose.get() ) + /* Only log the stack trace if we're still processing */ + if( mProcessing ) { mLog.error( "Error encountered during decimation process", e ); } } /* Check to see if we've been shutdown */ - if( mDispose.get() ) + if( !mProcessing ) { + mBuffer.clear(); mSampleBuffers.clear(); - cleanup(); } } } diff --git a/src/source/tuner/TunerController.java b/src/source/tuner/TunerController.java index aacb5f6b..44682e7a 100644 --- a/src/source/tuner/TunerController.java +++ b/src/source/tuner/TunerController.java @@ -25,9 +25,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import source.SourceException; -import source.tuner.frequency.FrequencyChangeListener; import source.tuner.frequency.FrequencyController; import source.tuner.frequency.FrequencyController.Tunable; +import source.tuner.frequency.IFrequencyChangeProcessor; +import source.tuner.frequency.IFrequencyChangeListener; import controller.ThreadPoolManager; public abstract class TunerController implements Tunable @@ -36,7 +37,11 @@ public abstract class TunerController implements Tunable LoggerFactory.getLogger( TunerController.class ); /* List of currently tuned channels being served to demod channels */ +<<<<<<< Upstream, based on origin/master private SortedSet mTunedChannels = new ConcurrentSkipListSet<>(); +======= + protected ArrayList mTunedChannels = new ArrayList<>(); +>>>>>>> 20e3bee Work in progress. -Started work on new channel editor channel viewer/controller -Removed a number of unused buffer package classes -Moved automatic frequency control to be a module and removed special handling -Added IUnfilteredRealBufferListener & Provider interfaces -Completely removed the ResourceManager class protected FrequencyController mFrequencyController; private int mMiddleUnusable; private double mUsableBandwidthPercentage; @@ -359,16 +364,16 @@ public abstract class TunerController implements Tunable * Note: this is normally used by the Tuner. Any additional listeners can * be registered on the tuner. */ - public void addListener( FrequencyChangeListener listener ) + public void addListener( IFrequencyChangeProcessor processor ) { - mFrequencyController.addListener( listener ); + mFrequencyController.addListener( processor ); } /** * Removes the frequency change listener */ - public void removeListener( FrequencyChangeListener listener ) + public void removeListener( IFrequencyChangeProcessor processor ) { - mFrequencyController.removeListener( listener ); + mFrequencyController.removeFrequencyChangeProcessor( processor ); } } diff --git a/src/source/tuner/TunerEditor.java b/src/source/tuner/TunerEditor.java index 15b3d650..2be792a1 100644 --- a/src/source/tuner/TunerEditor.java +++ b/src/source/tuner/TunerEditor.java @@ -17,21 +17,23 @@ ******************************************************************************/ package source.tuner; +import controller.channel.Channel; +import controller.channel.ConfigurationValidationException; import gui.control.JFrequencyControl; import source.SourceEditor; +import source.SourceManager; import source.config.SourceConfigTuner; import source.config.SourceConfiguration; -import controller.ResourceManager; public class TunerEditor extends SourceEditor { private static final long serialVersionUID = 1L; private JFrequencyControl mFrequencyControl; - public TunerEditor( ResourceManager resourceManager, + public TunerEditor( SourceManager sourceManager, SourceConfiguration config ) { - super( resourceManager, config ); + super( sourceManager, config ); initGUI(); } @@ -56,4 +58,18 @@ public class TunerEditor extends SourceEditor add( mFrequencyControl ); } + + @Override + public void setConfiguration( Channel channel ) + { + // TODO Auto-generated method stub + + } + + @Override + public void validateConfiguration() throws ConfigurationValidationException + { + // TODO Auto-generated method stub + + } } diff --git a/src/source/tuner/TunerManager.java b/src/source/tuner/TunerManager.java index 345757f3..3d6f351c 100644 --- a/src/source/tuner/TunerManager.java +++ b/src/source/tuner/TunerManager.java @@ -30,6 +30,7 @@ import org.usb4java.DeviceDescriptor; import org.usb4java.DeviceList; import org.usb4java.LibUsb; +import settings.SettingsManager; import source.Source; import source.SourceException; import source.config.SourceConfigTuner; @@ -46,22 +47,25 @@ import source.tuner.rtl.RTL2832Tuner; import source.tuner.rtl.RTL2832TunerController; import source.tuner.rtl.e4k.E4KTunerController; import source.tuner.rtl.r820t.R820TTunerController; -import controller.ResourceManager; +import controller.ThreadPoolManager; public class TunerManager { private final static Logger mLog = LoggerFactory.getLogger( TunerManager.class ); - private ResourceManager mResourceManager; private ArrayList mTuners = new ArrayList(); + + private MixerManager mMixerManager; + private SettingsManager mSettingsManager; + private ThreadPoolManager mThreadPoolManager; - public TunerManager( ResourceManager resourceManager ) + public TunerManager( MixerManager mixerManager, + SettingsManager settingsManager, + ThreadPoolManager threadPoolManager ) { - mResourceManager = resourceManager; - - /* initialize mixers */ - MixerManager.getInstance(); + mSettingsManager = settingsManager; + mThreadPoolManager = threadPoolManager; initTuners(); } @@ -100,8 +104,7 @@ public class TunerManager try { - retVal = tuner.getChannel( - mResourceManager.getThreadPoolManager(), tunerChannel ); + retVal = tuner.getChannel( mThreadPoolManager, tunerChannel ); } catch ( RejectedExecutionException ree ) { @@ -261,7 +264,7 @@ public class TunerManager try { AirspyTunerController airspyController = - new AirspyTunerController( device, mResourceManager.getThreadPoolManager() ); + new AirspyTunerController( device, mThreadPoolManager ); airspyController.init(); @@ -397,7 +400,7 @@ public class TunerManager { HackRFTunerController hackRFController = new HackRFTunerController( device, descriptor, - mResourceManager.getThreadPoolManager() ); + mThreadPoolManager ); hackRFController.init(); @@ -449,7 +452,7 @@ public class TunerManager { E4KTunerController controller = new E4KTunerController( device, deviceDescriptor, - mResourceManager.getThreadPoolManager() ); + mThreadPoolManager ); controller.init(); @@ -476,7 +479,7 @@ public class TunerManager { R820TTunerController controller = new R820TTunerController( device, deviceDescriptor, - mResourceManager.getThreadPoolManager() ); + mThreadPoolManager ); controller.init(); @@ -526,12 +529,11 @@ public class TunerManager */ private TunerConfiguration getTunerConfiguration( Tuner tuner ) { - TunerConfigurationAssignment selected = mResourceManager - .getSettingsManager().getSelectedTunerConfiguration( - tuner.getTunerType(), tuner.getUniqueID() ); + TunerConfigurationAssignment selected = mSettingsManager + .getSelectedTunerConfiguration( tuner.getTunerType(), tuner.getUniqueID() ); - ArrayList configs = mResourceManager - .getSettingsManager().getTunerConfigurations( tuner.getTunerType() ); + ArrayList configs = mSettingsManager + .getTunerConfigurations( tuner.getTunerType() ); TunerConfiguration config = null; @@ -553,13 +555,13 @@ public class TunerManager //If we're still null at this point, create a default config if( config == null ) { - config = mResourceManager.getSettingsManager() - .addNewTunerConfiguration( tuner.getTunerType(), "Default" ); + config = mSettingsManager.addNewTunerConfiguration( + tuner.getTunerType(), "Default" ); } //Persist the config as the selected tuner configuration. The method //auto-saves the setting - mResourceManager.getSettingsManager().setSelectedTunerConfiguration( + mSettingsManager.setSelectedTunerConfiguration( tuner.getTunerType(), tuner.getUniqueID(), config ); return config; @@ -579,7 +581,7 @@ public class TunerManager private MixerTunerDataLine getMixerTunerDataLine( TunerType tunerClass ) { Collection datalines = - MixerManager.getInstance().getMixerTunerDataLines(); + mMixerManager.getMixerTunerDataLines(); for( MixerTunerDataLine mixerTDL: datalines ) { diff --git a/src/source/tuner/TunerNode.java b/src/source/tuner/TunerNode.java index 626cda6b..63aabd9c 100644 --- a/src/source/tuner/TunerNode.java +++ b/src/source/tuner/TunerNode.java @@ -28,15 +28,17 @@ import javax.swing.JPopupMenu; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import playlist.PlaylistManager; +import settings.SettingsManager; import source.SourceException; import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeProcessor; import spectrum.SpectrumFrame; import controller.BaseNode; -import controller.ResourceManager; import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; -public class TunerNode extends BaseNode implements FrequencyChangeListener +public class TunerNode extends BaseNode implements IFrequencyChangeProcessor { private final static Logger mLog = LoggerFactory.getLogger( TunerNode.class ); @@ -48,14 +50,24 @@ public class TunerNode extends BaseNode implements FrequencyChangeListener private long mFrequency; private int mSampleRate; private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; + private PlaylistManager mPlaylistManager; + private SettingsManager mSettingsManager; - public TunerNode( ChannelModel channelModel, Tuner tuner ) + public TunerNode( ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, + PlaylistManager playlistManager, + SettingsManager settingsManager, + Tuner tuner ) { super( tuner ); //does this make it a super tuner? mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; + mPlaylistManager = playlistManager; + mSettingsManager = settingsManager; - tuner.addListener( this ); + tuner.addFrequencyChangeProcessor( this ); try { @@ -72,7 +84,7 @@ public class TunerNode extends BaseNode implements FrequencyChangeListener @Override public JPanel getEditor() { - return getTuner().getEditor( getModel().getResourceManager() ); + return getTuner().getEditor( mSettingsManager ); } public Tuner getTuner() @@ -117,11 +129,9 @@ public class TunerNode extends BaseNode implements FrequencyChangeListener @Override public void actionPerformed( ActionEvent e ) { - ResourceManager rm = getModel().getResourceManager(); - - SpectrumFrame frame = new SpectrumFrame( mChannelModel, - rm.getController(), rm.getPlaylistManager(), - rm.getSettingsManager(), getTuner() ); + SpectrumFrame frame = new SpectrumFrame( getModel(), mChannelModel, + mChannelProcessingManager, mPlaylistManager, + mSettingsManager, getTuner() ); } } ); @@ -135,7 +145,7 @@ public class TunerNode extends BaseNode implements FrequencyChangeListener { switch( event.getEvent() ) { - case FREQUENCY_CHANGE_NOTIFICATION: + case NOTIFICATION_FREQUENCY_CHANGE: long frequency = event.getValue().longValue(); if( mFrequency != frequency ) @@ -144,7 +154,7 @@ public class TunerNode extends BaseNode implements FrequencyChangeListener getModel().nodeChanged( TunerNode.this ); } break; - case SAMPLE_RATE_CHANGE_NOTIFICATION: + case NOTIFICATION_SAMPLE_RATE_CHANGE: int sampleRate = event.getValue().intValue(); if( mSampleRate != sampleRate ) diff --git a/src/source/tuner/airspy/AirspyTuner.java b/src/source/tuner/airspy/AirspyTuner.java index 36df0315..345b3456 100644 --- a/src/source/tuner/airspy/AirspyTuner.java +++ b/src/source/tuner/airspy/AirspyTuner.java @@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory; import sample.Listener; import sample.complex.ComplexBuffer; +import settings.SettingsManager; import source.SourceException; import source.tuner.Tuner; import source.tuner.TunerChannel; @@ -34,7 +35,6 @@ import source.tuner.TunerClass; import source.tuner.TunerConfiguration; import source.tuner.TunerType; import source.tuner.airspy.AirspyTunerController.BoardID; -import controller.ResourceManager; import controller.ThreadPoolManager; public class AirspyTuner extends Tuner @@ -55,9 +55,9 @@ public class AirspyTuner extends Tuner } @Override - public JPanel getEditor( ResourceManager resourceManager ) + public JPanel getEditor( SettingsManager settingsManager ) { - return new AirspyTunerEditorPanel( this, resourceManager ); + return new AirspyTunerEditorPanel( this, settingsManager ); } public AirspyTunerController getController() diff --git a/src/source/tuner/airspy/AirspyTunerConfigurationPanel.java b/src/source/tuner/airspy/AirspyTunerConfigurationPanel.java index e2d31475..937a63f8 100644 --- a/src/source/tuner/airspy/AirspyTunerConfigurationPanel.java +++ b/src/source/tuner/airspy/AirspyTunerConfigurationPanel.java @@ -45,13 +45,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.LibUsbException; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerConfiguration; import source.tuner.TunerConfigurationAssignment; import source.tuner.TunerType; import source.tuner.airspy.AirspyTunerController.Gain; import source.tuner.airspy.AirspyTunerController.GainMode; -import controller.ResourceManager; public class AirspyTunerConfigurationPanel extends JPanel { @@ -60,7 +60,7 @@ public class AirspyTunerConfigurationPanel extends JPanel private static final long serialVersionUID = 1L; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; private AirspyTunerController mController; private AirspyTunerConfiguration mSelectedConfig; @@ -94,10 +94,10 @@ public class AirspyTunerConfigurationPanel extends JPanel private JSlider mMixerGain; - public AirspyTunerConfigurationPanel( ResourceManager resourceManager, + public AirspyTunerConfigurationPanel( SettingsManager settingsManager, AirspyTunerController controller ) { - mResourceManager = resourceManager; + mSettingsManager = settingsManager; mController = controller; init(); @@ -126,8 +126,7 @@ public class AirspyTunerConfigurationPanel extends JPanel if( serial != null ) { - savedConfig = mResourceManager - .getSettingsManager().getSelectedTunerConfiguration( + savedConfig = mSettingsManager.getSelectedTunerConfiguration( TunerType.AIRSPY_R820T, serial ); } @@ -146,8 +145,7 @@ public class AirspyTunerConfigurationPanel extends JPanel if( serial != null ) { - mResourceManager.getSettingsManager() - .setSelectedTunerConfiguration( TunerType.AIRSPY_R820T, + mSettingsManager.setSelectedTunerConfiguration( TunerType.AIRSPY_R820T, serial, mSelectedConfig ); } } @@ -518,8 +516,7 @@ public class AirspyTunerConfigurationPanel extends JPanel public void actionPerformed( ActionEvent e ) { TunerConfiguration config = - mResourceManager.getSettingsManager() - .addNewTunerConfiguration( + mSettingsManager.addNewTunerConfiguration( TunerType.AIRSPY_R820T, "New Configuration" ); @@ -558,8 +555,7 @@ public class AirspyTunerConfigurationPanel extends JPanel if( n == JOptionPane.YES_OPTION ) { - mResourceManager.getSettingsManager() - .deleteTunerConfiguration( selected ); + mSettingsManager.deleteTunerConfiguration( selected ); mComboConfigurations.setModel( getModel() ); @@ -635,7 +631,7 @@ public class AirspyTunerConfigurationPanel extends JPanel mGainModeCombo.setSelectedItem( Gain.getGainMode( gain ) ); mGain.setValue( gain.getValue() ); - mResourceManager.getSettingsManager().setSelectedTunerConfiguration( + mSettingsManager.setSelectedTunerConfiguration( TunerType.AIRSPY_R820T, mController.getDeviceInfo().getSerialNumber(), config ); save(); @@ -661,8 +657,7 @@ public class AirspyTunerConfigurationPanel extends JPanel private ComboBoxModel getModel() { ArrayList configs = - mResourceManager.getSettingsManager() - .getTunerConfigurations( TunerType.AIRSPY_R820T ); + mSettingsManager.getTunerConfigurations( TunerType.AIRSPY_R820T ); DefaultComboBoxModel model = new DefaultComboBoxModel(); @@ -681,8 +676,7 @@ public class AirspyTunerConfigurationPanel extends JPanel private AirspyTunerConfiguration getNamedConfiguration( String name ) { ArrayList configs = - mResourceManager.getSettingsManager() - .getTunerConfigurations( TunerType.AIRSPY_R820T ); + mSettingsManager.getTunerConfigurations( TunerType.AIRSPY_R820T ); for( TunerConfiguration config: configs ) { @@ -700,6 +694,6 @@ public class AirspyTunerConfigurationPanel extends JPanel */ private void save() { - mResourceManager.getSettingsManager().save(); + mSettingsManager.save(); } } diff --git a/src/source/tuner/airspy/AirspyTunerEditorPanel.java b/src/source/tuner/airspy/AirspyTunerEditorPanel.java index d15037f7..7305fce4 100644 --- a/src/source/tuner/airspy/AirspyTunerEditorPanel.java +++ b/src/source/tuner/airspy/AirspyTunerEditorPanel.java @@ -29,17 +29,16 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import settings.SettingsManager; import source.SourceException; import source.tuner.frequency.FrequencyChangeEvent; import source.tuner.frequency.FrequencyChangeEvent.Event; -import source.tuner.frequency.FrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeProcessor; import com.jidesoft.swing.JideTabbedPane; -import controller.ResourceManager; - public class AirspyTunerEditorPanel extends JPanel - implements FrequencyChangeListener + implements IFrequencyChangeProcessor { private static final long serialVersionUID = 1L; @@ -50,14 +49,14 @@ public class AirspyTunerEditorPanel extends JPanel private AirspyTuner mTuner; private AirspyTunerController mController; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; public AirspyTunerEditorPanel( AirspyTuner tuner, - ResourceManager resourceManager ) + SettingsManager settingsManager ) { mTuner = tuner; mController = mTuner.getController(); - mResourceManager = resourceManager; + mSettingsManager = settingsManager; initGUI(); } @@ -76,7 +75,7 @@ public class AirspyTunerEditorPanel extends JPanel /* Add frequency control as frequency change listener. This creates a * feedback loop, so the control does not rebroadcast the event */ - mTuner.addListener( mFrequencyControl ); + mTuner.addFrequencyChangeProcessor( mFrequencyControl ); mFrequencyControl.setFrequency( mController.getFrequency(), false ); @@ -89,7 +88,7 @@ public class AirspyTunerEditorPanel extends JPanel add( tabs, "span,grow,push" ); tabs.add( "Config", new AirspyTunerConfigurationPanel( - mResourceManager, mTuner.getController() ) ); + mSettingsManager, mTuner.getController() ) ); tabs.add( "Info", new AirspyInformationPanel( mTuner.getController() ) ); } @@ -97,7 +96,7 @@ public class AirspyTunerEditorPanel extends JPanel @Override public void frequencyChanged( FrequencyChangeEvent event ) { - if( event.getEvent() == Event.FREQUENCY_CHANGE_NOTIFICATION ) + if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE ) { try { diff --git a/src/source/tuner/ettus/B100Tuner.java b/src/source/tuner/ettus/B100Tuner.java index 9ec27054..11cbc94f 100644 --- a/src/source/tuner/ettus/B100Tuner.java +++ b/src/source/tuner/ettus/B100Tuner.java @@ -24,6 +24,7 @@ import javax.swing.JPanel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import settings.SettingsManager; import source.SourceException; import source.tuner.Tuner; import source.tuner.TunerChannel; @@ -32,7 +33,6 @@ import source.tuner.TunerClass; import source.tuner.TunerConfiguration; import source.tuner.TunerType; import source.tuner.usb.USBTunerDevice; -import controller.ResourceManager; import controller.ThreadPoolManager; public class B100Tuner extends Tuner @@ -69,9 +69,9 @@ public class B100Tuner extends Tuner } @Override - public JPanel getEditor( ResourceManager resourceManager ) + public JPanel getEditor( SettingsManager settingsManager ) { - return new B100TunerEditorPanel( this, resourceManager ); + return new B100TunerEditorPanel( this, settingsManager ); } @Override diff --git a/src/source/tuner/ettus/B100TunerEditorPanel.java b/src/source/tuner/ettus/B100TunerEditorPanel.java index 964ac4ee..aadd2fec 100644 --- a/src/source/tuner/ettus/B100TunerEditorPanel.java +++ b/src/source/tuner/ettus/B100TunerEditorPanel.java @@ -22,19 +22,19 @@ import javax.swing.JLabel; import javax.swing.JPanel; import net.miginfocom.swing.MigLayout; -import controller.ResourceManager; +import settings.SettingsManager; public class B100TunerEditorPanel extends JPanel { private static final long serialVersionUID = 1L; private B100Tuner mTuner; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; - public B100TunerEditorPanel( B100Tuner tuner, ResourceManager resourceManager ) + public B100TunerEditorPanel( B100Tuner tuner, SettingsManager settingsManager ) { mTuner = tuner; - mResourceManager = resourceManager; + mSettingsManager = settingsManager; init(); } diff --git a/src/source/tuner/fcd/FCDTuner.java b/src/source/tuner/fcd/FCDTuner.java index ea84063c..e3d680a5 100644 --- a/src/source/tuner/fcd/FCDTuner.java +++ b/src/source/tuner/fcd/FCDTuner.java @@ -24,6 +24,7 @@ import javax.swing.JPanel; import sample.Listener; import sample.adapter.ShortAdapter; import sample.complex.ComplexBuffer; +import settings.SettingsManager; import source.SourceException; import source.tuner.MixerTuner; import source.tuner.MixerTunerDataLine; @@ -32,8 +33,6 @@ import source.tuner.TunerChannelSource; import source.tuner.TunerClass; import source.tuner.TunerConfiguration; import source.tuner.TunerType; -import source.tuner.frequency.FrequencyChangeListener; -import controller.ResourceManager; import controller.ThreadPoolManager; public class FCDTuner extends MixerTuner @@ -48,7 +47,7 @@ public class FCDTuner extends MixerTuner new ShortAdapter() ); mController = controller; - mController.addListener( (FrequencyChangeListener)this ); + mController.addListener( this ); } public void dispose() @@ -82,9 +81,9 @@ public class FCDTuner extends MixerTuner } @Override - public JPanel getEditor( ResourceManager resourceManager ) + public JPanel getEditor( SettingsManager settingsManager ) { - return mController.getEditor( this, resourceManager ); + return mController.getEditor( this, settingsManager ); } @Override diff --git a/src/source/tuner/fcd/FCDTunerController.java b/src/source/tuner/fcd/FCDTunerController.java index 8e6ff030..ca602ed2 100644 --- a/src/source/tuner/fcd/FCDTunerController.java +++ b/src/source/tuner/fcd/FCDTunerController.java @@ -33,13 +33,13 @@ import org.usb4java.DeviceHandle; import org.usb4java.LibUsb; import org.usb4java.LibUsbException; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerClass; import source.tuner.TunerConfiguration; import source.tuner.TunerController; import source.tuner.TunerType; import source.tuner.fcd.proV1.FCD1TunerController.Block; -import controller.ResourceManager; public abstract class FCDTunerController extends TunerController { @@ -150,7 +150,7 @@ public abstract class FCDTunerController extends TunerController * Editor panel (GUI) component. */ public abstract JPanel getEditor( FCDTuner tuner, - ResourceManager resourceManager ); + SettingsManager settingsManager ); /** * Applies the settings in the tuner configuration diff --git a/src/source/tuner/fcd/proV1/FCD1TunerConfigurationPanel.java b/src/source/tuner/fcd/proV1/FCD1TunerConfigurationPanel.java index bfbe7fbb..b54bef0b 100644 --- a/src/source/tuner/fcd/proV1/FCD1TunerConfigurationPanel.java +++ b/src/source/tuner/fcd/proV1/FCD1TunerConfigurationPanel.java @@ -47,6 +47,7 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerConfiguration; import source.tuner.TunerType; @@ -56,15 +57,13 @@ import source.tuner.fcd.proV1.FCD1TunerController.MixerGain; import com.jidesoft.swing.JideTabbedPane; -import controller.ResourceManager; - public class FCD1TunerConfigurationPanel extends JPanel { private final static Logger mLog = LoggerFactory.getLogger( FCD1TunerConfigurationPanel.class ); private static final long serialVersionUID = 1L; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; private FCD1TunerController mController; private FCD1TunerConfiguration mSelectedConfig; @@ -81,10 +80,10 @@ public class FCD1TunerConfigurationPanel extends JPanel private CorrectionSpinner mCorrectionGain; private CorrectionSpinner mCorrectionPhase; - public FCD1TunerConfigurationPanel( ResourceManager resourceManager, + public FCD1TunerConfigurationPanel( SettingsManager settingsManager, FCD1TunerController controller ) { - mResourceManager = resourceManager; + mSettingsManager = settingsManager; mController = controller; mSelectedConfig = controller.getTunerConfiguration(); @@ -428,8 +427,7 @@ public class FCD1TunerConfigurationPanel extends JPanel public void actionPerformed( ActionEvent e ) { TunerConfiguration config = - mResourceManager.getSettingsManager() - .addNewTunerConfiguration( + mSettingsManager.addNewTunerConfiguration( TunerType.FUNCUBE_DONGLE_PRO, "New Configuration" ); @@ -472,8 +470,7 @@ public class FCD1TunerConfigurationPanel extends JPanel if( n == JOptionPane.YES_OPTION ) { - mResourceManager.getSettingsManager() - .deleteTunerConfiguration( selected ); + mSettingsManager.deleteTunerConfiguration( selected ); mComboConfigurations.setModel( getModel() ); @@ -508,8 +505,7 @@ public class FCD1TunerConfigurationPanel extends JPanel mCorrectionGain.setValue( config.getGainCorrection() ); mCorrectionPhase.setValue( config.getPhaseCorrection() ); - mResourceManager.getSettingsManager() - .setSelectedTunerConfiguration( + mSettingsManager.setSelectedTunerConfiguration( TunerType.FUNCUBE_DONGLE_PRO, mController.getUSBAddress(), config ); } @@ -529,8 +525,7 @@ public class FCD1TunerConfigurationPanel extends JPanel private ComboBoxModel getModel() { ArrayList configs = - mResourceManager.getSettingsManager() - .getTunerConfigurations( TunerType.FUNCUBE_DONGLE_PRO ); + mSettingsManager.getTunerConfigurations( TunerType.FUNCUBE_DONGLE_PRO ); DefaultComboBoxModel model = new DefaultComboBoxModel(); @@ -547,8 +542,7 @@ public class FCD1TunerConfigurationPanel extends JPanel private FCD1TunerConfiguration getNamedConfiguration( String name ) { ArrayList configs = - mResourceManager.getSettingsManager() - .getTunerConfigurations( TunerType.FUNCUBE_DONGLE_PRO ); + mSettingsManager.getTunerConfigurations( TunerType.FUNCUBE_DONGLE_PRO ); for( TunerConfiguration config: configs ) { @@ -563,7 +557,7 @@ public class FCD1TunerConfigurationPanel extends JPanel private void save() { - mResourceManager.getSettingsManager().save(); + mSettingsManager.save(); } public enum Correction { GAIN, PHASE, DC_INPHASE, DC_QUADRATURE }; diff --git a/src/source/tuner/fcd/proV1/FCD1TunerController.java b/src/source/tuner/fcd/proV1/FCD1TunerController.java index 9d56b0f4..8de3053c 100644 --- a/src/source/tuner/fcd/proV1/FCD1TunerController.java +++ b/src/source/tuner/fcd/proV1/FCD1TunerController.java @@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory; import org.usb4java.Device; import org.usb4java.DeviceDescriptor; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerClass; import source.tuner.TunerConfiguration; @@ -36,7 +37,6 @@ import source.tuner.TunerType; import source.tuner.fcd.FCDCommand; import source.tuner.fcd.FCDTuner; import source.tuner.fcd.FCDTunerController; -import controller.ResourceManager; public class FCD1TunerController extends FCDTunerController { @@ -148,11 +148,11 @@ public class FCD1TunerController extends FCDTunerController } @Override - public JPanel getEditor( FCDTuner tuner, ResourceManager resourceManager ) + public JPanel getEditor( FCDTuner tuner, SettingsManager settingsManager ) { if( mEditor == null ) { - mEditor = new FCD1TunerEditorPanel( tuner, resourceManager ); + mEditor = new FCD1TunerEditorPanel( tuner, settingsManager ); } return mEditor; diff --git a/src/source/tuner/fcd/proV1/FCD1TunerEditorPanel.java b/src/source/tuner/fcd/proV1/FCD1TunerEditorPanel.java index 317fcaeb..83512616 100644 --- a/src/source/tuner/fcd/proV1/FCD1TunerEditorPanel.java +++ b/src/source/tuner/fcd/proV1/FCD1TunerEditorPanel.java @@ -29,18 +29,17 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import settings.SettingsManager; import source.SourceException; import source.tuner.fcd.FCDTuner; import source.tuner.fcd.FCDTunerDetailsPanel; import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeListener; import source.tuner.frequency.FrequencyChangeEvent.Event; +import source.tuner.frequency.IFrequencyChangeProcessor; import com.jidesoft.swing.JideTabbedPane; -import controller.ResourceManager; - -public class FCD1TunerEditorPanel extends JPanel implements FrequencyChangeListener +public class FCD1TunerEditorPanel extends JPanel implements IFrequencyChangeProcessor { private static final long serialVersionUID = 1L; private final static Logger mLog = @@ -48,15 +47,15 @@ public class FCD1TunerEditorPanel extends JPanel implements FrequencyChangeListe private FCDTuner mTuner; private FCD1TunerController mController; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; private JFrequencyControl mFrequencyControl; - public FCD1TunerEditorPanel( FCDTuner tuner, ResourceManager resourceManager ) + public FCD1TunerEditorPanel( FCDTuner tuner, SettingsManager settingsManager ) { mTuner = tuner; mController = (FCD1TunerController)mTuner.getController(); - mResourceManager = resourceManager; + mSettingsManager = settingsManager; initGUI(); } @@ -74,7 +73,7 @@ public class FCD1TunerEditorPanel extends JPanel implements FrequencyChangeListe /* Add frequency control as listener to tuner to get frequency changes * that are invoked elsewhere, to keep the control in sync with the * frequency */ - mTuner.addListener( mFrequencyControl ); + mTuner.addFrequencyChangeProcessor( mFrequencyControl ); mFrequencyControl.setFrequency( mController.getFrequency(), false ); @@ -85,7 +84,7 @@ public class FCD1TunerEditorPanel extends JPanel implements FrequencyChangeListe tabs.setForeground( Color.BLACK ); tabs.add( "Configuration", new FCD1TunerConfigurationPanel( - mResourceManager, mController ) ); + mSettingsManager, mController ) ); tabs.add( "Info", new FCDTunerDetailsPanel( mController ) ); @@ -95,7 +94,7 @@ public class FCD1TunerEditorPanel extends JPanel implements FrequencyChangeListe @Override public void frequencyChanged( FrequencyChangeEvent event ) { - if( event.getEvent() == Event.FREQUENCY_CHANGE_NOTIFICATION ) + if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE ) { try { diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerConfigurationPanel.java b/src/source/tuner/fcd/proplusV2/FCD2TunerConfigurationPanel.java index bb6acbea..8bd20ac9 100644 --- a/src/source/tuner/fcd/proplusV2/FCD2TunerConfigurationPanel.java +++ b/src/source/tuner/fcd/proplusV2/FCD2TunerConfigurationPanel.java @@ -46,11 +46,11 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerConfiguration; import source.tuner.TunerConfigurationAssignment; import source.tuner.TunerType; -import controller.ResourceManager; public class FCD2TunerConfigurationPanel extends JPanel { @@ -58,7 +58,7 @@ public class FCD2TunerConfigurationPanel extends JPanel LoggerFactory.getLogger( FCD2TunerConfigurationPanel.class ); private static final long serialVersionUID = 1L; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; private FCD2TunerController mController; private FCD2TunerConfiguration mSelectedConfig; private JButton mNewConfiguration; @@ -69,10 +69,10 @@ public class FCD2TunerConfigurationPanel extends JPanel private JCheckBox mMixerGain; private JSpinner mFrequencyCorrection; - public FCD2TunerConfigurationPanel( ResourceManager resourceManager, + public FCD2TunerConfigurationPanel( SettingsManager settingsManager, FCD2TunerController controller ) { - mResourceManager = resourceManager; + mSettingsManager = settingsManager; mController = controller; init(); @@ -218,8 +218,8 @@ public class FCD2TunerConfigurationPanel extends JPanel * Lookup the save config and apply that config to update all of the * controls */ - TunerConfigurationAssignment savedConfig = mResourceManager - .getSettingsManager().getSelectedTunerConfiguration( + TunerConfigurationAssignment savedConfig = mSettingsManager + .getSelectedTunerConfiguration( TunerType.FUNCUBE_DONGLE_PRO_PLUS, mController.getUSBAddress() ); @@ -234,7 +234,7 @@ public class FCD2TunerConfigurationPanel extends JPanel mSelectedConfig = mComboConfigurations.getItemAt( 0 ); //Store this config as the default for this tuner at this address - mResourceManager.getSettingsManager() + mSettingsManager .setSelectedTunerConfiguration( TunerType.FUNCUBE_DONGLE_PRO_PLUS, mController.getUSBAddress(), mSelectedConfig ); @@ -253,7 +253,7 @@ public class FCD2TunerConfigurationPanel extends JPanel public void actionPerformed( ActionEvent e ) { TunerConfiguration config = - mResourceManager.getSettingsManager() + mSettingsManager .addNewTunerConfiguration( TunerType.FUNCUBE_DONGLE_PRO_PLUS, "New Configuration" ); @@ -293,7 +293,7 @@ public class FCD2TunerConfigurationPanel extends JPanel if( n == JOptionPane.YES_OPTION ) { - mResourceManager.getSettingsManager() + mSettingsManager .deleteTunerConfiguration( selected ); mComboConfigurations.setModel( getModel() ); @@ -322,7 +322,7 @@ public class FCD2TunerConfigurationPanel extends JPanel mMixerGain.setSelected( config.getGainMixer() ); mFrequencyCorrection.setValue( config.getFrequencyCorrection() ); - mResourceManager.getSettingsManager().setSelectedTunerConfiguration( + mSettingsManager.setSelectedTunerConfiguration( TunerType.FUNCUBE_DONGLE_PRO_PLUS, mController.getUSBAddress(), config ); } @@ -342,7 +342,7 @@ public class FCD2TunerConfigurationPanel extends JPanel private ComboBoxModel getModel() { ArrayList configs = - mResourceManager.getSettingsManager() + mSettingsManager .getTunerConfigurations( TunerType.FUNCUBE_DONGLE_PRO_PLUS ); DefaultComboBoxModel model = @@ -359,7 +359,7 @@ public class FCD2TunerConfigurationPanel extends JPanel private FCD2TunerConfiguration getNamedConfiguration( String name ) { ArrayList configs = - mResourceManager.getSettingsManager() + mSettingsManager .getTunerConfigurations( TunerType.FUNCUBE_DONGLE_PRO_PLUS ); for( TunerConfiguration config: configs ) @@ -375,6 +375,6 @@ public class FCD2TunerConfigurationPanel extends JPanel private void save() { - mResourceManager.getSettingsManager().save(); + mSettingsManager.save(); } } diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerController.java b/src/source/tuner/fcd/proplusV2/FCD2TunerController.java index 8640ea50..96a31b85 100644 --- a/src/source/tuner/fcd/proplusV2/FCD2TunerController.java +++ b/src/source/tuner/fcd/proplusV2/FCD2TunerController.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import org.usb4java.Device; import org.usb4java.DeviceDescriptor; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerClass; import source.tuner.TunerConfiguration; @@ -34,7 +35,6 @@ import source.tuner.TunerType; import source.tuner.fcd.FCDCommand; import source.tuner.fcd.FCDTuner; import source.tuner.fcd.FCDTunerController; -import controller.ResourceManager; public class FCD2TunerController extends FCDTunerController { @@ -89,11 +89,11 @@ public class FCD2TunerController extends FCDTunerController return TunerType.FUNCUBE_DONGLE_PRO_PLUS; } - public JPanel getEditor( FCDTuner tuner, ResourceManager resourceManager ) + public JPanel getEditor( FCDTuner tuner, SettingsManager settingsManager ) { if( mEditor == null ) { - mEditor = new FCD2TunerEditorPanel( tuner, resourceManager ); + mEditor = new FCD2TunerEditorPanel( tuner, settingsManager ); } return mEditor; diff --git a/src/source/tuner/fcd/proplusV2/FCD2TunerEditorPanel.java b/src/source/tuner/fcd/proplusV2/FCD2TunerEditorPanel.java index e372a544..6f49b4dc 100644 --- a/src/source/tuner/fcd/proplusV2/FCD2TunerEditorPanel.java +++ b/src/source/tuner/fcd/proplusV2/FCD2TunerEditorPanel.java @@ -29,18 +29,17 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import settings.SettingsManager; import source.SourceException; import source.tuner.fcd.FCDTuner; import source.tuner.fcd.FCDTunerDetailsPanel; import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeListener; import source.tuner.frequency.FrequencyChangeEvent.Event; +import source.tuner.frequency.IFrequencyChangeProcessor; import com.jidesoft.swing.JideTabbedPane; -import controller.ResourceManager; - -public class FCD2TunerEditorPanel extends JPanel implements FrequencyChangeListener +public class FCD2TunerEditorPanel extends JPanel implements IFrequencyChangeProcessor { private final static Logger mLog = LoggerFactory.getLogger( FCD2TunerEditorPanel.class ); @@ -48,15 +47,15 @@ public class FCD2TunerEditorPanel extends JPanel implements FrequencyChangeListe private FCDTuner mTuner; private FCD2TunerController mController; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; private JFrequencyControl mFrequencyControl; - public FCD2TunerEditorPanel( FCDTuner tuner, ResourceManager resourceManager ) + public FCD2TunerEditorPanel( FCDTuner tuner, SettingsManager settingsManager ) { mTuner = tuner; mController = (FCD2TunerController)mTuner.getController(); - mResourceManager = resourceManager; + mSettingsManager = settingsManager; initGUI(); } @@ -73,7 +72,7 @@ public class FCD2TunerEditorPanel extends JPanel implements FrequencyChangeListe /* Add frequency control as frequency change listener. This creates a * feedback loop, so the control does not rebroadcast the event */ - mTuner.addListener( mFrequencyControl ); + mTuner.addFrequencyChangeProcessor( mFrequencyControl ); mFrequencyControl.setFrequency( mController.getFrequency(), false ); @@ -84,7 +83,7 @@ public class FCD2TunerEditorPanel extends JPanel implements FrequencyChangeListe tabs.setForeground( Color.BLACK ); tabs.add( "Configuration", new FCD2TunerConfigurationPanel( - mResourceManager, mController ) ); + mSettingsManager, mController ) ); tabs.add( "Info", new FCDTunerDetailsPanel( mController ) ); @@ -94,7 +93,7 @@ public class FCD2TunerEditorPanel extends JPanel implements FrequencyChangeListe @Override public void frequencyChanged( FrequencyChangeEvent event ) { - if( event.getEvent() == Event.FREQUENCY_CHANGE_NOTIFICATION ) + if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE ) { //Disregard the bandwidth, which shouldn't be changing try diff --git a/src/source/tuner/frequency/AutomaticFrequencyControl_R.java b/src/source/tuner/frequency/AutomaticFrequencyControl_R.java deleted file mode 100644 index 994ff0c6..00000000 --- a/src/source/tuner/frequency/AutomaticFrequencyControl_R.java +++ /dev/null @@ -1,181 +0,0 @@ -package source.tuner.frequency; - -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2015 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sample.real.RealSampleListener; -import source.tuner.frequency.FrequencyChangeEvent.Event; -import buffer.FloatAveragingBuffer; - -public class AutomaticFrequencyControl_R extends FrequencyCorrectionControl - implements RealSampleListener -{ - private final static Logger mLog = - LoggerFactory.getLogger( AutomaticFrequencyControl_R.class ); - - private static float LARGE_ERROR = 0.300f; - private static float MEDIUM_ERROR = 0.150f; - private static float SMALL_ERROR = 0.030f; - private static float FINE_ERROR = 0.015f; - - private static int LARGE_FREQUENCY_CORRECTION = 1500; //Hertz - private static int MEDIUM_FREQUENCY_CORRECTION = 500; - private static int SMALL_FREQUENCY_CORRECTION = 100; - private static int FINE_FREQUENCY_CORRECTION = 50; - - private static int SAMPLE_THRESHOLD = 15000; - private Mode mMode = Mode.FAST; - - private FloatAveragingBuffer mBuffer = - new FloatAveragingBuffer( SAMPLE_THRESHOLD ); - - private int mSampleCounter = 0; - private int mSkipCounter = 0; - private int mSkipThreshold = 4; - private int mErrorCorrection = 0; - - public enum Mode - { - NORMAL,FAST; - } - - /** - * Automatic Frequency Control. Monitors DC bias present in an FM - * demodulated audio stream and issues automatic frequency corrections to - * maintain the DC bias close to zero. - * - * @param maximum - maximum allowable (+/-) frequency correction value. - */ - public AutomaticFrequencyControl_R( int maximum ) - { - super( maximum ); - } - - public void dispose() - { - super.dispose(); - - mBuffer = null; - } - - /** - * Resets the error correction value to zero and broadcasts a change event - */ - public void reset() - { - mErrorCorrection = 0; - mMode = Mode.FAST; - dispatch(); - } - - /** - * Broadcasts a frequency correction change event to the registered listener - */ - private void dispatch() - { - if( mListener != null ) - { - mListener.frequencyChanged( - new FrequencyChangeEvent( Event.CHANNEL_FREQUENCY_CORRECTION_CHANGE_NOTIFICATION, - mErrorCorrection ) ); - } - } - - @Override - public void receive( float sample ) - { - if( mMode == Mode.FAST ) - { - float average = mBuffer.get( sample ); - - mSampleCounter++; - - if( mSampleCounter >= SAMPLE_THRESHOLD ) - { - mSampleCounter = 0; - - update( average ); - } - } - else - { - mSkipCounter++; - - if( mSkipCounter >= mSkipThreshold ) - { - mSkipCounter = 0; - - float average = mBuffer.get( sample ); - - mSampleCounter++; - - if( mSampleCounter >= SAMPLE_THRESHOLD ) - { - mSampleCounter = 0; - - update( average ); - } - } - } - } - - private void update( float average ) - { - mMode = Mode.NORMAL; - - if( average > LARGE_ERROR ) - { - adjust( -LARGE_FREQUENCY_CORRECTION ); - mMode = Mode.FAST; - } - else if( average > MEDIUM_ERROR ) - { - adjust( -MEDIUM_FREQUENCY_CORRECTION ); - mMode = Mode.FAST; - } - else if( average > SMALL_ERROR ) - { - adjust( -SMALL_FREQUENCY_CORRECTION ); - } - else if( average > FINE_ERROR ) - { - adjust( -FINE_FREQUENCY_CORRECTION ); - } - else if( average < -LARGE_ERROR ) - { - adjust( LARGE_FREQUENCY_CORRECTION ); - mMode = Mode.FAST; - } - else if( average < -MEDIUM_ERROR ) - { - adjust( MEDIUM_FREQUENCY_CORRECTION ); - mMode = Mode.FAST; - } - else if( average < -SMALL_ERROR ) - { - adjust( SMALL_FREQUENCY_CORRECTION ); - } - else if( average < -FINE_ERROR ) - { - adjust( FINE_FREQUENCY_CORRECTION ); - } - } -} diff --git a/src/source/tuner/frequency/AutomaticFrequencyControl_RB.java b/src/source/tuner/frequency/AutomaticFrequencyControl_RB.java deleted file mode 100644 index 73970ec2..00000000 --- a/src/source/tuner/frequency/AutomaticFrequencyControl_RB.java +++ /dev/null @@ -1,197 +0,0 @@ -package source.tuner.frequency; - -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2015 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sample.Listener; -import sample.real.RealBuffer; -import source.tuner.frequency.FrequencyChangeEvent.Event; -import buffer.FloatAveragingBuffer; - -public class AutomaticFrequencyControl_RB extends FrequencyCorrectionControl - implements Listener -{ - private final static Logger mLog = - LoggerFactory.getLogger( AutomaticFrequencyControl_RB.class ); - - private static float LARGE_ERROR = 0.300f; - private static float MEDIUM_ERROR = 0.150f; - private static float SMALL_ERROR = 0.030f; - private static float FINE_ERROR = 0.015f; - - private static int LARGE_FREQUENCY_CORRECTION = 1500; //Hertz - private static int MEDIUM_FREQUENCY_CORRECTION = 500; - private static int SMALL_FREQUENCY_CORRECTION = 100; - private static int FINE_FREQUENCY_CORRECTION = 50; - - private static int SAMPLE_THRESHOLD = 15000; - private Mode mMode = Mode.FAST; - - private FloatAveragingBuffer mBuffer = - new FloatAveragingBuffer( SAMPLE_THRESHOLD ); - - private int mSampleCounter = 0; - private int mSkipCounter = 0; - private int mSkipThreshold = 4; - private int mErrorCorrection = 0; - - public enum Mode - { - NORMAL,FAST; - } - - /** - * Automatic Frequency Control. Monitors DC bias present in an FM - * demodulated audio stream and issues automatic frequency corrections to - * maintain the DC bias close to zero. - * - * @param maximum - maximum allowable (+/-) frequency correction value. - */ - public AutomaticFrequencyControl_RB( int maximum ) - { - super( maximum ); - } - - public void dispose() - { - super.dispose(); - - mBuffer = null; - } - - /** - * Resets the error correction value to zero and broadcasts a change event - */ - public void reset() - { - super.reset(); - - mMode = Mode.FAST; - dispatch(); - } - - /** - * Broadcasts a frequency correction change event to the registered listener - */ - private void dispatch() - { - if( mListener != null ) - { - mListener.frequencyChanged( - new FrequencyChangeEvent( Event.CHANNEL_FREQUENCY_CORRECTION_CHANGE_NOTIFICATION, - mErrorCorrection ) ); - } - } - - @Override - public void receive( RealBuffer t ) - { - float average = 0.0f; - - for( float sample: t.getSamples() ) - { - average = mBuffer.get( sample ); - } - - //TODO: fix this - -// update( average ); - } - - public void receive( float sample ) - { - if( mMode == Mode.FAST ) - { - float average = mBuffer.get( sample ); - - mSampleCounter++; - - if( mSampleCounter >= SAMPLE_THRESHOLD ) - { - mSampleCounter = 0; - - update( average ); - } - } - else - { - mSkipCounter++; - - if( mSkipCounter >= mSkipThreshold ) - { - mSkipCounter = 0; - - float average = mBuffer.get( sample ); - - mSampleCounter++; - - if( mSampleCounter >= SAMPLE_THRESHOLD ) - { - mSampleCounter = 0; - - update( average ); - } - } - } - } - - private void update( float average ) - { - mMode = Mode.NORMAL; - - if( average > LARGE_ERROR ) - { - adjust( -LARGE_FREQUENCY_CORRECTION ); - mMode = Mode.FAST; - } - else if( average > MEDIUM_ERROR ) - { - adjust( -MEDIUM_FREQUENCY_CORRECTION ); - mMode = Mode.FAST; - } - else if( average > SMALL_ERROR ) - { - adjust( -SMALL_FREQUENCY_CORRECTION ); - } - else if( average > FINE_ERROR ) - { - adjust( -FINE_FREQUENCY_CORRECTION ); - } - else if( average < -LARGE_ERROR ) - { - adjust( LARGE_FREQUENCY_CORRECTION ); - mMode = Mode.FAST; - } - else if( average < -MEDIUM_ERROR ) - { - adjust( MEDIUM_FREQUENCY_CORRECTION ); - mMode = Mode.FAST; - } - else if( average < -SMALL_ERROR ) - { - adjust( SMALL_FREQUENCY_CORRECTION ); - } - else if( average < -FINE_ERROR ) - { - adjust( FINE_FREQUENCY_CORRECTION ); - } - } -} diff --git a/src/source/tuner/frequency/DCTrackingFrequencyControl.java b/src/source/tuner/frequency/DCTrackingFrequencyControl.java new file mode 100644 index 00000000..17704dfd --- /dev/null +++ b/src/source/tuner/frequency/DCTrackingFrequencyControl.java @@ -0,0 +1,135 @@ +package source.tuner.frequency; + +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2016 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + ******************************************************************************/ + +import module.Module; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sample.Listener; +import sample.real.RealBuffer; +import source.tuner.frequency.FrequencyChangeEvent.Event; + +public class DCTrackingFrequencyControl extends Module + implements Listener, IFrequencyChangeProvider +{ + private final static Logger mLog = + LoggerFactory.getLogger( DCTrackingFrequencyControl.class ); + + private static float COARSE_THRESHOLD = 0.1f; + private static float LOOP_GAIN = 0.1f; + private static float COARSE_GAIN = 1000.0f; + private static float FINE_GAIN = 100.0f; + private int mErrorCorrection = 0; + private int mMaximumFrequencyCorrection = 0; + private Listener mListener; + + /** + * DC offset Automatic Frequency Control. Monitors DC bias present in an FM + * demodulated audio stream and issues automatic frequency corrections to + * maintain the DC bias close to zero. + * + * @param maximum - maximum allowable (+/-) frequency correction value. + */ + public DCTrackingFrequencyControl( int maximum ) + { + mMaximumFrequencyCorrection = maximum; + } + + public void dispose() + { + mListener = null; + } + + @Override + public void receive( RealBuffer buffer ) + { + double sum = 0.0d; + + for( float sample: buffer.getSamples() ) + { + sum += sample; + } + + float mean = (float)( sum / (double)buffer.getSamples().length ); + + float adjustment = 0.0f; + + if( mean > COARSE_THRESHOLD || mean < - COARSE_THRESHOLD ) + { + adjustment = mean * COARSE_GAIN; + } + else + { + adjustment = mean * FINE_GAIN; + } + + setErrorCorrection( mErrorCorrection + (int)( (float)adjustment * LOOP_GAIN ) ); + } + + private void setErrorCorrection( int correction ) + { + if( mListener != null ) + { + if( correction > mMaximumFrequencyCorrection ) + { + correction = mMaximumFrequencyCorrection; + } + else if( correction < -mMaximumFrequencyCorrection ) + { + correction = -mMaximumFrequencyCorrection; + } + + if( correction != mErrorCorrection ) + { + mListener.receive( new FrequencyChangeEvent( + Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, correction ) ); + } + } + } + + @Override + public void setFrequencyChangeListener( Listener listener ) + { + mListener = listener; + } + + @Override + public void removeFrequencyChangeListener() + { + mListener = null; + } + + @Override + public void start() + { + } + + @Override + public void stop() + { + } + + @Override + public void reset() + { + setErrorCorrection( 0 ); + } +} diff --git a/src/source/tuner/frequency/FrequencyChangeBroadcaster.java b/src/source/tuner/frequency/FrequencyChangeBroadcaster.java deleted file mode 100644 index 89c03bf6..00000000 --- a/src/source/tuner/frequency/FrequencyChangeBroadcaster.java +++ /dev/null @@ -1,26 +0,0 @@ -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -package source.tuner.frequency; - - -public interface FrequencyChangeBroadcaster -{ - public abstract void addListener( FrequencyChangeListener listener ); - - public abstract void removeListener( FrequencyChangeListener listener ); -} diff --git a/src/source/tuner/frequency/FrequencyChangeEvent.java b/src/source/tuner/frequency/FrequencyChangeEvent.java index 87f82f08..afadd058 100644 --- a/src/source/tuner/frequency/FrequencyChangeEvent.java +++ b/src/source/tuner/frequency/FrequencyChangeEvent.java @@ -4,11 +4,12 @@ public class FrequencyChangeEvent { public enum Event { - FREQUENCY_CHANGE_NOTIFICATION, - FREQUENCY_CORRECTION_CHANGE_NOTIFICATION, - CHANNEL_FREQUENCY_CORRECTION_CHANGE_NOTIFICATION, - CHANNEL_FREQUENCY_CORRECTION_CHANGE_REQUEST, - SAMPLE_RATE_CHANGE_NOTIFICATION; + NOTIFICATION_FREQUENCY_CHANGE, + NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, + NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE, + NOTIFICATION_SAMPLE_RATE_CHANGE, + + REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE; } private Event mEvent; diff --git a/src/source/tuner/frequency/FrequencyChangeProcessorWrapper.java b/src/source/tuner/frequency/FrequencyChangeProcessorWrapper.java new file mode 100644 index 00000000..1b96ae8b --- /dev/null +++ b/src/source/tuner/frequency/FrequencyChangeProcessorWrapper.java @@ -0,0 +1,32 @@ +package source.tuner.frequency; + +import sample.Listener; + +public class FrequencyChangeProcessorWrapper + implements IFrequencyChangeListener, Listener +{ + private IFrequencyChangeProcessor mProcessor; + + /** + * Provides IFrequencyChangeListener wrapper for an IFrequencyChangeProcessor + */ + public FrequencyChangeProcessorWrapper( IFrequencyChangeProcessor handler ) + { + mProcessor = handler; + } + + @Override + public Listener getFrequencyChangeListener() + { + return this; + } + + @Override + public void receive( FrequencyChangeEvent event ) + { + if( mProcessor != null ) + { + mProcessor.frequencyChanged( event ); + } + } +} diff --git a/src/source/tuner/frequency/FrequencyController.java b/src/source/tuner/frequency/FrequencyController.java index 51bab16f..f1c6753d 100644 --- a/src/source/tuner/frequency/FrequencyController.java +++ b/src/source/tuner/frequency/FrequencyController.java @@ -37,8 +37,7 @@ public class FrequencyController private double mFrequencyCorrection = 0d; private int mSampleRate = 0; - private ArrayList mListeners = - new ArrayList(); + private ArrayList mProcessors = new ArrayList<>(); public FrequencyController( Tunable tunable, long minFrequency, @@ -181,20 +180,21 @@ public class FrequencyController /** * Adds listener to receive frequency change events */ - public void addListener( FrequencyChangeListener listener ) +//TODO: rename this to addFrequencyChangeProcessor() + public void addListener( IFrequencyChangeProcessor processor ) { - if( !mListeners.contains( listener ) ) + if( !mProcessors.contains( processor ) ) { - mListeners.add( listener ); + mProcessors.add( processor ); } } /** * Removes listener from receiving frequency change events */ - public void removeListener( FrequencyChangeListener listener ) + public void removeFrequencyChangeProcessor( IFrequencyChangeProcessor processor ) { - mListeners.remove( listener ); + mProcessors.remove( processor ); } @@ -205,7 +205,7 @@ public class FrequencyController protected void broadcastFrequencyChange() { broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.FREQUENCY_CHANGE_NOTIFICATION, mFrequency ) ); + new FrequencyChangeEvent( Event.NOTIFICATION_FREQUENCY_CHANGE, mFrequency ) ); } /** @@ -214,7 +214,7 @@ public class FrequencyController protected void broadcastFrequencyErrorChange() { broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.FREQUENCY_CORRECTION_CHANGE_NOTIFICATION, mFrequencyCorrection ) ); + new FrequencyChangeEvent( Event.NOTIFICATION_FREQUENCY_CORRECTION_CHANGE, mFrequencyCorrection ) ); } /** @@ -223,14 +223,14 @@ public class FrequencyController protected void broadcastSampleRateChange() { broadcastFrequencyChangeEvent( - new FrequencyChangeEvent( Event.SAMPLE_RATE_CHANGE_NOTIFICATION, mSampleRate ) ); + new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, mSampleRate ) ); } public void broadcastFrequencyChangeEvent( FrequencyChangeEvent event ) { - for( FrequencyChangeListener listener: mListeners ) + for( IFrequencyChangeProcessor processor: mProcessors ) { - listener.frequencyChanged( event ); + processor.frequencyChanged( event ); } } diff --git a/src/source/tuner/frequency/FrequencyCorrectionControl.java b/src/source/tuner/frequency/FrequencyCorrectionControl.java deleted file mode 100644 index 77255bb0..00000000 --- a/src/source/tuner/frequency/FrequencyCorrectionControl.java +++ /dev/null @@ -1,161 +0,0 @@ -package source.tuner.frequency; - -/******************************************************************************* - * SDR Trunk - * Copyright (C) 2015 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - ******************************************************************************/ -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import source.tuner.frequency.FrequencyChangeEvent.Event; - -/** - * Frequency control for providing frequency error correction to a tuned channel - */ -public class FrequencyCorrectionControl implements FrequencyChangeListener -{ - private final static Logger mLog = - LoggerFactory.getLogger( FrequencyCorrectionControl.class ); - - protected FrequencyChangeListener mListener; - protected FrequencyCorrectionResetListener mResetListener; - - protected int mChannelFrequencyCorrection = 0; - private int mMaximumCorrection = 3000; - - /** - * Frequency correction controller. - * - * @param maximum - defines the maximum +/- allowable frequency correction - * value - */ - public FrequencyCorrectionControl( int maximum ) - { - mMaximumCorrection = maximum; - } - - /** - * Cleanup - */ - public void dispose() - { - mListener = null; - mResetListener = null; - } - - /** - * Registers a listener to receive frequency correction events - */ - public void setFrequencyChangeListener( FrequencyChangeListener listener ) - { - mListener = listener; - } - - /** - * Registers a listener to receive frequency correction resets - */ - public void setListener( FrequencyCorrectionResetListener listener ) - { - mResetListener = listener; - } - - /** - * Listener for source frequency or frequency offset value changes to - * reset this frequency controller value to zero. - */ - @Override - public void frequencyChanged( FrequencyChangeEvent event ) - { - Event attribute = event.getEvent(); - - switch( attribute ) - { - //Direct the listener to reset when frequency correction (PPM) or - //sample rate changes - case FREQUENCY_CORRECTION_CHANGE_NOTIFICATION: - case SAMPLE_RATE_CHANGE_NOTIFICATION: - reset(); - break; - default: - break; - } - } - - /** - * Resets frequency correction value to zero and notifies any reset - * listeners - */ - public void reset() - { - if( mResetListener != null ) - { - mResetListener.resetFrequencyCorrection(); - } - } - - /** - * Sets frequency correction to the specified value and broadcasts a change - * request - */ - public void setFrequencyCorrection( int correction ) - { - mChannelFrequencyCorrection = correction; - - /* Limit frequency correction to +/- max correction value */ - if( mChannelFrequencyCorrection > mMaximumCorrection ) - { - mChannelFrequencyCorrection = mMaximumCorrection; - } - else if( mChannelFrequencyCorrection < -mMaximumCorrection ) - { - mChannelFrequencyCorrection = -mMaximumCorrection; - } - - /* Broadcast a change request */ - if( mListener != null ) - { - mListener.frequencyChanged( new FrequencyChangeEvent( - Event.CHANNEL_FREQUENCY_CORRECTION_CHANGE_REQUEST, - mChannelFrequencyCorrection ) ); - } - } - - /** - * Increments the frequency error correction value by the specified adjustment - */ - public void adjust( int adjustment ) - { - setFrequencyCorrection( getErrorCorrection() + adjustment ); - } - - /** - * Current frequency error correction value - * @return - */ - public int getErrorCorrection() - { - return mChannelFrequencyCorrection; - } - - /** - * Listener interface to be notified when this control is reset to a zero - * frequency correction value - */ - public interface FrequencyCorrectionResetListener - { - public void resetFrequencyCorrection(); - } -} diff --git a/src/source/tuner/frequency/FrequencyChangeListener.java b/src/source/tuner/frequency/IFrequencyChangeListener.java similarity index 90% rename from src/source/tuner/frequency/FrequencyChangeListener.java rename to src/source/tuner/frequency/IFrequencyChangeListener.java index fab9f28a..99b75804 100644 --- a/src/source/tuner/frequency/FrequencyChangeListener.java +++ b/src/source/tuner/frequency/IFrequencyChangeListener.java @@ -17,6 +17,8 @@ ******************************************************************************/ package source.tuner.frequency; +import sample.Listener; + /** * Interface for broadcasting changes to frequency, bandwidth, sample rate, @@ -26,7 +28,7 @@ package source.tuner.frequency; * frequency value. All frequency correction aspects should be handled within * the device that implements the tuning of the frequency. */ -public interface FrequencyChangeListener +public interface IFrequencyChangeListener { - public void frequencyChanged( FrequencyChangeEvent event ); + public Listener getFrequencyChangeListener(); } diff --git a/src/source/tuner/frequency/IFrequencyChangeProcessor.java b/src/source/tuner/frequency/IFrequencyChangeProcessor.java new file mode 100644 index 00000000..479d8e8d --- /dev/null +++ b/src/source/tuner/frequency/IFrequencyChangeProcessor.java @@ -0,0 +1,6 @@ +package source.tuner.frequency; + +public interface IFrequencyChangeProcessor +{ + public void frequencyChanged( FrequencyChangeEvent event ); +} diff --git a/src/source/tuner/frequency/IFrequencyChangeProvider.java b/src/source/tuner/frequency/IFrequencyChangeProvider.java new file mode 100644 index 00000000..919126e7 --- /dev/null +++ b/src/source/tuner/frequency/IFrequencyChangeProvider.java @@ -0,0 +1,9 @@ +package source.tuner.frequency; + +import sample.Listener; + +public interface IFrequencyChangeProvider +{ + public void setFrequencyChangeListener( Listener listener ); + public void removeFrequencyChangeListener(); +} diff --git a/src/source/tuner/frequency/IFrequencyCorrectionController.java b/src/source/tuner/frequency/IFrequencyCorrectionController.java deleted file mode 100644 index e937bd4a..00000000 --- a/src/source/tuner/frequency/IFrequencyCorrectionController.java +++ /dev/null @@ -1,8 +0,0 @@ -package source.tuner.frequency; - - -public interface IFrequencyCorrectionController -{ - public boolean hasFrequencyCorrectionControl(); - public FrequencyCorrectionControl getFrequencyCorrectionControl(); -} diff --git a/src/source/tuner/hackrf/HackRFTuner.java b/src/source/tuner/hackrf/HackRFTuner.java index 4b38c4bd..154c55bc 100644 --- a/src/source/tuner/hackrf/HackRFTuner.java +++ b/src/source/tuner/hackrf/HackRFTuner.java @@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory; import sample.Listener; import sample.complex.ComplexBuffer; +import settings.SettingsManager; import source.SourceException; import source.tuner.Tuner; import source.tuner.TunerChannel; @@ -34,7 +35,6 @@ import source.tuner.TunerClass; import source.tuner.TunerConfiguration; import source.tuner.TunerType; import source.tuner.hackrf.HackRFTunerController.BoardID; -import controller.ResourceManager; import controller.ThreadPoolManager; public class HackRFTuner extends Tuner @@ -76,9 +76,9 @@ public class HackRFTuner extends Tuner } @Override - public JPanel getEditor( ResourceManager resourceManager ) + public JPanel getEditor( SettingsManager settingsManager ) { - return new HackRFTunerEditorPanel( this, resourceManager ); + return new HackRFTunerEditorPanel( this, settingsManager ); } @Override diff --git a/src/source/tuner/hackrf/HackRFTunerConfigurationPanel.java b/src/source/tuner/hackrf/HackRFTunerConfigurationPanel.java index b5ec8acb..1acd7269 100644 --- a/src/source/tuner/hackrf/HackRFTunerConfigurationPanel.java +++ b/src/source/tuner/hackrf/HackRFTunerConfigurationPanel.java @@ -48,6 +48,7 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerConfiguration; import source.tuner.TunerConfigurationAssignment; @@ -55,7 +56,6 @@ import source.tuner.TunerType; import source.tuner.hackrf.HackRFTunerController.HackRFLNAGain; import source.tuner.hackrf.HackRFTunerController.HackRFSampleRate; import source.tuner.hackrf.HackRFTunerController.HackRFVGAGain; -import controller.ResourceManager; public class HackRFTunerConfigurationPanel extends JPanel { @@ -64,7 +64,7 @@ public class HackRFTunerConfigurationPanel extends JPanel private static final long serialVersionUID = 1L; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; private HackRFTunerController mController; private HackRFTunerConfiguration mSelectedConfig; @@ -81,10 +81,10 @@ public class HackRFTunerConfigurationPanel extends JPanel private JComboBox mComboSampleRate; - public HackRFTunerConfigurationPanel( ResourceManager resourceManager, + public HackRFTunerConfigurationPanel( SettingsManager settingsManager, HackRFTunerController controller ) { - mResourceManager = resourceManager; + mSettingsManager = settingsManager; mController = controller; init(); @@ -119,8 +119,7 @@ public class HackRFTunerConfigurationPanel extends JPanel if( serial != null ) { - savedConfig = mResourceManager - .getSettingsManager().getSelectedTunerConfiguration( + savedConfig = mSettingsManager.getSelectedTunerConfiguration( TunerType.HACKRF, serial ); } @@ -139,7 +138,7 @@ public class HackRFTunerConfigurationPanel extends JPanel if( serial != null ) { - mResourceManager.getSettingsManager() + mSettingsManager .setSelectedTunerConfiguration( TunerType.HACKRF, serial, mSelectedConfig ); } @@ -418,7 +417,7 @@ public class HackRFTunerConfigurationPanel extends JPanel public void actionPerformed( ActionEvent e ) { TunerConfiguration config = - mResourceManager.getSettingsManager() + mSettingsManager .addNewTunerConfiguration( TunerType.HACKRF, "New Configuration" ); @@ -458,7 +457,7 @@ public class HackRFTunerConfigurationPanel extends JPanel if( n == JOptionPane.YES_OPTION ) { - mResourceManager.getSettingsManager() + mSettingsManager .deleteTunerConfiguration( selected ); mComboConfigurations.setModel( getModel() ); @@ -494,7 +493,7 @@ public class HackRFTunerConfigurationPanel extends JPanel mComboSampleRate.setSelectedItem( mSelectedConfig.getSampleRate() ); - mResourceManager.getSettingsManager().setSelectedTunerConfiguration( + mSettingsManager.setSelectedTunerConfiguration( TunerType.HACKRF, mController.getSerial().getSerialNumber(), config ); } catch ( UsbException | SourceException e1 ) @@ -518,7 +517,7 @@ public class HackRFTunerConfigurationPanel extends JPanel private ComboBoxModel getModel() { ArrayList configs = - mResourceManager.getSettingsManager() + mSettingsManager .getTunerConfigurations( TunerType.HACKRF ); DefaultComboBoxModel model = @@ -538,7 +537,7 @@ public class HackRFTunerConfigurationPanel extends JPanel private HackRFTunerConfiguration getNamedConfiguration( String name ) { ArrayList configs = - mResourceManager.getSettingsManager() + mSettingsManager .getTunerConfigurations( TunerType.HACKRF ); for( TunerConfiguration config: configs ) @@ -557,6 +556,6 @@ public class HackRFTunerConfigurationPanel extends JPanel */ private void save() { - mResourceManager.getSettingsManager().save(); + mSettingsManager.save(); } } diff --git a/src/source/tuner/hackrf/HackRFTunerEditorPanel.java b/src/source/tuner/hackrf/HackRFTunerEditorPanel.java index 9c4d8490..fe32f16a 100644 --- a/src/source/tuner/hackrf/HackRFTunerEditorPanel.java +++ b/src/source/tuner/hackrf/HackRFTunerEditorPanel.java @@ -30,18 +30,16 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import settings.SettingsManager; import source.SourceException; import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeListener; import source.tuner.frequency.FrequencyChangeEvent.Event; +import source.tuner.frequency.IFrequencyChangeProcessor; import source.tuner.hackrf.HackRFTunerController.BoardID; import com.jidesoft.swing.JideTabbedPane; -import controller.ResourceManager; - -public class HackRFTunerEditorPanel extends JPanel - implements FrequencyChangeListener +public class HackRFTunerEditorPanel extends JPanel implements IFrequencyChangeProcessor { private static final long serialVersionUID = 1L; @@ -52,14 +50,14 @@ public class HackRFTunerEditorPanel extends JPanel private HackRFTuner mTuner; private HackRFTunerController mController; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; public HackRFTunerEditorPanel( HackRFTuner tuner, - ResourceManager resourceManager ) + SettingsManager resourceManager ) { mTuner = tuner; mController = mTuner.getController(); - mResourceManager = resourceManager; + mSettingsManager = resourceManager; initGUI(); } @@ -89,7 +87,7 @@ public class HackRFTunerEditorPanel extends JPanel /* Add frequency control as frequency change listener. This creates a * feedback loop, so the control does not rebroadcast the event */ - mTuner.addListener( mFrequencyControl ); + mTuner.addFrequencyChangeProcessor( mFrequencyControl ); mFrequencyControl.setFrequency( mController.getFrequency(), false ); @@ -102,7 +100,7 @@ public class HackRFTunerEditorPanel extends JPanel add( tabs, "span,grow,push" ); tabs.add( "Config", new HackRFTunerConfigurationPanel( - mResourceManager, mTuner.getController() ) ); + mSettingsManager, mTuner.getController() ) ); tabs.add( "Info", new HackRFInformationPanel( mTuner.getController() ) ); } @@ -110,7 +108,7 @@ public class HackRFTunerEditorPanel extends JPanel @Override public void frequencyChanged( FrequencyChangeEvent event ) { - if( event.getEvent() == Event.FREQUENCY_CHANGE_NOTIFICATION ) + if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE ) { try { diff --git a/src/source/tuner/rtl/RTL2832Tuner.java b/src/source/tuner/rtl/RTL2832Tuner.java index 02634918..4af1a8d7 100644 --- a/src/source/tuner/rtl/RTL2832Tuner.java +++ b/src/source/tuner/rtl/RTL2832Tuner.java @@ -27,6 +27,7 @@ import org.usb4java.LibUsbException; import sample.Listener; import sample.complex.ComplexBuffer; +import settings.SettingsManager; import source.SourceException; import source.tuner.Tuner; import source.tuner.TunerChannel; @@ -34,9 +35,8 @@ import source.tuner.TunerChannelSource; import source.tuner.TunerClass; import source.tuner.TunerConfiguration; import source.tuner.TunerType; -import source.tuner.frequency.FrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeListener; import source.tuner.rtl.RTL2832TunerController.SampleRate; -import controller.ResourceManager; import controller.ThreadPoolManager; public class RTL2832Tuner extends Tuner @@ -57,7 +57,7 @@ public class RTL2832Tuner extends Tuner mTunerClass = tunerClass; mController = controller; - mController.addListener( (FrequencyChangeListener)this ); + mController.addListener( this ); } public void dispose() @@ -89,9 +89,9 @@ public class RTL2832Tuner extends Tuner } @Override - public JPanel getEditor( ResourceManager resourceManager ) + public JPanel getEditor( SettingsManager settingsManager ) { - return mController.getEditor( resourceManager ); + return mController.getEditor( settingsManager ); } @Override diff --git a/src/source/tuner/rtl/RTL2832TunerController.java b/src/source/tuner/rtl/RTL2832TunerController.java index d419c782..e972c1d0 100644 --- a/src/source/tuner/rtl/RTL2832TunerController.java +++ b/src/source/tuner/rtl/RTL2832TunerController.java @@ -52,13 +52,11 @@ import sample.Broadcaster; import sample.Listener; import sample.adapter.ByteSampleAdapter; import sample.complex.ComplexBuffer; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerController; import source.tuner.TunerType; -import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeEvent.Event; import buffer.FloatAveragingBuffer; -import controller.ResourceManager; import controller.ThreadPoolManager; import controller.ThreadPoolManager.ThreadType; @@ -364,7 +362,7 @@ public abstract class RTL2832TunerController extends TunerController } } - public abstract JPanel getEditor( ResourceManager resourceManager ); + public abstract JPanel getEditor( SettingsManager settingsManager ); public abstract void setSampleRateFilters( int sampleRate ) throws SourceException; diff --git a/src/source/tuner/rtl/e4k/E4KTunerConfigurationPanel.java b/src/source/tuner/rtl/e4k/E4KTunerConfigurationPanel.java index cce675b4..ed3a4ac7 100644 --- a/src/source/tuner/rtl/e4k/E4KTunerConfigurationPanel.java +++ b/src/source/tuner/rtl/e4k/E4KTunerConfigurationPanel.java @@ -48,6 +48,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.LibUsbException; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerConfiguration; import source.tuner.TunerConfigurationAssignment; @@ -57,7 +58,6 @@ import source.tuner.rtl.e4k.E4KTunerController.E4KEnhanceGain; import source.tuner.rtl.e4k.E4KTunerController.E4KGain; import source.tuner.rtl.e4k.E4KTunerController.E4KLNAGain; import source.tuner.rtl.e4k.E4KTunerController.E4KMixerGain; -import controller.ResourceManager; public class E4KTunerConfigurationPanel extends JPanel { @@ -65,7 +65,7 @@ public class E4KTunerConfigurationPanel extends JPanel private final static Logger mLog = LoggerFactory.getLogger( E4KTunerConfigurationPanel.class ); - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; private E4KTunerController mController; private E4KTunerConfiguration mSelectedConfig; @@ -84,10 +84,10 @@ public class E4KTunerConfigurationPanel extends JPanel private JComboBox mComboSampleRate; - public E4KTunerConfigurationPanel( ResourceManager resourceManager, + public E4KTunerConfigurationPanel( SettingsManager settingsManager, E4KTunerController controller ) { - mResourceManager = resourceManager; + mSettingsManager = settingsManager; mController = controller; init(); @@ -108,8 +108,8 @@ public class E4KTunerConfigurationPanel extends JPanel mComboConfigurations.setModel( getModel() ); /* Determine which tuner configuration should be selected/displayed */ - TunerConfigurationAssignment savedConfig = mResourceManager - .getSettingsManager().getSelectedTunerConfiguration( + TunerConfigurationAssignment savedConfig = mSettingsManager + .getSelectedTunerConfiguration( TunerType.ELONICS_E4000, mController.getUniqueID() ); if( savedConfig != null ) @@ -124,7 +124,7 @@ public class E4KTunerConfigurationPanel extends JPanel mSelectedConfig = mComboConfigurations.getItemAt( 0 ); //Store this config as the default for this tuner at this address - mResourceManager.getSettingsManager() + mSettingsManager .setSelectedTunerConfiguration( TunerType.ELONICS_E4000, mController.getUniqueID(), mSelectedConfig ); @@ -168,7 +168,7 @@ public class E4KTunerConfigurationPanel extends JPanel /* Reset this named config as the assigned config for this * tuner */ - mResourceManager.getSettingsManager() + mSettingsManager .setSelectedTunerConfiguration( TunerType.ELONICS_E4000, mController.getUniqueID(), mSelectedConfig ); @@ -542,7 +542,7 @@ public class E4KTunerConfigurationPanel extends JPanel public void actionPerformed( ActionEvent e ) { TunerConfiguration config = - mResourceManager.getSettingsManager() + mSettingsManager .addNewTunerConfiguration( TunerType.ELONICS_E4000, "New Configuration" ); @@ -582,7 +582,7 @@ public class E4KTunerConfigurationPanel extends JPanel if( n == JOptionPane.YES_OPTION ) { - mResourceManager.getSettingsManager() + mSettingsManager .deleteTunerConfiguration( selected ); mComboConfigurations.setModel( getModel() ); @@ -623,7 +623,7 @@ public class E4KTunerConfigurationPanel extends JPanel mComboSampleRate.setSelectedItem( mSelectedConfig.getSampleRate() ); - mResourceManager.getSettingsManager().setSelectedTunerConfiguration( + mSettingsManager.setSelectedTunerConfiguration( TunerType.ELONICS_E4000, mController.getUniqueID(), config ); } catch ( SourceException e1 ) @@ -647,7 +647,7 @@ public class E4KTunerConfigurationPanel extends JPanel private ComboBoxModel getModel() { ArrayList configs = - mResourceManager.getSettingsManager() + mSettingsManager .getTunerConfigurations( TunerType.ELONICS_E4000 ); DefaultComboBoxModel model = @@ -667,7 +667,7 @@ public class E4KTunerConfigurationPanel extends JPanel private E4KTunerConfiguration getNamedConfiguration( String name ) { ArrayList configs = - mResourceManager.getSettingsManager() + mSettingsManager .getTunerConfigurations( TunerType.ELONICS_E4000 ); for( TunerConfiguration config: configs ) @@ -686,6 +686,6 @@ public class E4KTunerConfigurationPanel extends JPanel */ private void save() { - mResourceManager.getSettingsManager().save(); + mSettingsManager.save(); } } diff --git a/src/source/tuner/rtl/e4k/E4KTunerController.java b/src/source/tuner/rtl/e4k/E4KTunerController.java index 6bdfb149..b727fa74 100644 --- a/src/source/tuner/rtl/e4k/E4KTunerController.java +++ b/src/source/tuner/rtl/e4k/E4KTunerController.java @@ -30,11 +30,11 @@ import org.usb4java.DeviceHandle; import org.usb4java.LibUsb; import org.usb4java.LibUsbException; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerConfiguration; import source.tuner.TunerType; import source.tuner.rtl.RTL2832TunerController; -import controller.ResourceManager; import controller.ThreadPoolManager; public class E4KTunerController extends RTL2832TunerController @@ -252,11 +252,11 @@ public class E4KTunerController extends RTL2832TunerController } } - public JPanel getEditor( ResourceManager resourceManager ) + public JPanel getEditor( SettingsManager settingsManager ) { if( mEditor == null ) { - mEditor = new E4KTunerEditorPanel( this, resourceManager ); + mEditor = new E4KTunerEditorPanel( this, settingsManager ); } return mEditor; diff --git a/src/source/tuner/rtl/e4k/E4KTunerEditorPanel.java b/src/source/tuner/rtl/e4k/E4KTunerEditorPanel.java index 5c883748..fe797d55 100644 --- a/src/source/tuner/rtl/e4k/E4KTunerEditorPanel.java +++ b/src/source/tuner/rtl/e4k/E4KTunerEditorPanel.java @@ -30,17 +30,16 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import settings.SettingsManager; import source.SourceException; import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeListener; import source.tuner.frequency.FrequencyChangeEvent.Event; +import source.tuner.frequency.IFrequencyChangeProcessor; import source.tuner.rtl.RTL2832InfoPanel; import com.jidesoft.swing.JideTabbedPane; -import controller.ResourceManager; - -public class E4KTunerEditorPanel extends JPanel implements FrequencyChangeListener +public class E4KTunerEditorPanel extends JPanel implements IFrequencyChangeProcessor { private final static Logger mLog = LoggerFactory.getLogger( E4KTunerEditorPanel.class ); @@ -48,15 +47,15 @@ public class E4KTunerEditorPanel extends JPanel implements FrequencyChangeListen private static final long serialVersionUID = 1L; private E4KTunerController mController; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; private JFrequencyControl mFrequencyControl; public E4KTunerEditorPanel( E4KTunerController controller, - ResourceManager resourceManager ) + SettingsManager settingsManager ) { mController = controller; - mResourceManager = resourceManager; + mSettingsManager = settingsManager; initGUI(); } @@ -84,7 +83,7 @@ public class E4KTunerEditorPanel extends JPanel implements FrequencyChangeListen tabs.setForeground( Color.BLACK ); tabs.addTab( "Configuration", - new E4KTunerConfigurationPanel( mResourceManager, mController ) ); + new E4KTunerConfigurationPanel( mSettingsManager, mController ) ); tabs.addTab( "Info", new RTL2832InfoPanel( mController ) ); @@ -98,7 +97,7 @@ public class E4KTunerEditorPanel extends JPanel implements FrequencyChangeListen @Override public void frequencyChanged( FrequencyChangeEvent event ) { - if( event.getEvent() == Event.FREQUENCY_CHANGE_NOTIFICATION ) + if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE ) { final long frequency = event.getValue().longValue(); diff --git a/src/source/tuner/rtl/r820t/R820TTunerConfigurationPanel.java b/src/source/tuner/rtl/r820t/R820TTunerConfigurationPanel.java index fe08d0c3..413dc8ea 100644 --- a/src/source/tuner/rtl/r820t/R820TTunerConfigurationPanel.java +++ b/src/source/tuner/rtl/r820t/R820TTunerConfigurationPanel.java @@ -48,6 +48,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.LibUsbException; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerConfiguration; import source.tuner.TunerConfigurationAssignment; @@ -57,7 +58,6 @@ import source.tuner.rtl.r820t.R820TTunerController.R820TGain; import source.tuner.rtl.r820t.R820TTunerController.R820TLNAGain; import source.tuner.rtl.r820t.R820TTunerController.R820TMixerGain; import source.tuner.rtl.r820t.R820TTunerController.R820TVGAGain; -import controller.ResourceManager; public class R820TTunerConfigurationPanel extends JPanel { @@ -67,7 +67,7 @@ public class R820TTunerConfigurationPanel extends JPanel private static final long serialVersionUID = 1L; private static final R820TGain DEFAULT_GAIN = R820TGain.GAIN_279; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; private R820TTunerController mController; private R820TTunerConfiguration mSelectedConfig; @@ -86,10 +86,10 @@ public class R820TTunerConfigurationPanel extends JPanel private JComboBox mComboSampleRate; - public R820TTunerConfigurationPanel( ResourceManager resourceManager, + public R820TTunerConfigurationPanel( SettingsManager settingsManager, R820TTunerController controller ) { - mResourceManager = resourceManager; + mSettingsManager = settingsManager; mController = controller; init(); @@ -110,8 +110,8 @@ public class R820TTunerConfigurationPanel extends JPanel mComboConfigurations.setModel( getModel() ); /* Determine which tuner configuration should be selected/displayed */ - TunerConfigurationAssignment savedConfig = mResourceManager - .getSettingsManager().getSelectedTunerConfiguration( + TunerConfigurationAssignment savedConfig = mSettingsManager + .getSelectedTunerConfiguration( TunerType.RAFAELMICRO_R820T, mController.getUniqueID() ); if( savedConfig != null ) @@ -126,7 +126,7 @@ public class R820TTunerConfigurationPanel extends JPanel mSelectedConfig = mComboConfigurations.getItemAt( 0 ); //Store this config as the default for this tuner at this address - mResourceManager.getSettingsManager() + mSettingsManager .setSelectedTunerConfiguration( TunerType.RAFAELMICRO_R820T, mController.getUniqueID(), mSelectedConfig ); @@ -549,7 +549,7 @@ public class R820TTunerConfigurationPanel extends JPanel public void actionPerformed( ActionEvent e ) { TunerConfiguration config = - mResourceManager.getSettingsManager() + mSettingsManager .addNewTunerConfiguration( TunerType.RAFAELMICRO_R820T, "New Configuration" ); @@ -589,7 +589,7 @@ public class R820TTunerConfigurationPanel extends JPanel if( n == JOptionPane.YES_OPTION ) { - mResourceManager.getSettingsManager() + mSettingsManager .deleteTunerConfiguration( selected ); mComboConfigurations.setModel( getModel() ); @@ -630,7 +630,7 @@ public class R820TTunerConfigurationPanel extends JPanel mComboSampleRate.setSelectedItem( mSelectedConfig.getSampleRate() ); - mResourceManager.getSettingsManager().setSelectedTunerConfiguration( + mSettingsManager.setSelectedTunerConfiguration( TunerType.RAFAELMICRO_R820T, mController.getUniqueID(), config ); } catch ( SourceException e1 ) @@ -654,7 +654,7 @@ public class R820TTunerConfigurationPanel extends JPanel private ComboBoxModel getModel() { ArrayList configs = - mResourceManager.getSettingsManager() + mSettingsManager .getTunerConfigurations( TunerType.RAFAELMICRO_R820T ); DefaultComboBoxModel model = @@ -674,7 +674,7 @@ public class R820TTunerConfigurationPanel extends JPanel private R820TTunerConfiguration getNamedConfiguration( String name ) { ArrayList configs = - mResourceManager.getSettingsManager() + mSettingsManager .getTunerConfigurations( TunerType.RAFAELMICRO_R820T ); for( TunerConfiguration config: configs ) @@ -693,6 +693,6 @@ public class R820TTunerConfigurationPanel extends JPanel */ private void save() { - mResourceManager.getSettingsManager().save(); + mSettingsManager.save(); } } diff --git a/src/source/tuner/rtl/r820t/R820TTunerController.java b/src/source/tuner/rtl/r820t/R820TTunerController.java index 4713697d..109458e2 100644 --- a/src/source/tuner/rtl/r820t/R820TTunerController.java +++ b/src/source/tuner/rtl/r820t/R820TTunerController.java @@ -33,11 +33,11 @@ import org.usb4java.Device; import org.usb4java.DeviceDescriptor; import org.usb4java.LibUsbException; +import settings.SettingsManager; import source.SourceException; import source.tuner.TunerConfiguration; import source.tuner.TunerType; import source.tuner.rtl.RTL2832TunerController; -import controller.ResourceManager; import controller.ThreadPoolManager; public class R820TTunerController extends RTL2832TunerController @@ -139,11 +139,11 @@ public class R820TTunerController extends RTL2832TunerController /** * Controller gui editor component for this tuner */ - public JPanel getEditor( ResourceManager resourceManager ) + public JPanel getEditor( SettingsManager settingsManager ) { if( mEditor == null ) { - mEditor = new R820TTunerEditorPanel( this, resourceManager ); + mEditor = new R820TTunerEditorPanel( this, settingsManager ); } return mEditor; diff --git a/src/source/tuner/rtl/r820t/R820TTunerEditorPanel.java b/src/source/tuner/rtl/r820t/R820TTunerEditorPanel.java index f59d29a7..c761a07a 100644 --- a/src/source/tuner/rtl/r820t/R820TTunerEditorPanel.java +++ b/src/source/tuner/rtl/r820t/R820TTunerEditorPanel.java @@ -30,17 +30,16 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import settings.SettingsManager; import source.SourceException; import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeListener; import source.tuner.frequency.FrequencyChangeEvent.Event; +import source.tuner.frequency.IFrequencyChangeProcessor; import source.tuner.rtl.RTL2832InfoPanel; import com.jidesoft.swing.JideTabbedPane; -import controller.ResourceManager; - -public class R820TTunerEditorPanel extends JPanel implements FrequencyChangeListener +public class R820TTunerEditorPanel extends JPanel implements IFrequencyChangeProcessor { private final static Logger mLog = LoggerFactory.getLogger( R820TTunerEditorPanel.class ); @@ -48,15 +47,15 @@ public class R820TTunerEditorPanel extends JPanel implements FrequencyChangeList private static final long serialVersionUID = 1L; private R820TTunerController mController; - private ResourceManager mResourceManager; + private SettingsManager mSettingsManager; private JFrequencyControl mFrequencyControl; public R820TTunerEditorPanel( R820TTunerController controller, - ResourceManager resourceManager ) + SettingsManager settingsManager ) { mController = controller; - mResourceManager = resourceManager; + mSettingsManager = settingsManager; initGUI(); } @@ -83,7 +82,7 @@ public class R820TTunerEditorPanel extends JPanel implements FrequencyChangeList tabs.setForeground( Color.BLACK ); tabs.addTab( "Configuration", - new R820TTunerConfigurationPanel( mResourceManager, mController ) ); + new R820TTunerConfigurationPanel( mSettingsManager, mController ) ); tabs.addTab( "Info", new RTL2832InfoPanel( mController ) ); @@ -97,7 +96,7 @@ public class R820TTunerEditorPanel extends JPanel implements FrequencyChangeList @Override public void frequencyChanged( FrequencyChangeEvent event ) { - if( event.getEvent() == Event.FREQUENCY_CHANGE_NOTIFICATION ) + if( event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE ) { final long frequency = event.getValue().longValue(); diff --git a/src/source/wave/ComplexWaveSource.java b/src/source/wave/ComplexWaveSource.java index 235c4717..9e4a65a8 100644 --- a/src/source/wave/ComplexWaveSource.java +++ b/src/source/wave/ComplexWaveSource.java @@ -51,11 +51,42 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil public ComplexWaveSource( File file ) throws IOException { - super( file.getAbsolutePath() ); - mFile = file; } + @Override + public void reset() + { + stop(); + start(); + } + + @Override + public void start() + { + try + { + open(); + } + catch ( IOException | UnsupportedAudioFileException e ) + { + mLog.error( "Error starting complex wave source" ); + } + } + + @Override + public void stop() + { + try + { + close(); + } + catch ( IOException e ) + { + mLog.error( "Error stopping complex wave source" ); + } + } + @Override public long getFrameCount() throws IOException { diff --git a/src/source/wave/RealWaveSource.java b/src/source/wave/RealWaveSource.java index a7251674..9af020c1 100644 --- a/src/source/wave/RealWaveSource.java +++ b/src/source/wave/RealWaveSource.java @@ -52,11 +52,46 @@ public class RealWaveSource extends RealSource public RealWaveSource( File file ) throws IOException { - super( file.getAbsolutePath() ); - mFile = file; } + + @Override + public void reset() + { + stop(); + start(); + } + + + @Override + public void start() + { + try + { + open(); + } + catch ( IOException | UnsupportedAudioFileException e ) + { + mLog.error( "Error starting real wave source", e ); + } + } + + + @Override + public void stop() + { + try + { + close(); + } + catch ( IOException e ) + { + mLog.error( "Error stopping real wave source", e ); + } + } + + @Override public long getFrameCount() throws IOException { diff --git a/src/spectrum/ChannelSpectrumPanel.java b/src/spectrum/ChannelSpectrumPanel.java index 5a87dc4c..9a2b134f 100644 --- a/src/spectrum/ChannelSpectrumPanel.java +++ b/src/spectrum/ChannelSpectrumPanel.java @@ -6,7 +6,6 @@ import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; -import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.JLayeredPane; @@ -16,15 +15,16 @@ import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.SwingUtilities; +import module.ProcessingChain; +import net.miginfocom.swing.MigLayout; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.miginfocom.swing.MigLayout; import sample.Listener; import sample.SampleType; import sample.complex.ComplexBuffer; import sample.real.RealBuffer; -import sample.real.RealSampleListener; import settings.ColorSetting.ColorSettingName; import settings.ColorSettingMenuItem; import settings.Setting; @@ -40,15 +40,13 @@ import spectrum.menu.FFTWindowTypeItem; import spectrum.menu.FrameRateItem; import spectrum.menu.SmoothingItem; import spectrum.menu.SmoothingTypeItem; -import controller.ResourceManager; import controller.channel.Channel; import controller.channel.ChannelEvent; import controller.channel.ChannelEventListener; +import controller.channel.ChannelProcessingManager; import dsp.filter.Filters; -import dsp.filter.FloatHalfBandFilter; import dsp.filter.Window.WindowType; import dsp.filter.halfband.real.HalfBandFilter_RB_RB; -import dsp.filter.hilbert.HilbertTransform; import dsp.filter.smoothing.SmoothingFilter.SmoothingType; public class ChannelSpectrumPanel extends JPanel @@ -61,7 +59,6 @@ public class ChannelSpectrumPanel extends JPanel private static final long serialVersionUID = 1L; -// private ResourceManager mResourceManager; private DFTProcessor mDFTProcessor = new DFTProcessor( SampleType.REAL ); private DFTResultsConverter mDFTConverter = new RealDecibelConverter(); private JLayeredPane mLayeredPane; @@ -80,10 +77,13 @@ public class ChannelSpectrumPanel extends JPanel private AtomicBoolean mEnabled = new AtomicBoolean(); private SettingsManager mSettingsManager; + private ChannelProcessingManager mChannelProcessingManager; - public ChannelSpectrumPanel( SettingsManager settingsManager ) + public ChannelSpectrumPanel( SettingsManager settingsManager, + ChannelProcessingManager channelProcessingManager ) { mSettingsManager = settingsManager; + mChannelProcessingManager = channelProcessingManager; if( mSettingsManager != null ) { @@ -99,7 +99,7 @@ public class ChannelSpectrumPanel extends JPanel /* Set the DFTProcessor to the decimated 24kHz sample rate */ mDFTProcessor.frequencyChanged( - new FrequencyChangeEvent( Event.SAMPLE_RATE_CHANGE_NOTIFICATION, 24000 ) ); + new FrequencyChangeEvent( Event.NOTIFICATION_SAMPLE_RATE_CHANGE, 24000 ) ); initGui(); } @@ -167,14 +167,17 @@ public class ChannelSpectrumPanel extends JPanel switch( event.getEvent() ) { case NOTIFICATION_SELECTION_CHANGE: - if( !event.getChannel().isSelected() ) + //ChannelSelectionManager ensures that only 1 channel can be + //selected and any previously selected channel will be first + //deselected before we get a new selection event + if( event.getChannel().isSelected() ) { - stop(); + if( mCurrentChannel != null ) + { + stop(); + mCurrentChannel = null; + } - mCurrentChannel = null; - } - else - { mCurrentChannel = event.getChannel(); if( mEnabled.get() ) @@ -182,17 +185,13 @@ public class ChannelSpectrumPanel extends JPanel start(); } } - if( event.getChannel().isSelected() && - mCurrentChannel != event.getChannel() ) + else { stop(); - - mCurrentChannel = event.getChannel(); - - start(); + mCurrentChannel = null; } break; - case REQUEST_DISABLE: + case NOTIFICATION_PROCESSING_STOP: if( event.getChannel() == mCurrentChannel ) { if( mEnabled.get() ) @@ -203,32 +202,36 @@ public class ChannelSpectrumPanel extends JPanel mCurrentChannel = null; } break; - case REQUEST_ENABLE: - if( event.getChannel() == mCurrentChannel && - !event.getChannel().isProcessing() ) - { - stop(); - mCurrentChannel = null; - } - break; } } private void start() { - if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.isProcessing() ) + if( mEnabled.get() && mCurrentChannel != null && mCurrentChannel.getEnabled() ) { - mCurrentChannel.getProcessingChain().addRealBufferListener( this ); - mDFTProcessor.start(); + ProcessingChain processingChain = mChannelProcessingManager + .getProcessingChain( mCurrentChannel ); + + if( processingChain != null ) + { + processingChain.addRealBufferListener( this ); + + mDFTProcessor.start(); + } } } private void stop() { - - if( mCurrentChannel != null && mCurrentChannel.isProcessing() ) + if( mCurrentChannel != null && mCurrentChannel.getEnabled() ) { - mCurrentChannel.getProcessingChain().removeRealBufferListener( this ); + ProcessingChain processingChain = mChannelProcessingManager + .getProcessingChain( mCurrentChannel ); + + if( processingChain != null ) + { + processingChain.removeRealBufferListener( this ); + } } mDFTProcessor.stop(); diff --git a/src/spectrum/DFTProcessor.java b/src/spectrum/DFTProcessor.java index ef9b745f..207d9eb9 100644 --- a/src/spectrum/DFTProcessor.java +++ b/src/spectrum/DFTProcessor.java @@ -36,7 +36,7 @@ import sample.Listener; import sample.SampleType; import sample.complex.ComplexBuffer; import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeProcessor; import spectrum.converter.DFTResultsConverter; import controller.NamingThreadFactory; import dsp.filter.Window; @@ -47,7 +47,7 @@ import dsp.filter.Window.WindowType; * of DFT results, using configurable fft size and output dispatch timelines. */ public class DFTProcessor implements Listener, - FrequencyChangeListener, + IFrequencyChangeProcessor, IDFTWidthChangeProcessor { private final static Logger mLog = @@ -494,7 +494,7 @@ public class DFTProcessor implements Listener, { switch( event.getEvent() ) { - case SAMPLE_RATE_CHANGE_NOTIFICATION: + case NOTIFICATION_SAMPLE_RATE_CHANGE: mSampleRate = event.getValue().intValue(); calculateConsumptionRate(); break; diff --git a/src/spectrum/OverlayPanel.java b/src/spectrum/OverlayPanel.java index f51f744d..7e1aadf1 100644 --- a/src/spectrum/OverlayPanel.java +++ b/src/spectrum/OverlayPanel.java @@ -46,7 +46,7 @@ import settings.SettingChangeListener; import settings.SettingsManager; import source.tuner.TunerChannel; import source.tuner.frequency.FrequencyChangeEvent; -import source.tuner.frequency.FrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeProcessor; import controller.channel.Channel; import controller.channel.Channel.ChannelType; import controller.channel.ChannelEvent; @@ -55,7 +55,7 @@ import controller.channel.ChannelModel; public class OverlayPanel extends JPanel implements ChannelEventListener, - FrequencyChangeListener, + IFrequencyChangeProcessor, SettingChangeListener { private static final long serialVersionUID = 1L; @@ -528,14 +528,14 @@ public class OverlayPanel extends JPanel { if( mChannelDisplay == ChannelDisplay.ALL || ( mChannelDisplay == ChannelDisplay.ENABLED && - channel.isProcessing() ) ) + channel.getEnabled() ) ) { //Choose the correct background color to use if( channel.isSelected() ) { graphics.setColor( mColorChannelConfigSelected ); } - else if( channel.isProcessing() ) + else if( channel.getEnabled() ) { graphics.setColor( mColorChannelConfigProcessing ); } @@ -613,9 +613,11 @@ public class OverlayPanel extends JPanel drawChannelCenterLine( graphics, frequencyAxis ); /* Draw Automatic Frequency Control line */ - if( channel.hasFrequencyControl() ) + int correction = channel.getChannelFrequencyCorrection(); + + if( correction != 0 ) { - long error = frequency + channel.getFrequencyCorrection(); + long error = frequency + correction; drawAFC( graphics, frequencyAxis, getAxisFromFrequency( error ) ); } @@ -674,11 +676,11 @@ public class OverlayPanel extends JPanel { switch( event.getEvent() ) { - case SAMPLE_RATE_CHANGE_NOTIFICATION: + case NOTIFICATION_SAMPLE_RATE_CHANGE: mBandwidth = event.getValue().intValue(); mLabelSizeMonitor.update(); break; - case FREQUENCY_CHANGE_NOTIFICATION: + case NOTIFICATION_FREQUENCY_CHANGE: mFrequency = event.getValue().longValue(); mLabelSizeMonitor.update(); break; diff --git a/src/spectrum/SpectralDisplayPanel.java b/src/spectrum/SpectralDisplayPanel.java index e03d27e9..f8d1acd9 100644 --- a/src/spectrum/SpectralDisplayPanel.java +++ b/src/spectrum/SpectralDisplayPanel.java @@ -66,7 +66,7 @@ import source.tuner.Tuner; import source.tuner.TunerSelectionListener; import source.tuner.frequency.FrequencyChangeEvent; import source.tuner.frequency.FrequencyChangeEvent.Event; -import source.tuner.frequency.FrequencyChangeListener; +import source.tuner.frequency.IFrequencyChangeProcessor; import spectrum.OverlayPanel.ChannelDisplay; import spectrum.converter.ComplexDecibelConverter; import spectrum.converter.DFTResultsConverter; @@ -82,13 +82,14 @@ import com.jidesoft.swing.JideSplitPane; import controller.ConfigurationControllerModel; import controller.channel.Channel; import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; import controller.channel.ChannelUtils; import dsp.filter.Window.WindowType; import dsp.filter.smoothing.SmoothingFilter.SmoothingType; public class SpectralDisplayPanel extends JPanel implements Listener, - FrequencyChangeListener, + IFrequencyChangeProcessor, IDFTWidthChangeProcessor, TunerSelectionListener { @@ -115,9 +116,11 @@ public class SpectralDisplayPanel extends JPanel private OverlayPanel mOverlayPanel; private DFTProcessor mDFTProcessor; private DFTResultsConverter mDFTConverter; + private ConfigurationControllerModel mControllerModel; + private ChannelModel mChannelModel; + private ChannelProcessingManager mChannelProcessingManager; private PlaylistManager mPlaylistManager; private SettingsManager mSettingsManager; - private ConfigurationControllerModel mController; private Tuner mTuner; /** @@ -132,23 +135,26 @@ public class SpectralDisplayPanel extends JPanel * the DFT is translated to decibels for display in the spectrum and * waterfall components. */ - public SpectralDisplayPanel( ChannelModel channelModel, - ConfigurationControllerModel controller, + public SpectralDisplayPanel( ConfigurationControllerModel controllerModel, + ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, PlaylistManager playlistManager, SettingsManager settingsManager ) { - mController = controller; + mControllerModel = controllerModel; + mChannelModel = channelModel; + mChannelProcessingManager = channelProcessingManager; mPlaylistManager = playlistManager; mSettingsManager = settingsManager; mSpectrumPanel = new SpectrumPanel( mSettingsManager ); - mOverlayPanel = new OverlayPanel( mSettingsManager, channelModel ); + mOverlayPanel = new OverlayPanel( mSettingsManager, mChannelModel ); mWaterfallPanel = new WaterfallPanel( mSettingsManager ); //Register to receive tuner selection events - if( mController != null ) + if( mControllerModel != null ) { - mController.addListener( (TunerSelectionListener)this ); + mControllerModel.addListener( (TunerSelectionListener)this ); } init(); @@ -189,12 +195,12 @@ public class SpectralDisplayPanel extends JPanel /* De-register from receiving samples when the window closes */ clearTuner(); - if( mController != null ) + if( mControllerModel != null ) { - mController.removeListener( (TunerSelectionListener)this ); + mControllerModel.removeListener( (TunerSelectionListener)this ); } - mController = null; + mControllerModel = null; mPlaylistManager = null; mSettingsManager = null; @@ -426,7 +432,6 @@ public class SpectralDisplayPanel extends JPanel /** * Receives frequency change events -- primarily from tuner components. */ - @Override public void frequencyChanged( FrequencyChangeEvent event ) { mOverlayPanel.frequencyChanged( event ); @@ -458,7 +463,7 @@ public class SpectralDisplayPanel extends JPanel if( mTuner != null ) { //Register to receive frequency change events - mTuner.addListener( (FrequencyChangeListener)this ); + mTuner.addFrequencyChangeProcessor( this ); //Register the dft processor to receive samples from the tuner mTuner.addListener( (Listener)mDFTProcessor ); @@ -470,10 +475,10 @@ public class SpectralDisplayPanel extends JPanel try { frequencyChanged( new FrequencyChangeEvent( - Event.FREQUENCY_CHANGE_NOTIFICATION, mTuner.getFrequency() ) ); + Event.NOTIFICATION_FREQUENCY_CHANGE, mTuner.getFrequency() ) ); frequencyChanged( new FrequencyChangeEvent( - Event.SAMPLE_RATE_CHANGE_NOTIFICATION, mTuner.getSampleRate() ) ); + Event.NOTIFICATION_SAMPLE_RATE_CHANGE, mTuner.getSampleRate() ) ); } catch ( SourceException e ) { @@ -491,7 +496,7 @@ public class SpectralDisplayPanel extends JPanel if( mTuner != null ) { //Deregister for frequency change events from the tuner - mTuner.removeListener( (FrequencyChangeListener)this ); + mTuner.removeFrequencyChangeProcessor( this ); //Deregister the dft processor from receiving samples mTuner.removeListener( (Listener)mDFTProcessor ); @@ -652,8 +657,9 @@ public class SpectralDisplayPanel extends JPanel for( Channel channel: channels ) { - JMenu channelMenu = ChannelUtils.getContextMenu( - mPlaylistManager, channel, SpectralDisplayPanel.this ); + JMenu channelMenu = ChannelUtils.getContextMenu( mChannelModel, + mChannelProcessingManager, mPlaylistManager, channel, + SpectralDisplayPanel.this ); if( channelMenu != null ) { @@ -675,10 +681,7 @@ public class SpectralDisplayPanel extends JPanel for( DecoderType type: DecoderType.getPrimaryDecoders() ) { - decoderMenu.add( new DecoderItem( mController, - frequency, - type ) ); - + decoderMenu.add( new DecoderItem( type, frequency ) ); } frequencyMenu.add( decoderMenu ); @@ -957,17 +960,14 @@ public class SpectralDisplayPanel extends JPanel { private static final long serialVersionUID = 1L; - private ConfigurationControllerModel mController; + private ChannelModel mChannelModel; private long mFrequency; private DecoderType mDecoder; - public DecoderItem( ConfigurationControllerModel controller, - long frequency, - DecoderType type ) + public DecoderItem( DecoderType type, long frequency ) { super( type.getDisplayString() ); - mController = controller; mFrequency = frequency; mDecoder = type; @@ -976,7 +976,7 @@ public class SpectralDisplayPanel extends JPanel @Override public void actionPerformed( ActionEvent e ) { - mController.createChannel( mFrequency, mDecoder ); + mChannelModel.createChannel( mDecoder, mFrequency ); } } ); } diff --git a/src/spectrum/SpectrumFrame.java b/src/spectrum/SpectrumFrame.java index aa39a2f2..f3e6eac4 100644 --- a/src/spectrum/SpectrumFrame.java +++ b/src/spectrum/SpectrumFrame.java @@ -28,18 +28,18 @@ import playlist.PlaylistManager; import settings.SettingsManager; import source.tuner.Tuner; import controller.ConfigurationControllerModel; -import controller.ResourceManager; import controller.channel.ChannelModel; +import controller.channel.ChannelProcessingManager; public class SpectrumFrame extends JFrame implements WindowListener { private static final long serialVersionUID = 1L; - private ResourceManager mResourceManager; private SpectralDisplayPanel mSpectralDisplayPanel; - public SpectrumFrame( ChannelModel channelModel, - ConfigurationControllerModel controller, + public SpectrumFrame( ConfigurationControllerModel controllerModel, + ChannelModel channelModel, + ChannelProcessingManager channelProcessingManager, PlaylistManager playlistManager, SettingsManager settingsManager, Tuner tuner ) @@ -50,8 +50,8 @@ public class SpectrumFrame extends JFrame implements WindowListener setLayout( new MigLayout( "insets 0 0 0 0", "[grow]", "[grow]") ); - mSpectralDisplayPanel = new SpectralDisplayPanel( channelModel, - controller, playlistManager, settingsManager ); + mSpectralDisplayPanel = new SpectralDisplayPanel( controllerModel, + channelModel, channelProcessingManager, playlistManager, settingsManager ); mSpectralDisplayPanel.tunerSelected( tuner ); add( mSpectralDisplayPanel, "grow" );