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);
}