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