mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-14 09:37:36 -05:00
330 WIP - P25 C4FM decoder fully functional using DQPSK decoder and
new symbol timing error detector.
This commit is contained in:
@@ -666,11 +666,11 @@ public class FilterFactory
|
|||||||
* @param alpha - roll-off factor
|
* @param alpha - roll-off factor
|
||||||
* @return - filter coefficients
|
* @return - filter coefficients
|
||||||
*/
|
*/
|
||||||
public static float[] getRootRaisedCosine(int samplesPerSymbol,
|
public static float[] getRootRaisedCosine(float samplesPerSymbol,
|
||||||
int symbols,
|
int symbols,
|
||||||
float alpha)
|
float alpha)
|
||||||
{
|
{
|
||||||
int taps = samplesPerSymbol * symbols + 1;
|
int taps = (int)(samplesPerSymbol * (float)symbols + 1.0f);
|
||||||
|
|
||||||
float scale = 0;
|
float scale = 0;
|
||||||
|
|
||||||
@@ -680,8 +680,8 @@ public class FilterFactory
|
|||||||
{
|
{
|
||||||
float index = (float) x - ((float) taps / 2.0f);
|
float index = (float) x - ((float) taps / 2.0f);
|
||||||
|
|
||||||
float x1 = (float) Math.PI * index / (float) samplesPerSymbol;
|
float x1 = (float) Math.PI * index / samplesPerSymbol;
|
||||||
float x2 = 4.0f * alpha * index / (float) samplesPerSymbol;
|
float x2 = 4.0f * alpha * index / samplesPerSymbol;
|
||||||
float x3 = x2 * x2 - 1.0f;
|
float x3 = x2 * x2 - 1.0f;
|
||||||
|
|
||||||
float numerator, denominator;
|
float numerator, denominator;
|
||||||
@@ -692,7 +692,7 @@ public class FilterFactory
|
|||||||
{
|
{
|
||||||
numerator = (float) Math.cos((1.0 + alpha) * x1) +
|
numerator = (float) Math.cos((1.0 + alpha) * x1) +
|
||||||
(float) Math.sin((1.0f - alpha) * x1) /
|
(float) Math.sin((1.0f - alpha) * x1) /
|
||||||
(4.0f * alpha * index / (float) samplesPerSymbol);
|
(4.0f * alpha * index / samplesPerSymbol);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -713,9 +713,9 @@ public class FilterFactory
|
|||||||
x3 = (1.0f - alpha) * x1;
|
x3 = (1.0f - alpha) * x1;
|
||||||
x2 = (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 *
|
Math.cos(x3) * ((1.0 - alpha) * Math.PI *
|
||||||
(double) samplesPerSymbol) / (4 * alpha * index) +
|
(double) samplesPerSymbol) / (4.0 * alpha * index) +
|
||||||
Math.sin(x3) * (double) samplesPerSymbol *
|
Math.sin(x3) * (double) samplesPerSymbol *
|
||||||
(double) samplesPerSymbol / (4.0 * alpha * index * index));
|
(double) samplesPerSymbol / (4.0 * alpha * index * index));
|
||||||
|
|
||||||
|
|||||||
259
src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java
Normal file
259
src/main/java/io/github/dsheirer/dsp/psk/DQPSKDemodulator.java
Normal file
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
*
|
||||||
|
******************************************************************************/
|
||||||
|
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<Complex> mSymbolListener;
|
||||||
|
private Listener<Double> mPLLErrorListener;
|
||||||
|
private Listener<Double> mPLLFrequencyListener;
|
||||||
|
private Listener<Double> mSamplesPerSymbolListener;
|
||||||
|
private Listener<Dibit> mDibitListener;
|
||||||
|
private Listener<SymbolDecisionData2> 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<Complex> listener)
|
||||||
|
{
|
||||||
|
mSymbolListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the listener to receive decoded QPSK dibits
|
||||||
|
*/
|
||||||
|
public void setDibitListener(Listener<Dibit> listener)
|
||||||
|
{
|
||||||
|
mDibitListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the listener to receive PLL error values
|
||||||
|
*/
|
||||||
|
public void setPLLErrorListener(Listener<Double> listener)
|
||||||
|
{
|
||||||
|
mPLLErrorListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the listener to receive PLL error values
|
||||||
|
*/
|
||||||
|
public void setPLLFrequencyListener(Listener<Double> listener)
|
||||||
|
{
|
||||||
|
mPLLFrequencyListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the listener to receive PLL error values
|
||||||
|
*/
|
||||||
|
public void setSamplesPerSymbolListener(Listener<Double> listener)
|
||||||
|
{
|
||||||
|
mSamplesPerSymbolListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData2> listener)
|
||||||
|
{
|
||||||
|
mSymbolDecisionDataListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,21 +19,18 @@ import io.github.dsheirer.sample.complex.Complex;
|
|||||||
|
|
||||||
public class DQPSKSymbolPhaseErrorCalculator extends QPSKSymbolPhaseErrorCalculator
|
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);
|
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(Math.PI / 4.0d);
|
||||||
|
|
||||||
|
private boolean mToggle;
|
||||||
|
|
||||||
|
public DQPSKSymbolPhaseErrorCalculator()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void adjust(Complex symbol)
|
public void adjust(Complex symbol)
|
||||||
{
|
{
|
||||||
symbol.multiply(DIFFERENTIAL_OFFSET);
|
symbol.multiply(DIFFERENTIAL_OFFSET);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getPhaseError(Complex symbol)
|
|
||||||
{
|
|
||||||
Complex copy = symbol.copy();
|
|
||||||
|
|
||||||
copy.multiply(DIFFERENTIAL_OFFSET);
|
|
||||||
return super.getPhaseError(copy);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
*
|
||||||
|
******************************************************************************/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
*
|
||||||
|
******************************************************************************/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,8 +41,8 @@ public class GardnerDetector
|
|||||||
*/
|
*/
|
||||||
public float getError(Complex previous, Complex middle, Complex current)
|
public float getError(Complex previous, Complex middle, Complex current)
|
||||||
{
|
{
|
||||||
mErrorInphase = (previous.inphase() - current.inphase()) * middle.inphase();
|
mErrorInphase = (Math.abs(previous.inphase()) - Math.abs(current.inphase())) * Math.abs(middle.inphase());
|
||||||
mErrorQuadrature = (previous.quadrature() - current.quadrature()) * middle.quadrature();
|
mErrorQuadrature = (Math.abs(previous.quadrature()) - Math.abs(current.quadrature())) * Math.abs(middle.quadrature());
|
||||||
mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f);
|
mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f);
|
||||||
|
|
||||||
return mError;
|
return mError;
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ package io.github.dsheirer.dsp.psk;
|
|||||||
|
|
||||||
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
|
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
|
||||||
import io.github.dsheirer.sample.complex.Complex;
|
import io.github.dsheirer.sample.complex.Complex;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class InterpolatingSymbolBuffer
|
public class InterpolatingSymbolBuffer
|
||||||
{
|
{
|
||||||
|
private final static Logger mLog = LoggerFactory.getLogger(InterpolatingSymbolBuffer.class);
|
||||||
|
|
||||||
private Complex mMiddleSample = new Complex(0,0);
|
private Complex mMiddleSample = new Complex(0,0);
|
||||||
private Complex mCurrentSample = new Complex(0,0);
|
private Complex mCurrentSample = new Complex(0,0);
|
||||||
|
|
||||||
@@ -88,10 +92,19 @@ public class InterpolatingSymbolBuffer
|
|||||||
mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + mDetectedSamplesPerSymbolGain * symbolTimingError;
|
mDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol + mDetectedSamplesPerSymbolGain * symbolTimingError;
|
||||||
mDetectedSamplesPerSymbol = mSamplesPerSymbol + clip(mDetectedSamplesPerSymbol - mSamplesPerSymbol, mMaxDeviation);
|
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
|
//Add another symbol's worth of samples to the counter and adjust timing based on gardner error
|
||||||
increaseSampleCounter((mDetectedSamplesPerSymbol + (mSampleCounterGain * symbolTimingError)));
|
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
|
* Indicates if this buffer has accumulated a enough samples to represent a full symbol
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
*
|
||||||
|
******************************************************************************/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ public class QPSKDemodulator implements ComplexSampleListener
|
|||||||
private Listener<Complex> mSymbolListener;
|
private Listener<Complex> mSymbolListener;
|
||||||
private Listener<Double> mPLLErrorListener;
|
private Listener<Double> mPLLErrorListener;
|
||||||
private Listener<Double> mPLLFrequencyListener;
|
private Listener<Double> mPLLFrequencyListener;
|
||||||
|
private Listener<Double> mSamplesPerSymbolListener;
|
||||||
private Listener<Dibit> mDibitListener;
|
private Listener<Dibit> mDibitListener;
|
||||||
private Listener<SymbolDecisionData2> mSymbolDecisionDataListener;
|
private Listener<SymbolDecisionData2> mSymbolDecisionDataListener;
|
||||||
|
|
||||||
@@ -136,26 +137,29 @@ public class QPSKDemodulator implements ComplexSampleListener
|
|||||||
mSymbolListener.receive(mCurrentSymbol);
|
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
|
//Symbol timing error calculations
|
||||||
mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol);
|
mSymbolTimingError = mGardnerDetector.getError(mPreviousSymbol, mMiddleSymbol, mCurrentSymbol);
|
||||||
mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError);
|
mInterpolatingSymbolBuffer.resetAndAdjust(mSymbolTimingError);
|
||||||
|
|
||||||
|
if(mSamplesPerSymbolListener != null)
|
||||||
|
{
|
||||||
|
mSamplesPerSymbolListener.receive((double)mInterpolatingSymbolBuffer.getSamplingPoint());
|
||||||
|
}
|
||||||
|
|
||||||
//Store current samples/symbols to use for the next period
|
//Store current samples/symbols to use for the next period
|
||||||
mPreviousSample.setValues(mCurrentSample);
|
mPreviousSample.setValues(mCurrentSample);
|
||||||
mPreviousMiddleSample.setValues(mMiddleSample);
|
mPreviousMiddleSample.setValues(mMiddleSample);
|
||||||
mPreviousSymbol.setValues(mCurrentSymbol);
|
mPreviousSymbol.setValues(mCurrentSymbol);
|
||||||
|
|
||||||
|
|
||||||
//Calculate the phase error of the current symbol relative to the expected constellation and provide
|
//Calculate the phase error of the current symbol relative to the expected constellation and provide
|
||||||
//feedback to the PLL
|
//feedback to the PLL
|
||||||
|
mSymbolPhaseErrorCalculator.adjust(mCurrentSymbol);
|
||||||
mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol);
|
mPhaseError = mSymbolPhaseErrorCalculator.getPhaseError(mCurrentSymbol);
|
||||||
|
|
||||||
mPhaseError = GardnerDetector.clip(mPhaseError, 0.15f);
|
mPhaseError = GardnerDetector.clip(mPhaseError, 0.15f);
|
||||||
mPLL.adjust(mPhaseError);
|
|
||||||
|
// mPLL.adjust(-mPhaseError);
|
||||||
|
mPLL.adjust(0.0);
|
||||||
|
|
||||||
if(mPLLErrorListener != null)
|
if(mPLLErrorListener != null)
|
||||||
{
|
{
|
||||||
@@ -213,6 +217,14 @@ public class QPSKDemodulator implements ComplexSampleListener
|
|||||||
mPLLFrequencyListener = listener;
|
mPLLFrequencyListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the listener to receive PLL error values
|
||||||
|
*/
|
||||||
|
public void setSamplesPerSymbolListener(Listener<Double> listener)
|
||||||
|
{
|
||||||
|
mSamplesPerSymbolListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData2> listener)
|
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData2> listener)
|
||||||
{
|
{
|
||||||
mSymbolDecisionDataListener = listener;
|
mSymbolDecisionDataListener = listener;
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||||
|
*
|
||||||
|
******************************************************************************/
|
||||||
package io.github.dsheirer.dsp.psk;
|
package io.github.dsheirer.dsp.psk;
|
||||||
|
|
||||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
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.Listener;
|
||||||
import io.github.dsheirer.sample.complex.Complex;
|
import io.github.dsheirer.sample.complex.Complex;
|
||||||
|
|
||||||
public class QPSKStarSlicer implements Listener<Complex>
|
public class QPSKStarSlicer implements IQPSKSymbolDecoder, Listener<Complex>
|
||||||
{
|
{
|
||||||
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slices a ComplexSample representing a phase shifted symbol according to
|
* Slices a Complex sample representing a phase shifted symbol according to the following constellation pattern:
|
||||||
* the following constellation pattern:
|
*
|
||||||
*
|
* 00 +3 | 01 +1
|
||||||
* 00 | 01
|
* ------|------
|
||||||
* ---|---
|
* 10 -1 | 11 -1
|
||||||
* 10 | 11
|
*/
|
||||||
*/
|
public QPSKStarSlicer()
|
||||||
public QPSKStarSlicer()
|
{
|
||||||
{
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void dispose()
|
public void dispose()
|
||||||
{
|
{
|
||||||
mBroadcaster.dispose();
|
mBroadcaster.dispose();
|
||||||
mBroadcaster = null;
|
mBroadcaster = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addListener( Listener<Dibit> listener )
|
public void addListener(Listener<Dibit> listener)
|
||||||
{
|
{
|
||||||
mBroadcaster.addListener( listener );
|
mBroadcaster.addListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeListener( Listener<Dibit> listener )
|
public void removeListener(Listener<Dibit> listener)
|
||||||
{
|
{
|
||||||
mBroadcaster.removeListener( listener );
|
mBroadcaster.removeListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void receive( Complex sample )
|
public void receive(Complex sample)
|
||||||
{
|
{
|
||||||
mBroadcaster.broadcast( decide( sample ) );
|
mBroadcaster.broadcast(decode(sample));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Dibit decide( Complex sample )
|
@Override
|
||||||
{
|
public Dibit decode(Complex sample)
|
||||||
if( sample.inphase() > 0 )
|
{
|
||||||
{
|
if(sample.inphase() > 0)
|
||||||
return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
|
{
|
||||||
}
|
return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
|
||||||
else
|
}
|
||||||
{
|
else
|
||||||
return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
|
{
|
||||||
}
|
return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import io.github.dsheirer.sample.complex.Complex;
|
|||||||
|
|
||||||
public class SymbolDecisionData2
|
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 ComplexCircularBuffer mBuffer;
|
||||||
private float mSamplingPoint;
|
private float mSamplingPoint;
|
||||||
|
|
||||||
@@ -60,6 +63,9 @@ public class SymbolDecisionData2
|
|||||||
{
|
{
|
||||||
Complex copy = samples[length + x].copy();
|
Complex copy = samples[length + x].copy();
|
||||||
copy.multiply(samples[x].conjugate());
|
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;
|
demodulated[x] = copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ public class CostasLoop implements IPhaseLockedLoop
|
|||||||
private double mAlphaGain;
|
private double mAlphaGain;
|
||||||
private double mBetaGain;
|
private double mBetaGain;
|
||||||
private double mLoopPhase = 0.0;
|
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 double mMaximumLoopFrequency;
|
||||||
private Complex mCurrentVector = new Complex(0, 0);
|
private Complex mCurrentVector = new Complex(0, 0);
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import javafx.scene.control.SeparatorMenuItem;
|
|||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
@@ -37,6 +39,8 @@ import java.io.File;
|
|||||||
*/
|
*/
|
||||||
public class DemodulatorViewerFX extends Application
|
public class DemodulatorViewerFX extends Application
|
||||||
{
|
{
|
||||||
|
private final static Logger mLog = LoggerFactory.getLogger(DemodulatorViewerFX.class);
|
||||||
|
|
||||||
private Stage mStage;
|
private Stage mStage;
|
||||||
private MenuBar mMenuBar;
|
private MenuBar mMenuBar;
|
||||||
private RecentFilesMenu mRecentFilesMenu;
|
private RecentFilesMenu mRecentFilesMenu;
|
||||||
@@ -52,7 +56,7 @@ public class DemodulatorViewerFX extends Application
|
|||||||
borderPane.setTop(getMenuBar());
|
borderPane.setTop(getMenuBar());
|
||||||
borderPane.setCenter(getViewerDesktop());
|
borderPane.setCenter(getViewerDesktop());
|
||||||
|
|
||||||
Scene scene = new Scene(borderPane, 1200, 800);
|
Scene scene = new Scene(borderPane, 1500, 900);
|
||||||
|
|
||||||
mStage.setScene(scene);
|
mStage.setScene(scene);
|
||||||
mStage.show();
|
mStage.show();
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ public class ComplexSampleLineChart extends LineChart implements Listener<Comple
|
|||||||
for(int x = 0; x < samples.length; x += 2)
|
for(int x = 0; x < samples.length; x += 2)
|
||||||
{
|
{
|
||||||
sample.setValues(samples[x], samples[x + 1]);
|
sample.setValues(samples[x], samples[x + 1]);
|
||||||
mComplexGain.apply(sample);
|
// mComplexGain.apply(sample);
|
||||||
|
sample.normalize();
|
||||||
|
|
||||||
Complex previous = mDemodulationCircularBuffer.get(sample.copy());
|
Complex previous = mDemodulationCircularBuffer.get(sample.copy());
|
||||||
sample.multiply(previous.conjugate());
|
sample.multiply(previous.conjugate());
|
||||||
|
|||||||
@@ -24,19 +24,18 @@ import javafx.scene.chart.NumberAxis;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class FrequencyLineChart extends LineChart implements Listener<Double>
|
public class DoubleLineChart extends LineChart implements Listener<Double>
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(FrequencyLineChart.class);
|
private final static Logger mLog = LoggerFactory.getLogger(DoubleLineChart.class);
|
||||||
|
|
||||||
private DoubleCircularBuffer mDoubleCircularBuffer;
|
private DoubleCircularBuffer mDoubleCircularBuffer;
|
||||||
private ObservableList<Data<Integer,Double>> mPhaseValues = FXCollections.observableArrayList();
|
private ObservableList<Data<Integer,Double>> 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),
|
super(new NumberAxis(1, length, 2), new NumberAxis(min, max, tick));
|
||||||
new NumberAxis("Frequency", -maxFrequency, maxFrequency, maxFrequency / 5.0));
|
|
||||||
|
|
||||||
Series<Integer,Double> phaseSeries = new Series<>("PLL Frequency", mPhaseValues);
|
Series<Integer,Double> phaseSeries = new Series<>(seriesLabel, mPhaseValues);
|
||||||
ObservableList<Series<Integer,Double>> observableList = FXCollections.observableArrayList(phaseSeries);
|
ObservableList<Series<Integer,Double>> observableList = FXCollections.observableArrayList(phaseSeries);
|
||||||
|
|
||||||
setData(observableList);
|
setData(observableList);
|
||||||
@@ -16,12 +16,14 @@
|
|||||||
package io.github.dsheirer.instrument.gui.viewer.decoder;
|
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.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.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.PhaseLineChart;
|
||||||
import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart;
|
import io.github.dsheirer.instrument.gui.viewer.chart.SymbolChart;
|
||||||
import io.github.dsheirer.module.decode.DecoderType;
|
import io.github.dsheirer.module.decode.DecoderType;
|
||||||
import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder2;
|
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.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -37,7 +39,9 @@ public class P25Phase1Pane extends DecoderPane
|
|||||||
private HBox mDecoderChartBox;
|
private HBox mDecoderChartBox;
|
||||||
private SymbolChart mSymbolChart;
|
private SymbolChart mSymbolChart;
|
||||||
private PhaseLineChart mPLLPhaseErrorLineChart;
|
private PhaseLineChart mPLLPhaseErrorLineChart;
|
||||||
private FrequencyLineChart mPLLFrequencyLineChart;
|
private DoubleLineChart mPLLFrequencyLineChart;
|
||||||
|
private DoubleLineChart mSamplesPerSymbolLineChart;
|
||||||
|
private Broadcaster<ComplexBuffer> mFilteredBufferBroadcaster = new Broadcaster<>();
|
||||||
private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null);
|
private P25_C4FMDecoder2 mDecoder = new P25_C4FMDecoder2(null);
|
||||||
|
|
||||||
public P25Phase1Pane()
|
public P25Phase1Pane()
|
||||||
@@ -48,13 +52,16 @@ public class P25Phase1Pane extends DecoderPane
|
|||||||
|
|
||||||
private void init()
|
private void init()
|
||||||
{
|
{
|
||||||
addListener(getSampleLineChart());
|
// addListener(getSampleLineChart());
|
||||||
addListener(getDecoder());
|
addListener(getDecoder());
|
||||||
|
|
||||||
|
getDecoder().setDEBUGFilteredBufferListener(mFilteredBufferBroadcaster);
|
||||||
getDecoder().setDEBUGSymbolListener(getSymbolChart());
|
getDecoder().setDEBUGSymbolListener(getSymbolChart());
|
||||||
getDecoder().setDEBUGPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
|
getDecoder().setDEBUGPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
|
||||||
getDecoder().setDEBUGPLLFrequencyListener(getPLLFrequencyLineChart());
|
getDecoder().setDEBUGPLLFrequencyListener(getPLLFrequencyLineChart());
|
||||||
getDecoder().setDEBUGSymbolDecisionDataListener(getEyeDiagramChart());
|
getDecoder().setDEBUGSymbolDecisionDataListener(getEyeDiagramChart());
|
||||||
|
getDecoder().setDEBUGSamplesPerSymbolListener(getSamplesPerSymbolLineChart());
|
||||||
|
mFilteredBufferBroadcaster.addListener(getSampleLineChart());
|
||||||
|
|
||||||
HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS);
|
HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS);
|
||||||
HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS);
|
HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS);
|
||||||
@@ -67,7 +74,10 @@ public class P25Phase1Pane extends DecoderPane
|
|||||||
mLog.debug("Configuring for sample rate: " + sampleRate);
|
mLog.debug("Configuring for sample rate: " + sampleRate);
|
||||||
|
|
||||||
mDecoder.setSampleRate(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()
|
private P25_C4FMDecoder2 getDecoder()
|
||||||
@@ -97,7 +107,8 @@ public class P25Phase1Pane extends DecoderPane
|
|||||||
HBox.setHgrow(getSymbolChart(), Priority.ALWAYS);
|
HBox.setHgrow(getSymbolChart(), Priority.ALWAYS);
|
||||||
HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS);
|
HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS);
|
||||||
HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS);
|
HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS);
|
||||||
mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(), getPLLFrequencyLineChart());
|
mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(),
|
||||||
|
getPLLFrequencyLineChart());
|
||||||
}
|
}
|
||||||
|
|
||||||
return mDecoderChartBox;
|
return mDecoderChartBox;
|
||||||
@@ -111,9 +122,12 @@ public class P25Phase1Pane extends DecoderPane
|
|||||||
mSampleChartBox.setMaxHeight(Double.MAX_VALUE);
|
mSampleChartBox.setMaxHeight(Double.MAX_VALUE);
|
||||||
getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
|
getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||||
getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE);
|
getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE);
|
||||||
|
getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE);
|
||||||
HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
|
HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
|
||||||
HBox.setHgrow(getEyeDiagramChart(), 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;
|
return mSampleChartBox;
|
||||||
@@ -149,13 +163,23 @@ public class P25Phase1Pane extends DecoderPane
|
|||||||
return mPLLPhaseErrorLineChart;
|
return mPLLPhaseErrorLineChart;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FrequencyLineChart getPLLFrequencyLineChart()
|
private DoubleLineChart getPLLFrequencyLineChart()
|
||||||
{
|
{
|
||||||
if(mPLLFrequencyLineChart == null)
|
if(mPLLFrequencyLineChart == null)
|
||||||
{
|
{
|
||||||
mPLLFrequencyLineChart = new FrequencyLineChart(2400, 40);
|
mPLLFrequencyLineChart = new DoubleLineChart( "PLL Frequency", -200, 200, 20, 40);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mPLLFrequencyLineChart;
|
return mPLLFrequencyLineChart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DoubleLineChart getSamplesPerSymbolLineChart()
|
||||||
|
{
|
||||||
|
if(mSamplesPerSymbolLineChart == null)
|
||||||
|
{
|
||||||
|
mSamplesPerSymbolLineChart = new DoubleLineChart( "Sample Point", 9.0, 11.0, 0.1, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mSamplesPerSymbolLineChart;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.FIRFilterSpecification;
|
||||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
|
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
|
||||||
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
|
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.DQPSKSymbolPhaseErrorCalculator;
|
||||||
import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder;
|
import io.github.dsheirer.dsp.psk.IQPSKSymbolDecoder;
|
||||||
import io.github.dsheirer.dsp.psk.ISymbolPhaseErrorCalculator;
|
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.QPSKSymbolDecoder;
|
||||||
import io.github.dsheirer.dsp.psk.SymbolDecisionData2;
|
import io.github.dsheirer.dsp.psk.SymbolDecisionData2;
|
||||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||||
@@ -53,24 +54,26 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
|
|||||||
private Map<Double,float[]> mSymbolFilters = new HashMap<>();
|
private Map<Double,float[]> mSymbolFilters = new HashMap<>();
|
||||||
private ComplexFIRFilter mSymbolFilter;
|
private ComplexFIRFilter mSymbolFilter;
|
||||||
|
|
||||||
private QPSKDemodulator mQPSKDemodulator;
|
private DQPSKDemodulator mQPSKDemodulator;
|
||||||
private CostasLoop mCostasLoop;
|
private CostasLoop mCostasLoop;
|
||||||
private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
|
private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
|
||||||
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
|
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
|
||||||
private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder();
|
private QPSKSymbolDecoder mQPSKSlicer = new QPSKSymbolDecoder();
|
||||||
private P25MessageFramer mMessageFramer;
|
private P25MessageFramer mMessageFramer;
|
||||||
private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator();
|
private ISymbolPhaseErrorCalculator mSymbolPhaseErrorCalculator = new DQPSKSymbolPhaseErrorCalculator();
|
||||||
private IQPSKSymbolDecoder mSymbolDecoder = new QPSKSymbolDecoder();
|
private IQPSKSymbolDecoder mSymbolDecoder = new QPSKStarSlicer();
|
||||||
private Listener<Complex> mDEBUGSymbolListener;
|
private Listener<Complex> mDEBUGSymbolListener;
|
||||||
private Listener<Double> mDEBUGPLLPhaseErrorListener;
|
private Listener<Double> mDEBUGPLLPhaseErrorListener;
|
||||||
private Listener<Double> mDEBUGPLLFrequencyListener;
|
private Listener<Double> mDEBUGPLLFrequencyListener;
|
||||||
private Listener<SymbolDecisionData2> mDEBUGSymbolDecisionDataListener;
|
private Listener<SymbolDecisionData2> mDEBUGSymbolDecisionDataListener;
|
||||||
|
private Listener<Double> mDEBUGSamplesPerSymbolListener;
|
||||||
|
private Listener<ComplexBuffer> mFilteredSymbolListener;
|
||||||
|
|
||||||
public P25_C4FMDecoder2(AliasList aliasList)
|
public P25_C4FMDecoder2(AliasList aliasList)
|
||||||
{
|
{
|
||||||
super(aliasList);
|
super(aliasList);
|
||||||
|
|
||||||
setSampleRate(48000.0);
|
setSampleRate(48600.0);
|
||||||
|
|
||||||
mStreamConverter.setListener(mAGC);
|
mStreamConverter.setListener(mAGC);
|
||||||
}
|
}
|
||||||
@@ -90,12 +93,13 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
|
|||||||
mCostasLoop.setTracking(Tracking.FINE);
|
mCostasLoop.setTracking(Tracking.FINE);
|
||||||
mCostasLoop.setAutomaticTracking(false);
|
mCostasLoop.setAutomaticTracking(false);
|
||||||
|
|
||||||
mQPSKDemodulator = new QPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder,
|
mQPSKDemodulator = new DQPSKDemodulator(mCostasLoop, mSymbolPhaseErrorCalculator, mSymbolDecoder,
|
||||||
mSampleRate, SYMBOL_RATE);
|
mSampleRate, SYMBOL_RATE);
|
||||||
mQPSKDemodulator.setSymbolListener(mDEBUGSymbolListener);
|
mQPSKDemodulator.setSymbolListener(mDEBUGSymbolListener);
|
||||||
mQPSKDemodulator.setPLLErrorListener(mDEBUGPLLPhaseErrorListener);
|
mQPSKDemodulator.setPLLErrorListener(mDEBUGPLLPhaseErrorListener);
|
||||||
mQPSKDemodulator.setPLLFrequencyListener(mDEBUGPLLFrequencyListener);
|
mQPSKDemodulator.setPLLFrequencyListener(mDEBUGPLLFrequencyListener);
|
||||||
mQPSKDemodulator.setSymbolDecisionDataListener(mDEBUGSymbolDecisionDataListener);
|
mQPSKDemodulator.setSymbolDecisionDataListener(mDEBUGSymbolDecisionDataListener);
|
||||||
|
mQPSKDemodulator.setSamplesPerSymbolListener(mDEBUGSamplesPerSymbolListener);
|
||||||
|
|
||||||
//Message framer can trigger a symbol-inversion correction to the PLL when detected
|
//Message framer can trigger a symbol-inversion correction to the PLL when detected
|
||||||
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
|
mMessageFramer = new P25MessageFramer(getAliasList(), mCostasLoop);
|
||||||
@@ -121,20 +125,36 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
|
|||||||
mQPSKDemodulator.setPLLFrequencyListener(listener);
|
mQPSKDemodulator.setPLLFrequencyListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDEBUGFilteredBufferListener(Listener<ComplexBuffer> listener)
|
||||||
|
{
|
||||||
|
mFilteredSymbolListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
public void setDEBUGSymbolDecisionDataListener(Listener<SymbolDecisionData2> listener)
|
public void setDEBUGSymbolDecisionDataListener(Listener<SymbolDecisionData2> listener)
|
||||||
{
|
{
|
||||||
mDEBUGSymbolDecisionDataListener = listener;
|
mDEBUGSymbolDecisionDataListener = listener;
|
||||||
mQPSKDemodulator.setSymbolDecisionDataListener(listener);
|
mQPSKDemodulator.setSymbolDecisionDataListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDEBUGSamplesPerSymbolListener(Listener<Double> listener)
|
||||||
|
{
|
||||||
|
mDEBUGSamplesPerSymbolListener = listener;
|
||||||
|
mQPSKDemodulator.setSamplesPerSymbolListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void receive(ComplexBuffer complexBuffer)
|
public void receive(ComplexBuffer complexBuffer)
|
||||||
{
|
{
|
||||||
ComplexBuffer basebandFiltered = mBasebandFilter.filter(complexBuffer);
|
ComplexBuffer basebandFiltered = mBasebandFilter.filter(complexBuffer);
|
||||||
ComplexBuffer symbolFiltered = mSymbolFilter.filter(basebandFiltered);
|
// ComplexBuffer symbolFiltered = mSymbolFilter.filter(complexBuffer);
|
||||||
// ComplexBuffer amplified = mAGC.filter(symbolFiltered);
|
// ComplexBuffer symbolFiltered = complexBuffer;
|
||||||
|
ComplexBuffer symbolFiltered = basebandFiltered;
|
||||||
mQPSKDemodulator.receive(symbolFiltered);
|
mQPSKDemodulator.receive(symbolFiltered);
|
||||||
// mQPSKDemodulator.receive(complexBuffer);
|
|
||||||
|
if(mFilteredSymbolListener != null)
|
||||||
|
{
|
||||||
|
mFilteredSymbolListener.receive(symbolFiltered);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private double getSampleRate()
|
private double getSampleRate()
|
||||||
@@ -150,11 +170,11 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
|
|||||||
{
|
{
|
||||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||||
.sampleRate((int)getSampleRate())
|
.sampleRate((int)getSampleRate())
|
||||||
.passBandCutoff(2880)
|
.passBandCutoff(6000)
|
||||||
.passBandAmplitude(1.0)
|
.passBandAmplitude(1.0)
|
||||||
.passBandRipple(0.01)
|
.passBandRipple(0.01)
|
||||||
.stopBandAmplitude(0.0)
|
.stopBandAmplitude(0.0)
|
||||||
.stopBandStart(3600)
|
.stopBandStart(6250)
|
||||||
.stopBandRipple(0.01)
|
.stopBandRipple(0.01)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -186,7 +206,8 @@ public class P25_C4FMDecoder2 extends P25Decoder implements IComplexBufferListen
|
|||||||
|
|
||||||
if(filter == null)
|
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);
|
mSymbolFilters.put(getSampleRate(), filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user