From 148ddd0bd57587c7a52a09df91cc69ca8ec6569e Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 25 Feb 2018 08:56:34 -0500 Subject: [PATCH] 330 WIP - P25 C4FM decoder fully functional using DQPSK decoder and new symbol timing error detector. Each component now has a subclass that supports instrumentation for use with the DemodulatorViewerFX application. --- .../dsp/filter/design/FilterViewer.java | 6 +- .../dsheirer/dsp/psk/DQPSKDemodulator.java | 235 ++++++------------ .../dsp/psk/DQPSKDemodulatorInstrumented.java | 134 ++++++++++ .../psk/DQPSKSymbolPhaseErrorCalculator.java | 9 +- .../dsp/psk/DQPSKTimingErrorDetector.java | 89 +++++++ .../dsheirer/dsp/psk/EarlyLateDetector.java | 76 ------ .../dsp/psk/ISymbolPhaseErrorCalculator.java | 7 - ...r2.java => InterpolatingSampleBuffer.java} | 164 ++++-------- ...nterpolatingSampleBufferInstrumented.java} | 38 +-- .../dsp/psk/InterpolatingSymbolBuffer.java | 219 ---------------- .../dsheirer/dsp/psk/LSMDemodulator.java | 7 + .../dsheirer/dsp/psk/QPSKDemodulator.java | 233 ----------------- .../psk/QPSKSymbolPhaseErrorCalculator.java | 6 - .../dsheirer/dsp/psk/SymbolDecisionData.java | 81 +++--- .../dsheirer/dsp/psk/SymbolDecisionData2.java | 74 ------ .../dsheirer/dsp/psk/pll/CostasLoop.java | 215 ++-------------- .../dsp/psk/pll/IPhaseLockedLoop.java | 5 +- .../github/dsheirer/dsp/psk/pll/Tracking.java | 45 +--- .../gui/viewer/chart/EyeDiagramChart.java | 6 +- .../gui/viewer/decoder/P25Phase1Pane.java | 22 +- .../module/decode/p25/P25_C4FMDecoder2.java | 143 +++-------- .../p25/P25_C4FMDecoder2Instrumented.java | 109 ++++++++ .../tsbk/osp/control/SyncBroadcast.java | 2 +- 23 files changed, 623 insertions(+), 1302 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java rename src/main/java/io/github/dsheirer/dsp/psk/{InterpolatingSymbolBuffer2.java => InterpolatingSampleBuffer.java} (52%) rename src/main/java/io/github/dsheirer/dsp/psk/{EarlyLateDetector2.java => InterpolatingSampleBufferInstrumented.java} (53%) delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java delete mode 100644 src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java index ff42ad85..480463ee 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java @@ -50,12 +50,12 @@ public class FilterViewer extends Application // .stopBandRipple(0.008) // .build(); FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() - .sampleRate((int)48000.0) - .passBandCutoff(2880) + .sampleRate(48000) + .passBandCutoff(5100) .passBandAmplitude(1.0) .passBandRipple(0.01) .stopBandAmplitude(0.0) - .stopBandStart(3600) + .stopBandStart(6500) .stopBandRipple(0.01) .build(); diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java index 4d4374a6..50326b13 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java @@ -15,7 +15,6 @@ ******************************************************************************/ package io.github.dsheirer.dsp.psk; -import io.github.dsheirer.dsp.psk.pll.CostasLoop; import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.sample.Listener; @@ -28,70 +27,46 @@ import org.slf4j.LoggerFactory; public class DQPSKDemodulator implements ComplexSampleListener { private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class); - private InterpolatingSymbolBuffer2 mInterpolatingSymbolBuffer; - private IPhaseLockedLoop mPLL; + protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; + protected IPhaseLockedLoop mPLL; + protected IQPSKSymbolDecoder mDQPSKSymbolDecoder; private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator; - private IQPSKSymbolDecoder mDQPSKSymbolDecoder; -// private GardnerDetector mGardnerDetector = new GardnerDetector(); - private EarlyLateDetector mEarlyLateDetector = new EarlyLateDetector(); - private EarlyLateDetector2 mEarlyLateDetector2 = new EarlyLateDetector2(); - private Listener mSymbolListener; - private Listener mPLLErrorListener; - private Listener mPLLFrequencyListener; - private Listener mSamplesPerSymbolListener; - private Listener mDibitListener; - private Listener mSymbolDecisionDataListener; + private DQPSKTimingErrorDetector mDQPSKTimingErrorDetector = new DQPSKTimingErrorDetector(); private Complex mReceivedSample = new Complex(0, 0); - private Complex mPreviousPrecedingSample = new Complex(0, 0); private Complex mPreviousCurrentSample = new Complex(0, 0); - private Complex mPreviousFollowingSample = new Complex(0, 0); - - private Complex mCurrentPrecedingSample = new Complex(0, 0); - private Complex mCurrentCurrentSample = new Complex(0, 0); - private Complex mCurrentFollowingSample = new Complex(0, 0); - + private Complex mPrecedingSample = new Complex(0, 0); + private Complex mCurrentSample = new Complex(0, 0); private Complex mPrecedingSymbol = new Complex(0, 0); - private Complex mCurrentSymbol = new Complex(0, 0); - private Complex mFollowingSymbol = new Complex(0, 0); + protected Complex mCurrentSymbol = new Complex(0, 0); - private float mPhaseError; + protected float mPhaseError; private float mSymbolTimingError; - private double mSampleRate; - + private Listener mDibitListener; /** - * Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a - * phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier - * frequency and to adjust for any changes in symbol timing. + * Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and + * a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to + * adjust for any changes in symbol timing. + * + * This detector is optimized for constant amplitude DQPSK symbols like C4FM. * * @param phaseLockedLoop for tracking carrier frequency error - * @param symbolPhaseErrorCalculator to calculate symbol phase errors and optionally adjust for differential encoding + * @param symbolPhaseErrorCalculator to calculate symbol phase errors to apply against the PLL * @param symbolDecoder to decode demodulated symbols into dibits - * @param sampleRate of the incoming complex sample stream - * @param symbolRate of the decoded QPSK symbols. */ public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator, - IQPSKSymbolDecoder symbolDecoder, double sampleRate, double symbolRate) + IQPSKSymbolDecoder symbolDecoder, InterpolatingSampleBuffer interpolatingSampleBuffer) { - if(sampleRate < (symbolRate * 2)) - { - throw new IllegalArgumentException("Sample rate [" + sampleRate + - "] must be at least 2 x symbol rate [" + symbolRate + "]"); - } - - mSampleRate = sampleRate; mPLL = phaseLockedLoop; mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; mDQPSKSymbolDecoder = symbolDecoder; - - float samplesPerSymbol = (float)(sampleRate / symbolRate); - mInterpolatingSymbolBuffer = new InterpolatingSymbolBuffer2(samplesPerSymbol); + mInterpolatingSampleBuffer = interpolatingSampleBuffer; } /** - * Submits a buffer containing complex samples for decoding + * Processes a (filtered) buffer containing complex samples for decoding * @param complexBuffer with complex samples */ public void receive(ComplexBuffer complexBuffer) @@ -105,7 +80,7 @@ public class DQPSKDemodulator implements ComplexSampleListener } /** - * Submits a complex sample for decoding. + * Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made. * @param inphase value for the sample * @param quadrature value for the sample */ @@ -118,109 +93,64 @@ public class DQPSKDemodulator implements ComplexSampleListener //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector()); - mInterpolatingSymbolBuffer.receive(mReceivedSample); + //Store the sample in the interpolating buffer + mInterpolatingSampleBuffer.receive(mReceivedSample); //Calculate the symbol once we've stored enough samples - if(mInterpolatingSymbolBuffer.hasSymbol()) + if(mInterpolatingSampleBuffer.hasSymbol()) { - //Get middle and current samples from the interpolating buffer - mCurrentPrecedingSample = mInterpolatingSymbolBuffer.getPreceedingSample(); - mCurrentCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample(); - mCurrentFollowingSample = mInterpolatingSymbolBuffer.getFollowingSample(); - - //Eye diagram listener - if(mSymbolDecisionDataListener != null) - { - mSymbolDecisionDataListener.receive(mInterpolatingSymbolBuffer.getSymbolDecisionData()); - } - - //Calculate preceding, current and following symbols as the delta rotation compared to the previous samples - mPrecedingSymbol.setInphase(Complex.multiplyInphase(mCurrentPrecedingSample.inphase(), mCurrentPrecedingSample.quadrature(), - mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); - mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentPrecedingSample.inphase(), mCurrentPrecedingSample.quadrature(), - mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); - - mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentCurrentSample.inphase(), mCurrentCurrentSample.quadrature(), - mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); - mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentCurrentSample.inphase(), mCurrentCurrentSample.quadrature(), - mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); - - mFollowingSymbol.setInphase(Complex.multiplyInphase(mCurrentFollowingSample.inphase(), mCurrentFollowingSample.quadrature(), - mPreviousFollowingSample.inphase(), -mPreviousFollowingSample.quadrature())); - mFollowingSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentFollowingSample.inphase(), mCurrentFollowingSample.quadrature(), - mPreviousFollowingSample.inphase(), -mPreviousFollowingSample.quadrature())); - - //Set gain to unity before we calculate the error value - mPrecedingSymbol.normalize(); - mCurrentSymbol.normalize(); - mFollowingSymbol.normalize(); - - //Send to an external constellation symbol listener when registered - if(mSymbolListener != null) - { - mSymbolListener.receive(mCurrentSymbol); - } - - //Symbol timing error calculations - mSymbolTimingError = mEarlyLateDetector.getError(mPrecedingSymbol, mCurrentSymbol, mFollowingSymbol); - - mSymbolTimingError = GardnerDetector.clip(mSymbolTimingError, 0.5f); - mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError); - - if(mSamplesPerSymbolListener != null) - { - mSamplesPerSymbolListener.receive((double)mInterpolatingSymbolBuffer.getSamplingPoint()); - } - - //Store current samples/symbols to use for the next period - mPreviousPrecedingSample.setValues(mCurrentPrecedingSample); - mPreviousCurrentSample.setValues(mCurrentCurrentSample); - mPreviousFollowingSample.setValues(mCurrentFollowingSample); - - //Calculate the phase error of the current symbol relative to the expected constellation and provide - //feedback to the PLL - mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); - mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); - - mPhaseError = GardnerDetector.clip(mPhaseError, 0.15f); - - mPLL.adjust(mPhaseError); -// mPLL.adjust(0.0); - - if(mPLLErrorListener != null) - { - mPLLErrorListener.receive((double)mPhaseError); - } - - if(mPLLFrequencyListener != null) - { - double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency(); - - loopFrequency *= mSampleRate / (2.0 * Math.PI); - - mPLLFrequencyListener.receive(loopFrequency); - } - - //Decode the dibit from the symbol and send to the listener - if(mDibitListener != null) - { - mDibitListener.receive(mDQPSKSymbolDecoder.decode(mCurrentSymbol)); - } - - //TODO: Assemble dibits here and broadcast + calculateSymbol(); } } /** - * Registers the listener to receive decoded QPSK symbols + * Calculates a symbol from the interpolating buffer */ - public void setSymbolListener(Listener listener) + protected void calculateSymbol() { - mSymbolListener = listener; + //Get preceding sample and an interpolated current sample from the interpolating buffer + mPrecedingSample = mInterpolatingSampleBuffer.getPrecedingSample(); + mCurrentSample = mInterpolatingSampleBuffer.getCurrentSample(); + + //Calculate preceding, current and following symbols as the delta rotation compared to the previous samples + mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(), + mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); + mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(), + mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature())); + + mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(), + mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature())); + + //Set gain to unity before we calculate the error value + mPrecedingSymbol.normalize(); + mCurrentSymbol.normalize(); + + //Symbol timing error calculation + mSymbolTimingError = mDQPSKTimingErrorDetector.getError(mPrecedingSymbol, mCurrentSymbol); + + mSymbolTimingError = clip(mSymbolTimingError, 0.5f); + mInterpolatingSampleBuffer.resetAndAdjust(mSymbolTimingError); + + //Store current samples/symbols to use for the next period + mPreviousPrecedingSample.setValues(mPrecedingSample); + mPreviousCurrentSample.setValues(mCurrentSample); + + //Calculate the phase error of the current symbol relative to the expected constellation and provide + //feedback to the PLL + mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); + + mPLL.adjust(mPhaseError); + + if(mDibitListener != null) + { + mDibitListener.receive(mDQPSKSymbolDecoder.decode(mCurrentSymbol)); + } } /** - * Registers the listener to receive decoded QPSK dibits + * Registers the listener to receive dibit symbol decisions from this demodulator */ public void setDibitListener(Listener listener) { @@ -228,32 +158,19 @@ public class DQPSKDemodulator implements ComplexSampleListener } /** - * Registers the listener to receive PLL error values + * Constrains value to the range of ( -maximum <> maximum ) */ - public void setPLLErrorListener(Listener listener) + public static float clip(float value, float maximum) { - mPLLErrorListener = listener; - } + if(value > maximum) + { + return maximum; + } + else if(value < -maximum) + { + return -maximum; + } - /** - * Registers the listener to receive PLL error values - */ - public void setPLLFrequencyListener(Listener listener) - { - mPLLFrequencyListener = listener; + return value; } - - /** - * Registers the listener to receive PLL error values - */ - public void setSamplesPerSymbolListener(Listener listener) - { - mSamplesPerSymbolListener = listener; - } - - public void setSymbolDecisionDataListener(Listener listener) - { - mSymbolDecisionDataListener = listener; - } - } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java new file mode 100644 index 00000000..3ca084d0 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk; + +import io.github.dsheirer.dsp.psk.pll.CostasLoop; +import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; + +public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator +{ + private Listener mSymbolDecisionDataListener; + private Listener mSamplesPerSymbolListener; + private Listener mSymbolListener; + private Listener mPLLErrorListener; + private Listener mPLLFrequencyListener; + private double mSampleRate; + + /** + * Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and + * a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to + * adjust for any changes in symbol timing. + * + * This detector is optimized for constant amplitude DQPSK symbols like C4FM. + * + * @param phaseLockedLoop for tracking carrier frequency error + * @param symbolPhaseErrorCalculator to calculate symbol phase errors to apply against the PLL + * @param symbolDecoder to decode demodulated symbols into dibits + * @param interpolatingSampleBuffer + * @param sampleRate of the incoming complex sample stream + */ + public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator + symbolPhaseErrorCalculator, IQPSKSymbolDecoder symbolDecoder, + InterpolatingSampleBufferInstrumented interpolatingSampleBuffer, + double sampleRate) + { + super(phaseLockedLoop, symbolPhaseErrorCalculator, symbolDecoder, interpolatingSampleBuffer); + mSampleRate = sampleRate; + } + + /** + * Overrides the parent class symbol calculation to capture eye diagram data + */ + protected void calculateSymbol() + { + //Eye diagram listener + if(mSymbolDecisionDataListener != null) + { + mSymbolDecisionDataListener.receive( + ((InterpolatingSampleBufferInstrumented)mInterpolatingSampleBuffer).getSymbolDecisionData()); + } + + super.calculateSymbol(); + + if(mSamplesPerSymbolListener != null) + { + mSamplesPerSymbolListener.receive((double)mInterpolatingSampleBuffer.getSamplingPoint()); + } + + //Send to an external constellation symbol listener when registered + if(mSymbolListener != null) + { + mSymbolListener.receive(mCurrentSymbol); + } + + if(mPLLErrorListener != null) + { + mPLLErrorListener.receive((double)mPhaseError); + } + + if(mPLLFrequencyListener != null) + { + double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency(); + + loopFrequency *= mSampleRate / (2.0 * Math.PI); + + mPLLFrequencyListener.receive(loopFrequency); + } + } + + /** + * Registers a listener to receive symbol decision data to produce an eye diagram. + */ + public void setSymbolDecisionDataListener(Listener listener) + { + mSymbolDecisionDataListener = listener; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setSamplesPerSymbolListener(Listener listener) + { + mSamplesPerSymbolListener = listener; + } + + /** + * Registers the listener to receive decoded QPSK symbols + */ + public void setSymbolListener(Listener listener) + { + mSymbolListener = listener; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setPLLErrorListener(Listener listener) + { + mPLLErrorListener = listener; + } + + /** + * Registers the listener to receive PLL error values + */ + public void setPLLFrequencyListener(Listener listener) + { + mPLLFrequencyListener = listener; + } + +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java index fce780e2..7eafb69a 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java @@ -21,16 +21,17 @@ public class DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalcula { //45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d); - - private boolean mToggle; + private Complex mSymbol = new Complex(0, 0); public DQPSKSymbolPhaseErrorCalculator() { } @Override - public void adjust(Complex symbol) + public float getPhaseError(Complex symbol) { - symbol.multiply(DIFFERENTIAL_OFFSET); + mSymbol.setValues(symbol); + mSymbol.multiply(DIFFERENTIAL_OFFSET); + return super.getPhaseError(mSymbol); } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java new file mode 100644 index 00000000..929e2b06 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKTimingErrorDetector.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk; + +import io.github.dsheirer.sample.complex.Complex; + +public class DQPSKTimingErrorDetector +{ + private static final float PI_4_ROTATION = (float)(Math.PI / 4.0); + + /** + * Custom early/late symbol timing error detector designed to optimize the sample timing point such that the + * current symbol's inphase and quadrature approach the optimal 45 degree (.707) sampling instant. + */ + public DQPSKTimingErrorDetector() + { + } + + /** + * Calculates a symbol sampling time error by comparing the current complex sample against a normal QPSK or + * Pi/4 rotated four symbol constellation. The error signal (advance/retard) is determined by the slope + * of each of the inphase and quadrature legs as detected by comparing an immediately preceding sample against + * the current sample and then calculating the error distance of the sampled symbol from the ideal Pi/4 sampling + * location. + * + * Note: both the preceding and the current symbols MUST be normalized (ie symbol.normalize()) to the unit circle + * prior to invoking this method. + * + * @param precedingSymbol + * @param currentSymbol + * @return timing error signal used to advance (positive) or retard (negative) the sampling point + */ + public float getError(Complex precedingSymbol, Complex currentSymbol) + { + return getError(precedingSymbol.inphase(), currentSymbol.inphase()) + + getError(precedingSymbol.quadrature(), currentSymbol.quadrature()); + } + + /** + * Calculates timing error of current value relative to an ideal 45 degree or Pi/4 radians (.707) value. + * + * @param early sample that closely precedes the current sampling instant - used to determine slope + * @param current sampling value + * @return error as the distance of the current value from an ideal value and the sign (advance/retard) + * appropriate for the detected slope + */ + private float getError(float early, float current) + { + if(early < current) //Ascending slope + { + if(current > 0.0) + { + //Positive quadrature hemisphere, ascending slope + return PI_4_ROTATION - current; + } + else + { + //Negative quadrature hemisphere, ascending slope + return -PI_4_ROTATION - current; + } + } + else //Descending slope + { + if(current > 0.0) + { + //Positive quadrature hemisphere, descending slope + return current - PI_4_ROTATION; + } + else + { + //Negative quadrature hemisphere, descending slope + return current + PI_4_ROTATION; + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java deleted file mode 100644 index d3a593b6..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java +++ /dev/null @@ -1,76 +0,0 @@ -/******************************************************************************* - * sdr-trunk - * Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk; - -import io.github.dsheirer.sample.complex.Complex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class EarlyLateDetector -{ - private static final float PI_4_ROTATION = (float)(Math.PI / 4.0); - private final static Logger mLog = LoggerFactory.getLogger(EarlyLateDetector.class); - - /** - * Symbol timing error detector designed to optimize the sample timing point such that the current symbol's - * inphase and quadrature approach the optimal 45 degree (.707) sampling instant. - */ - public EarlyLateDetector() - { - } - - public float getError(Complex early, Complex current, Complex late) - { - float inphaseError = getError(early.inphase(), current.inphase(), late.inphase()); - float quadratureError = getError(early.quadrature(), current.quadrature(), late.quadrature()); - float error = inphaseError + quadratureError; - - mLog.debug("I:" + inphaseError + " Q:" + quadratureError + " E:" + error); - return error; -// return getError(early.inphase(), current.inphase(), late.inphase()) - -// getError(early.quadrature(), current.quadrature(), late.quadrature()); - } - - private float getError(float early, float current, float late) - { - if(early < current) //Ascending slope - { - if(current > 0.0) - { - //Positive hemisphere, ascending slope - return PI_4_ROTATION - current; - } - else - { - //Negative hemisphere, ascending slope - return -PI_4_ROTATION - current; - } - } - else //Descending slope - { - if(current > 0.0) - { - //Positive hemisphere, descending slope - return current - PI_4_ROTATION; - } - else - { - //Negative hemisphere, descending slope - return current + PI_4_ROTATION; - } - } - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java index 5b5df5a9..4e413d88 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/ISymbolPhaseErrorCalculator.java @@ -19,13 +19,6 @@ import io.github.dsheirer.sample.complex.Complex; public interface ISymbolPhaseErrorCalculator { - /** - * Adjusts the calculated symbol as necessary before calculating any phase error. This might be useful for - * differential encoding to remove a 45 degree rotational spin. - * @param symbol to adjust - */ - void adjust(Complex symbol); - /** * Calculates the phase error of the symbol relative to an alignment for the constellation * @param symbol to calculate error diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java similarity index 52% rename from src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java rename to src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java index ba8d731a..bc3cf830 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java @@ -17,59 +17,60 @@ package io.github.dsheirer.dsp.psk; import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator; import io.github.dsheirer.sample.complex.Complex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -public class InterpolatingSymbolBuffer2 +public class InterpolatingSampleBuffer { - private static final float MAXIMUM_SAMPLES_PER_SYMBOL_DEVIATION = 0.02f; //2% - private final static Logger mLog = LoggerFactory.getLogger(InterpolatingSymbolBuffer2.class); - private Complex mPreceedingSample = new Complex(0,0); + private static final float MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL = 0.02f; // +/- 2% deviation + + private Complex mPrecedingSample = new Complex(0,0); private Complex mCurrentSample = new Complex(0,0); - private Complex mFollowingSample = new Complex(0,0); private float[] mDelayLineInphase; private float[] mDelayLineQuadrature; + private int mDelayLinePointer = 0; + private int mTwiceSamplesPerSymbol; + private float mSamplingPoint; private float mSampleCounterGain = 0.05f; private float mDetectedSamplesPerSymbol; private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain; - private float mMaximumSamplesPerSymbol;; - private float mMinimumSamplesPerSymbol;; + private float mMaximumSamplesPerSymbol; + private float mMinimumSamplesPerSymbol; - private int mDelayLinePointer = 0; - private int mTwiceSamplesPerSymbol; private RealInterpolator mInterpolator = new RealInterpolator(1.0f); - private SymbolDecisionData2 mSymbolDecisionData2; - public InterpolatingSymbolBuffer2(float samplesPerSymbol) + /** + * Buffer to store complex sample data and produce interpolated samples. + * @param samplesPerSymbol + */ + public InterpolatingSampleBuffer(float samplesPerSymbol) { mSamplingPoint = samplesPerSymbol; mDetectedSamplesPerSymbol = samplesPerSymbol; - mMaximumSamplesPerSymbol = samplesPerSymbol * (1.0f + MAXIMUM_SAMPLES_PER_SYMBOL_DEVIATION); - mMinimumSamplesPerSymbol = samplesPerSymbol * (1.0f - MAXIMUM_SAMPLES_PER_SYMBOL_DEVIATION); + mMaximumSamplesPerSymbol = samplesPerSymbol * (1.0f + MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL); + mMinimumSamplesPerSymbol = samplesPerSymbol * (1.0f - MAXIMUM_DEVIATION_SAMPLES_PER_SYMBOL); mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol]; - - mSymbolDecisionData2 = new SymbolDecisionData2((int)samplesPerSymbol); } - public void receive(Complex currentSample) + /** + * Stores the sample in the buffer and updates pointers. + * @param sample + */ + public void receive(Complex sample) { - mSymbolDecisionData2.receive(currentSample); mSamplingPoint--; //Fill up the delay line to use with the interpolator - mDelayLineInphase[mDelayLinePointer] = currentSample.inphase(); - mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.inphase(); - mDelayLineQuadrature[mDelayLinePointer] = currentSample.quadrature(); - mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature(); + mDelayLineInphase[mDelayLinePointer] = sample.inphase(); + mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.inphase(); + mDelayLineQuadrature[mDelayLinePointer] = sample.quadrature(); + mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample.quadrature(); //Increment pointer and keep pointer in bounds mDelayLinePointer++; mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol; - } /** @@ -89,15 +90,22 @@ public class InterpolatingSymbolBuffer2 */ public void resetAndAdjust(float symbolTimingError) { - //mDetectedSamplesPerSymbol is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per - //symbol + //Adjust detected samples per symbol based on timing error mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + (symbolTimingError * mDetectedSamplesPerSymbolGain); - mDetectedSamplesPerSymbol = constrain(mDetectedSamplesPerSymbol, mMinimumSamplesPerSymbol, mMaximumSamplesPerSymbol); - mLog.debug("Detected Samples Per Symbol: " + mDetectedSamplesPerSymbol); + //Constrain detected samples per symbol to min/max values + if(mDetectedSamplesPerSymbol > mMaximumSamplesPerSymbol) + { + mDetectedSamplesPerSymbol = mMaximumSamplesPerSymbol; + } + + if(mDetectedSamplesPerSymbol < mMinimumSamplesPerSymbol) + { + mDetectedSamplesPerSymbol = mMinimumSamplesPerSymbol; + } + //Add another symbol's worth of samples to the counter and adjust timing based on gardner error - increaseSampleCounter((mDetectedSamplesPerSymbol + (mSampleCounterGain * symbolTimingError))); -// increaseSampleCounter(mDetectedSamplesPerSymbol); + increaseSampleCounter((mDetectedSamplesPerSymbol + (symbolTimingError * mSampleCounterGain))); } /** @@ -117,13 +125,13 @@ public class InterpolatingSymbolBuffer2 } /** - * Samples that precedes the current sample (uninterpolated). + * Un-interpolated sample that precedes the current interpolated sampling point. The current sampling point falls + * somewhere between sample index 3 and index 4, therefore the preceding sample is located at index 3. */ - public Complex getPreceedingSample() + public Complex getPrecedingSample() { - mPreceedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]); - - return mPreceedingSample; + mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]); + return mPrecedingSample; } /** @@ -134,33 +142,9 @@ public class InterpolatingSymbolBuffer2 { /* Calculate interpolated current sample */ mCurrentSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint)); - return mCurrentSample; } - /** - * Samples that follows the current sample (uninterpolated). - */ - public Complex getFollowingSample() - { - mFollowingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 4]); - - return mFollowingSample; - } - - - - /** - * Contents of the interpolating buffer and the current buffer index and symbol decision offset. This data can - * be used to support an external eye-diagram chart. - * @return symbol decision data. - */ - public SymbolDecisionData2 getSymbolDecisionData() - { - mSymbolDecisionData2.setSamplingPoint(mSamplingPoint); - return mSymbolDecisionData2; - } - /** * Returns the interpolated inphase value for the specified offset * @param interpolation into the buffer to calculate the interpolated sample @@ -171,17 +155,6 @@ public class InterpolatingSymbolBuffer2 return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation); } - /** - * Returns the interpolated inphase value for the specified offset - * @param delayLineOffset offset into the inphase delay line buffer - * @param interpolation into the buffer to calculate the interpolated sample - * @return inphase value of the interpolated sample - */ - public float getInphase(int delayLineOffset, float interpolation) - { - return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + delayLineOffset, interpolation); - } - /** * Returns the interpolated quadrature value for the specified offset * @param interpolation into the buffer to calculate the interpolated sample @@ -191,55 +164,4 @@ public class InterpolatingSymbolBuffer2 { return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation); } - - /** - * Returns the interpolated quadrature value for the specified offset - * @param delayLineOffset offset into the quadrature delay line buffer - * @param interpolation into the buffer to calculate the interpolated sample - * @return quadrature value of the interpolated sample - */ - public float getQuadrature(int delayLineOffset, float interpolation) - { - return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + delayLineOffset, interpolation); - } - - /** - * Constrains value to the range of ( -maximum <> maximum ) - */ - public static float clip(float value, float maximum) - { - if(value > maximum) - { - return maximum; - } - else if(value < -maximum) - { - return -maximum; - } - - return value; - } - - /** - * Constrains the value within the minimum-maximum range - * - * @param value to constrain - * @param minimum value - * @param maximum value - * @return constrained value - */ - public static float constrain(float value, float minimum, float maximum) - { - if(value > maximum) - { - return maximum; - } - - if(value < minimum) - { - return minimum; - } - - return value; - } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java similarity index 53% rename from src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java rename to src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java index 16ea9bce..6c71fc25 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBufferInstrumented.java @@ -17,24 +17,32 @@ package io.github.dsheirer.dsp.psk; import io.github.dsheirer.sample.complex.Complex; -public class EarlyLateDetector2 +public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBuffer { - //45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient - public static final Complex POSITIVE_OFFSET = Complex.fromAngle(Math.PI / 4.0d); - private Complex mSymbol = new Complex(0,0); + private SymbolDecisionData mSymbolDecisionData; - public float getError(Complex symbol) + public InterpolatingSampleBufferInstrumented(float samplesPerSymbol) { - mSymbol.setValues(symbol); - mSymbol.multiply(POSITIVE_OFFSET); + super(samplesPerSymbol); - if(Math.abs(mSymbol.quadrature()) > Math.abs(mSymbol.inphase())) - { - return mSymbol.inphase(); - } - else - { - return mSymbol.quadrature(); - } + mSymbolDecisionData = new SymbolDecisionData((int)samplesPerSymbol); } + + public void receive(Complex sample) + { + super.receive(sample); + mSymbolDecisionData.receive(sample); + } + + /** + * Contents of the interpolating buffer and the current buffer index and symbol decision offset. This data can + * be used to support an external eye-diagram chart. + * @return symbol decision data. + */ + public SymbolDecisionData getSymbolDecisionData() + { + mSymbolDecisionData.setSamplingPoint(getSamplingPoint()); + return mSymbolDecisionData; + } + } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java deleted file mode 100644 index cccba75d..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java +++ /dev/null @@ -1,219 +0,0 @@ -/******************************************************************************* - * sdr-trunk - * Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk; - -import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator; -import io.github.dsheirer.sample.complex.Complex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class InterpolatingSymbolBuffer -{ - private final static Logger mLog = LoggerFactory.getLogger(InterpolatingSymbolBuffer.class); - - private Complex mMiddleSample = new Complex(0,0); - private Complex mCurrentSample = new Complex(0,0); - - private float[] mDelayLineInphase; - private float[] mDelayLineQuadrature; - private float mSamplingPoint; //Sample Counter - private float mSampleCounterGain = 0.05f; //Sample Counter Gain - private float mSamplesPerSymbol; - private float mDetectedSamplesPerSymbol; - private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain; - private float mMaxDeviation = 0.005f; - - private int mDelayLinePointer = 0; - private int mTwiceSamplesPerSymbol; - private RealInterpolator mInterpolator = new RealInterpolator(1.0f); - private SymbolDecisionData2 mSymbolDecisionData2; - - public InterpolatingSymbolBuffer(float samplesPerSymbol) - { - mSamplingPoint = samplesPerSymbol; - mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol); - mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol]; - mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol]; - mDetectedSamplesPerSymbol = samplesPerSymbol; - mSamplesPerSymbol = samplesPerSymbol; - - mSymbolDecisionData2 = new SymbolDecisionData2((int)samplesPerSymbol); - } - - public void receive(Complex currentSample) - { - mSymbolDecisionData2.receive(currentSample); - mSamplingPoint--; - - //Fill up the delay line to use with the interpolator - mDelayLineInphase[mDelayLinePointer] = currentSample.inphase(); - mDelayLineInphase[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.inphase(); - mDelayLineQuadrature[mDelayLinePointer] = currentSample.quadrature(); - mDelayLineQuadrature[mDelayLinePointer + mTwiceSamplesPerSymbol] = currentSample.quadrature(); - - //Increment pointer and keep pointer in bounds - mDelayLinePointer++; - mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol; - - } - - /** - * Updates the internal sample counter with additional samples to add to the symbol sample counter. - * @param samplesToAdd to the sample counter - */ - public void increaseSampleCounter(float samplesToAdd) - { - mSamplingPoint += samplesToAdd; - } - - /** - * Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another - * symbol. - * - * @param symbolTimingError from a symbol timing error detector - */ - public void resetAndAdjust(float symbolTimingError) - { - //mDetectedSamplesPerSymbol is samples per symbol and is constrained to floating between +/- .005 of the nominal samples per - //symbol - mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + mDetectedSamplesPerSymbolGain * symbolTimingError; - mDetectedSamplesPerSymbol = mSamplesPerSymbol + clip(mDetectedSamplesPerSymbol - mSamplesPerSymbol, mMaxDeviation); - - mLog.debug("Detected Samples Per Symbol:" + mDetectedSamplesPerSymbol); - //Add another symbol's worth of samples to the counter and adjust timing based on gardner error - increaseSampleCounter((mDetectedSamplesPerSymbol + (mSampleCounterGain * symbolTimingError))); - } - - /** - * Current value of the detected samples per symbol - */ - public float getSamplingPoint() - { - return mSamplingPoint; - } - - /** - * Indicates if this buffer has accumulated a enough samples to represent a full symbol - */ - public boolean hasSymbol() - { - return mSamplingPoint < 1.0f; - } - - /** - * Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a - * complete symbol with the hasSymbol() method. - */ - public Complex getMiddleSample() - { - /* Calculate interpolated middle sample and current sample */ - mMiddleSample.setValues(getInphase(mSamplingPoint), getQuadrature(mSamplingPoint)); - - return mMiddleSample; - } - - /** - * Interpolated current sample for the symbol. This sample represents the ideal symbol sampling point. - * Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method. - */ - public Complex getCurrentSample() - { - float halfDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol / 2.0f; - int halfSamplingPointOffset = (int)Math.floor(halfDetectedSamplesPerSymbol); - float halfSamplingPoint = mSamplingPoint + halfDetectedSamplesPerSymbol - halfSamplingPointOffset; - - if(halfSamplingPoint > 1.0) - { - halfSamplingPoint -= 1.0; - halfSamplingPointOffset += 1; - } - - mCurrentSample.setValues(getInphase(halfSamplingPointOffset, halfSamplingPoint), - getQuadrature(halfSamplingPointOffset, halfSamplingPoint)); - - return mCurrentSample; - } - - /** - * Contents of the interpolating buffer and the current buffer index and symbol decision offset. This data can - * be used to support an external eye-diagram chart. - * @return symbol decision data. - */ - public SymbolDecisionData2 getSymbolDecisionData() - { - mSymbolDecisionData2.setSamplingPoint(mSamplingPoint); - return mSymbolDecisionData2; - } - - /** - * Returns the interpolated inphase value for the specified offset - * @param interpolation into the buffer to calculate the interpolated sample - * @return inphase value of the interpolated sample - */ - public float getInphase(float interpolation) - { - return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation); - } - - /** - * Returns the interpolated inphase value for the specified offset - * @param delayLineOffset offset into the inphase delay line buffer - * @param interpolation into the buffer to calculate the interpolated sample - * @return inphase value of the interpolated sample - */ - public float getInphase(int delayLineOffset, float interpolation) - { - return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + delayLineOffset, interpolation); - } - - /** - * Returns the interpolated quadrature value for the specified offset - * @param interpolation into the buffer to calculate the interpolated sample - * @return quadrature value of the interpolated sample - */ - public float getQuadrature(float interpolation) - { - return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation); - } - - /** - * Returns the interpolated quadrature value for the specified offset - * @param delayLineOffset offset into the quadrature delay line buffer - * @param interpolation into the buffer to calculate the interpolated sample - * @return quadrature value of the interpolated sample - */ - public float getQuadrature(int delayLineOffset, float interpolation) - { - return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + delayLineOffset, interpolation); - } - - /** - * Constrains value to the range of ( -maximum <> maximum ) - */ - public static float clip(float value, float maximum) - { - if(value > maximum) - { - return maximum; - } - else if(value < -maximum) - { - return -maximum; - } - - return value; - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java index aba3a907..1a21ae75 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java @@ -449,6 +449,13 @@ public class LSMDemodulator implements Instrumentable, ComplexSampleListener unwrapPhase(); } + @Override + public void reset() + { + mLoopPhase = 0.0f; + mLoopFrequency = 0.0f; + } + @Override public void adjust(double error) { diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java deleted file mode 100644 index 9bcda428..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java +++ /dev/null @@ -1,233 +0,0 @@ -/******************************************************************************* - * sdr-trunk - * Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk; - -import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop; -import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; -import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.ComplexSampleListener; - -public class QPSKDemodulator implements ComplexSampleListener -{ - private InterpolatingSymbolBuffer mInterpolatingSymbolBuffer; - private IPhaseLockedLoop mPLL; - private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator; - private IQPSKSymbolDecoder mQPSKSymbolDecoder; - private GardnerDetector mGardnerDetector = new GardnerDetector(); - private Listener mSymbolListener; - private Listener mPLLErrorListener; - private Listener mPLLFrequencyListener; - private Listener mSamplesPerSymbolListener; - private Listener mDibitListener; - private Listener mSymbolDecisionDataListener; - - private Complex mPreviousSample = new Complex(0, 0); - private Complex mPreviousMiddleSample = new Complex(0, 0); - private Complex mCurrentSample = new Complex(0, 0); - private Complex mMiddleSample = new Complex(0, 0); - private Complex mPreviousSymbol = new Complex(0, 0); - private Complex mMiddleSymbol = new Complex(0, 0); - private Complex mCurrentSymbol = new Complex(0, 0); - private float mPhaseError; - private float mSymbolTimingError; - private double mSampleRate; - - - /** - * Decoder for Quaternary Phase Shift Keying (QPSK) and Differential QPSK (DQPSK). This decoder uses both a - * phase-locked loop and a gardner symbol timing error detector to automatically align to the incoming carrier - * frequency and to adjust for any changes in symbol timing. - * - * @param phaseLockedLoop for tracking carrier frequency error - * @param symbolPhaseErrorCalculator to calculate symbol phase errors and optionally adjust for differential encoding - * @param symbolDecoder to decode demodulated symbols into dibits - * @param sampleRate of the incoming complex sample stream - * @param symbolRate of the decoded QPSK symbols. - */ - public QPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator, - IQPSKSymbolDecoder symbolDecoder, double sampleRate, double symbolRate) - { - if(sampleRate < (symbolRate * 2)) - { - throw new IllegalArgumentException("Sample rate [" + sampleRate + - "] must be at least 2 x symbol rate [" + symbolRate + "]"); - } - - mSampleRate = sampleRate; - mPLL = phaseLockedLoop; - mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; - mQPSKSymbolDecoder = symbolDecoder; - - float samplesPerSymbol = (float)(sampleRate / symbolRate); - mInterpolatingSymbolBuffer = new InterpolatingSymbolBuffer(samplesPerSymbol); - } - - /** - * Submits a buffer containing complex samples for decoding - * @param complexBuffer with complex samples - */ - public void receive(ComplexBuffer complexBuffer) - { - float[] samples = complexBuffer.getSamples(); - - for(int x = 0; x < samples.length; x += 2) - { - receive(samples[x], samples[x + 1]); - } - } - - /** - * Submits a complex sample for decoding. - * @param inphase value for the sample - * @param quadrature value for the sample - */ - @Override - public void receive(float inphase, float quadrature) - { - //Update current sample with values - mCurrentSample.setValues(inphase, quadrature); - - //Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency - mCurrentSample.multiply(mPLL.incrementAndGetCurrentVector()); - - mInterpolatingSymbolBuffer.receive(mCurrentSample); - - //Calculate the symbol once we've stored enough samples - if(mInterpolatingSymbolBuffer.hasSymbol()) - { - //Get middle and current samples from the interpolating buffer - mMiddleSample = mInterpolatingSymbolBuffer.getMiddleSample(); - mCurrentSample = mInterpolatingSymbolBuffer.getCurrentSample(); - - //Eye diagram listener - if(mSymbolDecisionDataListener != null) - { - mSymbolDecisionDataListener.receive(mInterpolatingSymbolBuffer.getSymbolDecisionData()); - } - - //Calculate middle and current symbols as the delta between the previous and current samples - mMiddleSymbol.setInphase(Complex.multiplyInphase(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); - mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(mMiddleSample.inphase(), mMiddleSample.quadrature(), mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature())); - mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature())); - mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(), mPreviousSample.inphase(), -mPreviousSample.quadrature())); - - //Set gain to unity before we calculate the error value - mMiddleSymbol.normalize(); - mCurrentSymbol.normalize(); - - //Send to an external constellation symbol listener when registered - if(mSymbolListener != null) - { - mSymbolListener.receive(mCurrentSymbol); - } - - //Symbol timing error calculations - mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol); - mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError); - - if(mSamplesPerSymbolListener != null) - { - mSamplesPerSymbolListener.receive((double)mInterpolatingSymbolBuffer.getSamplingPoint()); - } - - //Store current samples/symbols to use for the next period - mPreviousSample.setValues(mCurrentSample); - mPreviousMiddleSample.setValues(mMiddleSample); - mPreviousSymbol.setValues(mCurrentSymbol); - - //Calculate the phase error of the current symbol relative to the expected constellation and provide - //feedback to the PLL - mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); - mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); - - mPhaseError = GardnerDetector.clip(mPhaseError, 0.15f); - -// mPLL.adjust(-mPhaseError); - mPLL.adjust(0.0); - - if(mPLLErrorListener != null) - { - mPLLErrorListener.receive((double)mPhaseError); - } - - if(mPLLFrequencyListener != null) - { - double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency(); - - loopFrequency *= mSampleRate / (2.0 * Math.PI); - - mPLLFrequencyListener.receive(loopFrequency); - } - - //Decode the dibit from the symbol and send to the listener - if(mDibitListener != null) - { - mDibitListener.receive(mQPSKSymbolDecoder.decode(mCurrentSymbol)); - } - - //TODO: Assemble dibits here and broadcast - } - } - - /** - * Registers the listener to receive decoded QPSK symbols - */ - public void setSymbolListener(Listener listener) - { - mSymbolListener = listener; - } - - /** - * Registers the listener to receive decoded QPSK dibits - */ - public void setDibitListener(Listener listener) - { - mDibitListener = listener; - } - - /** - * Registers the listener to receive PLL error values - */ - public void setPLLErrorListener(Listener listener) - { - mPLLErrorListener = listener; - } - - /** - * Registers the listener to receive PLL error values - */ - public void setPLLFrequencyListener(Listener listener) - { - mPLLFrequencyListener = listener; - } - - /** - * Registers the listener to receive PLL error values - */ - public void setSamplesPerSymbolListener(Listener listener) - { - mSamplesPerSymbolListener = listener; - } - - public void setSymbolDecisionDataListener(Listener listener) - { - mSymbolDecisionDataListener = listener; - } - -} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java index d7c94323..fed580ce 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKSymbolPhaseErrorCalculator.java @@ -24,12 +24,6 @@ public class QPSKSymbolPhaseErrorCalculator implements ISymbolPhaseErrorCalculat { private float mPhaseError; - @Override - public void adjust(Complex symbol) - { - //No-op - since there is no differential, we don't have to adjust - } - @Override public float getPhaseError(Complex symbol) { diff --git a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java index 75a70332..e06f3d22 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData.java @@ -15,56 +15,79 @@ ******************************************************************************/ package io.github.dsheirer.dsp.psk; +import io.github.dsheirer.buffer.ComplexCircularBuffer; +import io.github.dsheirer.sample.complex.Complex; public class SymbolDecisionData { - private float[] mSamplesInphase; - private float[] mSamplesQuadrature; - private float mSamplesPerSymbol; - private int mSampleIndex; - private float mSampleIndexOffset; + //45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient + public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d); + + private ComplexCircularBuffer mBuffer; + private float mSamplingPoint; /** - * Symbol decision data - samples and timing decision for a single symbol decision instant. - * - * @param samplesInphase - * @param samplesQuadrature - * @param samplesPerSymbol - * @param sampleIndex - * @param sampleIndexOffset + * Circular buffer for capturing two symbols worth of sample data for use in instrumentation. + * @param samplesPerSymbol to size the buffer */ - public SymbolDecisionData(float[] samplesInphase, float[] samplesQuadrature, float samplesPerSymbol, - int sampleIndex, float sampleIndexOffset) + public SymbolDecisionData(int samplesPerSymbol) { - mSamplesInphase = samplesInphase; - mSamplesQuadrature = samplesQuadrature; - mSamplesPerSymbol = samplesPerSymbol; - mSampleIndex = sampleIndex; - mSampleIndexOffset = sampleIndexOffset; + mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol); } - public float[] getInphaseSamples() + /** + * Stores the complex sample in the buffer + */ + public void receive(float inphase, float quadrature) { - return mSamplesInphase; + mBuffer.put(new Complex(inphase, quadrature)); } - public float[] getQuadratureSamples() + /** + * Stores the complex sample in the buffer + */ + public void receive(Complex complex) { - return mSamplesQuadrature; + mBuffer.put(complex.copy()); } - public float getSamplesPerSymbol() + /** + * Sets the fractional sampling point for the current symbol to use in interpolating the current symbol. + */ + public void setSamplingPoint(float samplingPoint) { - return mSamplesPerSymbol; + mSamplingPoint = samplingPoint; } - public int getSampleIndex() + /** + * Fractional sampling point + */ + public float getSamplingPoint() { - return mSampleIndex; + return mSamplingPoint; } - public float getSampleIndexOffset() + /** + * Array of demodulated samples representing the current symbol where each current sample is demodulated against + * the previous symbol's sample to produce the differential demodulated sample. Samples are in time order. + * + * Note: differential decoding is on an integral samples-per-symbol basis and does not use fractional interpolation. + */ + public Complex[] getDemodulated() { - return mSampleIndexOffset; + Complex[] samples = mBuffer.getAll(); + + int length = samples.length / 2; + + Complex[] demodulated = new Complex[length]; + + for(int x = 0; x < samples.length / 2; x++) + { + Complex copy = samples[length + x].copy(); + copy.multiply(samples[x].conjugate()); + demodulated[x] = copy; + } + + return demodulated; } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java deleted file mode 100644 index 64d9ebe9..00000000 --- a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java +++ /dev/null @@ -1,74 +0,0 @@ -/******************************************************************************* - * sdr-trunk - * Copyright (C) 2014-2018 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 io.github.dsheirer.dsp.psk; - -import io.github.dsheirer.buffer.ComplexCircularBuffer; -import io.github.dsheirer.sample.complex.Complex; - -public class SymbolDecisionData2 -{ - //45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient - public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d); - - private ComplexCircularBuffer mBuffer; - private float mSamplingPoint; - - public SymbolDecisionData2(int samplesPerSymbol) - { - mBuffer = new ComplexCircularBuffer(2 * samplesPerSymbol); - } - - public void receive(float inphase, float quadrature) - { - mBuffer.put(new Complex(inphase, quadrature)); - } - - public void receive(Complex complex) - { - mBuffer.put(complex.copy()); - } - - public void setSamplingPoint(float samplingPoint) - { - mSamplingPoint = samplingPoint; - } - - public float getSamplingPoint() - { - return mSamplingPoint; - } - - public Complex[] getDemodulated() - { - Complex[] samples = mBuffer.getAll(); - - int length = samples.length / 2; - - Complex[] demodulated = new Complex[length]; - - for(int x = 0; x < samples.length / 2; x++) - { - Complex copy = samples[length + x].copy(); - copy.multiply(samples[x].conjugate()); - - //Test - rotate the demodulated symbol by 45 degrees to see the impact on the eye diagram -// copy.multiply(DIFFERENTIAL_OFFSET); - demodulated[x] = copy; - } - - return demodulated; - } -} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java index 187e1c0c..38b8d101 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/CostasLoop.java @@ -15,13 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.dsp.psk.pll; -import io.github.dsheirer.buffer.DoubleCircularBuffer; -import io.github.dsheirer.dsp.mixer.Oscillator; import io.github.dsheirer.sample.complex.Complex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.DecimalFormat; /** * Costas Loop - phase locked loop designed to automatically synchronize to the incoming carrier frequency in order to @@ -34,21 +28,16 @@ import java.text.DecimalFormat; */ public class CostasLoop implements IPhaseLockedLoop { - private final static Logger mLog = LoggerFactory.getLogger(CostasLoop.class); public static final double TWO_PI = 2.0 * Math.PI; - private DoubleCircularBuffer mErrorBuffer = new DoubleCircularBuffer(10); + + private Complex mCurrentVector = new Complex(0, 0); + private double mLoopPhase = 0.0; + private double mLoopFrequency = 0.0; + private double mMaximumLoopFrequency; private double mDamping = Math.sqrt(2.0) / 2.0; private double mAlphaGain; private double mBetaGain; - private double mLoopPhase = 0.0; -// private double mLoopFrequency = 0.0; - private double mLoopFrequency = Math.PI * 2.0 * -10.0 / 48000.0; - private double mMaximumLoopFrequency; - private Complex mCurrentVector = new Complex(0, 0); - - private Tracking mTracking = Tracking.SEARCHING; - private ITrackingStateListener mTrackingStateListener; - private boolean mAutomaticTrackingEnabled = true; + private Tracking mTracking = Tracking.NORMAL; /** * Costas Loop for tracking and correcting frequency error in a received carrier signal. @@ -75,12 +64,12 @@ public class CostasLoop implements IPhaseLockedLoop { mLoopFrequency += correction; - if(mLoopFrequency > mMaximumLoopFrequency) + while(mLoopFrequency > mMaximumLoopFrequency) { mLoopFrequency -= 2.0 * mMaximumLoopFrequency; } - if(mLoopFrequency < -mMaximumLoopFrequency) + while(mLoopFrequency < -mMaximumLoopFrequency) { mLoopFrequency += 2.0 * mMaximumLoopFrequency; } @@ -107,35 +96,6 @@ public class CostasLoop implements IPhaseLockedLoop { mTracking = tracking; updateLoopBandwidth(); - - mLog.debug("Tracking: " + tracking.name()); - - if(mTrackingStateListener != null) - { - mTrackingStateListener.trackingStateChanged(tracking); - } - } - - /** - * Sets the listener to receive notifications when the tracking state changes - */ - public void setTrackingStateListener(ITrackingStateListener listener) - { - mTrackingStateListener = listener; - } - - /** - * Enables or disables automatic tracking state updates. When enabled, the loop will automatically increase or - * decrease the loop alpha/beta gain values based on the variance of the error tracking values submitted to the - * loop control. As the variance decreases, so will the gain values, and vice-versa. Setting this to disabled - * will force the tracker to retain the gain values associated with either the default or user-specified tracking - * value. - * - * @param enabled true (default) to enable automatic gain tracking - */ - public void setAutomaticTracking(boolean enabled) - { - mAutomaticTrackingEnabled = enabled; } /** @@ -145,8 +105,16 @@ public class CostasLoop implements IPhaseLockedLoop { mLoopPhase += mLoopFrequency; - /* Keep the loop phase in bounds */ - normalizePhase(); + /* Normalize phase between +/- 2 * PI */ + if(mLoopPhase > TWO_PI) + { + mLoopPhase -= TWO_PI; + } + + if(mLoopPhase < -TWO_PI) + { + mLoopPhase += TWO_PI; + } } /** @@ -171,11 +139,6 @@ public class CostasLoop implements IPhaseLockedLoop return mLoopFrequency; } - public double getMaximumLoopFrequency() - { - return mMaximumLoopFrequency; - } - /** * Updates the costas loop frequency and phase to adjust for the phase * error value @@ -187,97 +150,18 @@ public class CostasLoop implements IPhaseLockedLoop mLoopFrequency += (mBetaGain * phaseError); mLoopPhase += mLoopFrequency + (mAlphaGain * phaseError); - /* Maintain phase between +/- 2 * PI */ - normalizePhase(); - - /* Limit frequency to +/- maximum loop frequency */ - limitFrequency(); - -// checkLoopBandwidth(); - } - - private void checkLoopBandwidth() - { - mErrorBuffer.put(mLoopFrequency); - - double standardDeviation = mErrorBuffer.standardDeviation(); - - switch(mTracking) - { - case SEARCHING: - if(Tracking.COARSE.contains(standardDeviation)) - { - setTracking(Tracking.COARSE); - } - break; - case COARSE: - if(Tracking.COARSE.contains(standardDeviation)) - { - //Promote if possible - otherwise remain - if(Tracking.FINE.contains(standardDeviation)) - { - setTracking(Tracking.FINE); - } - } - else - { - setTracking(Tracking.SEARCHING); - } - break; - case FINE: - if(Tracking.FINE.contains(standardDeviation)) - { - //Promote if possible - otherwise remain - if(Tracking.LOCKED.contains(standardDeviation)) - { - setTracking(Tracking.LOCKED); - } - } - else - { - setTracking(Tracking.COARSE); - } - break; - case LOCKED: - if(!Tracking.LOCKED.contains(standardDeviation)) - { - setTracking(Tracking.FINE); - } - break; - } - } - - public double getErrorMean() - { - return mErrorBuffer.mean(); - } - - public double getErrorVariance() - { - return mErrorBuffer.variance(); - } - - /** - * Normalizes the phase tracker to maintain within +/- 2 Pi - */ - private void normalizePhase() - { - while(mLoopPhase > TWO_PI) + /* Normalize phase between +/- 2 * PI */ + if(mLoopPhase > TWO_PI) { mLoopPhase -= TWO_PI; } - while(mLoopPhase < -TWO_PI) + if(mLoopPhase < -TWO_PI) { mLoopPhase += TWO_PI; } - } - /** - * Constrains the frequency within the bounds of +/- loop frequency - */ - private void limitFrequency() - { + /* Limit frequency to +/- maximum loop frequency */ if(mLoopFrequency > mMaximumLoopFrequency) { mLoopFrequency = mMaximumLoopFrequency; @@ -289,57 +173,10 @@ public class CostasLoop implements IPhaseLockedLoop } } - /** - * Tracking state listener interface for receiving updates when the tracking state of the costas loop changes - */ - public interface ITrackingStateListener + @Override + public void reset() { - void trackingStateChanged(Tracking tracking); + mLoopPhase = 0.0; + mLoopFrequency = 0.0; } - - public static void main(String[] arguments) - { - DecimalFormat decimalFormat = new DecimalFormat("0.000000"); - double sampleRate = 25000.0; - int errorFrequency = 411; - double loopBandwidth = 50.0; - - Oscillator oscillator = new Oscillator(errorFrequency, (int)sampleRate); - CostasLoop costasLoop = new CostasLoop(sampleRate, 4800.0); - costasLoop.setTrackingStateListener(new ITrackingStateListener() - { - @Override - public void trackingStateChanged(Tracking tracking) - { - mLog.debug("Tracking State Changed: " + tracking.name()); - } - }); - - for(int x = 0; x < 1000; x++) - { - Complex complex = oscillator.getComplex(); - - oscillator.rotate(); - costasLoop.increment(); - - if(x % 5 == 0) - { - complex.multiply(costasLoop.getCurrentVector().conjugate()); - - double phaseError = complex.angle(); - costasLoop.adjust(phaseError); - - double loopFrequency = costasLoop.getLoopFrequency() * sampleRate / TWO_PI; - double maxFrequency = costasLoop.getMaximumLoopFrequency() * sampleRate / TWO_PI; - - mLog.debug(x + " Frequency: " + errorFrequency + - " Loop:" + decimalFormat.format(loopFrequency) + - " Max:" + maxFrequency + - " Detected Error:" + decimalFormat.format(phaseError) + - " Avg:" + decimalFormat.format(costasLoop.getErrorMean()) + - " Var:" + decimalFormat.format(costasLoop.getErrorVariance())); - } - } - } - } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java index 1f4f2a52..631a772c 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/IPhaseLockedLoop.java @@ -52,5 +52,8 @@ public interface IPhaseLockedLoop */ Complex incrementAndGetCurrentVector(); - + /** + * Reset tracking to 0 + */ + void reset(); } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java index d89133b2..a4611d20 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/pll/Tracking.java @@ -15,26 +15,21 @@ ******************************************************************************/ package io.github.dsheirer.dsp.psk.pll; +/** + * Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency + */ public enum Tracking { -// SEARCHING(50.0, 0.01, Math.PI), -// COARSE(100.0, 0.001, 0.01), -// FINE(150.0, 0.0001, 0.001), -// LOCKED(200.0, 0.0, 0.0001); - SEARCHING(100.0, 0.4, Math.PI), - COARSE(200.0, 0.2, 0.4), - FINE(400.0, 0.1, 0.2), - LOCKED(600.0, 0.0, 0.1); + FASTEST(50.0), + FAST(100.0), + NORMAL(200.0), + SLOW(400.0); private double mLoopBandwidth; - private double mThresholdMinimum; - private double mThresholdMaximum; - Tracking(double loopBandwidth, double thresholdMinimum, double thresholdMaximum) + Tracking(double loopBandwidth) { mLoopBandwidth = loopBandwidth; - mThresholdMinimum = thresholdMinimum; - mThresholdMaximum = thresholdMaximum; } /** @@ -44,28 +39,4 @@ public enum Tracking { return mLoopBandwidth; } - - /** - * Minimum error value for this entry - */ - public double getThresholdMinimum() - { - return mThresholdMinimum; - } - - /** - * Maximum error value for this entry - */ - public double getThresholdMaximum() - { - return mThresholdMaximum; - } - - /** - * Indicates if the error value is within the range for this entry - */ - public boolean contains(double standardDeviation) - { - return getThresholdMinimum() <= standardDeviation && standardDeviation < getThresholdMaximum(); - } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java index d71c0911..8c9db35d 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/EyeDiagramChart.java @@ -15,7 +15,7 @@ ******************************************************************************/ package io.github.dsheirer.instrument.gui.viewer.chart; -import io.github.dsheirer.dsp.psk.SymbolDecisionData2; +import io.github.dsheirer.dsp.psk.SymbolDecisionData; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; import javafx.collections.FXCollections; @@ -25,7 +25,7 @@ import javafx.scene.chart.NumberAxis; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class EyeDiagramChart extends LineChart implements Listener +public class EyeDiagramChart extends LineChart implements Listener { private final static Logger mLog = LoggerFactory.getLogger(EyeDiagramChart.class); @@ -81,7 +81,7 @@ public class EyeDiagramChart extends LineChart implements Listener mFilteredBufferBroadcaster = new Broadcaster<>(); - private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null); + private P25_C4FMDecoder2Instrumented mDecoder = new P25_C4FMDecoder2Instrumented(null); public P25Phase1Pane() { @@ -52,15 +52,14 @@ public class P25Phase1Pane extends DecoderPane private void init() { -// addListener(getSampleLineChart()); addListener(getDecoder()); - getDecoder().setDEBUGFilteredBufferListener(mFilteredBufferBroadcaster); - getDecoder().setDEBUGSymbolListener(getSymbolChart()); - getDecoder().setDEBUGPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); - getDecoder().setDEBUGPLLFrequencyListener(getPLLFrequencyLineChart()); - getDecoder().setDEBUGSymbolDecisionDataListener(getEyeDiagramChart()); - getDecoder().setDEBUGSamplesPerSymbolListener(getSamplesPerSymbolLineChart()); + getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster); + getDecoder().setSymbolListener(getSymbolChart()); + getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); + getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart()); + getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart()); + getDecoder().setSamplesPerSymbolListener(getSamplesPerSymbolLineChart()); mFilteredBufferBroadcaster.addListener(getSampleLineChart()); HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS); @@ -77,10 +76,9 @@ public class P25Phase1Pane extends DecoderPane double samplesPerSymbol = sampleRate / 4800.0; getSampleLineChart().setSamplesPerSymbol((int)samplesPerSymbol); -// mSamplesPerSymbolLineChart = new DoubleLineChart( "Samples/Symbol", samplesPerSymbol - 1, samplesPerSymbol + 1, 0.1, 40); } - private P25_C4FMDecoder2 getDecoder() + private P25_C4FMDecoder2Instrumented getDecoder() { return mDecoder; } @@ -167,7 +165,7 @@ public class P25Phase1Pane extends DecoderPane { if(mPLLFrequencyLineChart == null) { - mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -200, 200, 20, 40); + mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -500, 500, 50, 40); } return mPLLFrequencyLineChart; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java index 706d6d9b..8d938239 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2.java @@ -20,20 +20,16 @@ import io.github.dsheirer.dsp.filter.FilterFactory; import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter; -import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl; import io.github.dsheirer.dsp.psk.DQPSKDemodulator; import io.github.dsheirer.dsp.psk.DQPSKSymbolPhaseErrorCalculator; import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder; import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator; +import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer; +import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; import io.github.dsheirer.dsp.psk.QPSKStarSlicer; -import io.github.dsheirer.dsp.psk.QPSKSymbolDecoder; -import io.github.dsheirer.dsp.psk.SymbolDecisionData2; import io.github.dsheirer.dsp.psk.pll.CostasLoop; -import io.github.dsheirer.dsp.psk.pll.Tracking; import io.github.dsheirer.sample.Listener; -import io.github.dsheirer.sample.complex.Complex; import io.github.dsheirer.sample.complex.ComplexBuffer; -import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter; import io.github.dsheirer.sample.complex.IComplexBufferListener; import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent; import org.slf4j.Logger; @@ -46,36 +42,21 @@ import java.util.concurrent.ScheduledExecutorService; public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListener, Listener { private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder2.class); - private static final double SYMBOL_RATE = 4800; - private double mSampleRate; - + protected static final double SYMBOL_RATE = 4800; + protected ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator(); + protected IQPSKSymbolDecoder mSymbolDecoder = new QPSKStarSlicer(); + protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; + protected DQPSKDemodulator mQPSKDemodulator; + protected CostasLoop mCostasLoop; + protected double mSampleRate; private Map mBasebandFilters = new HashMap<>(); private ComplexFIRFilter mBasebandFilter; - private Map mSymbolFilters = new HashMap<>(); - private ComplexFIRFilter mSymbolFilter; - - private DQPSKDemodulator mQPSKDemodulator; - private CostasLoop mCostasLoop; - private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter(); - private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32); - private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder(); private P25MessageFramer mMessageFramer; - private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator(); - private IQPSKSymbolDecoder mSymbolDecoder = new QPSKStarSlicer(); - private Listener mDEBUGSymbolListener; - private Listener mDEBUGPLLPhaseErrorListener; - private Listener mDEBUGPLLFrequencyListener; - private Listener mDEBUGSymbolDecisionDataListener; - private Listener mDEBUGSamplesPerSymbolListener; - private Listener mFilteredSymbolListener; public P25_C4FMDecoder2(AliasList aliasList) { super(aliasList); - - setSampleRate(48600.0); - - mStreamConverter.setListener(mAGC); + setSampleRate(48000.0); } public void setSampleRate(double sampleRate) @@ -87,19 +68,12 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mSampleRate = sampleRate; mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f); - mSymbolFilter = new ComplexFIRFilter(getSymbolFilter(), 1.0f); mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); - mCostasLoop.setTracking(Tracking.FINE); - mCostasLoop.setAutomaticTracking(false); + mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, - mSampleRate, SYMBOL_RATE); - mQPSKDemodulator.setSymbolListener(mDEBUGSymbolListener); - mQPSKDemodulator.setPLLErrorListener(mDEBUGPLLPhaseErrorListener); - mQPSKDemodulator.setPLLFrequencyListener(mDEBUGPLLFrequencyListener); - mQPSKDemodulator.setSymbolDecisionDataListener(mDEBUGSymbolDecisionDataListener); - mQPSKDemodulator.setSamplesPerSymbolListener(mDEBUGSamplesPerSymbolListener); + mInterpolatingSampleBuffer); //Message framer can trigger a symbol-inversion correction to the PLL when detected mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); @@ -107,54 +81,16 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mQPSKDemodulator.setDibitListener(mMessageFramer); } - public void setDEBUGSymbolListener(Listener listener) - { - mDEBUGSymbolListener = listener; - mQPSKDemodulator.setSymbolListener(listener); - } - - public void setDEBUGPLLPhaseErrorListener(Listener listener) - { - mDEBUGPLLPhaseErrorListener = listener; - mQPSKDemodulator.setPLLErrorListener(listener); - } - - public void setDEBUGPLLFrequencyListener(Listener listener) - { - mDEBUGPLLFrequencyListener = listener; - mQPSKDemodulator.setPLLFrequencyListener(listener); - } - - public void setDEBUGFilteredBufferListener(Listener listener) - { - mFilteredSymbolListener = listener; - } - - public void setDEBUGSymbolDecisionDataListener(Listener listener) - { - mDEBUGSymbolDecisionDataListener = listener; - mQPSKDemodulator.setSymbolDecisionDataListener(listener); - } - - public void setDEBUGSamplesPerSymbolListener(Listener listener) - { - mDEBUGSamplesPerSymbolListener = listener; - mQPSKDemodulator.setSamplesPerSymbolListener(listener); - } - @Override public void receive(ComplexBuffer complexBuffer) { - ComplexBuffer basebandFiltered = mBasebandFilter.filter(complexBuffer); -// ComplexBuffer symbolFiltered = mSymbolFilter.filter(complexBuffer); -// ComplexBuffer symbolFiltered = complexBuffer; - ComplexBuffer symbolFiltered = basebandFiltered; - mQPSKDemodulator.receive(symbolFiltered); + ComplexBuffer filtered = filter(complexBuffer); + mQPSKDemodulator.receive(filtered); + } - if(mFilteredSymbolListener != null) - { - mFilteredSymbolListener.receive(symbolFiltered); - } + protected ComplexBuffer filter(ComplexBuffer complexBuffer) + { + return mBasebandFilter.filter(complexBuffer); } private double getSampleRate() @@ -170,11 +106,11 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen { FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() .sampleRate((int)getSampleRate()) - .passBandCutoff(6000) + .passBandCutoff(5100) .passBandAmplitude(1.0) .passBandRipple(0.01) .stopBandAmplitude(0.0) - .stopBandStart(6250) + .stopBandStart(6500) .stopBandRipple(0.01) .build(); @@ -200,20 +136,6 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen return filter; } - private float[] getSymbolFilter() - { - float[] filter = mSymbolFilters.get(getSampleRate()); - - if(filter == null) - { - float samplesPerSymbol = (float)(getSampleRate() / 4800.0); - filter = FilterFactory.getRootRaisedCosine(samplesPerSymbol, 40, 0.2f); - mSymbolFilters.put(getSampleRate(), filter); - } - - return filter; - } - public void dispose() { super.dispose(); @@ -221,15 +143,6 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mBasebandFilter.dispose(); mBasebandFilter = null; - mStreamConverter.dispose(); - mStreamConverter = null; - - mAGC.dispose(); - mAGC = null; - - mQPSKSlicer.dispose(); - mQPSKSlicer = null; - mMessageFramer.dispose(); mMessageFramer = null; } @@ -252,7 +165,15 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen @Override public void receive(FrequencyChangeEvent frequencyChangeEvent) { - //Ignored + switch(frequencyChangeEvent.getEvent()) + { + case NOTIFICATION_FREQUENCY_CHANGE: + case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: + case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE: + case NOTIFICATION_SAMPLE_RATE_CHANGE: + mCostasLoop.reset(); + break; + } } }; } @@ -271,20 +192,16 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen @Override public void reset() { - // TODO Auto-generated method stub - + mCostasLoop.reset(); } @Override public void start(ScheduledExecutorService executor) { - // TODO Auto-generated method stub - } @Override public void stop() { - // TODO Auto-generated method stub } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java new file mode 100644 index 00000000..e5b93aa2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder2Instrumented.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * sdr-trunk + * Copyright (C) 2014-2018 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 io.github.dsheirer.module.decode.p25; + +import io.github.dsheirer.alias.AliasList; +import io.github.dsheirer.dsp.psk.DQPSKDemodulatorInstrumented; +import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented; +import io.github.dsheirer.dsp.psk.SymbolDecisionData; +import io.github.dsheirer.sample.Listener; +import io.github.dsheirer.sample.complex.Complex; +import io.github.dsheirer.sample.complex.ComplexBuffer; + +public class P25_C4FMDecoder2Instrumented extends P25_C4FMDecoder2 +{ + private Listener mPLLPhaseErrorListener; + private Listener mPLLFrequencyListener; + private Listener mSamplesPerSymbolListener; + private Listener mSymbolListener; + private Listener mFilteredSymbolListener; + private Listener mSymbolDecisionDataListener; + + public P25_C4FMDecoder2Instrumented(AliasList aliasList) + { + super(aliasList); + } + + /** + * Overrides the filter method so that we can capture the filtered samples for instrumentation + */ + protected ComplexBuffer filter(ComplexBuffer complexBuffer) + { + ComplexBuffer filtered = super.filter(complexBuffer); + + if(mFilteredSymbolListener != null) + { + mFilteredSymbolListener.receive(filtered); + } + + return filtered; + } + + /** + * Overrides this method so we can correctly configure for instrumented operations + */ + public void setSampleRate(double sampleRate) + { + super.setSampleRate(sampleRate); + InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); + mInterpolatingSampleBuffer = instrumentedBuffer; + + DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, + mSymbolPhaseErrorCalculator, mSymbolDecoder, instrumentedBuffer, mSampleRate); + mQPSKDemodulator = instrumented; + + instrumented.setSymbolListener(mSymbolListener); + instrumented.setPLLErrorListener(mPLLPhaseErrorListener); + instrumented.setPLLFrequencyListener(mPLLFrequencyListener); + instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener); + instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener); + } + + public void setSymbolListener(Listener listener) + { + mSymbolListener = listener; + ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolListener(listener); + } + + public void setPLLPhaseErrorListener(Listener listener) + { + mPLLPhaseErrorListener = listener; + ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener); + } + + public void setPLLFrequencyListener(Listener listener) + { + mPLLFrequencyListener = listener; + ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener); + } + + public void setFilteredBufferListener(Listener listener) + { + mFilteredSymbolListener = listener; + } + + public void setSymbolDecisionDataListener(Listener listener) + { + mSymbolDecisionDataListener = listener; + ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener); + } + + public void setSamplesPerSymbolListener(Listener listener) + { + mSamplesPerSymbolListener = listener; + ((DQPSKDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java b/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java index 2f8f1988..e7ed4a93 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/message/tsbk/osp/control/SyncBroadcast.java @@ -78,7 +78,7 @@ public class SyncBroadcast extends TSBKMessage if( isMicroslotsLockedToMinuteRollover() ) { - sb.append( " MICROSLOT-MINUTE ROLLOVER:LOCKED" ); + sb.append( " MICROSLOT-MINUTE ROLLOVER:SLOW" ); } else {