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 2dfd2e21..f8b6aabd 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java @@ -29,11 +29,7 @@ public class DQPSKDemodulator implements ComplexSampleListener private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class); protected InterpolatingSampleBuffer mInterpolatingSampleBuffer; protected IPhaseLockedLoop mPLL; - protected IQPSKSymbolDecoder mSymbolDecoder; - private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator; - private DQPSKTimingErrorDetector mSymbolTimingErrorDetector = new DQPSKTimingErrorDetector(); - private DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator(); - + protected DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator(); private Complex mReceivedSample = new Complex(0, 0); private Complex mPreviousPrecedingSample = new Complex(0, 0); private Complex mPreviousCurrentSample = new Complex(0, 0); @@ -41,9 +37,6 @@ public class DQPSKDemodulator implements ComplexSampleListener private Complex mCurrentSample = new Complex(0, 0); private Complex mPrecedingSymbol = new Complex(0, 0); protected Complex mCurrentSymbol = new Complex(0, 0); - - protected float mPhaseError; - private float mSymbolTimingError; private Listener mDibitListener; /** @@ -54,15 +47,11 @@ public class DQPSKDemodulator implements ComplexSampleListener * 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 to hold samples for interpolating a symbol */ - public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, ISymbolPhaseErrorCalculator symbolPhaseErrorCalculator, - IQPSKSymbolDecoder symbolDecoder, InterpolatingSampleBuffer interpolatingSampleBuffer) + public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer) { mPLL = phaseLockedLoop; - mSymbolPhaseErrorCalculator = symbolPhaseErrorCalculator; - mSymbolDecoder = symbolDecoder; mInterpolatingSampleBuffer = interpolatingSampleBuffer; } @@ -128,13 +117,9 @@ public class DQPSKDemodulator implements ComplexSampleListener mPrecedingSymbol.normalize(); mCurrentSymbol.normalize(); - mSymbolEvaluator.setSymbol(mCurrentSymbol); + mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol); - //Symbol timing error calculation - mSymbolTimingError = mSymbolTimingErrorDetector.getError(mPrecedingSymbol, mCurrentSymbol); - - mSymbolTimingError = clip(mSymbolTimingError, 0.5f); - mInterpolatingSampleBuffer.resetAndAdjust(mSymbolTimingError); + mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError()); //Store current samples/symbols to use for the next period mPreviousPrecedingSample.setValues(mPrecedingSample); @@ -142,15 +127,11 @@ public class DQPSKDemodulator implements ComplexSampleListener //Calculate the phase error of the current symbol relative to the expected constellation and provide //feedback to the PLL - mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol); - - mLog.debug("Phase error - ORIG:" + mPhaseError + " NEW:" + mSymbolEvaluator.getPhaseError()); - - mPLL.adjust(mPhaseError); + mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f)); if(mDibitListener != null) { - mDibitListener.receive(mSymbolDecoder.decode(mCurrentSymbol)); + mDibitListener.receive(mSymbolEvaluator.getSymbolDecision()); } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java index 3ca084d0..cc90a742 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulatorInstrumented.java @@ -37,17 +37,14 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator * 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, + public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBufferInstrumented interpolatingSampleBuffer, double sampleRate) { - super(phaseLockedLoop, symbolPhaseErrorCalculator, symbolDecoder, interpolatingSampleBuffer); + super(phaseLockedLoop, interpolatingSampleBuffer); mSampleRate = sampleRate; } @@ -78,7 +75,7 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator if(mPLLErrorListener != null) { - mPLLErrorListener.receive((double)mPhaseError); + mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError()); } if(mPLLFrequencyListener != null) diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java index 3ae2d214..16b9018d 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolEvaluator.java @@ -25,74 +25,86 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0); private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * Math.PI / 4.0); - private Complex mSymbolError = new Complex(0,0); + private Complex mSymbolError = new Complex(0, 0); private float mPhaseError = 0.0f; private float mTimingError = 0.0f; - private float mTimingErrorSign = 1.0f; + private float mTimingErrorPolarity = 1.0f; private Dibit mSymbolDecision = Dibit.D00_PLUS_1; + /** + * Data-directed symbol phase and timing error detector and symbol decision slicer. + * + * Symbol decision is based on the closest reference quadrant for the sampled symbol. + * + * Phase and timing error are calculated as the angular distance of the sampled symbol from the reference + * symbol. Timing error is further corrected to compensate for clock-wise or counter-clock-wise vector rotation + * relative to the immediately preceding symbol. + */ public DQPSKSymbolEvaluator() { } /** - * Sets the symbol to be evaluated and calculates the symbol decision and phase and timing errors of the symbol - * relative to the four QPSK reference symbol locations. After invoking this method, you can access the phase and - * timing errors and the symbol decision via their respective accessor methods. + * Sets the preceding and current symbols to be evaluated for phase and timing errors and to determine the + * transmitted symbol relative to the closest reference symbol. After invoking this method, you can access the + * phase and timing errors and the symbol decision via their respective accessor methods. * - * Phase and timing error values are calculated by determining the symbol's quadrant and multiplying the symbol - * by the complex conjugate of the reference symbol for that quadrant and then deriving the radian angle error - * value. Both phase and timing error use this angle error, however the timing error is corrected with the - * appropriate sign so that the error value indicates the correct error direction. + * Phase and timing error values are calculated by first determining the symbol and then calculating the phase + * and timing errors relative to the reference symbol. The timing error is corrected with the appropriate sign + * relative to the angular vector rotation so that the error value indicates the correct error direction. * - * @param complex symbol to be evaluated + * @param preceding differential-decoded sample/symbol used to detect rotation - does NOT need to be interpolated + * since it's only a reference + * @param current symbol to be evaluated - interpolated */ @Override - public void setSymbol(Complex complex) + public void setSymbol(Complex preceding, Complex current) { - mSymbolError.setValues(complex); + mSymbolError.setValues(current); if(mSymbolError.quadrature() > 0.0f) { if(mSymbolError.inphase() > 0.0f) { mSymbolDecision = Dibit.D00_PLUS_1; + mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f); mSymbolError.multiply(ROTATE_FROM_PLUS_45); } else { mSymbolDecision = Dibit.D01_PLUS_3; + mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f); mSymbolError.multiply(ROTATE_FROM_PLUS_135); } - mTimingErrorSign = 1.0f; } else { if(mSymbolError.inphase() > 0.0f) { mSymbolDecision = Dibit.D10_MINUS_1; + mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f); mSymbolError.multiply(ROTATE_FROM_MINUS_45); } else { mSymbolDecision = Dibit.D11_MINUS_3; + mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f); mSymbolError.multiply(ROTATE_FROM_MINUS_135); } - - mTimingErrorSign = -1.0f; } //Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the //arctan of the error angle relative to 0 radians and this provides our error value mPhaseError = -mSymbolError.quadrature(); - //Timing error is the same as phase error with the sign corrected according to the symbol's hemisphere - mTimingError = mPhaseError * mTimingErrorSign; + //Timing error is the same as phase error with the sign corrected according to the vector's angular rotation + mTimingError = mSymbolError.quadrature() * mTimingErrorPolarity; } /** * Phase error of the symbol relative to the nearest reference symbol. + * * @return phase error in radians of distance from the reference symbol. */ @Override diff --git a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java b/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java index da650c53..d0213a0d 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/IDQPSKSymbolEvaluator.java @@ -23,9 +23,11 @@ public interface IDQPSKSymbolEvaluator { /** * Sets the sampled symbol to be evaluated. Note: the symbol's gain value MUST be normalized to the unit circle. - * @param complex symbol + * + * @param preceding symbol used to detect (counter-)clock-wise rotation + * @param symbol currently being sampled */ - void setSymbol(Complex complex); + void setSymbol(Complex preceding, Complex symbol); /** * Phase error of the symbol relative to the closest reference symbol diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java index bc3cf830..15adbd9a 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSampleBuffer.java @@ -31,7 +31,7 @@ public class InterpolatingSampleBuffer private int mTwiceSamplesPerSymbol; private float mSamplingPoint; - private float mSampleCounterGain = 0.05f; + private float mSampleCounterGain = 1.0f; private float mDetectedSamplesPerSymbol; private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain; private float mMaximumSamplesPerSymbol; @@ -130,7 +130,7 @@ public class InterpolatingSampleBuffer */ public Complex getPrecedingSample() { - mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]); + mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 3]); return mPrecedingSample; } @@ -152,7 +152,7 @@ public class InterpolatingSampleBuffer */ public float getInphase(float interpolation) { - return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation); + return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation); } /** @@ -162,6 +162,6 @@ public class InterpolatingSampleBuffer */ public float getQuadrature(float interpolation) { - return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation); + return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation); } } diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java index 4bcee01e..42d9e813 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/decoder/P25Phase1Pane.java @@ -175,7 +175,7 @@ public class P25Phase1Pane extends DecoderPane { if(mSamplesPerSymbolLineChart == null) { - mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.0, 11.0, 0.1, 40); + mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.5, 11.5, 0.1, 40); } return mSamplesPerSymbolLineChart; 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 8d938239..7ae55d67 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 @@ -21,12 +21,8 @@ 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.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.pll.CostasLoop; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.ComplexBuffer; @@ -43,8 +39,6 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen { private final static Logger mLog = LoggerFactory.getLogger(P25_C4FMDecoder2.class); 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; @@ -72,8 +66,7 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mCostasLoop = new CostasLoop(mSampleRate, SYMBOL_RATE); mInterpolatingSampleBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); - mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, - mInterpolatingSampleBuffer); + mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mInterpolatingSampleBuffer); //Message framer can trigger a symbol-inversion correction to the PLL when detected mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); @@ -91,6 +84,7 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen protected ComplexBuffer filter(ComplexBuffer complexBuffer) { return mBasebandFilter.filter(complexBuffer); +// return complexBuffer; } private double getSampleRate() 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 index e5b93aa2..48b28e15 100644 --- 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 @@ -61,8 +61,7 @@ public class P25_C4FMDecoder2Instrumented extends P25_C4FMDecoder2 InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented((float)(sampleRate / SYMBOL_RATE)); mInterpolatingSampleBuffer = instrumentedBuffer; - DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, - mSymbolPhaseErrorCalculator, mSymbolDecoder, instrumentedBuffer, mSampleRate); + DQPSKDemodulatorInstrumented instrumented = new DQPSKDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, mSampleRate); mQPSKDemodulator = instrumented; instrumented.setSymbolListener(mSymbolListener); diff --git a/src/main/java/io/github/dsheirer/sample/complex/Complex.java b/src/main/java/io/github/dsheirer/sample/complex/Complex.java index 8b98e9e8..93c15627 100644 --- a/src/main/java/io/github/dsheirer/sample/complex/Complex.java +++ b/src/main/java/io/github/dsheirer/sample/complex/Complex.java @@ -437,17 +437,14 @@ public class Complex implements Serializable public static void main(String[] args) { - double angle = Math.PI; //180 degrees + Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0); + Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0); + mLog.debug("REF: " + ROTATE_FROM_MINUS_45.toString()); - Complex s1 = new Complex((float)Math.sin(angle), (float)Math.cos(angle)); - - Complex tap = new Complex(1.0f, -0.1f); - - Complex convolved = Complex.multiply(s1, tap); - - mLog.debug("s: " + s1.toString() + - " t: " + tap.toString() + - " convolved: " + convolved.toString()); + Complex symbol = new Complex(0.7665009f, -0.6422432f); + mLog.debug("SYM: " + symbol.toString()); + symbol.multiply(ROTATE_FROM_MINUS_45); + mLog.debug("DER: " + symbol.toString()); } }