From a0630102e661c25be42bbfbf98c583ccfeee6675 Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 25 Feb 2018 05:04:20 -0500 Subject: [PATCH] 330 WIP - P25 C4FM decoder fully functional using DQPSK decoder and new symbol timing error detector. --- .../dsheirer/dsp/filter/FilterFactory.java | 14 +- .../dsheirer/dsp/psk/DQPSKDemodulator.java | 259 ++++++++++++++++++ .../psk/DQPSKSymbolPhaseErrorCalculator.java | 17 +- .../dsheirer/dsp/psk/EarlyLateDetector.java | 76 +++++ .../dsheirer/dsp/psk/EarlyLateDetector2.java | 40 +++ .../dsheirer/dsp/psk/GardnerDetector.java | 4 +- .../dsp/psk/InterpolatingSymbolBuffer.java | 13 + .../dsp/psk/InterpolatingSymbolBuffer2.java | 245 +++++++++++++++++ .../dsheirer/dsp/psk/QPSKDemodulator.java | 24 +- .../dsheirer/dsp/psk/QPSKStarSlicer.java | 105 ++++--- .../dsheirer/dsp/psk/SymbolDecisionData2.java | 6 + .../dsheirer/dsp/psk/pll/CostasLoop.java | 3 +- .../gui/viewer/DemodulatorViewerFX.java | 6 +- .../viewer/chart/ComplexSampleLineChart.java | 3 +- ...ncyLineChart.java => DoubleLineChart.java} | 11 +- .../gui/viewer/decoder/P25Phase1Pane.java | 40 ++- .../module/decode/p25/P25_C4FMDecoder2.java | 43 ++- 17 files changed, 811 insertions(+), 98 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java create mode 100644 src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java rename src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/{FrequencyLineChart.java => DoubleLineChart.java} (82%) diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index 01706df0..074448f4 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -666,11 +666,11 @@ public class FilterFactory * @param alpha - roll-off factor * @return - filter coefficients */ - public static float[] getRootRaisedCosine(int samplesPerSymbol, + public static float[] getRootRaisedCosine(float samplesPerSymbol, int symbols, float alpha) { - int taps = samplesPerSymbol * symbols + 1; + int taps = (int)(samplesPerSymbol * (float)symbols + 1.0f); float scale = 0; @@ -680,8 +680,8 @@ public class FilterFactory { float index = (float) x - ((float) taps / 2.0f); - float x1 = (float) Math.PI * index / (float) samplesPerSymbol; - float x2 = 4.0f * alpha * index / (float) samplesPerSymbol; + float x1 = (float) Math.PI * index / samplesPerSymbol; + float x2 = 4.0f * alpha * index / samplesPerSymbol; float x3 = x2 * x2 - 1.0f; float numerator, denominator; @@ -692,7 +692,7 @@ public class FilterFactory { numerator = (float) Math.cos((1.0 + alpha) * x1) + (float) Math.sin((1.0f - alpha) * x1) / - (4.0f * alpha * index / (float) samplesPerSymbol); + (4.0f * alpha * index / samplesPerSymbol); } else { @@ -713,9 +713,9 @@ public class FilterFactory x3 = (1.0f - alpha) * x1; x2 = (1.0f + alpha) * x1; - numerator = (float) (Math.sin(x2) * (1.0f + alpha) * Math.PI - + numerator = (float) (Math.sin(x2) * (1.0 + alpha) * Math.PI - Math.cos(x3) * ((1.0 - alpha) * Math.PI * - (double) samplesPerSymbol) / (4 * alpha * index) + + (double) samplesPerSymbol) / (4.0 * alpha * index) + Math.sin(x3) * (double) samplesPerSymbol * (double) samplesPerSymbol / (4.0 * alpha * index * index)); diff --git a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java new file mode 100644 index 00000000..4d4374a6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java @@ -0,0 +1,259 @@ +/******************************************************************************* + * 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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DQPSKDemodulator implements ComplexSampleListener +{ + private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class); + private InterpolatingSymbolBuffer2 mInterpolatingSymbolBuffer; + private IPhaseLockedLoop mPLL; + 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 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 mPrecedingSymbol = new Complex(0, 0); + private Complex mCurrentSymbol = new Complex(0, 0); + private Complex mFollowingSymbol = 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 DQPSKDemodulator(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; + mDQPSKSymbolDecoder = symbolDecoder; + + float samplesPerSymbol = (float)(sampleRate / symbolRate); + mInterpolatingSymbolBuffer = new InterpolatingSymbolBuffer2(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 + mReceivedSample.setValues(inphase, quadrature); + + //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); + + //Calculate the symbol once we've stored enough samples + if(mInterpolatingSymbolBuffer.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 + } + } + + /** + * 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/DQPSKSymbolPhaseErrorCalculator.java b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java index cf5dd9c6..fce780e2 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/DQPSKSymbolPhaseErrorCalculator.java @@ -19,21 +19,18 @@ import io.github.dsheirer.sample.complex.Complex; public class DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalculator { - //Negative 45 degrees rotation to offset for differential additive 45 degrees of rotation + //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; + + public DQPSKSymbolPhaseErrorCalculator() + { + } + @Override public void adjust(Complex symbol) { symbol.multiply(DIFFERENTIAL_OFFSET); } - - @Override - public float getPhaseError(Complex symbol) - { - Complex copy = symbol.copy(); - - copy.multiply(DIFFERENTIAL_OFFSET); - return super.getPhaseError(copy); - } } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java new file mode 100644 index 00000000..d3a593b6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * 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/EarlyLateDetector2.java b/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java new file mode 100644 index 00000000..16ea9bce --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/EarlyLateDetector2.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * 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 EarlyLateDetector2 +{ + //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); + + public float getError(Complex symbol) + { + mSymbol.setValues(symbol); + mSymbol.multiply(POSITIVE_OFFSET); + + if(Math.abs(mSymbol.quadrature()) > Math.abs(mSymbol.inphase())) + { + return mSymbol.inphase(); + } + else + { + return mSymbol.quadrature(); + } + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java b/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java index 023b3e87..9b338e97 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/GardnerDetector.java @@ -41,8 +41,8 @@ public class GardnerDetector */ public float getError(Complex previous, Complex middle, Complex current) { - mErrorInphase = (previous.inphase() - current.inphase()) * middle.inphase(); - mErrorQuadrature = (previous.quadrature() - current.quadrature()) * middle.quadrature(); + mErrorInphase = (Math.abs(previous.inphase()) - Math.abs(current.inphase())) * Math.abs(middle.inphase()); + mErrorQuadrature = (Math.abs(previous.quadrature()) - Math.abs(current.quadrature())) * Math.abs(middle.quadrature()); mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f); return mError; diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java index fa050e5e..cccba75d 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer.java @@ -17,9 +17,13 @@ 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); @@ -88,10 +92,19 @@ public class InterpolatingSymbolBuffer 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 */ diff --git a/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java new file mode 100644 index 00000000..ba8d731a --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/psk/InterpolatingSymbolBuffer2.java @@ -0,0 +1,245 @@ +/******************************************************************************* + * 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 InterpolatingSymbolBuffer2 +{ + 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 Complex mCurrentSample = new Complex(0,0); + private Complex mFollowingSample = new Complex(0,0); + + private float[] mDelayLineInphase; + private float[] mDelayLineQuadrature; + 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 int mDelayLinePointer = 0; + private int mTwiceSamplesPerSymbol; + private RealInterpolator mInterpolator = new RealInterpolator(1.0f); + private SymbolDecisionData2 mSymbolDecisionData2; + + public InterpolatingSymbolBuffer2(float samplesPerSymbol) + { + mSamplingPoint = samplesPerSymbol; + mDetectedSamplesPerSymbol = samplesPerSymbol; + mMaximumSamplesPerSymbol = samplesPerSymbol * (1.0f + MAXIMUM_SAMPLES_PER_SYMBOL_DEVIATION); + mMinimumSamplesPerSymbol = samplesPerSymbol * (1.0f - MAXIMUM_SAMPLES_PER_SYMBOL_DEVIATION); + 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) + { + 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 + (symbolTimingError * mDetectedSamplesPerSymbolGain); + mDetectedSamplesPerSymbol = constrain(mDetectedSamplesPerSymbol, mMinimumSamplesPerSymbol, mMaximumSamplesPerSymbol); + + 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))); +// increaseSampleCounter(mDetectedSamplesPerSymbol); + } + + /** + * 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; + } + + /** + * Samples that precedes the current sample (uninterpolated). + */ + public Complex getPreceedingSample() + { + mPreceedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]); + + return mPreceedingSample; + } + + /** + * 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 getCurrentSample() + { + /* 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 + * @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; + } + + /** + * 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/QPSKDemodulator.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java index bb06e223..9bcda428 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKDemodulator.java @@ -33,6 +33,7 @@ public class QPSKDemodulator implements ComplexSampleListener private Listener mSymbolListener; private Listener mPLLErrorListener; private Listener mPLLFrequencyListener; + private Listener mSamplesPerSymbolListener; private Listener mDibitListener; private Listener mSymbolDecisionDataListener; @@ -136,26 +137,29 @@ public class QPSKDemodulator implements ComplexSampleListener mSymbolListener.receive(mCurrentSymbol); } - //We rotate the symbol from star pattern to polar pattern to facilitate phase error calculation -// mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol); -// mSymbolPhaseErrorCalculator.adjust(mMiddleSymbol); - //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(-mPhaseError); + mPLL.adjust(0.0); if(mPLLErrorListener != null) { @@ -213,6 +217,14 @@ public class QPSKDemodulator implements ComplexSampleListener 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/QPSKStarSlicer.java b/src/main/java/io/github/dsheirer/dsp/psk/QPSKStarSlicer.java index f6b23861..b8295145 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/QPSKStarSlicer.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/QPSKStarSlicer.java @@ -1,3 +1,18 @@ +/******************************************************************************* + * 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.symbol.Dibit; @@ -5,54 +20,54 @@ import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.complex.Complex; -public class QPSKStarSlicer implements Listener +public class QPSKStarSlicer implements IQPSKSymbolDecoder, Listener { - private Broadcaster mBroadcaster = new Broadcaster(); + private Broadcaster mBroadcaster = new Broadcaster(); - /** - * Slices a ComplexSample representing a phase shifted symbol according to - * the following constellation pattern: - * - * 00 | 01 - * ---|--- - * 10 | 11 - */ - public QPSKStarSlicer() - { - } - - public void dispose() - { - mBroadcaster.dispose(); - mBroadcaster = null; - } + /** + * Slices a Complex sample representing a phase shifted symbol according to the following constellation pattern: + * + * 00 +3 | 01 +1 + * ------|------ + * 10 -1 | 11 -1 + */ + public QPSKStarSlicer() + { + } - public void addListener( Listener listener ) - { - mBroadcaster.addListener( listener ); - } + public void dispose() + { + mBroadcaster.dispose(); + mBroadcaster = null; + } - public void removeListener( Listener listener ) - { - mBroadcaster.removeListener( listener ); - } + public void addListener(Listener listener) + { + mBroadcaster.addListener(listener); + } + + public void removeListener(Listener listener) + { + mBroadcaster.removeListener(listener); + } + + @Override + public void receive(Complex sample) + { + mBroadcaster.broadcast(decode(sample)); + } + + @Override + public Dibit decode(Complex sample) + { + if(sample.inphase() > 0) + { + return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1; + } + else + { + return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3; + } + } - @Override - public void receive( Complex sample ) - { - mBroadcaster.broadcast( decide( sample ) ); - } - - public static Dibit decide( Complex sample ) - { - if( sample.inphase() > 0 ) - { - return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1; - } - else - { - return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3; - } - } - } diff --git a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java index e8bde425..64d9ebe9 100644 --- a/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java +++ b/src/main/java/io/github/dsheirer/dsp/psk/SymbolDecisionData2.java @@ -20,6 +20,9 @@ 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; @@ -60,6 +63,9 @@ public class SymbolDecisionData2 { 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; } 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 76aee9a7..187e1c0c 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 @@ -41,7 +41,8 @@ public class CostasLoop implements IPhaseLockedLoop private double mAlphaGain; private double mBetaGain; private double mLoopPhase = 0.0; - private double mLoopFrequency = 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); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java index 0046606f..5e9745a6 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/DemodulatorViewerFX.java @@ -29,6 +29,8 @@ import javafx.scene.control.SeparatorMenuItem; import javafx.scene.layout.BorderPane; import javafx.stage.FileChooser; import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; @@ -37,6 +39,8 @@ import java.io.File; */ public class DemodulatorViewerFX extends Application { + private final static Logger mLog = LoggerFactory.getLogger(DemodulatorViewerFX.class); + private Stage mStage; private MenuBar mMenuBar; private RecentFilesMenu mRecentFilesMenu; @@ -52,7 +56,7 @@ public class DemodulatorViewerFX extends Application borderPane.setTop(getMenuBar()); borderPane.setCenter(getViewerDesktop()); - Scene scene = new Scene(borderPane, 1200, 800); + Scene scene = new Scene(borderPane, 1500, 900); mStage.setScene(scene); mStage.show(); diff --git a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java index e0dbae54..2bf0fe8c 100644 --- a/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java +++ b/src/main/java/io/github/dsheirer/instrument/gui/viewer/chart/ComplexSampleLineChart.java @@ -101,7 +101,8 @@ public class ComplexSampleLineChart extends LineChart implements Listener +public class DoubleLineChart extends LineChart implements Listener { - private final static Logger mLog = LoggerFactory.getLogger(FrequencyLineChart.class); + private final static Logger mLog = LoggerFactory.getLogger(DoubleLineChart.class); private DoubleCircularBuffer mDoubleCircularBuffer; private ObservableList> mPhaseValues = FXCollections.observableArrayList(); - public FrequencyLineChart(double maxFrequency, int length) + public DoubleLineChart(String seriesLabel, double min, double max, double tick, int length) { - super(new NumberAxis("Time", 1, length, 2), - new NumberAxis("Frequency", -maxFrequency, maxFrequency, maxFrequency / 5.0)); + super(new NumberAxis(1, length, 2), new NumberAxis(min, max, tick)); - Series phaseSeries = new Series<>("PLL Frequency", mPhaseValues); + Series phaseSeries = new Series<>(seriesLabel, mPhaseValues); ObservableList> observableList = FXCollections.observableArrayList(phaseSeries); setData(observableList); 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 cb9eb617..2b2800be 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 @@ -16,12 +16,14 @@ package io.github.dsheirer.instrument.gui.viewer.decoder; import io.github.dsheirer.instrument.gui.viewer.chart.ComplexSampleLineChart; +import io.github.dsheirer.instrument.gui.viewer.chart.DoubleLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.EyeDiagramChart; -import io.github.dsheirer.instrument.gui.viewer.chart.FrequencyLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.PhaseLineChart; import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder2; +import io.github.dsheirer.sample.Broadcaster; +import io.github.dsheirer.sample.complex.ComplexBuffer; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import org.slf4j.Logger; @@ -37,7 +39,9 @@ public class P25Phase1Pane extends DecoderPane private HBox mDecoderChartBox; private SymbolChart mSymbolChart; private PhaseLineChart mPLLPhaseErrorLineChart; - private FrequencyLineChart mPLLFrequencyLineChart; + private DoubleLineChart mPLLFrequencyLineChart; + private DoubleLineChart mSamplesPerSymbolLineChart; + private Broadcaster mFilteredBufferBroadcaster = new Broadcaster<>(); private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null); public P25Phase1Pane() @@ -48,13 +52,16 @@ public class P25Phase1Pane extends DecoderPane private void init() { - addListener(getSampleLineChart()); +// addListener(getSampleLineChart()); addListener(getDecoder()); + getDecoder().setDEBUGFilteredBufferListener(mFilteredBufferBroadcaster); getDecoder().setDEBUGSymbolListener(getSymbolChart()); getDecoder().setDEBUGPLLPhaseErrorListener(getPLLPhaseErrorLineChart()); getDecoder().setDEBUGPLLFrequencyListener(getPLLFrequencyLineChart()); getDecoder().setDEBUGSymbolDecisionDataListener(getEyeDiagramChart()); + getDecoder().setDEBUGSamplesPerSymbolListener(getSamplesPerSymbolLineChart()); + mFilteredBufferBroadcaster.addListener(getSampleLineChart()); HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS); HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS); @@ -67,7 +74,10 @@ public class P25Phase1Pane extends DecoderPane mLog.debug("Configuring for sample rate: " + sampleRate); mDecoder.setSampleRate(sampleRate); - getSampleLineChart().setSamplesPerSymbol((int)(sampleRate / 4800)); + 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() @@ -97,7 +107,8 @@ public class P25Phase1Pane extends DecoderPane HBox.setHgrow(getSymbolChart(), Priority.ALWAYS); HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS); HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS); - mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(), getPLLFrequencyLineChart()); + mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(), + getPLLFrequencyLineChart()); } return mDecoderChartBox; @@ -111,9 +122,12 @@ public class P25Phase1Pane extends DecoderPane mSampleChartBox.setMaxHeight(Double.MAX_VALUE); getSampleLineChart().setMaxWidth(Double.MAX_VALUE); getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE); + getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS); HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS); - mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart()); + HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS); + mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(), + getSamplesPerSymbolLineChart()); } return mSampleChartBox; @@ -149,13 +163,23 @@ public class P25Phase1Pane extends DecoderPane return mPLLPhaseErrorLineChart; } - private FrequencyLineChart getPLLFrequencyLineChart() + private DoubleLineChart getPLLFrequencyLineChart() { if(mPLLFrequencyLineChart == null) { - mPLLFrequencyLineChart = new FrequencyLineChart(2400, 40); + mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -200, 200, 20, 40); } return mPLLFrequencyLineChart; } + + private DoubleLineChart getSamplesPerSymbolLineChart() + { + if(mSamplesPerSymbolLineChart == null) + { + mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.0, 11.0, 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 b5ccec57..706d6d9b 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,10 +21,11 @@ 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.QPSKDemodulator; +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; @@ -53,24 +54,26 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen private Map mSymbolFilters = new HashMap<>(); private ComplexFIRFilter mSymbolFilter; - private QPSKDemodulator mQPSKDemodulator; + 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 QPSKSymbolDecoder(); + 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(48000.0); + setSampleRate(48600.0); mStreamConverter.setListener(mAGC); } @@ -90,12 +93,13 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen mCostasLoop.setTracking(Tracking.FINE); mCostasLoop.setAutomaticTracking(false); - mQPSKDemodulator = new QPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, + mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder, mSampleRate, SYMBOL_RATE); mQPSKDemodulator.setSymbolListener(mDEBUGSymbolListener); mQPSKDemodulator.setPLLErrorListener(mDEBUGPLLPhaseErrorListener); mQPSKDemodulator.setPLLFrequencyListener(mDEBUGPLLFrequencyListener); mQPSKDemodulator.setSymbolDecisionDataListener(mDEBUGSymbolDecisionDataListener); + mQPSKDemodulator.setSamplesPerSymbolListener(mDEBUGSamplesPerSymbolListener); //Message framer can trigger a symbol-inversion correction to the PLL when detected mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop); @@ -121,20 +125,36 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen 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(basebandFiltered); -// ComplexBuffer amplified = mAGC.filter(symbolFiltered); +// ComplexBuffer symbolFiltered = mSymbolFilter.filter(complexBuffer); +// ComplexBuffer symbolFiltered = complexBuffer; + ComplexBuffer symbolFiltered = basebandFiltered; mQPSKDemodulator.receive(symbolFiltered); -// mQPSKDemodulator.receive(complexBuffer); + + if(mFilteredSymbolListener != null) + { + mFilteredSymbolListener.receive(symbolFiltered); + } } private double getSampleRate() @@ -150,11 +170,11 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen { FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder() .sampleRate((int)getSampleRate()) - .passBandCutoff(2880) + .passBandCutoff(6000) .passBandAmplitude(1.0) .passBandRipple(0.01) .stopBandAmplitude(0.0) - .stopBandStart(3600) + .stopBandStart(6250) .stopBandRipple(0.01) .build(); @@ -186,7 +206,8 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen if(filter == null) { - filter = FilterFactory.getRootRaisedCosine(10, 10, 0.2f); + float samplesPerSymbol = (float)(getSampleRate() / 4800.0); + filter = FilterFactory.getRootRaisedCosine(samplesPerSymbol, 40, 0.2f); mSymbolFilters.put(getSampleRate(), filter); }