diff --git a/src/controller/ConfigurationEditor.java b/src/controller/ConfigurationEditor.java index a07765dd..84a99a30 100644 --- a/src/controller/ConfigurationEditor.java +++ b/src/controller/ConfigurationEditor.java @@ -20,7 +20,6 @@ package controller; import java.awt.BorderLayout; -import javax.swing.BorderFactory; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTree; diff --git a/src/controller/Editor.java b/src/controller/Editor.java index b938eb87..56411ce1 100644 --- a/src/controller/Editor.java +++ b/src/controller/Editor.java @@ -20,6 +20,7 @@ package controller; import javax.swing.JPanel; import net.miginfocom.swing.MigLayout; +import controller.channel.ChannelValidationException; public abstract class Editor extends JPanel { @@ -33,4 +34,18 @@ public abstract class Editor extends JPanel public abstract void save(); public abstract void reset(); + + /** + * Override this method to allow the editor to inspect the argument for + * valid 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 + */ + public void validate( Editor editor ) throws ChannelValidationException + { + } } diff --git a/src/controller/channel/ChannelEditor.java b/src/controller/channel/ChannelEditor.java index ef64415a..082829a6 100644 --- a/src/controller/channel/ChannelEditor.java +++ b/src/controller/channel/ChannelEditor.java @@ -30,11 +30,16 @@ import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JTextField; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; import net.miginfocom.swing.MigLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import record.RecordComponentEditor; import source.SourceComponentEditor; import alias.AliasList; @@ -48,7 +53,9 @@ import eventlog.EventLogComponentEditor; public class ChannelEditor extends Editor implements ActionListener { - 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; @@ -171,32 +178,51 @@ public class ChannelEditor extends Editor implements ActionListener @Override public void save() { - boolean expanded = mChannelNode.getModel().getTree() - .isExpanded( new TreePath( mChannelNode ) ); - - mSourceEditor.save(); - mDecodeEditor.save(); - mAuxDecodeEditor.save(); - mEventLogEditor.save(); - mRecordEditor.save(); - - mChannelNode.getChannel().setName( mChannelName.getText(), false ); - mChannelNode.getChannel().setEnabled( mChannelEnabled.isSelected(), false ); - - AliasList selected = mComboAliasLists.getItemAt( - mComboAliasLists.getSelectedIndex() ); - if( selected != null ) + try { - mChannelNode.getChannel().setAliasListName( selected.getName(), false ); + /** + * 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(), false ); + mChannelNode.getChannel().setEnabled( mChannelEnabled.isSelected(), false ); + + AliasList selected = mComboAliasLists.getItemAt( + mComboAliasLists.getSelectedIndex() ); + if( selected != null ) + { + mChannelNode.getChannel().setAliasListName( selected.getName(), false ); + } + + + mChannelNode.save(); + mChannelNode.show(); + + if( expanded ) + { + mChannelNode.getModel().getTree().expandPath( new TreePath( mChannelNode ) ); + } } - - - mChannelNode.save(); - mChannelNode.show(); - - if( expanded ) + catch ( ChannelValidationException validationException ) { - mChannelNode.getModel().getTree().expandPath( new TreePath( mChannelNode ) ); + /* ChannelValidationException can be thrown by any of the component + * editors. Show the validation error text in a popup menu to the + * user */ + JOptionPane.showMessageDialog( ChannelEditor.this, + validationException.getMessage(), + "Channel Configuration Error", + JOptionPane.ERROR_MESSAGE ); } } @@ -232,4 +258,17 @@ public class ChannelEditor extends Editor implements ActionListener mEventLogEditor.reset(); mRecordEditor.reset(); } + + /** + * 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 ); + } } diff --git a/src/controller/channel/ChannelValidationException.java b/src/controller/channel/ChannelValidationException.java new file mode 100644 index 00000000..53422a11 --- /dev/null +++ b/src/controller/channel/ChannelValidationException.java @@ -0,0 +1,11 @@ +package controller.channel; + +public class ChannelValidationException extends Exception +{ + private static final long serialVersionUID = 1L; + + public ChannelValidationException( String validationMessage ) + { + super( validationMessage ); + } +} diff --git a/src/decode/AuxDecodeComponentEditor.java b/src/decode/AuxDecodeComponentEditor.java index 16b37ac6..5013c8f0 100644 --- a/src/decode/AuxDecodeComponentEditor.java +++ b/src/decode/AuxDecodeComponentEditor.java @@ -21,12 +21,13 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -import javax.swing.BorderFactory; import javax.swing.JCheckBox; import javax.swing.SwingUtilities; +import controller.Editor; import controller.channel.AbstractChannelEditor; import controller.channel.ChannelNode; +import controller.channel.ChannelValidationException; import decode.config.AuxDecodeConfiguration; public class AuxDecodeComponentEditor extends AbstractChannelEditor @@ -93,11 +94,12 @@ public class AuxDecodeComponentEditor extends AbstractChannelEditor } } }); - } public class AuxDecoderCheckBox extends JCheckBox { + private static final long serialVersionUID = 1L; + private DecoderType mDecoderType; public AuxDecoderCheckBox( DecoderType decoder ) diff --git a/src/decode/DecodeComponentEditor.java b/src/decode/DecodeComponentEditor.java index ee9e754f..8061cea9 100644 --- a/src/decode/DecodeComponentEditor.java +++ b/src/decode/DecodeComponentEditor.java @@ -25,8 +25,10 @@ import javax.swing.DefaultComboBoxModel; import javax.swing.JComboBox; import javax.swing.SwingUtilities; +import controller.Editor; import controller.channel.AbstractChannelEditor; import controller.channel.ChannelNode; +import controller.channel.ChannelValidationException; import decode.config.DecodeConfigFactory; import decode.config.DecodeConfiguration; @@ -133,4 +135,15 @@ public class DecodeComponentEditor extends AbstractChannelEditor mChannelNode.getChannel() .setDecodeConfiguration( mEditor.getConfig(), false ); } + + /** + * Validates the editor against the current decode editor + */ + @Override + public void validate( Editor editor ) throws ChannelValidationException + { + super.validate( editor ); + + mEditor.validate( editor ); + } } diff --git a/src/decode/DecodeEditor.java b/src/decode/DecodeEditor.java index 9f1b40fd..09ef5340 100644 --- a/src/decode/DecodeEditor.java +++ b/src/decode/DecodeEditor.java @@ -30,6 +30,8 @@ import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import controller.Editor; +import controller.channel.Channel; +import controller.channel.ChannelValidationException; import decode.config.DecodeConfiguration; public class DecodeEditor extends Editor @@ -104,12 +106,10 @@ public class DecodeEditor extends Editor @Override public void save() { - } @Override public void reset() { - } } diff --git a/src/decode/DecoderFactory.java b/src/decode/DecoderFactory.java index 41fd789a..cb9b75b4 100644 --- a/src/decode/DecoderFactory.java +++ b/src/decode/DecoderFactory.java @@ -28,6 +28,7 @@ import decode.config.AuxDecodeConfiguration; import decode.config.DecodeConfigLTRNet; import decode.config.DecodeConfigLTRStandard; import decode.config.DecodeConfigMPT1327; +import decode.config.DecodeConfigP25Phase1; import decode.config.DecodeConfiguration; import decode.fleetsync2.Fleetsync2Decoder; import decode.fleetsync2.FleetsyncChannelState; @@ -99,7 +100,10 @@ public class DecoderFactory retVal = new PassportDecoder( sampleType, aliasList ); break; case P25_PHASE1: - retVal = new P25Decoder( sampleType, aliasList ); + DecodeConfigP25Phase1 p25Config = (DecodeConfigP25Phase1)config; + + retVal = new P25Decoder( sampleType, p25Config.getModulation(), + aliasList ); break; default: throw new IllegalArgumentException( diff --git a/src/decode/config/DecodeConfigP25Phase1.java b/src/decode/config/DecodeConfigP25Phase1.java index bb064b22..7de83b52 100644 --- a/src/decode/config/DecodeConfigP25Phase1.java +++ b/src/decode/config/DecodeConfigP25Phase1.java @@ -17,10 +17,16 @@ ******************************************************************************/ package decode.config; +import javax.xml.bind.annotation.XmlAttribute; + import decode.DecoderType; +import decode.p25.P25Decoder; +import decode.p25.P25Decoder.Modulation; public class DecodeConfigP25Phase1 extends DecodeConfiguration { + private P25Decoder.Modulation mModulation = Modulation.C4FM; + public DecodeConfigP25Phase1() { super( DecoderType.P25_PHASE1 ); @@ -32,4 +38,15 @@ public class DecodeConfigP25Phase1 extends DecodeConfiguration { return false; } + + @XmlAttribute( name = "modulation" ) + public P25Decoder.Modulation getModulation() + { + return mModulation; + } + + public void setModulation( P25Decoder.Modulation modulation ) + { + mModulation = modulation; + } } diff --git a/src/decode/lj1200/LJ1200Message.java b/src/decode/lj1200/LJ1200Message.java index 29b30417..7a3d4b14 100644 --- a/src/decode/lj1200/LJ1200Message.java +++ b/src/decode/lj1200/LJ1200Message.java @@ -318,7 +318,7 @@ public class LJ1200Message extends Message F3_SPEED_UP( 0x3, "3-SPEED-UP" ), F4_TEST( 0x4, "4-TEST" ), F5( 0x5, "5-UNKNOWN" ), - F6_SET_RATE( 0x6, "6-SET RATE" ), + F6_SET_RATE( 0x6, "6-UNKNOWN" ), //Delete F7( 0x7, "7-UNKNOWN" ), F8_SITE_ID( 0x8, "8-SITE ID" ), F9( 0x9, "9-UNKNOWN" ), @@ -326,7 +326,7 @@ public class LJ1200Message extends Message FB( 0xB, "B-UNKNOWN" ), FC_DEACTIVATE( 0xC, "C-DEACTIVATE" ), FD( 0xD, "D-UNKNOWN" ), - FE_TRACK_PULSE( 0xE, "E-TRACK PULSE" ), + FE_TRACK_PULSE( 0xE, "E-UNKNOWN" ), FF_TRACK_PULSE( 0xF, "F-TRACK PULSE" ), UNKNOWN( -1, "UNKNOWN" ); diff --git a/src/decode/p25/CQPSKP25Decoder.java b/src/decode/p25/CQPSKP25Decoder.java new file mode 100644 index 00000000..7e003710 --- /dev/null +++ b/src/decode/p25/CQPSKP25Decoder.java @@ -0,0 +1,241 @@ +/******************************************************************************* + * 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 decode.p25; + +import instrument.Instrumentable; +import instrument.tap.Tap; +import instrument.tap.stream.DibitTap; +import instrument.tap.stream.FloatTap; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sample.Listener; +import sample.complex.ComplexSample; +import sample.real.RealSampleListener; +import source.Source.SampleType; +import source.tuner.DirectFrequencyController; +import source.tuner.FrequencyChangeListener; +import alias.AliasList; +import decode.Decoder; +import decode.DecoderType; +import dsp.filter.ComplexFIRFilter; +import dsp.filter.FilterFactory; +import dsp.filter.FloatFIRFilter; +import dsp.filter.Window.WindowType; +import dsp.gain.ComplexAutomaticGainControl; +import dsp.gain.DirectGainControl; +import dsp.psk.CQPSKDemodulator; +import dsp.symbol.FrameSync; + +@Deprecated +public class CQPSKP25Decoder extends Decoder + implements DirectFrequencyController, Instrumentable +{ + private final static Logger mLog = LoggerFactory.getLogger( CQPSKP25Decoder.class ); + + /* Instrumentation Taps */ + private static final String INSTRUMENT_INPUT_TO_AGC = "Tap Point: Input to AGC"; + private static final String INSTRUMENT_AGC_TO_SYMBOL_FILTER = "Tap Point: AGC to Symbol Filter"; + private static final String INSTRUMENT_SYMBOL_FILTER_TO_SLICER = "Tap Point: Symbol Filter to Slicer"; + private static final String INSTRUMENT_SLICER_OUTPUT = "Tap Point: Slicer Output"; + private List mAvailableTaps; + + private ComplexFIRFilter mBasebandFilter = new ComplexFIRFilter( + FilterFactory.getLowPass( 48000, 6500, 7500, 48, WindowType.HANNING, true ), 1.0 ); + private CQPSKDemodulator mDemodulator = new CQPSKDemodulator(); + private FloatFIRFilter mAudioFilter = new FloatFIRFilter( + FilterFactory.getLowPass( 48000, 3000, 4000, 48, WindowType.HAMMING, true ), 1.0 ); + + private DirectGainControl mGainControl = + new DirectGainControl( 15.0f, 0.1f, 25.0f, 0.3f ); + private C4FMSymbolFilter mSymbolFilter; + private C4FMSlicer mSlicer = new C4FMSlicer(); + private P25MessageFramer mNormalFramer; + private P25MessageFramer mInvertedFramer; + private P25MessageProcessor mMessageProcessor; + + private ComplexAutomaticGainControl mAGC = new ComplexAutomaticGainControl(); + + private AliasList mAliasList; + + public CQPSKP25Decoder( SampleType sampleType, AliasList aliasList ) + { + super( sampleType ); + + mAliasList = aliasList; + + mSymbolFilter = new C4FMSymbolFilter( mGainControl ); + + /** + * Only setup a demod chain if we're receiving complex samples. If + * we're receiving demodulated samples, they'll be handled the same + * way as we handle the output of the demodulator. + */ + if( mSourceSampleType == SampleType.COMPLEX ) + { + /** + * The Decoder super class is both a Complex listener and a float + * listener. So, we add the demod to listen to the incoming + * quadrature samples, and we wire the output of the demod right + * back to this class, so we can receive the demodulated output + * to process + */ + this.addComplexListener( mAGC ); + mAGC.setListener( mBasebandFilter ); + mBasebandFilter.setListener( mDemodulator ); + mDemodulator.setListener( new Listener() + { + @Override + public void receive( ComplexSample sample ) + { + mLog.debug( "Output real:" + sample.real() + " imag:" + sample.imaginary() + " angle:" + Math.toDegrees( sample.angle() ) ); + } + } ); + } + else + { + throw new IllegalArgumentException( "CQPSK can only process complex samples" ); + } + + addRealSampleListener( mAudioFilter ); + mAudioFilter.setListener( mGainControl ); + mGainControl.setListener( mSymbolFilter ); + + mSymbolFilter.setListener( mSlicer ); + + mMessageProcessor = new P25MessageProcessor( mAliasList ); + mMessageProcessor.addMessageListener( this ); + + mNormalFramer = new P25MessageFramer( + FrameSync.P25_PHASE1.getSync(), 64, false, mAliasList ); + mSlicer.addListener( mNormalFramer ); + mNormalFramer.setListener( mMessageProcessor ); + + mInvertedFramer = new P25MessageFramer( + FrameSync.P25_PHASE1_INVERTED.getSync(), 64, true, mAliasList ); + mSlicer.addListener( mInvertedFramer ); + mInvertedFramer.setListener( mMessageProcessor ); + + } + + @Override + public DecoderType getType() + { + return DecoderType.P25_PHASE1; + } + + @Override + public List getTaps() + { + if( mAvailableTaps == null ) + { + mAvailableTaps = new ArrayList(); + + mAvailableTaps.add( new FloatTap( INSTRUMENT_INPUT_TO_AGC, 0, 1.0f ) ); + mAvailableTaps.add( new FloatTap( INSTRUMENT_AGC_TO_SYMBOL_FILTER, 0, 1.0f ) ); + mAvailableTaps.add( new FloatTap( INSTRUMENT_SYMBOL_FILTER_TO_SLICER, 0, 0.1f ) ); + mAvailableTaps.add( new DibitTap( INSTRUMENT_SLICER_OUTPUT, 0, 0.1f ) ); + + mAvailableTaps.addAll( mSymbolFilter.getTaps() ); + } + + return mAvailableTaps; + } + + @Override + public void addTap( Tap tap ) + { + mSymbolFilter.addTap( tap ); + + switch( tap.getName() ) + { + case INSTRUMENT_INPUT_TO_AGC: + FloatTap inputAGC = (FloatTap)tap; + removeRealListener( mGainControl ); + addRealSampleListener( inputAGC ); + inputAGC.setListener( mGainControl ); + break; + case INSTRUMENT_AGC_TO_SYMBOL_FILTER: + FloatTap agcSymbol = (FloatTap)tap; + mGainControl.setListener( agcSymbol ); + agcSymbol.setListener( mSymbolFilter ); + break; + case INSTRUMENT_SYMBOL_FILTER_TO_SLICER: + FloatTap symbolSlicer = (FloatTap)tap; + mSymbolFilter.setListener( symbolSlicer ); + symbolSlicer.setListener( mSlicer ); + break; + case INSTRUMENT_SLICER_OUTPUT: + DibitTap slicer = (DibitTap)tap; + mSlicer.addListener( slicer ); + break; + } + } + + @Override + public void removeTap( Tap tap ) + { + mSymbolFilter.removeTap( tap ); + + switch( tap.getName() ) + { + case INSTRUMENT_INPUT_TO_AGC: + FloatTap inputAGC = (FloatTap)tap; + removeRealListener( inputAGC ); + addRealSampleListener( mGainControl ); + break; + case INSTRUMENT_AGC_TO_SYMBOL_FILTER: + mGainControl.setListener( mSymbolFilter ); + break; + case INSTRUMENT_SYMBOL_FILTER_TO_SLICER: + mSymbolFilter.setListener( mSlicer ); + break; + case INSTRUMENT_SLICER_OUTPUT: + DibitTap slicerTap = (DibitTap)tap; + mSlicer.removeListener( slicerTap ); + break; + } + } + + @Override + public void addUnfilteredRealSampleListener( RealSampleListener listener ) + { + throw new IllegalArgumentException( "unfiltered real sample provider " + + "not implemented in P25 Decoder" ); + } + + /** + * Registers a frequency change listener to receive frequency correction + * events from the symbol filter + */ + @Override + public void setListener( FrequencyChangeListener listener ) + { + mSymbolFilter.addListener( listener ); + } + + @Override + public long getFrequencyCorrection() + { + return mSymbolFilter.getFrequencyCorrection(); + } +} diff --git a/src/decode/p25/CQPSKSlicer.java b/src/decode/p25/CQPSKSlicer.java new file mode 100644 index 00000000..fd214bfc --- /dev/null +++ b/src/decode/p25/CQPSKSlicer.java @@ -0,0 +1,62 @@ +package decode.p25; + +import sample.Broadcaster; +import sample.Listener; +import sample.complex.ComplexSample; +import dsp.symbol.Dibit; + +public class CQPSKSlicer implements Listener +{ + private Broadcaster mBroadcaster = new Broadcaster(); + + public CQPSKSlicer() + { + } + + public void dispose() + { + mBroadcaster.dispose(); + mBroadcaster = null; + } + + public void addListener( Listener listener ) + { + mBroadcaster.addListener( listener ); + } + + public void removeListener( Listener listener ) + { + mBroadcaster.removeListener( listener ); + } + + /** + * Receives a complex sample and maps the value into one of four quadrants + * corresponding to the CQPSK LSM pattern. + */ + @Override + public void receive( ComplexSample sample ) + { + if( sample.real() > 0 ) + { + if( sample.imaginary() > 0 ) + { + mBroadcaster.broadcast( Dibit.D00_PLUS_1 ); + } + else + { + mBroadcaster.broadcast( Dibit.D10_MINUS_1 ); + } + } + else + { + if( sample.imaginary() > 0 ) + { + mBroadcaster.broadcast( Dibit.D01_PLUS_3 ); + } + else + { + mBroadcaster.broadcast( Dibit.D11_MINUS_3 ); + } + } + } +} diff --git a/src/decode/p25/P25Decoder.java b/src/decode/p25/P25Decoder.java index 39f786aa..9a0f3262 100644 --- a/src/decode/p25/P25Decoder.java +++ b/src/decode/p25/P25Decoder.java @@ -19,8 +19,10 @@ package decode.p25; import instrument.Instrumentable; import instrument.tap.Tap; +import instrument.tap.stream.ComplexTap; import instrument.tap.stream.DibitTap; import instrument.tap.stream.FloatTap; +import instrument.tap.stream.QPSKTap; import java.util.ArrayList; import java.util.List; @@ -39,9 +41,10 @@ import dsp.filter.ComplexFIRFilter; import dsp.filter.FilterFactory; import dsp.filter.FloatFIRFilter; import dsp.filter.Window.WindowType; +import dsp.gain.ComplexAutomaticGainControl; import dsp.gain.DirectGainControl; import dsp.nbfm.FMDiscriminator; -import dsp.nbfm.FilteringNBFMDemodulator; +import dsp.psk.CQPSKDemodulator; import dsp.symbol.FrameSync; public class P25Decoder extends Decoder @@ -50,74 +53,122 @@ public class P25Decoder extends Decoder private final static Logger mLog = LoggerFactory.getLogger( P25Decoder.class ); /* Instrumentation Taps */ - private static final String INSTRUMENT_INPUT_TO_AGC = "Tap Point: Input to AGC"; - private static final String INSTRUMENT_AGC_TO_SYMBOL_FILTER = "Tap Point: AGC to Symbol Filter"; - private static final String INSTRUMENT_SYMBOL_FILTER_TO_SLICER = "Tap Point: Symbol Filter to Slicer"; - private static final String INSTRUMENT_SLICER_OUTPUT = "Tap Point: Slicer Output"; - private List mAvailableTaps; - - private ComplexFIRFilter mBasebandFilter = new ComplexFIRFilter( - FilterFactory.getLowPass( 48000, 6000, 7000, 48, WindowType.HANNING, true ), 1.0 ); - private FMDiscriminator mDemodulator = new FMDiscriminator( 1 ); - private FloatFIRFilter mAudioFilter = new FloatFIRFilter( - FilterFactory.getLowPass( 48000, 3000, 4000, 48, WindowType.HAMMING, true ), 1.0 ); - - private DirectGainControl mGainControl = - new DirectGainControl( 15.0f, 0.1f, 25.0f, 0.3f ); + private static final String INSTRUMENT_COMPLEX_INPUT = "Tap Point: Complex Input"; + private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output"; + + private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output"; + private static final String INSTRUMENT_QPSK_DEMODULATOR_OUTPUT = "Tap Point: CQPSK Demodulator Output"; + private static final String INSTRUMENT_CQPSK_SLICER_OUTPUT = "Tap Point: CQPSK Slicer Output"; + + private static final String INSTRUMENT_REAL_INPUT = "Tap Point: Real Input"; + private static final String INSTRUMENT_DGC_OUTPUT = "Tap Point: DGC Output"; + 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 List mAvailableTaps; + + /* Demods */ + private FMDiscriminator mFMDemodulator; + private CQPSKDemodulator mCQPSKDemodulator; + + /* Gain */ + private ComplexAutomaticGainControl mAGC; + private DirectGainControl mDGC; + + /* Filters */ + private ComplexFIRFilter mBasebandFilter; + private ComplexFIRFilter mRootRaisedCosineFilter; + private FloatFIRFilter mAudioFilter; private C4FMSymbolFilter mSymbolFilter; - private C4FMSlicer mSlicer = new C4FMSlicer(); + + /* Slicers */ + private C4FMSlicer mC4FMSlicer; + private CQPSKSlicer mCQPSKSlicer; + + /* Message framers and handlers */ private P25MessageFramer mNormalFramer; private P25MessageFramer mInvertedFramer; private P25MessageProcessor mMessageProcessor; private AliasList mAliasList; + private Modulation mModulation; - public P25Decoder( SampleType sampleType, AliasList aliasList ) + public P25Decoder( SampleType sampleType, + Modulation modulation, + AliasList aliasList ) { super( sampleType ); + mModulation = modulation; mAliasList = aliasList; - mSymbolFilter = new C4FMSymbolFilter( mGainControl ); - - /** - * Only setup a demod chain if we're receiving complex samples. If - * we're receiving demodulated samples, they'll be handled the same - * way as we handle the output of the demodulator. - */ - if( mSourceSampleType == SampleType.COMPLEX ) - { - /** - * The Decoder super class is both a Complex listener and a float - * listener. So, we add the demod to listen to the incoming - * quadrature samples, and we wire the output of the demod right - * back to this class, so we can receive the demodulated output - * to process - */ - this.addComplexListener( mBasebandFilter ); - mBasebandFilter.setListener( mDemodulator ); - mDemodulator.setListener( getRealReceiver() ); - } - - addRealSampleListener( mAudioFilter ); - mAudioFilter.setListener( mGainControl ); - mGainControl.setListener( mSymbolFilter ); - - mSymbolFilter.setListener( mSlicer ); - mMessageProcessor = new P25MessageProcessor( mAliasList ); mMessageProcessor.addMessageListener( this ); mNormalFramer = new P25MessageFramer( FrameSync.P25_PHASE1.getSync(), 64, false, mAliasList ); - mSlicer.addListener( mNormalFramer ); mNormalFramer.setListener( mMessageProcessor ); mInvertedFramer = new P25MessageFramer( FrameSync.P25_PHASE1_INVERTED.getSync(), 64, true, mAliasList ); - mSlicer.addListener( mInvertedFramer ); mInvertedFramer.setListener( mMessageProcessor ); - + + /* Setup demodulation chains based on sample type (real or complex) and + * modulation (C4FM or CQPSK) */ + if( mSourceSampleType == SampleType.COMPLEX ) + { + mBasebandFilter = new ComplexFIRFilter( FilterFactory.getLowPass( + 48000, 6500, 8000, 48, WindowType.HANNING, true ), 1.0 ); + + if( modulation == Modulation.CQPSK ) + { + this.addComplexListener( mBasebandFilter ); + + mAGC = new ComplexAutomaticGainControl(); + mBasebandFilter.setListener( mAGC ); + + mCQPSKDemodulator = new CQPSKDemodulator(); + mAGC.setListener( mCQPSKDemodulator ); + + mCQPSKSlicer = new CQPSKSlicer(); + mCQPSKDemodulator.setListener( mCQPSKSlicer ); + + mCQPSKSlicer.addListener( mNormalFramer ); + mCQPSKSlicer.addListener( mInvertedFramer ); + } + else /* C4FM */ + { + this.addComplexListener( mBasebandFilter ); + + mFMDemodulator = new FMDiscriminator( 1.0f ); + mBasebandFilter.setListener( mFMDemodulator ); + + /* Route output of the fm demod back to this channel so that we + * can process the output as if it were coming from any other + * real sample source */ + mFMDemodulator.setListener( getRealReceiver() ); + } + } + + /* Processing chain for real samples & demodulated complex samples */ + if( modulation == Modulation.C4FM ) + { + mAudioFilter = new FloatFIRFilter( FilterFactory.getLowPass( 48000, + 3000, 4000, 48, WindowType.HAMMING, true ), 1.0 ); + addRealSampleListener( mAudioFilter ); + + mDGC = new DirectGainControl( 15.0f, 0.1f, 35.0f, 0.3f ); + mAudioFilter.setListener( mDGC ); + + mSymbolFilter = new C4FMSymbolFilter( mDGC ); + mDGC.setListener( mSymbolFilter ); + + mC4FMSlicer = new C4FMSlicer(); + mSymbolFilter.setListener( mC4FMSlicer ); + + mC4FMSlicer.addListener( mNormalFramer ); + mC4FMSlicer.addListener( mInvertedFramer ); + } } @Override @@ -133,12 +184,33 @@ public class P25Decoder extends Decoder { mAvailableTaps = new ArrayList(); - mAvailableTaps.add( new FloatTap( INSTRUMENT_INPUT_TO_AGC, 0, 1.0f ) ); - mAvailableTaps.add( new FloatTap( INSTRUMENT_AGC_TO_SYMBOL_FILTER, 0, 1.0f ) ); - mAvailableTaps.add( new FloatTap( INSTRUMENT_SYMBOL_FILTER_TO_SLICER, 0, 0.1f ) ); - mAvailableTaps.add( new DibitTap( INSTRUMENT_SLICER_OUTPUT, 0, 0.1f ) ); + if( mSourceSampleType == SampleType.COMPLEX ) + { + mAvailableTaps.add( new ComplexTap( INSTRUMENT_COMPLEX_INPUT, 0, 1.0f ) ); + mAvailableTaps.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) ); + } + else + { + mAvailableTaps.add( new FloatTap( INSTRUMENT_REAL_INPUT, 0, 1.0f ) ); + } - mAvailableTaps.addAll( mSymbolFilter.getTaps() ); + if( mModulation == Modulation.C4FM ) + { + mAvailableTaps.add( new FloatTap( INSTRUMENT_DGC_OUTPUT, 0, 1.0f ) ); + mAvailableTaps.add( new FloatTap( INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT, 0, 0.1f ) ); + mAvailableTaps.add( new DibitTap( INSTRUMENT_C4FM_SLICER_OUTPUT, 0, 0.1f ) ); + + if( mSymbolFilter != null ) + { + mAvailableTaps.addAll( mSymbolFilter.getTaps() ); + } + } + else + { + mAvailableTaps.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) ); + mAvailableTaps.add( new QPSKTap( INSTRUMENT_QPSK_DEMODULATOR_OUTPUT, 0, 1.0f ) ); + mAvailableTaps.add( new DibitTap( INSTRUMENT_CQPSK_SLICER_OUTPUT, 0, 0.1f ) ); + } } return mAvailableTaps; @@ -147,29 +219,62 @@ public class P25Decoder extends Decoder @Override public void addTap( Tap tap ) { - mSymbolFilter.addTap( tap ); + if( mSymbolFilter != null ) + { + mSymbolFilter.addTap( tap ); + } switch( tap.getName() ) { - case INSTRUMENT_INPUT_TO_AGC: + case INSTRUMENT_COMPLEX_INPUT: + addComplexListener( (ComplexTap)tap ); + break; + case INSTRUMENT_BASEBAND_FILTER_OUTPUT: + ComplexTap baseband = (ComplexTap)tap; + mBasebandFilter.setListener( baseband ); + baseband.setListener( mAGC ); + break; + case INSTRUMENT_AGC_OUTPUT: + ComplexTap agcSymbol = (ComplexTap)tap; + mAGC.setListener( agcSymbol ); + agcSymbol.setListener( mCQPSKDemodulator ); + break; + case INSTRUMENT_QPSK_DEMODULATOR_OUTPUT: + QPSKTap qpsk = (QPSKTap)tap; + mCQPSKDemodulator.setListener( qpsk ); + qpsk.setListener( mCQPSKSlicer ); + break; + case INSTRUMENT_CQPSK_SLICER_OUTPUT: + mCQPSKSlicer.addListener( (DibitTap)tap ); + break; + case INSTRUMENT_REAL_INPUT: FloatTap inputAGC = (FloatTap)tap; - removeRealListener( mGainControl ); + removeRealListener( mDGC ); addRealSampleListener( inputAGC ); - inputAGC.setListener( mGainControl ); + inputAGC.setListener( mDGC ); break; - case INSTRUMENT_AGC_TO_SYMBOL_FILTER: - FloatTap agcSymbol = (FloatTap)tap; - mGainControl.setListener( agcSymbol ); - agcSymbol.setListener( mSymbolFilter ); + case INSTRUMENT_DGC_OUTPUT: + FloatTap dgcSymbol = (FloatTap)tap; + mDGC.setListener( dgcSymbol ); + dgcSymbol.setListener( mSymbolFilter ); break; - case INSTRUMENT_SYMBOL_FILTER_TO_SLICER: + case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT: FloatTap symbolSlicer = (FloatTap)tap; mSymbolFilter.setListener( symbolSlicer ); - symbolSlicer.setListener( mSlicer ); + symbolSlicer.setListener( mC4FMSlicer ); break; - case INSTRUMENT_SLICER_OUTPUT: + case INSTRUMENT_C4FM_SLICER_OUTPUT: DibitTap slicer = (DibitTap)tap; - mSlicer.addListener( slicer ); + + if( mC4FMSlicer != null ) + { + mC4FMSlicer.addListener( slicer ); + } + + if( mCQPSKSlicer != null ) + { + mCQPSKSlicer.addListener( slicer ); + } break; } } @@ -177,24 +282,51 @@ public class P25Decoder extends Decoder @Override public void removeTap( Tap tap ) { - mSymbolFilter.removeTap( tap ); + if( mSymbolFilter != null ) + { + mSymbolFilter.removeTap( tap ); + } switch( tap.getName() ) { - case INSTRUMENT_INPUT_TO_AGC: + case INSTRUMENT_COMPLEX_INPUT: + removeComplexListener( (ComplexTap)tap ); + break; + case INSTRUMENT_BASEBAND_FILTER_OUTPUT: + mBasebandFilter.setListener( mAGC ); + break; + case INSTRUMENT_AGC_OUTPUT: + mAGC.setListener( mCQPSKDemodulator ); + break; + case INSTRUMENT_QPSK_DEMODULATOR_OUTPUT: + mCQPSKDemodulator.setListener( mCQPSKSlicer ); + break; + case INSTRUMENT_CQPSK_SLICER_OUTPUT: + mCQPSKSlicer.removeListener( (DibitTap)tap ); + break; + case INSTRUMENT_REAL_INPUT: FloatTap inputAGC = (FloatTap)tap; removeRealListener( inputAGC ); - addRealSampleListener( mGainControl ); + addRealSampleListener( mDGC ); break; - case INSTRUMENT_AGC_TO_SYMBOL_FILTER: - mGainControl.setListener( mSymbolFilter ); + case INSTRUMENT_DGC_OUTPUT: + mDGC.setListener( mSymbolFilter ); break; - case INSTRUMENT_SYMBOL_FILTER_TO_SLICER: - mSymbolFilter.setListener( mSlicer ); + case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT: + mSymbolFilter.setListener( mC4FMSlicer ); break; - case INSTRUMENT_SLICER_OUTPUT: + case INSTRUMENT_C4FM_SLICER_OUTPUT: DibitTap slicerTap = (DibitTap)tap; - mSlicer.removeListener( slicerTap ); + + if( mC4FMSlicer != null ) + { + mC4FMSlicer.removeListener( slicerTap ); + } + + if( mCQPSKSlicer != null ) + { + mCQPSKSlicer.removeListener( slicerTap ); + } break; } } @@ -213,12 +345,44 @@ public class P25Decoder extends Decoder @Override public void setListener( FrequencyChangeListener listener ) { - mSymbolFilter.addListener( listener ); + if( mSymbolFilter != null ) + { + mSymbolFilter.addListener( listener ); + } } @Override public long getFrequencyCorrection() { - return mSymbolFilter.getFrequencyCorrection(); + if( mSymbolFilter != null ) + { + return mSymbolFilter.getFrequencyCorrection(); + } + + return 0; } + + public enum Modulation + { + C4FM( "C4FM" ), + CQPSK( "LSM SIMULCAST" ); + + private String mLabel; + + private Modulation( String label ) + { + mLabel = label; + } + + public String getLabel() + { + return mLabel; + } + + public String toString() + { + return getLabel(); + } + }; + } diff --git a/src/decode/p25/P25Editor.java b/src/decode/p25/P25Editor.java index abfb7512..f9e9f86e 100644 --- a/src/decode/p25/P25Editor.java +++ b/src/decode/p25/P25Editor.java @@ -17,15 +17,84 @@ ******************************************************************************/ package decode.p25; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import source.SourceEditor; +import source.tuner.TunerEditor; +import controller.Editor; +import controller.channel.ChannelValidationException; import decode.DecodeEditor; +import decode.config.DecodeConfigP25Phase1; import decode.config.DecodeConfiguration; +import decode.p25.P25Decoder.Modulation; public class P25Editor extends DecodeEditor { - private static final long serialVersionUID = 1L; + private final static Logger mLog = LoggerFactory.getLogger( P25Editor.class ); + + private static final long serialVersionUID = 1L; - public P25Editor( DecodeConfiguration config ) + private JComboBox mComboModulation; + + public P25Editor( DecodeConfiguration config ) { super( config ); + + initGUI(); } + + private void initGUI() + { + mComboModulation = new JComboBox(); + + mComboModulation.setModel( new DefaultComboBoxModel( + P25Decoder.Modulation.values() ) ); + + mComboModulation.setSelectedItem( ((DecodeConfigP25Phase1)mConfig).getModulation() ); + + mComboModulation.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + P25Decoder.Modulation selected = mComboModulation + .getItemAt( mComboModulation.getSelectedIndex() ); + + if( selected != null ) + { + ((DecodeConfigP25Phase1)mConfig).setModulation( selected ); + } + } + } ); + + add( mComboModulation, "span 2,wrap" ); + } + + /** + * 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" ); + + } + } + } } diff --git a/src/dsp/am/AMDemodulator.java b/src/dsp/am/AMDemodulator.java index 2978f494..64cfc62b 100644 --- a/src/dsp/am/AMDemodulator.java +++ b/src/dsp/am/AMDemodulator.java @@ -18,8 +18,8 @@ public class AMDemodulator implements Listener, RealSampleProvide { float demodulated = (float)Math.sqrt( ( sample.real() * sample.real() ) + - ( sample.imaginery() * - sample.imaginery() ) ); + ( sample.imaginary() * + sample.imaginary() ) ); if( mListener != null ) { diff --git a/src/dsp/filter/FilterFactory.java b/src/dsp/filter/FilterFactory.java index 8d04de76..f3d1f10b 100644 --- a/src/dsp/filter/FilterFactory.java +++ b/src/dsp/filter/FilterFactory.java @@ -1,7 +1,12 @@ /******************************************************************************* * SDR Trunk - * Copyright (C) 2014 Dennis Sheirer - * + * Copyright (C) 2014,2015 Dennis Sheirer + * + * Root Raised Cosine filter designer: + * Copyright 2002,2007,2008,2012,2013 Free Software Foundation, Inc. + * http://gnuradio.org/redmine/projects/gnuradio/repository/changes/gr-filter + * /lib/firdes.cc?rev=435b1d166f0c7092bbd5e1f788e75dbb6ade3a4b + * * 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 @@ -727,4 +732,93 @@ public class FilterFactory return cicArray; } + + /** + * Creates root raised cosine filter coefficients. + * + * Ported to java from gnuradio/filter/firdes.cc + * + * @param gain - gain value to apply + * @param sampleRate - sample rate in hertz + * @param symbolRate - symbol rate in hertz + * @param alpha - roll-off factor + * @param tapCount - odd number of taps, or will be increased to make odd + * @return - filter coefficients + */ + public static double[] getRootRaisedCosine( double gain, + double sampleRate, + double symbolRate, + double alpha, + int tapCount ) + { + /* Ensure odd number of taps */ + int taps = tapCount | 1; + + double scale = 0; + + double samplesPerSymbol = sampleRate / symbolRate; + + double[] coefficients = new double[ taps ]; + + for( int x = 0; x < taps; x++ ) + { + double index = x - ( taps / 2 ); + + double x1 = Math.PI * index / samplesPerSymbol; + double x2 = 4.0 * alpha * index / samplesPerSymbol; + double x3 = x2 * x2 - 1; + + double numerator, denominator; + + if( Math.abs( x3 ) >= 0.000001 ) + { + if( x != taps / 2 ) + { + numerator = Math.cos( ( 1.0 + alpha ) * x1 ) + + Math.sin( ( 1.0 - alpha ) * x1 ) / + ( 4.0 * alpha * index / samplesPerSymbol ); + } + else + { + numerator = Math.cos( ( 1.0 + alpha ) * x1 ) + + ( 1.0 - alpha ) * Math.PI / ( 4.0 * alpha ); + } + + denominator = x3 * Math.PI; + } + else + { + if( alpha == 1.0 ) + { + coefficients[ x ] = -1.0; + continue; + } + + x3 = ( 1.0 - alpha ) * x1; + x2 = ( 1.0 + alpha ) * x1; + + numerator = ( Math.sin( x2 ) * ( 1.0 + alpha ) * Math.PI - + Math.cos( x3 ) * ( ( 1.0 - alpha ) * Math.PI * samplesPerSymbol ) / + ( 4 * alpha * index ) + + Math.sin( x3 ) * samplesPerSymbol * samplesPerSymbol / + ( 4.0 * alpha * index * index ) ); + + denominator = -32.0 * Math.PI * alpha * alpha * index / samplesPerSymbol; + } + + coefficients[ x ] = 4.0 * alpha * numerator / denominator; + + scale += coefficients[ x ]; + } + + /** + * Normalize (scale) coefficients to 1.0 sum and apply gain + */ + for( int x = 0; x < taps; x++ ) + { + coefficients[ x ] = coefficients[ x ] * gain / scale; + } + + return coefficients; + } } diff --git a/src/dsp/filter/interpolator/Interpolator.java b/src/dsp/filter/interpolator/Interpolator.java new file mode 100644 index 00000000..3c296898 --- /dev/null +++ b/src/dsp/filter/interpolator/Interpolator.java @@ -0,0 +1,161 @@ +package dsp.filter.interpolator; + +/******************************************************************************* + * SDR Trunk + * Copyright (C) 2015 Dennis Sheirer + * + * Copyright (C) 2002,2012 Free Software Foundation, Inc. + * Ported to java from gnuradio - interpolator_taps.h + * + * 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 + ******************************************************************************/ +public abstract class Interpolator +{ + public static final int NTAPS = 8; + public static final int NSTEPS = 128; + + public static final float TAPS[][] = new float[][] + { + // -4 -3 -2 -1 0 1 2 3 mu + { 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 0/128 + { -1.54700e-04f, 8.53777e-04f, -2.76968e-03f, 7.89295e-03f, 9.98534e-01f, -5.41054e-03f, 1.24642e-03f, -1.98993e-04f }, // 1/128 + { -3.09412e-04f, 1.70888e-03f, -5.55134e-03f, 1.58840e-02f, 9.96891e-01f, -1.07209e-02f, 2.47942e-03f, -3.96391e-04f }, // 2/128 + { -4.64053e-04f, 2.56486e-03f, -8.34364e-03f, 2.39714e-02f, 9.95074e-01f, -1.59305e-02f, 3.69852e-03f, -5.92100e-04f }, // 3/128 + { -6.18544e-04f, 3.42130e-03f, -1.11453e-02f, 3.21531e-02f, 9.93082e-01f, -2.10389e-02f, 4.90322e-03f, -7.86031e-04f }, // 4/128 + { -7.72802e-04f, 4.27773e-03f, -1.39548e-02f, 4.04274e-02f, 9.90917e-01f, -2.60456e-02f, 6.09305e-03f, -9.78093e-04f }, // 5/128 + { -9.26747e-04f, 5.13372e-03f, -1.67710e-02f, 4.87921e-02f, 9.88580e-01f, -3.09503e-02f, 7.26755e-03f, -1.16820e-03f }, // 6/128 + { -1.08030e-03f, 5.98883e-03f, -1.95925e-02f, 5.72454e-02f, 9.86071e-01f, -3.57525e-02f, 8.42626e-03f, -1.35627e-03f }, // 7/128 + { -1.23337e-03f, 6.84261e-03f, -2.24178e-02f, 6.57852e-02f, 9.83392e-01f, -4.04519e-02f, 9.56876e-03f, -1.54221e-03f }, // 8/128 + { -1.38589e-03f, 7.69462e-03f, -2.52457e-02f, 7.44095e-02f, 9.80543e-01f, -4.50483e-02f, 1.06946e-02f, -1.72594e-03f }, // 9/128 + { -1.53777e-03f, 8.54441e-03f, -2.80746e-02f, 8.31162e-02f, 9.77526e-01f, -4.95412e-02f, 1.18034e-02f, -1.90738e-03f }, // 10/128 + { -1.68894e-03f, 9.39154e-03f, -3.09033e-02f, 9.19033e-02f, 9.74342e-01f, -5.39305e-02f, 1.28947e-02f, -2.08645e-03f }, // 11/128 + { -1.83931e-03f, 1.02356e-02f, -3.37303e-02f, 1.00769e-01f, 9.70992e-01f, -5.82159e-02f, 1.39681e-02f, -2.26307e-03f }, // 12/128 + { -1.98880e-03f, 1.10760e-02f, -3.65541e-02f, 1.09710e-01f, 9.67477e-01f, -6.23972e-02f, 1.50233e-02f, -2.43718e-03f }, // 13/128 + { -2.13733e-03f, 1.19125e-02f, -3.93735e-02f, 1.18725e-01f, 9.63798e-01f, -6.64743e-02f, 1.60599e-02f, -2.60868e-03f }, // 14/128 + { -2.28483e-03f, 1.27445e-02f, -4.21869e-02f, 1.27812e-01f, 9.59958e-01f, -7.04471e-02f, 1.70776e-02f, -2.77751e-03f }, // 15/128 + { -2.43121e-03f, 1.35716e-02f, -4.49929e-02f, 1.36968e-01f, 9.55956e-01f, -7.43154e-02f, 1.80759e-02f, -2.94361e-03f }, // 16/128 + { -2.57640e-03f, 1.43934e-02f, -4.77900e-02f, 1.46192e-01f, 9.51795e-01f, -7.80792e-02f, 1.90545e-02f, -3.10689e-03f }, // 17/128 + { -2.72032e-03f, 1.52095e-02f, -5.05770e-02f, 1.55480e-01f, 9.47477e-01f, -8.17385e-02f, 2.00132e-02f, -3.26730e-03f }, // 18/128 + { -2.86289e-03f, 1.60193e-02f, -5.33522e-02f, 1.64831e-01f, 9.43001e-01f, -8.52933e-02f, 2.09516e-02f, -3.42477e-03f }, // 19/128 + { -3.00403e-03f, 1.68225e-02f, -5.61142e-02f, 1.74242e-01f, 9.38371e-01f, -8.87435e-02f, 2.18695e-02f, -3.57923e-03f }, // 20/128 + { -3.14367e-03f, 1.76185e-02f, -5.88617e-02f, 1.83711e-01f, 9.33586e-01f, -9.20893e-02f, 2.27664e-02f, -3.73062e-03f }, // 21/128 + { -3.28174e-03f, 1.84071e-02f, -6.15931e-02f, 1.93236e-01f, 9.28650e-01f, -9.53307e-02f, 2.36423e-02f, -3.87888e-03f }, // 22/128 + { -3.41815e-03f, 1.91877e-02f, -6.43069e-02f, 2.02814e-01f, 9.23564e-01f, -9.84679e-02f, 2.44967e-02f, -4.02397e-03f }, // 23/128 + { -3.55283e-03f, 1.99599e-02f, -6.70018e-02f, 2.12443e-01f, 9.18329e-01f, -1.01501e-01f, 2.53295e-02f, -4.16581e-03f }, // 24/128 + { -3.68570e-03f, 2.07233e-02f, -6.96762e-02f, 2.22120e-01f, 9.12947e-01f, -1.04430e-01f, 2.61404e-02f, -4.30435e-03f }, // 25/128 + { -3.81671e-03f, 2.14774e-02f, -7.23286e-02f, 2.31843e-01f, 9.07420e-01f, -1.07256e-01f, 2.69293e-02f, -4.43955e-03f }, // 26/128 + { -3.94576e-03f, 2.22218e-02f, -7.49577e-02f, 2.41609e-01f, 9.01749e-01f, -1.09978e-01f, 2.76957e-02f, -4.57135e-03f }, // 27/128 + { -4.07279e-03f, 2.29562e-02f, -7.75620e-02f, 2.51417e-01f, 8.95936e-01f, -1.12597e-01f, 2.84397e-02f, -4.69970e-03f }, // 28/128 + { -4.19774e-03f, 2.36801e-02f, -8.01399e-02f, 2.61263e-01f, 8.89984e-01f, -1.15113e-01f, 2.91609e-02f, -4.82456e-03f }, // 29/128 + { -4.32052e-03f, 2.43930e-02f, -8.26900e-02f, 2.71144e-01f, 8.83893e-01f, -1.17526e-01f, 2.98593e-02f, -4.94589e-03f }, // 30/128 + { -4.44107e-03f, 2.50946e-02f, -8.52109e-02f, 2.81060e-01f, 8.77666e-01f, -1.19837e-01f, 3.05345e-02f, -5.06363e-03f }, // 31/128 + { -4.55932e-03f, 2.57844e-02f, -8.77011e-02f, 2.91006e-01f, 8.71305e-01f, -1.22047e-01f, 3.11866e-02f, -5.17776e-03f }, // 32/128 + { -4.67520e-03f, 2.64621e-02f, -9.01591e-02f, 3.00980e-01f, 8.64812e-01f, -1.24154e-01f, 3.18153e-02f, -5.28823e-03f }, // 33/128 + { -4.78866e-03f, 2.71272e-02f, -9.25834e-02f, 3.10980e-01f, 8.58189e-01f, -1.26161e-01f, 3.24205e-02f, -5.39500e-03f }, // 34/128 + { -4.89961e-03f, 2.77794e-02f, -9.49727e-02f, 3.21004e-01f, 8.51437e-01f, -1.28068e-01f, 3.30021e-02f, -5.49804e-03f }, // 35/128 + { -5.00800e-03f, 2.84182e-02f, -9.73254e-02f, 3.31048e-01f, 8.44559e-01f, -1.29874e-01f, 3.35600e-02f, -5.59731e-03f }, // 36/128 + { -5.11376e-03f, 2.90433e-02f, -9.96402e-02f, 3.41109e-01f, 8.37557e-01f, -1.31581e-01f, 3.40940e-02f, -5.69280e-03f }, // 37/128 + { -5.21683e-03f, 2.96543e-02f, -1.01915e-01f, 3.51186e-01f, 8.30432e-01f, -1.33189e-01f, 3.46042e-02f, -5.78446e-03f }, // 38/128 + { -5.31716e-03f, 3.02507e-02f, -1.04150e-01f, 3.61276e-01f, 8.23188e-01f, -1.34699e-01f, 3.50903e-02f, -5.87227e-03f }, // 39/128 + { -5.41467e-03f, 3.08323e-02f, -1.06342e-01f, 3.71376e-01f, 8.15826e-01f, -1.36111e-01f, 3.55525e-02f, -5.95620e-03f }, // 40/128 + { -5.50931e-03f, 3.13987e-02f, -1.08490e-01f, 3.81484e-01f, 8.08348e-01f, -1.37426e-01f, 3.59905e-02f, -6.03624e-03f }, // 41/128 + { -5.60103e-03f, 3.19495e-02f, -1.10593e-01f, 3.91596e-01f, 8.00757e-01f, -1.38644e-01f, 3.64044e-02f, -6.11236e-03f }, // 42/128 + { -5.68976e-03f, 3.24843e-02f, -1.12650e-01f, 4.01710e-01f, 7.93055e-01f, -1.39767e-01f, 3.67941e-02f, -6.18454e-03f }, // 43/128 + { -5.77544e-03f, 3.30027e-02f, -1.14659e-01f, 4.11823e-01f, 7.85244e-01f, -1.40794e-01f, 3.71596e-02f, -6.25277e-03f }, // 44/128 + { -5.85804e-03f, 3.35046e-02f, -1.16618e-01f, 4.21934e-01f, 7.77327e-01f, -1.41727e-01f, 3.75010e-02f, -6.31703e-03f }, // 45/128 + { -5.93749e-03f, 3.39894e-02f, -1.18526e-01f, 4.32038e-01f, 7.69305e-01f, -1.42566e-01f, 3.78182e-02f, -6.37730e-03f }, // 46/128 + { -6.01374e-03f, 3.44568e-02f, -1.20382e-01f, 4.42134e-01f, 7.61181e-01f, -1.43313e-01f, 3.81111e-02f, -6.43358e-03f }, // 47/128 + { -6.08674e-03f, 3.49066e-02f, -1.22185e-01f, 4.52218e-01f, 7.52958e-01f, -1.43968e-01f, 3.83800e-02f, -6.48585e-03f }, // 48/128 + { -6.15644e-03f, 3.53384e-02f, -1.23933e-01f, 4.62289e-01f, 7.44637e-01f, -1.44531e-01f, 3.86247e-02f, -6.53412e-03f }, // 49/128 + { -6.22280e-03f, 3.57519e-02f, -1.25624e-01f, 4.72342e-01f, 7.36222e-01f, -1.45004e-01f, 3.88454e-02f, -6.57836e-03f }, // 50/128 + { -6.28577e-03f, 3.61468e-02f, -1.27258e-01f, 4.82377e-01f, 7.27714e-01f, -1.45387e-01f, 3.90420e-02f, -6.61859e-03f }, // 51/128 + { -6.34530e-03f, 3.65227e-02f, -1.28832e-01f, 4.92389e-01f, 7.19116e-01f, -1.45682e-01f, 3.92147e-02f, -6.65479e-03f }, // 52/128 + { -6.40135e-03f, 3.68795e-02f, -1.30347e-01f, 5.02377e-01f, 7.10431e-01f, -1.45889e-01f, 3.93636e-02f, -6.68698e-03f }, // 53/128 + { -6.45388e-03f, 3.72167e-02f, -1.31800e-01f, 5.12337e-01f, 7.01661e-01f, -1.46009e-01f, 3.94886e-02f, -6.71514e-03f }, // 54/128 + { -6.50285e-03f, 3.75341e-02f, -1.33190e-01f, 5.22267e-01f, 6.92808e-01f, -1.46043e-01f, 3.95900e-02f, -6.73929e-03f }, // 55/128 + { -6.54823e-03f, 3.78315e-02f, -1.34515e-01f, 5.32164e-01f, 6.83875e-01f, -1.45993e-01f, 3.96678e-02f, -6.75943e-03f }, // 56/128 + { -6.58996e-03f, 3.81085e-02f, -1.35775e-01f, 5.42025e-01f, 6.74865e-01f, -1.45859e-01f, 3.97222e-02f, -6.77557e-03f }, // 57/128 + { -6.62802e-03f, 3.83650e-02f, -1.36969e-01f, 5.51849e-01f, 6.65779e-01f, -1.45641e-01f, 3.97532e-02f, -6.78771e-03f }, // 58/128 + { -6.66238e-03f, 3.86006e-02f, -1.38094e-01f, 5.61631e-01f, 6.56621e-01f, -1.45343e-01f, 3.97610e-02f, -6.79588e-03f }, // 59/128 + { -6.69300e-03f, 3.88151e-02f, -1.39150e-01f, 5.71370e-01f, 6.47394e-01f, -1.44963e-01f, 3.97458e-02f, -6.80007e-03f }, // 60/128 + { -6.71985e-03f, 3.90083e-02f, -1.40136e-01f, 5.81063e-01f, 6.38099e-01f, -1.44503e-01f, 3.97077e-02f, -6.80032e-03f }, // 61/128 + { -6.74291e-03f, 3.91800e-02f, -1.41050e-01f, 5.90706e-01f, 6.28739e-01f, -1.43965e-01f, 3.96469e-02f, -6.79662e-03f }, // 62/128 + { -6.76214e-03f, 3.93299e-02f, -1.41891e-01f, 6.00298e-01f, 6.19318e-01f, -1.43350e-01f, 3.95635e-02f, -6.78902e-03f }, // 63/128 + { -6.77751e-03f, 3.94578e-02f, -1.42658e-01f, 6.09836e-01f, 6.09836e-01f, -1.42658e-01f, 3.94578e-02f, -6.77751e-03f }, // 64/128 + { -6.78902e-03f, 3.95635e-02f, -1.43350e-01f, 6.19318e-01f, 6.00298e-01f, -1.41891e-01f, 3.93299e-02f, -6.76214e-03f }, // 65/128 + { -6.79662e-03f, 3.96469e-02f, -1.43965e-01f, 6.28739e-01f, 5.90706e-01f, -1.41050e-01f, 3.91800e-02f, -6.74291e-03f }, // 66/128 + { -6.80032e-03f, 3.97077e-02f, -1.44503e-01f, 6.38099e-01f, 5.81063e-01f, -1.40136e-01f, 3.90083e-02f, -6.71985e-03f }, // 67/128 + { -6.80007e-03f, 3.97458e-02f, -1.44963e-01f, 6.47394e-01f, 5.71370e-01f, -1.39150e-01f, 3.88151e-02f, -6.69300e-03f }, // 68/128 + { -6.79588e-03f, 3.97610e-02f, -1.45343e-01f, 6.56621e-01f, 5.61631e-01f, -1.38094e-01f, 3.86006e-02f, -6.66238e-03f }, // 69/128 + { -6.78771e-03f, 3.97532e-02f, -1.45641e-01f, 6.65779e-01f, 5.51849e-01f, -1.36969e-01f, 3.83650e-02f, -6.62802e-03f }, // 70/128 + { -6.77557e-03f, 3.97222e-02f, -1.45859e-01f, 6.74865e-01f, 5.42025e-01f, -1.35775e-01f, 3.81085e-02f, -6.58996e-03f }, // 71/128 + { -6.75943e-03f, 3.96678e-02f, -1.45993e-01f, 6.83875e-01f, 5.32164e-01f, -1.34515e-01f, 3.78315e-02f, -6.54823e-03f }, // 72/128 + { -6.73929e-03f, 3.95900e-02f, -1.46043e-01f, 6.92808e-01f, 5.22267e-01f, -1.33190e-01f, 3.75341e-02f, -6.50285e-03f }, // 73/128 + { -6.71514e-03f, 3.94886e-02f, -1.46009e-01f, 7.01661e-01f, 5.12337e-01f, -1.31800e-01f, 3.72167e-02f, -6.45388e-03f }, // 74/128 + { -6.68698e-03f, 3.93636e-02f, -1.45889e-01f, 7.10431e-01f, 5.02377e-01f, -1.30347e-01f, 3.68795e-02f, -6.40135e-03f }, // 75/128 + { -6.65479e-03f, 3.92147e-02f, -1.45682e-01f, 7.19116e-01f, 4.92389e-01f, -1.28832e-01f, 3.65227e-02f, -6.34530e-03f }, // 76/128 + { -6.61859e-03f, 3.90420e-02f, -1.45387e-01f, 7.27714e-01f, 4.82377e-01f, -1.27258e-01f, 3.61468e-02f, -6.28577e-03f }, // 77/128 + { -6.57836e-03f, 3.88454e-02f, -1.45004e-01f, 7.36222e-01f, 4.72342e-01f, -1.25624e-01f, 3.57519e-02f, -6.22280e-03f }, // 78/128 + { -6.53412e-03f, 3.86247e-02f, -1.44531e-01f, 7.44637e-01f, 4.62289e-01f, -1.23933e-01f, 3.53384e-02f, -6.15644e-03f }, // 79/128 + { -6.48585e-03f, 3.83800e-02f, -1.43968e-01f, 7.52958e-01f, 4.52218e-01f, -1.22185e-01f, 3.49066e-02f, -6.08674e-03f }, // 80/128 + { -6.43358e-03f, 3.81111e-02f, -1.43313e-01f, 7.61181e-01f, 4.42134e-01f, -1.20382e-01f, 3.44568e-02f, -6.01374e-03f }, // 81/128 + { -6.37730e-03f, 3.78182e-02f, -1.42566e-01f, 7.69305e-01f, 4.32038e-01f, -1.18526e-01f, 3.39894e-02f, -5.93749e-03f }, // 82/128 + { -6.31703e-03f, 3.75010e-02f, -1.41727e-01f, 7.77327e-01f, 4.21934e-01f, -1.16618e-01f, 3.35046e-02f, -5.85804e-03f }, // 83/128 + { -6.25277e-03f, 3.71596e-02f, -1.40794e-01f, 7.85244e-01f, 4.11823e-01f, -1.14659e-01f, 3.30027e-02f, -5.77544e-03f }, // 84/128 + { -6.18454e-03f, 3.67941e-02f, -1.39767e-01f, 7.93055e-01f, 4.01710e-01f, -1.12650e-01f, 3.24843e-02f, -5.68976e-03f }, // 85/128 + { -6.11236e-03f, 3.64044e-02f, -1.38644e-01f, 8.00757e-01f, 3.91596e-01f, -1.10593e-01f, 3.19495e-02f, -5.60103e-03f }, // 86/128 + { -6.03624e-03f, 3.59905e-02f, -1.37426e-01f, 8.08348e-01f, 3.81484e-01f, -1.08490e-01f, 3.13987e-02f, -5.50931e-03f }, // 87/128 + { -5.95620e-03f, 3.55525e-02f, -1.36111e-01f, 8.15826e-01f, 3.71376e-01f, -1.06342e-01f, 3.08323e-02f, -5.41467e-03f }, // 88/128 + { -5.87227e-03f, 3.50903e-02f, -1.34699e-01f, 8.23188e-01f, 3.61276e-01f, -1.04150e-01f, 3.02507e-02f, -5.31716e-03f }, // 89/128 + { -5.78446e-03f, 3.46042e-02f, -1.33189e-01f, 8.30432e-01f, 3.51186e-01f, -1.01915e-01f, 2.96543e-02f, -5.21683e-03f }, // 90/128 + { -5.69280e-03f, 3.40940e-02f, -1.31581e-01f, 8.37557e-01f, 3.41109e-01f, -9.96402e-02f, 2.90433e-02f, -5.11376e-03f }, // 91/128 + { -5.59731e-03f, 3.35600e-02f, -1.29874e-01f, 8.44559e-01f, 3.31048e-01f, -9.73254e-02f, 2.84182e-02f, -5.00800e-03f }, // 92/128 + { -5.49804e-03f, 3.30021e-02f, -1.28068e-01f, 8.51437e-01f, 3.21004e-01f, -9.49727e-02f, 2.77794e-02f, -4.89961e-03f }, // 93/128 + { -5.39500e-03f, 3.24205e-02f, -1.26161e-01f, 8.58189e-01f, 3.10980e-01f, -9.25834e-02f, 2.71272e-02f, -4.78866e-03f }, // 94/128 + { -5.28823e-03f, 3.18153e-02f, -1.24154e-01f, 8.64812e-01f, 3.00980e-01f, -9.01591e-02f, 2.64621e-02f, -4.67520e-03f }, // 95/128 + { -5.17776e-03f, 3.11866e-02f, -1.22047e-01f, 8.71305e-01f, 2.91006e-01f, -8.77011e-02f, 2.57844e-02f, -4.55932e-03f }, // 96/128 + { -5.06363e-03f, 3.05345e-02f, -1.19837e-01f, 8.77666e-01f, 2.81060e-01f, -8.52109e-02f, 2.50946e-02f, -4.44107e-03f }, // 97/128 + { -4.94589e-03f, 2.98593e-02f, -1.17526e-01f, 8.83893e-01f, 2.71144e-01f, -8.26900e-02f, 2.43930e-02f, -4.32052e-03f }, // 98/128 + { -4.82456e-03f, 2.91609e-02f, -1.15113e-01f, 8.89984e-01f, 2.61263e-01f, -8.01399e-02f, 2.36801e-02f, -4.19774e-03f }, // 99/128 + { -4.69970e-03f, 2.84397e-02f, -1.12597e-01f, 8.95936e-01f, 2.51417e-01f, -7.75620e-02f, 2.29562e-02f, -4.07279e-03f }, // 100/128 + { -4.57135e-03f, 2.76957e-02f, -1.09978e-01f, 9.01749e-01f, 2.41609e-01f, -7.49577e-02f, 2.22218e-02f, -3.94576e-03f }, // 101/128 + { -4.43955e-03f, 2.69293e-02f, -1.07256e-01f, 9.07420e-01f, 2.31843e-01f, -7.23286e-02f, 2.14774e-02f, -3.81671e-03f }, // 102/128 + { -4.30435e-03f, 2.61404e-02f, -1.04430e-01f, 9.12947e-01f, 2.22120e-01f, -6.96762e-02f, 2.07233e-02f, -3.68570e-03f }, // 103/128 + { -4.16581e-03f, 2.53295e-02f, -1.01501e-01f, 9.18329e-01f, 2.12443e-01f, -6.70018e-02f, 1.99599e-02f, -3.55283e-03f }, // 104/128 + { -4.02397e-03f, 2.44967e-02f, -9.84679e-02f, 9.23564e-01f, 2.02814e-01f, -6.43069e-02f, 1.91877e-02f, -3.41815e-03f }, // 105/128 + { -3.87888e-03f, 2.36423e-02f, -9.53307e-02f, 9.28650e-01f, 1.93236e-01f, -6.15931e-02f, 1.84071e-02f, -3.28174e-03f }, // 106/128 + { -3.73062e-03f, 2.27664e-02f, -9.20893e-02f, 9.33586e-01f, 1.83711e-01f, -5.88617e-02f, 1.76185e-02f, -3.14367e-03f }, // 107/128 + { -3.57923e-03f, 2.18695e-02f, -8.87435e-02f, 9.38371e-01f, 1.74242e-01f, -5.61142e-02f, 1.68225e-02f, -3.00403e-03f }, // 108/128 + { -3.42477e-03f, 2.09516e-02f, -8.52933e-02f, 9.43001e-01f, 1.64831e-01f, -5.33522e-02f, 1.60193e-02f, -2.86289e-03f }, // 109/128 + { -3.26730e-03f, 2.00132e-02f, -8.17385e-02f, 9.47477e-01f, 1.55480e-01f, -5.05770e-02f, 1.52095e-02f, -2.72032e-03f }, // 110/128 + { -3.10689e-03f, 1.90545e-02f, -7.80792e-02f, 9.51795e-01f, 1.46192e-01f, -4.77900e-02f, 1.43934e-02f, -2.57640e-03f }, // 111/128 + { -2.94361e-03f, 1.80759e-02f, -7.43154e-02f, 9.55956e-01f, 1.36968e-01f, -4.49929e-02f, 1.35716e-02f, -2.43121e-03f }, // 112/128 + { -2.77751e-03f, 1.70776e-02f, -7.04471e-02f, 9.59958e-01f, 1.27812e-01f, -4.21869e-02f, 1.27445e-02f, -2.28483e-03f }, // 113/128 + { -2.60868e-03f, 1.60599e-02f, -6.64743e-02f, 9.63798e-01f, 1.18725e-01f, -3.93735e-02f, 1.19125e-02f, -2.13733e-03f }, // 114/128 + { -2.43718e-03f, 1.50233e-02f, -6.23972e-02f, 9.67477e-01f, 1.09710e-01f, -3.65541e-02f, 1.10760e-02f, -1.98880e-03f }, // 115/128 + { -2.26307e-03f, 1.39681e-02f, -5.82159e-02f, 9.70992e-01f, 1.00769e-01f, -3.37303e-02f, 1.02356e-02f, -1.83931e-03f }, // 116/128 + { -2.08645e-03f, 1.28947e-02f, -5.39305e-02f, 9.74342e-01f, 9.19033e-02f, -3.09033e-02f, 9.39154e-03f, -1.68894e-03f }, // 117/128 + { -1.90738e-03f, 1.18034e-02f, -4.95412e-02f, 9.77526e-01f, 8.31162e-02f, -2.80746e-02f, 8.54441e-03f, -1.53777e-03f }, // 118/128 + { -1.72594e-03f, 1.06946e-02f, -4.50483e-02f, 9.80543e-01f, 7.44095e-02f, -2.52457e-02f, 7.69462e-03f, -1.38589e-03f }, // 119/128 + { -1.54221e-03f, 9.56876e-03f, -4.04519e-02f, 9.83392e-01f, 6.57852e-02f, -2.24178e-02f, 6.84261e-03f, -1.23337e-03f }, // 120/128 + { -1.35627e-03f, 8.42626e-03f, -3.57525e-02f, 9.86071e-01f, 5.72454e-02f, -1.95925e-02f, 5.98883e-03f, -1.08030e-03f }, // 121/128 + { -1.16820e-03f, 7.26755e-03f, -3.09503e-02f, 9.88580e-01f, 4.87921e-02f, -1.67710e-02f, 5.13372e-03f, -9.26747e-04f }, // 122/128 + { -9.78093e-04f, 6.09305e-03f, -2.60456e-02f, 9.90917e-01f, 4.04274e-02f, -1.39548e-02f, 4.27773e-03f, -7.72802e-04f }, // 123/128 + { -7.86031e-04f, 4.90322e-03f, -2.10389e-02f, 9.93082e-01f, 3.21531e-02f, -1.11453e-02f, 3.42130e-03f, -6.18544e-04f }, // 124/128 + { -5.92100e-04f, 3.69852e-03f, -1.59305e-02f, 9.95074e-01f, 2.39714e-02f, -8.34364e-03f, 2.56486e-03f, -4.64053e-04f }, // 125/128 + { -3.96391e-04f, 2.47942e-03f, -1.07209e-02f, 9.96891e-01f, 1.58840e-02f, -5.55134e-03f, 1.70888e-03f, -3.09412e-04f }, // 126/128 + { -1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f }, // 127/128 + { 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 128/128 + }; +} diff --git a/src/dsp/filter/interpolator/QPSKInterpolator.java b/src/dsp/filter/interpolator/QPSKInterpolator.java new file mode 100644 index 00000000..a53326bf --- /dev/null +++ b/src/dsp/filter/interpolator/QPSKInterpolator.java @@ -0,0 +1,55 @@ +package dsp.filter.interpolator; + +import java.util.Arrays; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sample.complex.ComplexSample; + +public class QPSKInterpolator extends Interpolator +{ + private final static Logger mLog = LoggerFactory.getLogger( QPSKInterpolator.class ); + + /** + * Calculates an interpolated value from the 8 samples beginning at the + * offset index. Uses the mu value to determine which of 128 filter kernels + * to use in order to closely approximate the ideal frequency response for + * the value of mu, ranging from 0.0 to 1.0, indicating the interpolated + * position in the sample set ranging from offset to offset + 7. + * + * @param samples - sample array of length at least offset + 7 + * + * @param mu - interpolated sample position between 0 and 1.0 + * + * @return - interpolated sample value + */ + public ComplexSample filter( ComplexSample[] samples, int offset, float mu ) + { + /* Ensure we have enough samples in the array */ + assert( samples.length >= offset + 7 ); + + /* Identify the filter bank that corresponds to mu */ + int index = (int)( NSTEPS * mu ); + + float realAccumulator = ( TAPS[ index ][ 7 ] * samples[ offset ].real() ); + realAccumulator += ( TAPS[ index ][ 6 ] * samples[ offset + 1 ].real() ); + realAccumulator += ( TAPS[ index ][ 5 ] * samples[ offset + 2 ].real() ); + realAccumulator += ( TAPS[ index ][ 4 ] * samples[ offset + 3 ].real() ); + realAccumulator += ( TAPS[ index ][ 3 ] * samples[ offset + 4 ].real() ); + realAccumulator += ( TAPS[ index ][ 2 ] * samples[ offset + 5 ].real() ); + realAccumulator += ( TAPS[ index ][ 1 ] * samples[ offset + 6 ].real() ); + realAccumulator += ( TAPS[ index ][ 0 ] * samples[ offset + 7 ].real() ); + + float imaginaryAccumulator = ( TAPS[ index ][ 7 ] * samples[ offset ].imaginary() ); + imaginaryAccumulator += ( TAPS[ index ][ 6 ] * samples[ offset + 1 ].imaginary() ); + imaginaryAccumulator += ( TAPS[ index ][ 5 ] * samples[ offset + 2 ].imaginary() ); + imaginaryAccumulator += ( TAPS[ index ][ 4 ] * samples[ offset + 3 ].imaginary() ); + imaginaryAccumulator += ( TAPS[ index ][ 3 ] * samples[ offset + 4 ].imaginary() ); + imaginaryAccumulator += ( TAPS[ index ][ 2 ] * samples[ offset + 5 ].imaginary() ); + imaginaryAccumulator += ( TAPS[ index ][ 1 ] * samples[ offset + 6 ].imaginary() ); + imaginaryAccumulator += ( TAPS[ index ][ 0 ] * samples[ offset + 7 ].imaginary() ); + + return new ComplexSample( realAccumulator, imaginaryAccumulator ); + } +} diff --git a/src/dsp/gain/DirectGainControl.java b/src/dsp/gain/DirectGainControl.java index 78223427..a83523bc 100644 --- a/src/dsp/gain/DirectGainControl.java +++ b/src/dsp/gain/DirectGainControl.java @@ -71,6 +71,8 @@ public class DirectGainControl implements GainController, RealSampleListener { mGain = mMaximum; } + + mLog.debug( "Gain Increase: " + mGain ); } @Override @@ -82,6 +84,7 @@ public class DirectGainControl implements GainController, RealSampleListener { mGain = mMinimum; } + mLog.debug( "Gain Decrease: " + mGain ); } public void setListener( RealSampleListener listener ) diff --git a/src/dsp/nbfm/FMDiscriminator.java b/src/dsp/nbfm/FMDiscriminator.java index 0f57b2b3..2fc6d8ce 100644 --- a/src/dsp/nbfm/FMDiscriminator.java +++ b/src/dsp/nbfm/FMDiscriminator.java @@ -74,7 +74,7 @@ public class FMDiscriminator implements Listener else { /** - * Use the arcus tangent of imaginary (q) divided by real (i) to + * Use the arc-tangent of imaginary (q) divided by real (i) to * get the phase angle (+/-) which was directly manipulated by the * original message waveform during the modulation. This value now * serves as the instantaneous amplitude of the demodulated signal diff --git a/src/dsp/psk/CQPSKDemodulator.java b/src/dsp/psk/CQPSKDemodulator.java new file mode 100644 index 00000000..8e353b6d --- /dev/null +++ b/src/dsp/psk/CQPSKDemodulator.java @@ -0,0 +1,384 @@ +package dsp.psk; + +/******************************************************************************* + * 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.Provider; +import sample.complex.ComplexSample; +import dsp.filter.interpolator.QPSKInterpolator; + +public class CQPSKDemodulator implements Listener, + Provider +{ + private final static Logger mLog = + LoggerFactory.getLogger( CQPSKDemodulator.class ); + + /* Sample Rate 48000 / Symbol Rate 4800 = Samples Per Symbol 10 */ + public static final int SAMPLES_PER_SYMBOL = 10; + public static final int d_twice_sps = 20; + public static final int half_sps = 5; + + public static final double TWO_PI = 2.0 * Math.PI; + + /* CQPSK constellation is offset from normal QPSK by 45 degrees or pi/4 */ + public static final float THETA = (float)( Math.PI / 4.0d ); + + private float d_mu = 10.5f; + private float d_gain_mu = 0.05f; + + /* samples per symbol */ + private float d_omega = 10.0f; + private float d_half_omega = 5.0f; + private float d_gain_omega = 0.1f * d_gain_mu * d_gain_mu; + private float d_omega_rel = 0.005f; + private float d_min_omega = d_omega * ( 1.0f - d_omega_rel ); + private float d_max_omega = d_omega * ( 1.0f + d_omega_rel ); + private float d_omega_mid = 10.0f; + private float d_phase = 0.0f; + private float d_freq = 0.0f; + + private ComplexSample[] d_dl = new ComplexSample[ 40 ]; + private int d_dl_index = 0; + + private Listener mListener; + private ComplexSample[] mConstellationPoints; + + private SymbolTracker mSymbolTracker = new SymbolTracker(); + private QPSKPhaseErrorTracker mPhaseTracker = new QPSKPhaseErrorTracker(); + + private QPSKInterpolator mInterpolator = new QPSKInterpolator(); + + private ComplexSample d_last_sample = new ComplexSample( 0.0f, 0.0f ); + + public CQPSKDemodulator() + { + createConstellationPoints(); + + /* prefill the delay line */ + for( int x = 0; x < 40; x++ ) + { + d_dl[ x ] = new ComplexSample( 0.0f, 0.0f ); + } + + } + + private void createConstellationPoints() + { + mConstellationPoints = new ComplexSample[ 4 ]; + + for( int x = 0; x < 4; x++ ) + { + mConstellationPoints[ x ] = ComplexSample.fromAngle( + ( (float)( TWO_PI / 4.0f ) * (float)x ) ); + } + } + + @Override + public void receive( ComplexSample sample ) + { + /* skip a number of symbols between sampling */ + d_mu--; + + /* increment phase based on the frequency of the rotation */ + d_phase += d_freq; + + /* keep phase clamped and not walk to infinity */ + unwrapPhase(); + + /* NCO value for derotating the current sample */ + ComplexSample nco = ComplexSample.fromAngle( d_phase + THETA ); + + /* Mixed/downconverted symbol */ + ComplexSample symbol = ComplexSample.multiply( nco, sample ); + + /* Fill up the delay line for the interpolator */ + d_dl[ d_dl_index ] = symbol; + d_dl[ d_dl_index + d_twice_sps ] = symbol; + + /* Keep the delay line index in bounds */ + d_dl_index = ( d_dl_index + 1 ) % d_twice_sps; + + if( d_mu <= 1.0f ) + { + ComplexSample interp_samp_mid = mInterpolator.filter( d_dl, d_dl_index, d_mu ); + + ComplexSample interp_samp = mInterpolator.filter( d_dl, d_dl_index + half_sps, d_mu ); + + float error_real = ( d_last_sample.real() - interp_samp.real() ) * interp_samp_mid.real(); + float error_imag = ( d_last_sample.imaginary() - interp_samp.imaginary() ) * interp_samp_mid.imaginary(); + + ComplexSample diffdec = ComplexSample.multiply( interp_samp, d_last_sample.conjugate() ); + + d_last_sample = interp_samp; + + float symbol_error = normalize( error_real + error_imag ); //Garner loop error + + d_omega = d_omega + d_gain_omega * symbol_error * interp_samp.magnitude(); + d_omega = d_omega_mid + clip( d_omega_mid, d_omega_rel ); + + d_mu += d_omega + d_gain_mu * symbol_error; + +// mSymbolTracker.receive( interpolated ); + + mPhaseTracker.receive( diffdec ); + + if( mListener != null ) + { +// mListener.receive( interp_samp ); + mListener.receive( diffdec ); + } + } + } + + private float normalize( float symbol_error ) + { + if( Float.isNaN( symbol_error ) ) + { + return 0.0f; + } + + if( symbol_error < -1.0 ) + { + return -1.0f; + } + + if( symbol_error > 1.0 ) + { + return 1.0f; + } + + return symbol_error; + } + + /** + * Constrains value to the range of ( -maximum <> maximum ) + */ + private float clip( float value, float maximum ) + { + if( value > maximum ) + { + return maximum; + } + else if( value < -maximum ) + { + return -maximum; + } + + return value; + } + + public static int getQPSKConstellationPoint( ComplexSample sample ) + { + if( sample.real() >= 0.0f ) + { + if( sample.imaginary() >= 0.0f ) + { + return 0; + } + else + { + return 2; + } + } + else + { + if( sample.imaginary() >= 0.0f ) + { + return 1; + } + else + { + return 3; + } + } + } + + private void unwrapPhase() + { + while( d_phase > TWO_PI ) + { + d_phase -= TWO_PI; + } + + while( d_phase < -TWO_PI ) + { + d_phase += TWO_PI; + } + } + + + + /** + * Mueller & Mueller symbol timing tracker + */ + public class SymbolTracker implements Listener + { + ComplexSample mPoint2T = new ComplexSample(); + ComplexSample mPoint1T = new ComplexSample(); + ComplexSample mPoint0T = new ComplexSample(); + + ComplexSample mConstellation2T = new ComplexSample(); + ComplexSample mConstellation1T = new ComplexSample(); + ComplexSample mConstellation0T = new ComplexSample(); + + + @Override + public void receive( ComplexSample sample ) + { + mPoint2T = mPoint1T; + mPoint1T = mPoint0T; + mPoint0T = sample; + + mConstellation2T = mConstellation1T; + mConstellation1T = mConstellation0T; + + int currentConstellation = getQPSKConstellationPoint( mPoint0T ); + mConstellation0T = mConstellationPoints[ currentConstellation ]; + + ComplexSample x = ComplexSample.multiply( + ComplexSample.subtract( mConstellation0T, mConstellation2T ), + mPoint1T.conjugate() ); + + ComplexSample y = ComplexSample.multiply( + ComplexSample.subtract( mPoint0T, mPoint2T ), + mConstellation1T.conjugate() ); + + ComplexSample u = ComplexSample.subtract( y, x ); + + /* Real part of u contains error -- constrain to -1.0 to 1.0 */ + float mm_error = clip( u.real(), 1.0f ); + + /* Update omega (samples per symbol) based on loop error */ + d_omega = d_omega + d_gain_omega * mm_error; + + /* Make sure we don't walk away */ + d_omega = d_omega_mid + clip( d_omega - d_omega_mid, d_omega_rel ); + + /* update mu based on loop error */ + d_mu += d_omega + d_gain_mu * mm_error; + } + } + + /** + * Implements a control loop for QPSK + * + * Ported to java from gnuradio/control_loop and gnuradio/mpsk_receiver_cc + * using initialization values from OP25/cqpsk.py + */ + public class QPSKPhaseErrorTracker implements Listener + { + private float d_loop_bw = 0.03f; + private float d_damping = (float)Math.sqrt( 2.0 ) / 2.0f; + private float d_alpha = 0.03f; + private float d_beta = 0.125f * d_alpha * d_alpha; + private float d_max_freq = ( 2.0f * (float)Math.PI * 1200.0f ) / 48000.0f; + + public QPSKPhaseErrorTracker() + { + } + + @Override + public void receive( ComplexSample sample ) + { + float phase_error = -getPhaseError( sample ); + + advance_loop(phase_error, sample ); + + unwrapPhase(); + + frequency_limit(); + } + + private void advance_loop( float phase_error, ComplexSample sample ) + { + float abs = sample.magnitude(); + + d_freq += d_beta * phase_error * abs; + + d_phase += d_freq + d_alpha * phase_error * abs; + } + + private void frequency_limit() + { + if( d_freq > d_max_freq ) + { + d_freq = d_max_freq; + } + + if( d_freq < -d_max_freq ) + { + d_freq = -d_max_freq; + } + } + + /** + * Calculates the phase error for a qpsk sample. Standard qpsk samples + * should be aligned along the x/y axis and any deviation from either + * axis represents an error. Whichever component, real or imaginary, + * that has the lesser absolute value, represents the error, since that + * component should be zero valued. + * + * For example, constellation point 0 is zero degrees and is represented + * by a sample that contains a real = +1 and an imaginary = 0 component. + * If we sample too early or too late, that will cause a +/- phase + * deviation in the resultant sample and the sample will reflect that + * error value in the imaginary component. That deviation is the error. + */ + public float getPhaseError( ComplexSample sample ) + { + if( sample.realAbsolute() > sample.imaginaryAbsolute() ) + { + if( sample.real() > 0 ) + { + return -sample.imaginary(); + } + else + { + return sample.imaginary(); + } + } + else + { + if( sample.imaginary() > 0 ) + { + return sample.real(); + } + else + { + return -sample.real(); + } + } + } + } + + @Override + public void setListener( Listener listener ) + { + mListener = listener; + } + + @Override + public void removeListener( Listener listener ) + { + mListener = null; + } +} diff --git a/src/dsp/symbol/FrameSync.java b/src/dsp/symbol/FrameSync.java index 01042c06..523d4fcf 100644 --- a/src/dsp/symbol/FrameSync.java +++ b/src/dsp/symbol/FrameSync.java @@ -2,7 +2,7 @@ package dsp.symbol; public enum FrameSync { - P25_PHASE1( 0x5575F5FF77FFl ), + P25_PHASE1( 0x5575F5FF77FFl ), //+3 +3 +3 +3 +3 -3 +3 +3 -3 -3 +3 +3 -3 -3 -3 -3 +3 -3 +3 -3 -3 -3 -3 -3 P25_PHASE1_INVERTED( 0xFFDF5F55DD55l ); private long mSync; diff --git a/src/eventlog/EventLogComponentEditor.java b/src/eventlog/EventLogComponentEditor.java index 7bd42d85..b9d7b20c 100644 --- a/src/eventlog/EventLogComponentEditor.java +++ b/src/eventlog/EventLogComponentEditor.java @@ -19,12 +19,13 @@ package eventlog; import java.util.ArrayList; -import javax.swing.BorderFactory; import javax.swing.JCheckBox; import javax.swing.SwingUtilities; +import controller.Editor; import controller.channel.AbstractChannelEditor; import controller.channel.ChannelNode; +import controller.channel.ChannelValidationException; import eventlog.config.EventLogConfiguration; public class EventLogComponentEditor extends AbstractChannelEditor diff --git a/src/gui/control/ConstellationViewer.java b/src/gui/control/ConstellationViewer.java index 3f5bea05..3aa317a7 100644 --- a/src/gui/control/ConstellationViewer.java +++ b/src/gui/control/ConstellationViewer.java @@ -80,6 +80,8 @@ public class ConstellationViewer extends JPanel { mBuffer.receive( sample ); + ComplexSample angle = ComplexSample.multiply( sample, mPrevious.conjugate() ); + repaint(); } @@ -105,7 +107,6 @@ public class ConstellationViewer extends JPanel Graphics2D graphics = (Graphics2D) g; -// graphics.setBackground( Color.BLACK ); graphics.setColor( Color.BLUE ); List samples = mBuffer.getElements(); @@ -113,15 +114,12 @@ public class ConstellationViewer extends JPanel double centerX = (double)getHeight() / 2.0d; double centerY = (double)getWidth() / 2.0d; -// double scale = centerX / 2.0d; - double scale = 0.5d; mCounter = 0; for( ComplexSample sample: samples ) { - if( mCounter > ( mOffset + mSamplesPerSymbol ) ) { /** @@ -135,7 +133,6 @@ public class ConstellationViewer extends JPanel double q = ( sample.quadrature() * mPrevious.inphase() ) + ( sample.inphase() * -mPrevious.quadrature() ); - mLog.debug( "i:" + i + " q:" + q ); double angle; //Check for divide by zero @@ -165,9 +162,6 @@ public class ConstellationViewer extends JPanel } mCounter++; - -// mLog.debug( "Plotting x:" + ( centerX - ( sample.x() * scale ) ) + -// " y:" + ( centerY - ( sample.y() * scale ) ) + " for sample " + sample.toString() ); } } diff --git a/src/instrument/gui/AudioSourceFrame.java b/src/instrument/gui/AudioSourceFrame.java index 1803f153..f4761f9c 100644 --- a/src/instrument/gui/AudioSourceFrame.java +++ b/src/instrument/gui/AudioSourceFrame.java @@ -40,7 +40,6 @@ import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import source.Source.SampleType; import source.wave.WaveSource; import source.wave.WaveSource.PositionListener; import decode.Decoder; @@ -52,6 +51,7 @@ import decode.mdc1200.MDCDecoder; import decode.mpt1327.MPT1327Decoder; import decode.mpt1327.MPT1327Decoder.Sync; import decode.p25.P25Decoder; +import decode.p25.P25Decoder.Modulation; import decode.passport.PassportDecoder; import decode.tait.Tait1200Decoder; @@ -66,6 +66,8 @@ public class AudioSourceFrame extends JInternalFrame implements PositionListener private JLabel mCurrentPosition = new JLabel( "0" ); private JComboBox mComboDecoders; + private JComboBox mComboModulations; + private JLabel mModulationLabel = new JLabel( "Modulation:" ); public AudioSourceFrame( WaveSource source, JDesktopPane desktop ) { @@ -115,10 +117,39 @@ public class AudioSourceFrame extends JInternalFrame implements PositionListener } mComboDecoders.setModel( model ); + + mComboDecoders.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( ActionEvent e ) + { + DecoderType selected = (DecoderType)mComboDecoders.getSelectedItem(); + + if( selected == DecoderType.P25_PHASE1 ) + { + mComboModulations.setVisible( true ); + mModulationLabel.setVisible( true ); + } + else + { + mComboModulations.setVisible( false ); + mModulationLabel.setVisible( false ); + } + } + + }); panel.add( new JLabel( "Decoders:" ) ); panel.add( mComboDecoders, "span 3,grow" ); - panel.add( new AddDecoderButton(), "grow" ); + panel.add( new AddDecoderButton(), "grow,wrap" ); + + mComboModulations = new JComboBox( + new DefaultComboBoxModel( Modulation.values() ) ); + mComboModulations.setVisible( false ); + mModulationLabel.setVisible( false ); + + panel.add( mModulationLabel ); + panel.add( mComboModulations ); add( panel ); } @@ -152,19 +183,20 @@ public class AudioSourceFrame extends JInternalFrame implements PositionListener decoder = new LJ1200Decoder( null ); break; case LTR_NET: - decoder = new LTRNetDecoder( SampleType.REAL, null, MessageDirection.OSW ); + decoder = new LTRNetDecoder( mSource.getSampleType(), null, MessageDirection.OSW ); break; case MDC1200: decoder = new MDCDecoder( null ); break; case MPT1327: - decoder = new MPT1327Decoder( SampleType.REAL, null, Sync.NORMAL ); + decoder = new MPT1327Decoder( mSource.getSampleType(), null, Sync.NORMAL ); break; case PASSPORT: - decoder = new PassportDecoder( SampleType.REAL, null ); + decoder = new PassportDecoder( mSource.getSampleType(), null ); break; case P25_PHASE1: - decoder = new P25Decoder( SampleType.REAL, null ); + decoder = new P25Decoder( mSource.getSampleType(), + (Modulation)mComboModulations.getSelectedItem(), null ); break; case TAIT_1200: decoder = new Tait1200Decoder( null ); diff --git a/src/instrument/gui/BasebandSourceFrame.java b/src/instrument/gui/BasebandSourceFrame.java deleted file mode 100644 index a56bf31e..00000000 --- a/src/instrument/gui/BasebandSourceFrame.java +++ /dev/null @@ -1,240 +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 instrument.gui; - -import java.awt.Dimension; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.io.IOException; - -import javax.swing.DefaultComboBoxModel; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JDesktopPane; -import javax.swing.JInternalFrame; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JTextField; - -import net.miginfocom.swing.MigLayout; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sample.Listener; -import sample.complex.ComplexSample; -import source.wave.ComplexWaveSource; -import source.wave.WaveSource; -import source.wave.WaveSource.PositionListener; - -public class BasebandSourceFrame extends JInternalFrame implements PositionListener -{ - private static final long serialVersionUID = 1L; - private final static Logger mLog = - LoggerFactory.getLogger( BasebandSourceFrame.class ); - - private ComplexWaveSource mSource; - private JDesktopPane mDesktop; - - private JLabel mCurrentPosition = new JLabel( "0" ); - private JComboBox mComboPanels; - - public BasebandSourceFrame( ComplexWaveSource source, JDesktopPane desktop ) - { - mSource = source; - mSource.addListener( this ); - - mDesktop = desktop; - - initGui(); - } - - private void initGui() - { - setTitle( "Baseband Source [" + mSource.getSampleType().toString() + "]" ); - setPreferredSize( new Dimension( 600, 150 ) ); - setSize( 600, 150 ); - - setResizable( true ); - setClosable( true ); - setIconifiable( true ); - setMaximizable( false ); - - JPanel panel = new JPanel(); - panel.setLayout( new MigLayout() ); - - JLabel fileLabel = new JLabel( "File: " + mSource.getFile().getName() ); - fileLabel.setToolTipText( mSource.getFile().getName() ); - panel.add( fileLabel, "span,wrap" ); - - panel.add( new JLabel( "Jump:" ) ); - panel.add( new JumpToField( mSource ) ); - panel.add( new NextSampleButton( mSource, ">", 1 ) ); - panel.add( new NextSampleButton( mSource, "> 100 >", 100 ) ); - panel.add( new NextSampleButton( mSource, "> 1000 >", 1000 ) ); - panel.add( new JLabel( "Posn:" ) ); - panel.add( mCurrentPosition, "wrap" ); - - /* ComboBox: Viewer Panels */ - mComboPanels = new JComboBox( - new DefaultComboBoxModel( Panels.values() ) ); - - panel.add( new JLabel( "Panels:" ) ); - panel.add( mComboPanels, "span 3,grow" ); - panel.add( new AddPanelButton(), "grow" ); - - add( panel ); - } - - public class AddPanelButton extends JButton - { - private static final long serialVersionUID = 1L; - - public AddPanelButton() - { - super( "Add" ); - - addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - Panels selected = - (Panels)mComboPanels.getSelectedItem(); - - mLog.info( "Selected:" + selected.name() ); - - switch( selected ) - { - case CONSTELLATION: - ConstellationPanel con = new ConstellationPanel(); - mDesktop.add( con ); - con.setVisible( true ); - - if( mSource != null ) - { - mSource.addListener( (Listener)con ); - } - break; - default: - break; - } - } - } ); - } - } - - public class NextSampleButton extends JButton - { - private static final long serialVersionUID = 1L; - - private WaveSource mSource; - private int mCount; - - public NextSampleButton( WaveSource source, String label, int count ) - { - super( label ); - mSource = source; - mCount = count; - - addActionListener( new ActionListener() - { - @Override - public void actionPerformed( ActionEvent arg0 ) - { - try - { - mSource.next( mCount ); - } - catch ( IOException e ) - { - mLog.error( "Viewer - error trying to fetch next [" + - mCount + "] samples", e ); - - JOptionPane.showMessageDialog( BasebandSourceFrame.this, - "Cannot read " + mCount + " more samples [" + - e.getLocalizedMessage() + "]", - "Wave File Error", - JOptionPane.ERROR_MESSAGE ); - } - } - } ); - } - } - - public class JumpToField extends JTextField - { - private static final long serialVersionUID = 1L; - - private WaveSource mSource; - - public JumpToField( WaveSource source ) - { - super( "0" ); - - mSource = source; - - setMinimumSize( new Dimension( 100, getHeight() ) ); - - addFocusListener( new FocusListener() - { - @Override - public void focusGained( FocusEvent arg0 ) {} - - @Override - public void focusLost( FocusEvent arg0 ) - { - try - { - long position = Long.parseLong( getText() ); - mSource.jumpTo( position ); - } - catch( Exception e ) - { - mLog.error( "WaveSourceFrame - exception during jump " - + "to", e ); - - JOptionPane.showMessageDialog( BasebandSourceFrame.this, - "Can't jump to position [" + getText() + "]", - "Error", - JOptionPane.ERROR_MESSAGE ); - } - } - } ); - } - } - - @Override - public void positionUpdated( long position, boolean reset ) - { - mCurrentPosition.setText( String.valueOf( position ) ); - - if( reset ) - { - - } - } - - public enum Panels - { - CONSTELLATION; - } -} diff --git a/src/instrument/gui/DecoderViewFrame.java b/src/instrument/gui/DecoderViewFrame.java index ecd3c993..343b71e5 100644 --- a/src/instrument/gui/DecoderViewFrame.java +++ b/src/instrument/gui/DecoderViewFrame.java @@ -22,10 +22,14 @@ import instrument.tap.Tap; import instrument.tap.TapViewPanel; import instrument.tap.stream.BinaryTap; import instrument.tap.stream.BinaryTapViewPanel; +import instrument.tap.stream.ComplexTap; +import instrument.tap.stream.ComplexTapViewPanel; import instrument.tap.stream.DibitTap; import instrument.tap.stream.DibitTapViewPanel; import instrument.tap.stream.FloatTap; import instrument.tap.stream.FloatTapViewPanel; +import instrument.tap.stream.QPSKTap; +import instrument.tap.stream.QPSKTapPanel; import instrument.tap.stream.SymbolEventTap; import instrument.tap.stream.SymbolEventTapViewPanel; @@ -52,6 +56,7 @@ import org.slf4j.LoggerFactory; import sample.Listener; import source.Source.SampleType; +import source.wave.ComplexWaveSource; import source.wave.FloatWaveSource; import source.wave.WaveSource; import source.wave.WaveSource.PositionListener; @@ -77,7 +82,8 @@ public class DecoderViewFrame extends JInternalFrame if( source.getSampleType() == SampleType.COMPLEX ) { - mLog.error( "Hey, we're not wired up yet for complex" ); + ComplexWaveSource cws = (ComplexWaveSource)mWaveSource; + cws.addListener( mDecoder.getComplexReceiver() ); } else if( source.getSampleType() == SampleType.REAL ) { @@ -167,6 +173,7 @@ public class DecoderViewFrame extends JInternalFrame panel = new BinaryTapViewPanel( (BinaryTap)tap ); break; case STREAM_COMPLEX: + panel = new ComplexTapViewPanel( (ComplexTap)tap ); break; case STREAM_DIBIT: panel = new DibitTapViewPanel( (DibitTap)tap ); @@ -174,6 +181,9 @@ public class DecoderViewFrame extends JInternalFrame case STREAM_FLOAT: panel = new FloatTapViewPanel( (FloatTap)tap ); break; + case STREAM_QPSK: + panel = new QPSKTapPanel( (QPSKTap)tap ); + break; case STREAM_SYMBOL: panel = new SymbolEventTapViewPanel( (SymbolEventTap)tap ); break; @@ -211,6 +221,8 @@ public class DecoderViewFrame extends JInternalFrame public class AddAllTapsItem extends JMenuItem { + private static final long serialVersionUID = 1L; + private final List mTaps; public AddAllTapsItem( final List taps ) diff --git a/src/instrument/gui/Viewer.java b/src/instrument/gui/Viewer.java index 1bbb80c0..7d0f23e6 100644 --- a/src/instrument/gui/Viewer.java +++ b/src/instrument/gui/Viewer.java @@ -137,10 +137,6 @@ public class Viewer mLog.info( "File opened as float wave file" ); - AudioSourceFrame sourcePanel = - new AudioSourceFrame( source, mDesktop ); - sourcePanel.setVisible( true ); - mDesktop.add( sourcePanel ); } catch( Exception e ) { @@ -158,13 +154,6 @@ public class Viewer source = complex; mLog.info( "File opened as complex wave file" ); - - BasebandSourceFrame sourcePanel = - new BasebandSourceFrame( complex, mDesktop ); - - sourcePanel.setVisible( true ); - - mDesktop.add( sourcePanel ); } catch( Exception e ) { @@ -175,7 +164,11 @@ public class Viewer if( source != null ) { mSource = source; - + + AudioSourceFrame sourcePanel = + new AudioSourceFrame( source, mDesktop ); + sourcePanel.setVisible( true ); + mDesktop.add( sourcePanel ); } } diff --git a/src/instrument/tap/TapType.java b/src/instrument/tap/TapType.java index c6cd5eb7..64111ab3 100644 --- a/src/instrument/tap/TapType.java +++ b/src/instrument/tap/TapType.java @@ -24,5 +24,6 @@ public enum TapType STREAM_COMPLEX, STREAM_DIBIT, STREAM_FLOAT, + STREAM_QPSK, STREAM_SYMBOL; } diff --git a/src/instrument/tap/stream/BinaryTapViewPanel.java b/src/instrument/tap/stream/BinaryTapViewPanel.java index 913d12bf..95b2c3e1 100644 --- a/src/instrument/tap/stream/BinaryTapViewPanel.java +++ b/src/instrument/tap/stream/BinaryTapViewPanel.java @@ -32,6 +32,8 @@ import java.util.Observable; public class BinaryTapViewPanel extends TapViewPanel { + private static final long serialVersionUID = 1L; + private Tap mTap; private List mSamples; private int mSampleCount; diff --git a/src/instrument/tap/stream/ComplexTap.java b/src/instrument/tap/stream/ComplexTap.java index 26f89214..43b81144 100644 --- a/src/instrument/tap/stream/ComplexTap.java +++ b/src/instrument/tap/stream/ComplexTap.java @@ -33,6 +33,11 @@ public class ComplexTap extends StreamTap { super( TapType.STREAM_COMPLEX, name, delay, sampleRateRatio ); } + + public ComplexTap( TapType tapType, String name, int delay, float sampleRateRatio ) + { + super( tapType, name, delay, sampleRateRatio ); + } @Override public void receive( ComplexSample t ) diff --git a/src/instrument/tap/stream/ComplexTapViewPanel.java b/src/instrument/tap/stream/ComplexTapViewPanel.java new file mode 100644 index 00000000..5bc9dda8 --- /dev/null +++ b/src/instrument/tap/stream/ComplexTapViewPanel.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * 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 instrument.tap.stream; + +import gui.SDRTrunk; +import instrument.gui.SampleModel; +import instrument.tap.Tap; +import instrument.tap.TapViewPanel; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.util.List; +import java.util.Observable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sample.complex.ComplexSample; + +public class ComplexTapViewPanel extends TapViewPanel +{ + private static final long serialVersionUID = 1L; + private final static Logger mLog = LoggerFactory.getLogger( SDRTrunk.class ); + + private Tap mTap; + private List mSamples; + private int mSampleCount; + + public ComplexTapViewPanel( ComplexTap tap ) + { + super( new SampleModel(), tap.getName() ); + + setSize( new Dimension( 400, 400 ) ); + + mTap = tap; + mTap.addListener( getModel() ); + + mSampleCount = (int)( 2000 * tap.getSampleRateRatio() ); + + getModel().setSampleCount( mSampleCount ); + getModel().setDelay( tap.getDelay() ); + + mLog.info( "Complex Tap Panel [" + tap.getName() + "] count: " + + getModel().getSampleCount() + " delay:" + getModel().getDelay() ); + } + + @Override + public void update( Observable arg0, Object arg1 ) + { + mSamples = (List)getModel().getSamples(); + repaint(); + } + + public void paintComponent( Graphics g ) + { + super.paintComponent( g ); + + Graphics2D g2 = (Graphics2D)g; + + int height = getHeight(); + int width = getWidth(); + + g2.setColor( Color.DARK_GRAY ); + + float middle = (float)height / 4.0f; + + g2.drawLine( 0, (int)middle, width, (int)middle ); + g2.drawLine( 0, (int)( 3 * middle ), width, (int)( 3 * middle ) ); + + if( mSamples != null && + mSamples.size() > 0 ) + { + g2.setColor( getForeground() ); + + g2.drawString( mLabel, 5 , 15 ); + + Path2D.Float polylineReal = + new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); + Path2D.Float polylineImag = + new Path2D.Float( Path2D.Float.WIND_EVEN_ODD, mSamples.size() ); + + /* Move to the first point */ + Point2D.Float firstReal = resolve( 0, mSamples.get( 0 ), Type.REAL ); + polylineReal.moveTo( firstReal.x, firstReal.y ); + + Point2D.Float firstImag = resolve( 0, mSamples.get( 0 ), Type.IMAGINERY ); + polylineImag.moveTo( firstImag.x, firstImag.y ); + + for( int x = 1; x < mSamples.size(); x++ ) + { + Point2D.Float pointReal = resolve( x, mSamples.get( x ), Type.REAL ); + polylineReal.lineTo( pointReal.x, pointReal.y ); + + Point2D.Float pointImag = resolve( x, mSamples.get( x ), Type.IMAGINERY ); + polylineImag.lineTo( pointImag.x, pointImag.y ); + +// mLog.debug( "Imag y:" + pointImag.getY() + " height:" + getHeight() ); + } + + g2.draw( polylineReal ); + g2.draw( polylineImag ); + } + + g2.dispose(); + } + + + private Point2D.Float resolve( int index, ComplexSample sample, Type type ) + { + int middle = (int)( getHeight() / 4 ); + + float y = middle - ( middle * mVerticalZoom * ( type == Type.REAL ? + sample.real() : sample.imaginary()) ); + + if( type == Type.IMAGINERY ) + { + y += ( middle * 2 ); + } + + /* Clip y to zero as necessary */ + y = ( y < 0 ) ? 0 : y; + + /* Clip y to max height as necessary */ + y = ( y > getHeight() ) ? getHeight() : y; + + float indexWidth = (float)getWidth() / (float)mSampleCount; + + float x = (float)index * indexWidth; + + Point2D.Float point = new Point2D.Float( x, y ); + + return new Point2D.Float( x , y ); + } + + public enum Type{ REAL,IMAGINERY }; +} diff --git a/src/instrument/tap/stream/QPSKTap.java b/src/instrument/tap/stream/QPSKTap.java new file mode 100644 index 00000000..55f598f9 --- /dev/null +++ b/src/instrument/tap/stream/QPSKTap.java @@ -0,0 +1,11 @@ +package instrument.tap.stream; + +import instrument.tap.TapType; + +public class QPSKTap extends ComplexTap +{ + public QPSKTap( String name, int delay, float sampleRateRatio ) + { + super( TapType.STREAM_QPSK, name, delay, sampleRateRatio ); + } +} diff --git a/src/instrument/tap/stream/QPSKTapPanel.java b/src/instrument/tap/stream/QPSKTapPanel.java new file mode 100644 index 00000000..e24c61ac --- /dev/null +++ b/src/instrument/tap/stream/QPSKTapPanel.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * 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 instrument.tap.stream; + +import gui.SDRTrunk; +import instrument.gui.SampleModel; +import instrument.tap.Tap; +import instrument.tap.TapViewPanel; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.geom.Ellipse2D; +import java.util.List; +import java.util.Observable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sample.complex.ComplexSample; + +public class QPSKTapPanel extends TapViewPanel +{ + private static final long serialVersionUID = 1L; + private final static Logger mLog = LoggerFactory.getLogger( SDRTrunk.class ); + + private Tap mTap; + private List mSamples; + private int mSampleCount; + + public QPSKTapPanel( ComplexTap tap ) + { + super( new SampleModel(), tap.getName() ); + + mTap = tap; + mTap.addListener( getModel() ); + + mSampleCount = (int)( 500 * tap.getSampleRateRatio() ); + + getModel().setSampleCount( mSampleCount ); + getModel().setDelay( tap.getDelay() ); + + mLog.info( "Complex Tap Panel [" + tap.getName() + "] count: " + + getModel().getSampleCount() + " delay:" + getModel().getDelay() ); + } + + @Override + public void update( Observable arg0, Object arg1 ) + { + mSamples = (List)getModel().getSamples(); + repaint(); + } + + public void paintComponent( Graphics g ) + { + super.paintComponent( g ); + + Graphics2D g2 = (Graphics2D)g; + + int min = ( getHeight() < getWidth() ? getHeight() : getWidth() ); + + float middle = (float)min / 2.0f; + + g2.setColor( Color.DARK_GRAY ); + + g2.drawLine( getWidth() / 2, 5, getWidth() / 2, getHeight() - 5 ); + g2.drawLine( getWidth() / 2 - (int)( middle ), getHeight() / 2, + getWidth() / 2 + (int)( middle ), getHeight() / 2 ); + + + if( mSamples != null && + mSamples.size() > 0 ) + { + g2.drawString( mLabel, 5 , 15 ); + + /* Set color to light gray for oldest 90% of constellations */ + g2.setColor( Color.LIGHT_GRAY ); + + int counter = 0; + + int threshold = (int)( mSamples.size() * 0.9f ); + + for( ComplexSample sample: mSamples ) + { + /* Set color to foreground for newest 10% of constellations */ + if( counter == threshold ) + { + g2.setColor( getForeground() ); + } + + float scale; + + if( sample.real() != 0 && sample.imaginary() != 0 ) + { + scale = 1.0f / sample.magnitude(); + } + else if( sample.real() == 0 ) + { + scale = 1.0f / sample.imaginary(); + } + else + { + scale = 1.0f / sample.real(); + } + + float x = ( getWidth() / 2 ) + + ( scale * sample.real() * middle * 0.75f * mVerticalZoom ); + + if( Float.isNaN( x ) ) + { + x = getWidth() / 2; + } + + float y = ( getHeight() / 2 ) + + ( scale * sample.imaginary() * middle * 0.75f * mVerticalZoom ); + + if( Float.isNaN( y ) ) + { + y = getHeight() / 2; + } + + g2.draw( new Ellipse2D.Float( x, y, 4.0f, 4.0f ) ); + + counter++; + } + } + + g2.dispose(); + } +} diff --git a/src/record/RecordComponentEditor.java b/src/record/RecordComponentEditor.java index 2cd44e14..b38a3e83 100644 --- a/src/record/RecordComponentEditor.java +++ b/src/record/RecordComponentEditor.java @@ -18,12 +18,13 @@ package record; -import javax.swing.BorderFactory; import javax.swing.JCheckBox; import record.config.RecordConfiguration; +import controller.Editor; import controller.channel.AbstractChannelEditor; import controller.channel.ChannelNode; +import controller.channel.ChannelValidationException; public class RecordComponentEditor extends AbstractChannelEditor { diff --git a/src/record/RecordEditor.java b/src/record/RecordEditor.java index ab416e0a..d67210e7 100644 --- a/src/record/RecordEditor.java +++ b/src/record/RecordEditor.java @@ -19,6 +19,8 @@ package record; import record.config.RecordConfiguration; import controller.Editor; +import controller.channel.Channel; +import controller.channel.ChannelValidationException; public class RecordEditor extends Editor { @@ -39,14 +41,10 @@ public class RecordEditor extends Editor @Override public void save() { - // TODO Auto-generated method stub - } @Override public void reset() { - // TODO Auto-generated method stub - } } diff --git a/src/sample/complex/ComplexSample.java b/src/sample/complex/ComplexSample.java index 63898c4f..e296738a 100644 --- a/src/sample/complex/ComplexSample.java +++ b/src/sample/complex/ComplexSample.java @@ -19,13 +19,19 @@ package sample.complex; import java.io.Serializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * ComplexSample for handling left/right channel audio, or inphase/quadrature * samples, etc. */ public class ComplexSample implements Serializable { - private static final long serialVersionUID = 1L; + private final static Logger mLog = + LoggerFactory.getLogger( ComplexSample.class ); + + private static final long serialVersionUID = 1L; private float mLeft; private float mRight; @@ -35,6 +41,11 @@ public class ComplexSample implements Serializable mLeft = left; mRight = right; } + + public ComplexSample() + { + this( 0.0f, 0.0f ); + } public ComplexSample copy() { @@ -43,7 +54,14 @@ public class ComplexSample implements Serializable public String toString() { - return "I:" + mLeft + " Q:" + mRight; + StringBuilder sb = new StringBuilder(); + + sb.append( "I:" ); + sb.append( mLeft ); + sb.append( " Q:" ); + sb.append( mRight ); + + return sb.toString(); } /** @@ -78,6 +96,9 @@ public class ComplexSample implements Serializable mRight = quadrature; } + /** + * Multiplies both samples returning a new sample with the results + */ public static ComplexSample multiply( ComplexSample sample1, ComplexSample sample2 ) { @@ -91,16 +112,16 @@ public class ComplexSample implements Serializable } public static ComplexSample multiply( ComplexSample sample, - Float left, Float right ) + Float inphase, Float quadrature ) { - float l = ( sample.left() * left ) - ( sample.right() * right ); - float r = ( sample.right() * left ) + ( sample.left() * right ); + float i = ( sample.inphase() * inphase ) - ( sample.quadrature() * quadrature ); + float q = ( sample.quadrature() * inphase ) + ( sample.inphase() * quadrature ); - return new ComplexSample( l, r ); + return new ComplexSample( i, q ); } /** - * Adds the adder sample values to this sample + * Adds the adder sample value to this sample */ public void add( ComplexSample adder ) { @@ -108,6 +129,24 @@ public class ComplexSample implements Serializable mRight += adder.right(); } + /** + * Adds the two complex samples returning a new complex sample with the result + */ + public static ComplexSample add( ComplexSample first, ComplexSample second ) + { + return new ComplexSample( first.left() + second.left(), + first.right() + second.right() ); + } + + /** + * Adds the two complex samples returning a new complex sample with the result + */ + public static ComplexSample subtract( ComplexSample first, ComplexSample second ) + { + return new ComplexSample( first.left() - second.left(), + first.right() - second.right() ); + } + /** * Magnitude of this sample */ @@ -130,7 +169,12 @@ public class ComplexSample implements Serializable */ public void normalize() { - multiply( (float)( 1.0f / magnitude() ) ); + float magnitude = magnitude(); + + if( magnitude != 0 ) + { + multiply( (float)( 1.0f / magnitude() ) ); + } } /** @@ -176,12 +220,28 @@ public class ComplexSample implements Serializable { return mLeft; } - - public float imaginery() + + /** + * Absolute value of real component + */ + public float realAbsolute() + { + return Math.abs( mLeft ); + } + + public float imaginary() { return mRight; } + /** + * Absolute value of imaginary component + */ + public float imaginaryAbsolute() + { + return Math.abs( mRight ); + } + /** * Returns the greater absolute value between left and right values */ @@ -208,4 +268,18 @@ public class ComplexSample implements Serializable return new ComplexSample( (float)Math.cos( angle ), (float)Math.sin( angle ) ); } + + public float angle() + { + return (float)Math.atan2( y(), x() ); + } + + public static void main( String[] args ) + { + ComplexSample sample1 = new ComplexSample( .45f, .45f ); + ComplexSample sample2 = new ComplexSample( .25f, .25f ); + ComplexSample sample1_2 = ComplexSample.subtract( sample1, sample2 ); + + mLog.debug( sample1_2.toString() ); + } } diff --git a/src/source/EmptySourceEditor.java b/src/source/EmptySourceEditor.java index ad1c1669..0f760647 100644 --- a/src/source/EmptySourceEditor.java +++ b/src/source/EmptySourceEditor.java @@ -21,6 +21,8 @@ import javax.swing.JLabel; import source.config.SourceConfiguration; import controller.ResourceManager; +import controller.channel.Channel; +import controller.channel.ChannelValidationException; public class EmptySourceEditor extends SourceEditor { diff --git a/src/source/SourceComponentEditor.java b/src/source/SourceComponentEditor.java index 3e879efe..19d60a9c 100644 --- a/src/source/SourceComponentEditor.java +++ b/src/source/SourceComponentEditor.java @@ -21,14 +21,15 @@ package source; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import javax.swing.BorderFactory; import javax.swing.DefaultComboBoxModel; import javax.swing.JComboBox; import source.config.SourceConfigFactory; import source.config.SourceConfiguration; +import controller.Editor; import controller.channel.AbstractChannelEditor; import controller.channel.ChannelNode; +import controller.channel.ChannelValidationException; public class SourceComponentEditor extends AbstractChannelEditor { @@ -89,6 +90,11 @@ public class SourceComponentEditor extends AbstractChannelEditor reset(); } + + public SourceEditor getSourceEditor() + { + return mEditor; + } public void reset() { diff --git a/src/source/SourceEditor.java b/src/source/SourceEditor.java index dd5ad11e..ec6bdfea 100644 --- a/src/source/SourceEditor.java +++ b/src/source/SourceEditor.java @@ -20,6 +20,8 @@ package source; import source.config.SourceConfiguration; import controller.Editor; import controller.ResourceManager; +import controller.channel.Channel; +import controller.channel.ChannelValidationException; public abstract class SourceEditor extends Editor { diff --git a/src/source/wave/ComplexWaveSource.java b/src/source/wave/ComplexWaveSource.java index 3cf7dc60..c3e3729e 100644 --- a/src/source/wave/ComplexWaveSource.java +++ b/src/source/wave/ComplexWaveSource.java @@ -23,20 +23,25 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.ShortBuffer; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.UnsupportedAudioFileException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import sample.Broadcaster; import sample.Listener; import sample.complex.ComplexSample; public class ComplexWaveSource extends WaveSource { + private final static Logger mLog = + LoggerFactory.getLogger( ComplexWaveSource.class ); + private boolean mRunning = false; private boolean mLoop = false; @@ -51,8 +56,7 @@ public class ComplexWaveSource extends WaveSource private static int BUFFER_SAMPLE_SIZE = 2000; - private ArrayList> mListeners = - new ArrayList>(); + private Broadcaster mBroadcaster = new Broadcaster(); /** * Single channel (mono) wave file playback source with optional looping @@ -223,7 +227,7 @@ public class ComplexWaveSource extends WaveSource if( broadcast ) { - send( new ComplexSample( (float)i, (float)q ) ); + send( new ComplexSample( (float)i / 32767.0f, (float)q / 32767.0f ) ); } success = true; @@ -276,12 +280,7 @@ public class ComplexWaveSource extends WaveSource */ private void send( ComplexSample sample ) { - Iterator> it = mListeners.iterator(); - - while( it.hasNext() ) - { - it.next().receive( sample ); - } + mBroadcaster.broadcast( sample ); } /** @@ -289,12 +288,12 @@ public class ComplexWaveSource extends WaveSource */ public void addListener( Listener listener ) { - mListeners.add( listener ); + mBroadcaster.addListener( listener ); } public void removeListener( Listener listener ) { - mListeners.remove( listener ); + mBroadcaster.removeListener( listener ); } @Override diff --git a/src/test/Interp.java b/src/test/Interp.java new file mode 100644 index 00000000..ddc3511c --- /dev/null +++ b/src/test/Interp.java @@ -0,0 +1,57 @@ +package test; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dsp.filter.interpolator.QPSKInterpolator; +import sample.complex.ComplexSample; +import util.Oscillator; + +public class Interp +{ + private final static Logger mLog = + LoggerFactory.getLogger( Interp.class ); + + private Oscillator mNCO = new Oscillator( 1200, 48000 ); + + public Interp() + { + + } + + public ComplexSample next() + { + return mNCO.nextComplex(); + } + + public static void main( String[] args ) + { + mLog.debug( "Starting" ); + + Interp interp = new Interp(); + + ComplexSample[] samples = new ComplexSample[ 10 ]; + + for( int x = 0; x < 10; x++ ) + { + samples[ x ] = interp.next(); + } + + QPSKInterpolator qi = new QPSKInterpolator(); + + ComplexSample result = qi.filter( samples, 1, 0.25f ); + + + for( int x = 0; x < 10; x++ ) + { + System.out.println( samples[x].toString() ); +// mLog.debug( "Sample " + x + ": " + samples[ x ].toString() ); + } + mLog.debug( "Result: " + result.toString() ); + mLog.debug( "Finished" ); + } + +} diff --git a/src/util/Oscillator.java b/src/util/Oscillator.java index f6dba831..01721df1 100644 --- a/src/util/Oscillator.java +++ b/src/util/Oscillator.java @@ -88,7 +88,7 @@ public class Oscillator public ComplexSample nextComplex() { rotate(); - return mCurrentAngle; + return mCurrentAngle.copy(); } /**