mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
39 - WIP - incorporates the latest P25 C4FM decoder from master and
updates the LSM decoder to follow the same design pattern. Adds LSM decoder support to the demodulator viewer application. The current symbol timing gain for LSM is set too high -- is using the C4FM gain and causes too much fluctuation in the timing error.
This commit is contained in:
@@ -18,10 +18,12 @@ package io.github.dsheirer.dsp.filter.fir.complex;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue;
|
||||
import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
|
||||
|
||||
public class ComplexFIRFilter extends FIRFilter
|
||||
{
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue();
|
||||
private RealFIRFilter mIFilter;
|
||||
private RealFIRFilter mQFilter;
|
||||
|
||||
@@ -61,24 +63,43 @@ public class ComplexFIRFilter extends FIRFilter
|
||||
return new Complex(i, q);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the complex samples from the buffer and returns a new complex buffer with the filtered output
|
||||
*
|
||||
* @param buffer with complex samples to filter
|
||||
* @return new buffer containing filtered complex samples
|
||||
*/
|
||||
public ComplexBuffer filter(ComplexBuffer buffer)
|
||||
public float[] filter(float[] samples)
|
||||
{
|
||||
float[] samples = buffer.getSamples();
|
||||
float[] filtered = new float[samples.length];
|
||||
float[] filteredSamples = new float[samples.length];
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
filtered[x] = filterInphase(samples[x]);
|
||||
filtered[x + 1] = filterQuadrature(samples[x + 1]);
|
||||
filteredSamples[x] = filterInphase(samples[x]);
|
||||
filteredSamples[x + 1] = filterQuadrature(samples[x + 1]);
|
||||
}
|
||||
|
||||
return new ComplexBuffer(filtered);
|
||||
return filteredSamples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the complex samples from the buffer and returns a new complex buffer with the filtered output
|
||||
*
|
||||
* @param originalBuffer with complex samples to filter
|
||||
* @return new buffer containing filtered complex samples
|
||||
*/
|
||||
public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer)
|
||||
{
|
||||
ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(originalBuffer.getSampleLength());
|
||||
filteredBuffer.setTimestamp(originalBuffer.getTimestamp());
|
||||
|
||||
float[] samples = originalBuffer.getSamples();
|
||||
float[] filteredSamples = filteredBuffer.getSamples();
|
||||
|
||||
for(int x = 0; x < originalBuffer.getSampleLength(); x += 2)
|
||||
{
|
||||
filteredSamples[x] = filterInphase(samples[x]);
|
||||
filteredSamples[x + 1] = filterQuadrature(samples[x + 1]);
|
||||
}
|
||||
|
||||
originalBuffer.decrementUserCount();
|
||||
filteredBuffer.incrementUserCount();
|
||||
|
||||
return filteredBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -86,5 +107,6 @@ public class ComplexFIRFilter extends FIRFilter
|
||||
{
|
||||
mIFilter.dispose();
|
||||
mQFilter.dispose();
|
||||
mReusableBufferQueue.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import io.github.dsheirer.buffer.FloatCircularBuffer;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import io.github.dsheirer.sample.complex.reusable.ReusableBufferQueue;
|
||||
import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
|
||||
|
||||
public class ComplexFeedForwardGainControl implements ComplexSampleListener
|
||||
{
|
||||
@@ -26,8 +28,8 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener
|
||||
public static final float MINIMUM_ENVELOPE = 0.0001f;
|
||||
|
||||
private ComplexSampleListener mListener;
|
||||
|
||||
private FloatCircularBuffer mEnvelopeHistory;
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue();
|
||||
|
||||
private float mMaxEnvelope = 0.0f;
|
||||
private float mGain = 1.0f;
|
||||
@@ -107,19 +109,59 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public ComplexBuffer filter(ComplexBuffer complexBuffer)
|
||||
public float[] filter(float[] complesSamples)
|
||||
{
|
||||
float[] samples = complexBuffer.getSamples();
|
||||
float[] processed = new float[samples.length];
|
||||
mMaxEnvelope = MINIMUM_ENVELOPE;
|
||||
float currentEnvelope;
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
for(int x = 0; x < complesSamples.length; x += 2)
|
||||
{
|
||||
process(samples[x], samples[x + 1]);
|
||||
currentEnvelope = Complex.envelope(complesSamples[x], complesSamples[x + 1]);
|
||||
|
||||
processed[x] = samples[x] * mGain;
|
||||
processed[x + 1] = samples[x + 1] * mGain;
|
||||
if(currentEnvelope > mMaxEnvelope)
|
||||
{
|
||||
mMaxEnvelope = currentEnvelope;
|
||||
}
|
||||
}
|
||||
|
||||
return new ComplexBuffer(processed);
|
||||
adjustGain();
|
||||
|
||||
float[] processed = new float[complesSamples.length];
|
||||
|
||||
for(int x = 0; x < complesSamples.length; x += 2)
|
||||
{
|
||||
processed[x] = complesSamples[x] * mGain;
|
||||
processed[x + 1] = complesSamples[x + 1] * mGain;
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies gain to the complex sample buffer
|
||||
* @param buffer to apply gain
|
||||
* @return buffer with gain applied samples
|
||||
*/
|
||||
public ComplexBuffer filter(ComplexBuffer buffer)
|
||||
{
|
||||
float[] filtered = filter(buffer.getSamples());
|
||||
return new ComplexBuffer(filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies gain to the complex sample buffer
|
||||
* @param buffer to apply gain
|
||||
* @return reusable buffer with the user count incremented to 1
|
||||
*/
|
||||
public ReusableComplexBuffer filter(ReusableComplexBuffer buffer)
|
||||
{
|
||||
float[] filtered = filter(buffer.getSamples());
|
||||
|
||||
ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(filtered.length);
|
||||
|
||||
filteredBuffer.reloadFrom(filtered, buffer.getTimestamp());
|
||||
filteredBuffer.incrementUserCount();
|
||||
|
||||
return filteredBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*******************************************************************************
|
||||
* 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.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class DQPSKDecisionDirectedDemodulator extends PSKDemodulator<Dibit>
|
||||
{
|
||||
protected DQPSKDecisionDirectedSymbolEvaluator mSymbolEvaluator = new DQPSKDecisionDirectedSymbolEvaluator();
|
||||
private Complex mPreviousPrecedingSample = new Complex(0, 0);
|
||||
private Complex mPreviousCurrentSample = new Complex(0, 0);
|
||||
private Complex mPrecedingSample = new Complex(0, 0);
|
||||
private Complex mCurrentSample = new Complex(0, 0);
|
||||
private Complex mPrecedingSymbol = new Complex(0, 0);
|
||||
protected Complex mCurrentSymbol = new Complex(0, 0);
|
||||
|
||||
/**
|
||||
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
|
||||
* a decision-directed DQPSK symbol timing and phase 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 interpolatingSampleBuffer to hold samples for interpolating a symbol
|
||||
*/
|
||||
public DQPSKDecisionDirectedDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
|
||||
{
|
||||
super(interpolatingSampleBuffer, phaseLockedLoop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a symbol from the interpolating buffer
|
||||
*/
|
||||
protected void calculateSymbol()
|
||||
{
|
||||
//Get preceding sample and an interpolated current sample from the interpolating buffer
|
||||
mPrecedingSample = getInterpolatingSampleBuffer().getPrecedingSample();
|
||||
mCurrentSample = getInterpolatingSampleBuffer().getCurrentSample();
|
||||
|
||||
//Differential decode preceding and current symbols by calculating the angular rotation between the previous and
|
||||
//current samples (current sample x complex conjugate of previous sample).
|
||||
|
||||
//Note: preceding symbol is a preceding measurement of the current symbol that is simply used as a reference
|
||||
//point to determine vector rotation to the current symbol -- it is not the true predecessor symbol
|
||||
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();
|
||||
|
||||
//Apply symbols to evaluator to calculate phase and timing error and make a symbol decision
|
||||
mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
|
||||
|
||||
//Update the symbol timing error
|
||||
getInterpolatingSampleBuffer().resetAndAdjust(mSymbolEvaluator.getTimingError());
|
||||
|
||||
//Update the costas loop (PLL) with any measured phase error
|
||||
getPLL().adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
|
||||
|
||||
//Store current samples/symbols to use for the next symbol period
|
||||
mPreviousPrecedingSample.setValues(mPrecedingSample);
|
||||
mPreviousCurrentSample.setValues(mCurrentSample);
|
||||
|
||||
broadcast(mSymbolEvaluator.getSymbolDecision());
|
||||
}
|
||||
}
|
||||
@@ -20,11 +20,11 @@ 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
|
||||
public class DQPSKDecisionDirectedDemodulatorInstrumented extends DQPSKDecisionDirectedDemodulator
|
||||
{
|
||||
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
|
||||
private Listener<Double> mSamplesPerSymbolListener;
|
||||
private Listener<Complex> mSymbolListener;
|
||||
private Listener<Complex> mComplexSymbolListener;
|
||||
private Listener<Double> mPLLErrorListener;
|
||||
private Listener<Double> mPLLFrequencyListener;
|
||||
private double mSampleRate;
|
||||
@@ -40,9 +40,9 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
|
||||
* @param interpolatingSampleBuffer
|
||||
* @param sampleRate of the incoming complex sample stream
|
||||
*/
|
||||
public DQPSKDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
|
||||
InterpolatingSampleBufferInstrumented interpolatingSampleBuffer,
|
||||
double sampleRate)
|
||||
public DQPSKDecisionDirectedDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
|
||||
InterpolatingSampleBufferInstrumented interpolatingSampleBuffer,
|
||||
double sampleRate)
|
||||
{
|
||||
super(phaseLockedLoop, interpolatingSampleBuffer);
|
||||
mSampleRate = sampleRate;
|
||||
@@ -57,20 +57,20 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
|
||||
if(mSymbolDecisionDataListener != null)
|
||||
{
|
||||
mSymbolDecisionDataListener.receive(
|
||||
((InterpolatingSampleBufferInstrumented)mInterpolatingSampleBuffer).getSymbolDecisionData());
|
||||
((InterpolatingSampleBufferInstrumented)getInterpolatingSampleBuffer()).getSymbolDecisionData());
|
||||
}
|
||||
|
||||
super.calculateSymbol();
|
||||
|
||||
if(mSamplesPerSymbolListener != null)
|
||||
{
|
||||
mSamplesPerSymbolListener.receive((double)mInterpolatingSampleBuffer.getSamplingPoint());
|
||||
mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint());
|
||||
}
|
||||
|
||||
//Send to an external constellation symbol listener when registered
|
||||
if(mSymbolListener != null)
|
||||
if(mComplexSymbolListener != null)
|
||||
{
|
||||
mSymbolListener.receive(mCurrentSymbol);
|
||||
mComplexSymbolListener.receive(mCurrentSymbol);
|
||||
}
|
||||
|
||||
if(mPLLErrorListener != null)
|
||||
@@ -80,7 +80,7 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
|
||||
|
||||
if(mPLLFrequencyListener != null)
|
||||
{
|
||||
double loopFrequency = ((CostasLoop)mPLL).getLoopFrequency();
|
||||
double loopFrequency = ((CostasLoop)getPLL()).getLoopFrequency();
|
||||
|
||||
loopFrequency *= mSampleRate / (2.0 * Math.PI);
|
||||
|
||||
@@ -107,9 +107,9 @@ public class DQPSKDemodulatorInstrumented extends DQPSKDemodulator
|
||||
/**
|
||||
* Registers the listener to receive decoded QPSK symbols
|
||||
*/
|
||||
public void setSymbolListener(Listener<Complex> listener)
|
||||
public void setComplexSymbolListener(Listener<Complex> listener)
|
||||
{
|
||||
mSymbolListener = listener;
|
||||
mComplexSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,7 +18,7 @@ package io.github.dsheirer.dsp.psk;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
|
||||
public class DQPSKDecisionDirectedSymbolEvaluator implements IPSKSymbolEvaluator<Dibit>
|
||||
{
|
||||
private static final Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0);
|
||||
private static final Complex ROTATE_FROM_PLUS_45 = Complex.fromAngle(-1.0 * Math.PI / 4.0);
|
||||
@@ -32,7 +32,7 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
|
||||
private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
|
||||
|
||||
/**
|
||||
* Decision-directed symbol phase and timing error detector and symbol decision slicer.
|
||||
* Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer.
|
||||
*
|
||||
* Symbol decision is based on the closest reference quadrant for the sampled symbol.
|
||||
*
|
||||
@@ -40,7 +40,7 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
|
||||
* symbol. Timing error is further corrected to compensate for clock-wise or counter-clock-wise vector rotation
|
||||
* relative to the immediately preceding symbol.
|
||||
*/
|
||||
public DQPSKSymbolEvaluator()
|
||||
public DQPSKDecisionDirectedSymbolEvaluator()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ public class DQPSKSymbolEvaluator implements IDQPSKSymbolEvaluator
|
||||
* since it's only a reference
|
||||
* @param current symbol to be evaluated - interpolated
|
||||
*/
|
||||
@Override
|
||||
public void setSymbol(Complex preceding, Complex current)
|
||||
{
|
||||
mSymbolError.setValues(current);
|
||||
@@ -1,162 +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.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class DQPSKDemodulator implements ComplexSampleListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DQPSKDemodulator.class);
|
||||
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
|
||||
protected IPhaseLockedLoop mPLL;
|
||||
protected DQPSKSymbolEvaluator mSymbolEvaluator = new DQPSKSymbolEvaluator();
|
||||
private Complex mReceivedSample = new Complex(0, 0);
|
||||
private Complex mPreviousPrecedingSample = new Complex(0, 0);
|
||||
private Complex mPreviousCurrentSample = new Complex(0, 0);
|
||||
private Complex mPrecedingSample = new Complex(0, 0);
|
||||
private Complex mCurrentSample = new Complex(0, 0);
|
||||
private Complex mPrecedingSymbol = new Complex(0, 0);
|
||||
protected Complex mCurrentSymbol = new Complex(0, 0);
|
||||
private Listener<Dibit> mDibitListener;
|
||||
|
||||
/**
|
||||
* 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 interpolatingSampleBuffer to hold samples for interpolating a symbol
|
||||
*/
|
||||
public DQPSKDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
|
||||
{
|
||||
mPLL = phaseLockedLoop;
|
||||
mInterpolatingSampleBuffer = interpolatingSampleBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a (filtered) 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]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made.
|
||||
* @param inphase value for the sample
|
||||
* @param quadrature value for the sample
|
||||
*/
|
||||
@Override
|
||||
public void receive(float inphase, float quadrature)
|
||||
{
|
||||
//Update current sample with values
|
||||
mReceivedSample.setValues(inphase, quadrature);
|
||||
|
||||
//Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency
|
||||
mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector());
|
||||
|
||||
//Store the sample in the interpolating buffer
|
||||
mInterpolatingSampleBuffer.receive(mReceivedSample);
|
||||
|
||||
//Calculate the symbol once we've stored enough samples
|
||||
if(mInterpolatingSampleBuffer.hasSymbol())
|
||||
{
|
||||
calculateSymbol();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a symbol from the interpolating buffer
|
||||
*/
|
||||
protected void calculateSymbol()
|
||||
{
|
||||
//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();
|
||||
|
||||
mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
|
||||
|
||||
mInterpolatingSampleBuffer.resetAndAdjust(mSymbolEvaluator.getTimingError());
|
||||
|
||||
//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
|
||||
mPLL.adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
|
||||
|
||||
if(mDibitListener != null)
|
||||
{
|
||||
mDibitListener.receive(mSymbolEvaluator.getSymbolDecision());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive dibit symbol decisions from this demodulator
|
||||
*/
|
||||
public void setDibitListener(Listener<Dibit> listener)
|
||||
{
|
||||
mDibitListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*******************************************************************************
|
||||
* 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.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class DQPSKGardnerDemodulator extends PSKDemodulator<Dibit>
|
||||
{
|
||||
protected DQPSKGardnerSymbolEvaluator mSymbolEvaluator = new DQPSKGardnerSymbolEvaluator();
|
||||
private Complex mPreviousCurrentSample = new Complex(0, 0);
|
||||
private Complex mPreviousMiddleSample = new Complex(0, 0);
|
||||
private Complex mMiddleSymbol = new Complex(0, 0);
|
||||
protected Complex mCurrentSymbol = new Complex(0, 0);
|
||||
|
||||
/**
|
||||
* Implements a Differential QPSK demodulator using a Costas Loop (PLL) and a Gardner timing error detector.
|
||||
*
|
||||
* This decoder is optimized for P25 Linear Simulcast Modulation (LSM).
|
||||
*
|
||||
* @param phaseLockedLoop for tracking carrier frequency error
|
||||
* @param interpolatingSampleBuffer to hold samples for interpolating a symbol
|
||||
*/
|
||||
public DQPSKGardnerDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
|
||||
{
|
||||
super(interpolatingSampleBuffer, phaseLockedLoop);
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void calculateSymbol()
|
||||
{
|
||||
//Note: the interpolating sample buffer holds 2 symbols worth of samples and the current sample method points
|
||||
//to the sample at the mid-point between those 2 symbol periods and the middle sample method points to the
|
||||
//sample that is half a symbol period after the current sample. Since we need a middle sample and a current
|
||||
//symbol sample for the gardner calculation, we'll treat the interpolating buffer's current sample as the
|
||||
//gardner mid-point and we'll treat the interpolating buffer's mid-point sample as the current symbol
|
||||
//sample (ie flip-flopped)
|
||||
Complex middleSample = getInterpolatingSampleBuffer().getCurrentSample();
|
||||
Complex currentSample = getInterpolatingSampleBuffer().getMiddleSample();
|
||||
|
||||
//Differential decode middle and current symbols by calculating the angular rotation between the previous and
|
||||
//current samples (current sample x complex conjugate of previous sample).
|
||||
mMiddleSymbol.setInphase(Complex.multiplyInphase(middleSample.inphase(), middleSample.quadrature(),
|
||||
mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
|
||||
mMiddleSymbol.setQuadrature(Complex.multiplyQuadrature(middleSample.inphase(), middleSample.quadrature(),
|
||||
mPreviousMiddleSample.inphase(), -mPreviousMiddleSample.quadrature()));
|
||||
|
||||
mCurrentSymbol.setInphase(Complex.multiplyInphase(currentSample.inphase(), currentSample.quadrature(),
|
||||
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
|
||||
mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(currentSample.inphase(), currentSample.quadrature(),
|
||||
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
|
||||
|
||||
//Set gain to unity before we calculate the error value
|
||||
mMiddleSymbol.normalize();
|
||||
mCurrentSymbol.normalize();
|
||||
|
||||
//Pass symbols to evaluator to determine timing and phase error and make symbol decision
|
||||
mSymbolEvaluator.setSymbols(mMiddleSymbol, mCurrentSymbol);
|
||||
|
||||
//Update symbol timing error
|
||||
getInterpolatingSampleBuffer().resetAndAdjust(mSymbolEvaluator.getTimingError());
|
||||
|
||||
//Update PLL phase error
|
||||
getPLL().adjust(mSymbolEvaluator.getPhaseError());
|
||||
|
||||
//Store current samples/symbols for next symbol calculation
|
||||
mPreviousMiddleSample.setValues(middleSample);
|
||||
mPreviousCurrentSample.setValues(currentSample);
|
||||
|
||||
broadcast(mSymbolEvaluator.getSymbolDecision());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*******************************************************************************
|
||||
* 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 DQPSKGardnerDemodulatorInstrumented extends DQPSKGardnerDemodulator
|
||||
{
|
||||
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
|
||||
private Listener<Double> mSamplesPerSymbolListener;
|
||||
private Listener<Complex> mComplexSymbolListener;
|
||||
private Listener<Double> mPLLErrorListener;
|
||||
private Listener<Double> mPLLFrequencyListener;
|
||||
private double mSampleRate;
|
||||
|
||||
/**
|
||||
* Implements a Differential QPSK demodulator using a Costas Loop (PLL) and a Gardner timing error detector.
|
||||
*
|
||||
* This decoder is optimized for P25 Linear Simulcast Modulation (LSM).
|
||||
*
|
||||
* @param phaseLockedLoop for tracking carrier frequency error
|
||||
* @param interpolatingSampleBuffer to hold samples for interpolating a symbol
|
||||
*/
|
||||
public DQPSKGardnerDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
|
||||
InterpolatingSampleBuffer interpolatingSampleBuffer,
|
||||
double sampleRate)
|
||||
{
|
||||
super(phaseLockedLoop, 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)getInterpolatingSampleBuffer()).getSymbolDecisionData());
|
||||
}
|
||||
|
||||
super.calculateSymbol();
|
||||
|
||||
if(mSamplesPerSymbolListener != null)
|
||||
{
|
||||
mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint());
|
||||
}
|
||||
|
||||
//Send to an external constellation symbol listener when registered
|
||||
if(mComplexSymbolListener != null)
|
||||
{
|
||||
mComplexSymbolListener.receive(mCurrentSymbol);
|
||||
}
|
||||
|
||||
if(mPLLErrorListener != null)
|
||||
{
|
||||
mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
|
||||
}
|
||||
|
||||
if(mPLLFrequencyListener != null)
|
||||
{
|
||||
double loopFrequency = ((CostasLoop)getPLL()).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 setComplexSymbolListener(Listener<Complex> listener)
|
||||
{
|
||||
mComplexSymbolListener = 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*******************************************************************************
|
||||
* 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.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class DQPSKGardnerSymbolEvaluator implements IPSKSymbolEvaluator<Dibit>
|
||||
{
|
||||
private float mPhaseError = 0.0f;
|
||||
private float mTimingError = 0.0f;
|
||||
private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
|
||||
private Complex mPreviousSymbol = new Complex(0, 0);
|
||||
|
||||
/**
|
||||
* Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer.
|
||||
*
|
||||
* Symbol decision is based on the closest reference quadrant for the sampled symbol.
|
||||
*
|
||||
* Phase error is calculated as the angular distance of the sampled symbol from the reference symbol.
|
||||
*
|
||||
* Timing error is calculated using the Gardner method by comparing the previous symbol to the current symbol and
|
||||
* amplifying the delta between the two using the intra-symbol sample to form the timing error.
|
||||
*/
|
||||
public DQPSKGardnerSymbolEvaluator()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the middle and current symbols to be evaluated for phase and timing errors and to determine the
|
||||
* transmitted symbol relative to the closest reference symbol. After invoking this method, you can access the
|
||||
* phase and timing errors and the symbol decision via their respective accessor methods.
|
||||
*
|
||||
* Phase and timing error values are calculated by first determining the symbol and then calculating the phase
|
||||
* and timing errors relative to the reference symbol. The timing error is corrected with the appropriate sign
|
||||
* relative to the angular vector rotation so that the error value indicates the correct error direction.
|
||||
*
|
||||
* @param middle interpolated differentially-decoded sample that falls midway between previous/current symbols
|
||||
* @param current interpolated differentially-decoded symbol
|
||||
*/
|
||||
public void setSymbols(Complex middle, Complex current)
|
||||
{
|
||||
/* Gardner timing error calculations */
|
||||
float errorInphase = (mPreviousSymbol.inphase() - current.inphase()) * middle.inphase();
|
||||
float errorQuadrature = (mPreviousSymbol.quadrature() - current.quadrature()) * middle.quadrature();
|
||||
mTimingError = normalize(errorInphase + errorQuadrature, 1.0f);
|
||||
|
||||
//Calculate phase error and determine the transmitted dibit from the symbol
|
||||
if(Math.abs(current.inphase()) > Math.abs(current.quadrature()))
|
||||
{
|
||||
if(current.inphase() > 0)
|
||||
{
|
||||
mSymbolDecision = Dibit.D10_MINUS_1;
|
||||
mPhaseError = -current.quadrature();
|
||||
}
|
||||
else
|
||||
{
|
||||
mSymbolDecision = Dibit.D01_PLUS_3;
|
||||
mPhaseError = current.quadrature();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(current.quadrature() > 0)
|
||||
{
|
||||
mSymbolDecision = Dibit.D00_PLUS_1;
|
||||
mPhaseError = current.inphase();
|
||||
}
|
||||
else
|
||||
{
|
||||
mSymbolDecision = Dibit.D11_MINUS_3;
|
||||
mPhaseError = -current.inphase();
|
||||
}
|
||||
}
|
||||
|
||||
mPreviousSymbol.setValues(current);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 timing error to +/- the maximum value and corrects any
|
||||
* floating point invalid numbers
|
||||
*/
|
||||
private float normalize(float error, float maximum)
|
||||
{
|
||||
if(Float.isNaN(error))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
return clip(error, maximum);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Phase error of the symbol relative to the nearest reference symbol.
|
||||
*
|
||||
* @return phase error in radians of distance from the reference symbol.
|
||||
*/
|
||||
@Override
|
||||
public float getPhaseError()
|
||||
{
|
||||
return mPhaseError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timing error of the symbol relative to the nearest reference symbol.
|
||||
*
|
||||
* @return timing error in radians of angular distance from the reference symbol recognizing that the symbol
|
||||
* originates at zero radians and rotates toward the intended reference symbol, therefore the error value indicates
|
||||
* if the symbol was sampled early (-) or late (+) relative to the reference symbol.
|
||||
*/
|
||||
@Override
|
||||
public float getTimingError()
|
||||
{
|
||||
return mTimingError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference symbol that is closest to the transmitted/sampled symbol.
|
||||
*/
|
||||
@Override
|
||||
public Dibit getSymbolDecision()
|
||||
{
|
||||
return mSymbolDecision;
|
||||
}
|
||||
}
|
||||
@@ -15,20 +15,8 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.dsp.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
|
||||
public interface IDQPSKSymbolEvaluator
|
||||
public interface IPSKSymbolEvaluator<T>
|
||||
{
|
||||
/**
|
||||
* Sets the sampled symbol to be evaluated. Note: the symbol's gain value MUST be normalized to the unit circle.
|
||||
*
|
||||
* @param preceding symbol used to detect (counter-)clock-wise rotation
|
||||
* @param symbol currently being sampled
|
||||
*/
|
||||
void setSymbol(Complex preceding, Complex symbol);
|
||||
|
||||
/**
|
||||
* Phase error of the symbol relative to the closest reference symbol
|
||||
* @return angular phase error in radians
|
||||
@@ -43,8 +31,8 @@ public interface IDQPSKSymbolEvaluator
|
||||
float getTimingError();
|
||||
|
||||
/**
|
||||
* Dibit decision indicating the reference symbol location closest to the symbol.
|
||||
* Symbol decision indicating the reference symbol location closest to the symbol.
|
||||
* @return symbol decision
|
||||
*/
|
||||
Dibit getSymbolDecision();
|
||||
T getSymbolDecision();
|
||||
}
|
||||
@@ -24,6 +24,7 @@ public class InterpolatingSampleBuffer
|
||||
|
||||
private Complex mPrecedingSample = new Complex(0,0);
|
||||
private Complex mCurrentSample = new Complex(0,0);
|
||||
private Complex mMiddleSample = new Complex(0,0);
|
||||
|
||||
private float[] mDelayLineInphase;
|
||||
private float[] mDelayLineQuadrature;
|
||||
@@ -42,8 +43,9 @@ public class InterpolatingSampleBuffer
|
||||
/**
|
||||
* Buffer to store complex sample data and produce interpolated samples.
|
||||
* @param samplesPerSymbol
|
||||
* @param symbolTimingGain for the symbol timing error adjustments
|
||||
*/
|
||||
public InterpolatingSampleBuffer(float samplesPerSymbol)
|
||||
public InterpolatingSampleBuffer(float samplesPerSymbol, float symbolTimingGain)
|
||||
{
|
||||
mSamplingPoint = samplesPerSymbol;
|
||||
mDetectedSamplesPerSymbol = samplesPerSymbol;
|
||||
@@ -52,6 +54,7 @@ public class InterpolatingSampleBuffer
|
||||
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
|
||||
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
|
||||
mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
|
||||
mDetectedSamplesPerSymbolGain = symbolTimingGain * mSampleCounterGain * mSampleCounterGain;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,18 +128,21 @@ public class InterpolatingSampleBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Un-interpolated sample that precedes the current interpolated sampling point. The current interpolated sampling
|
||||
* point falls somewhere between sample index 3 and index 4 and the sample is interpolated from sample
|
||||
* indexes 0 - 7, therefore the uninterpolated sample that immediately precedes the current sample is
|
||||
* located at index 3.
|
||||
*/
|
||||
public Complex getPrecedingSample()
|
||||
{
|
||||
mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 4], mDelayLineQuadrature[mDelayLinePointer + 3]);
|
||||
mPrecedingSample.setValues(mDelayLineInphase[mDelayLinePointer + 3], mDelayLineQuadrature[mDelayLinePointer + 3]);
|
||||
return mPrecedingSample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolated middle sample for the symbol. Note: this method should only be invoked after testing for a
|
||||
* complete symbol with the hasSymbol() method.
|
||||
* Interpolated current sample for the symbol.
|
||||
*
|
||||
* Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method.
|
||||
*/
|
||||
public Complex getCurrentSample()
|
||||
{
|
||||
@@ -145,6 +151,21 @@ public class InterpolatingSampleBuffer
|
||||
return mCurrentSample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolated sample that is 1/2 symbol away from (after) the current sample.
|
||||
*
|
||||
* Note: this method should only be invoked after testing for a complete symbol with the hasSymbol() method.
|
||||
*/
|
||||
public Complex getMiddleSample()
|
||||
{
|
||||
float halfDetectedSamplesPerSymbol = mDetectedSamplesPerSymbol / 2.0f;
|
||||
|
||||
//Interpolated sample that is half a symbol away from (occurred before) the current sample.
|
||||
mMiddleSample.setValues(getInphase(halfDetectedSamplesPerSymbol),
|
||||
getQuadrature(halfDetectedSamplesPerSymbol));
|
||||
return mMiddleSample;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interpolated inphase value for the specified offset
|
||||
* @param interpolation into the buffer to calculate the interpolated sample
|
||||
@@ -152,7 +173,15 @@ public class InterpolatingSampleBuffer
|
||||
*/
|
||||
public float getInphase(float interpolation)
|
||||
{
|
||||
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + 1, interpolation);
|
||||
if(interpolation < 1.0f)
|
||||
{
|
||||
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer, interpolation);
|
||||
}
|
||||
else
|
||||
{
|
||||
int offset = (int)Math.floor(interpolation);
|
||||
return mInterpolator.filter(mDelayLineInphase, mDelayLinePointer + offset, interpolation - offset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,6 +191,14 @@ public class InterpolatingSampleBuffer
|
||||
*/
|
||||
public float getQuadrature(float interpolation)
|
||||
{
|
||||
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + 1, interpolation);
|
||||
if(interpolation < 1.0f)
|
||||
{
|
||||
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer, interpolation);
|
||||
}
|
||||
else
|
||||
{
|
||||
int offset = (int)Math.floor(interpolation);
|
||||
return mInterpolator.filter(mDelayLineQuadrature, mDelayLinePointer + offset, interpolation - offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,9 @@ public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBu
|
||||
{
|
||||
private SymbolDecisionData mSymbolDecisionData;
|
||||
|
||||
public InterpolatingSampleBufferInstrumented(float samplesPerSymbol)
|
||||
public InterpolatingSampleBufferInstrumented(float samplesPerSymbol, float symbolTimingGain)
|
||||
{
|
||||
super(samplesPerSymbol);
|
||||
|
||||
super(samplesPerSymbol, symbolTimingGain);
|
||||
mSymbolDecisionData = new SymbolDecisionData((int)samplesPerSymbol);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,614 +0,0 @@
|
||||
package io.github.dsheirer.dsp.psk;
|
||||
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 - 2018 Dennis Sheirer
|
||||
*
|
||||
* Gardner detector, Costas loop and Interpolator derived from:
|
||||
*
|
||||
* OP25 - gardner_costas_cc_impl.cc
|
||||
* Copyright 2010, 2011, 2012, 2013 KA1RBI (gardner_costas_cc_impl.cc)
|
||||
*
|
||||
* OP25 - scope.py
|
||||
* Copyright 2008-2011 Steve Glass
|
||||
* Copyright 2011, 2012, 2013 KA1RBI
|
||||
*
|
||||
* GNURadio - control_loop and mpsk_receiver_cc
|
||||
* Copyright 2011,2013 Free Software Foundation, Inc.
|
||||
*
|
||||
* 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/>
|
||||
******************************************************************************/
|
||||
|
||||
import io.github.dsheirer.buffer.FloatAveragingBuffer;
|
||||
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.instrument.Instrumentable;
|
||||
import io.github.dsheirer.instrument.tap.Tap;
|
||||
import io.github.dsheirer.instrument.tap.TapGroup;
|
||||
import io.github.dsheirer.instrument.tap.stream.EyeDiagramData;
|
||||
import io.github.dsheirer.instrument.tap.stream.EyeDiagramDataTap;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implements a LSM (Pi/4) demodulator using a Gardner Detector to determine
|
||||
* optimal sampling timing and a Costas Loop as a phase locked loop synchronized
|
||||
* with the incoming signal carrier frequency.
|
||||
*
|
||||
* Sample Rate: 48000
|
||||
* Symbol Rate: 4800
|
||||
*/
|
||||
public class LSMDemodulator implements Instrumentable, ComplexSampleListener
|
||||
{
|
||||
private List<TapGroup> mAvailableTaps;
|
||||
|
||||
/* 45 degree rotation angle */
|
||||
public static final float THETA = (float)(Math.PI / 4.0d);
|
||||
|
||||
/* 45 degree point */
|
||||
public static final Complex POINT_45_DEGREES =
|
||||
new Complex( (float)Math.sin( THETA ), (float)Math.cos( THETA ) );
|
||||
|
||||
private Listener<Complex> mSymbolListener;
|
||||
|
||||
private GardnerSymbolTiming mGardnerDetector = new GardnerSymbolTiming();
|
||||
|
||||
private CostasLoop mCostasLoop = new CostasLoop();
|
||||
|
||||
private ISourceEventListener mFrequencyChangeListener;
|
||||
|
||||
private EyeDiagramDataTap mEyeDiagramDataTap;
|
||||
|
||||
public LSMDemodulator()
|
||||
{
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mSymbolListener = null;
|
||||
mFrequencyChangeListener = null;
|
||||
}
|
||||
|
||||
public void setSymbolListener( Listener<Complex> listener )
|
||||
{
|
||||
mSymbolListener = listener;
|
||||
}
|
||||
|
||||
public void removeSymbolListener()
|
||||
{
|
||||
mSymbolListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( float inphase, float quadrature )
|
||||
{
|
||||
mGardnerDetector.receive( inphase, quadrature );
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a phase correction value to the costas loop to correct when a
|
||||
* phase lock error is detected in the binary output stream.
|
||||
*
|
||||
* @param correction - value in radians
|
||||
*/
|
||||
public void correctPhaseError( double correction )
|
||||
{
|
||||
mCostasLoop.correctPhaseError( correction );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 timing error to +/- the maximum value and corrects any
|
||||
* floating point invalid numbers
|
||||
*/
|
||||
private float normalize( float error, float maximum )
|
||||
{
|
||||
if( Float.isNaN( error ) )
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
return clip( error, maximum );
|
||||
}
|
||||
}
|
||||
|
||||
public void addListener( ISourceEventListener listener )
|
||||
{
|
||||
mFrequencyChangeListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener( ISourceEventListener listener )
|
||||
{
|
||||
mFrequencyChangeListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the current loop frequency of the costas loop and broadcasts
|
||||
* tuner frequency offset adjustments 10 times a second as needed to keep
|
||||
* the frequency offset below half of the maximum frequency
|
||||
*/
|
||||
public class FrequencyControl
|
||||
{
|
||||
/* Set the trigger threshold at half of the maximum costas loop control
|
||||
* frequency */
|
||||
private static final float CORRECTION_THRESHOLD_FREQUENCY =
|
||||
(2.0f * (float)Math.PI * 600.0f) / 48000.0f;
|
||||
|
||||
private FloatAveragingBuffer mBuffer = new FloatAveragingBuffer(40);
|
||||
|
||||
private long mFrequencyError = 0;
|
||||
|
||||
private int mCounter = 0;
|
||||
|
||||
public void receive(float frequency)
|
||||
{
|
||||
// mCounter++;
|
||||
//
|
||||
// if( mCounter >= 480 )
|
||||
// {
|
||||
// float average = mBuffer.get( frequency );
|
||||
// mLog.debug( "Avg:" + average + " Threshold:" + CORRECTION_THRESHOLD_FREQUENCY);
|
||||
//
|
||||
// boolean correctionNeeded = false;
|
||||
//
|
||||
// if( average > CORRECTION_THRESHOLD_FREQUENCY )
|
||||
// {
|
||||
// mFrequencyError--;
|
||||
//
|
||||
// correctionNeeded = true;
|
||||
// }
|
||||
// else if( average < -CORRECTION_THRESHOLD_FREQUENCY )
|
||||
// {
|
||||
// mFrequencyError++;
|
||||
//
|
||||
// correctionNeeded = true;
|
||||
// }
|
||||
//
|
||||
// if( correctionNeeded && mFrequencyChangeListener != null )
|
||||
// {
|
||||
// mLog.debug( "Issuing Frequency Correction: " + mFrequencyError );
|
||||
// mFrequencyChangeListener.process(
|
||||
// new SourceEvent( Attribute.FREQUENCY_ERROR,
|
||||
// mFrequencyError ) );
|
||||
// }
|
||||
//
|
||||
// mCounter = 0;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// mBuffer.get( frequency );
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gardner symbol timing detector
|
||||
*/
|
||||
public class GardnerSymbolTiming implements ComplexSampleListener
|
||||
{
|
||||
public static final int HALF_SAMPLES_PER_SYMBOL = 5;
|
||||
public static final int SAMPLES_PER_SYMBOL = 10;
|
||||
public static final int TWICE_SAMPLES_PER_SYMBOL = 20;
|
||||
|
||||
private float[] mDelayLineInphase = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
|
||||
private float[] mDelayLineQuadrature = new float[2 * TWICE_SAMPLES_PER_SYMBOL];
|
||||
|
||||
private int mDelayLinePointer = 0;
|
||||
|
||||
/* Sampling point */
|
||||
private float mMu = 10.0f;
|
||||
private float mGainMu = 0.05f;
|
||||
|
||||
/* Samples per symbol */
|
||||
private float mOmega = 10.0f;
|
||||
private float mGainOmega = 0.1f * mGainMu * mGainMu;
|
||||
private float mOmegaRel = 0.005f;
|
||||
private float mOmegaMid = 10.0f;
|
||||
|
||||
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
|
||||
|
||||
private Complex mPreviousSample = new Complex(0, 0);
|
||||
private Complex mPreviousMiddleSample = new Complex(0, 0);
|
||||
private Complex mPreviousSymbol = new Complex(0, 0);
|
||||
|
||||
/**
|
||||
* Provides symbol sampling timing control
|
||||
*/
|
||||
public GardnerSymbolTiming()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(float inphase, float quadrature)
|
||||
{
|
||||
/* Count down samples per symbol until we calculate the symbol */
|
||||
mMu--;
|
||||
|
||||
mCostasLoop.increment();
|
||||
|
||||
/* Mix incoming sample with costas loop to remove any rotation
|
||||
* that is present from a mis-tuned carrier frequency */
|
||||
|
||||
Complex costas = mCostasLoop.getCurrentVector();
|
||||
|
||||
float derotatedInphase = Complex.multiplyInphase(inphase, quadrature, costas.inphase(), costas.quadrature());
|
||||
float derotatedQuadrature = Complex.multiplyQuadrature(inphase, quadrature, costas.inphase(), costas.quadrature());
|
||||
|
||||
/* Fill up the delay line to use with the interpolator */
|
||||
mDelayLineInphase[mDelayLinePointer] = derotatedInphase;
|
||||
mDelayLineInphase[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedInphase;
|
||||
|
||||
mDelayLineQuadrature[mDelayLinePointer] = derotatedQuadrature;
|
||||
mDelayLineQuadrature[mDelayLinePointer + TWICE_SAMPLES_PER_SYMBOL] = derotatedQuadrature;
|
||||
|
||||
/* Increment pointer and keep pointer in bounds */
|
||||
mDelayLinePointer = (mDelayLinePointer + 1) % TWICE_SAMPLES_PER_SYMBOL;
|
||||
|
||||
/* Calculate the symbol once we've stored enough samples */
|
||||
if(mMu <= 1.0f)
|
||||
{
|
||||
float half_omega = mOmega / 2.0f;
|
||||
int half_sps = (int)Math.floor(half_omega);
|
||||
float half_mu = mMu + half_omega - (float)half_sps;
|
||||
|
||||
if(half_mu > 1.0)
|
||||
{
|
||||
half_mu -= 1.0;
|
||||
half_sps += 1;
|
||||
}
|
||||
|
||||
/* Calculate interpolated middle sample and current sample */
|
||||
float middleSampleInphase = mInterpolator.filter(
|
||||
mDelayLineInphase, mDelayLinePointer, mMu);
|
||||
float middleSampleQuadrature = mInterpolator.filter(
|
||||
mDelayLineQuadrature, mDelayLinePointer, mMu);
|
||||
|
||||
int index = mDelayLinePointer + half_sps;
|
||||
|
||||
float currentSampleInphase = mInterpolator.filter(
|
||||
mDelayLineInphase, index, half_mu);
|
||||
float currentSampleQuadrature = mInterpolator.filter(
|
||||
mDelayLineQuadrature, index, half_mu);
|
||||
|
||||
/* Multiply current sample and conjugate (negated quadrature) of
|
||||
* previous sample to get symbols to use for gardner error feedback */
|
||||
Complex middleSymbol = Complex.multiply(middleSampleInphase,
|
||||
middleSampleQuadrature, mPreviousMiddleSample.conjugate());
|
||||
Complex currentSymbol = Complex.multiply(currentSampleInphase,
|
||||
currentSampleQuadrature, mPreviousSample.conjugate());
|
||||
|
||||
/* Set gain to unity */
|
||||
middleSymbol.normalize();
|
||||
currentSymbol.normalize();
|
||||
|
||||
/* Gardner timing error calculations */
|
||||
float errorInphase = (mPreviousSymbol.inphase() -
|
||||
currentSymbol.inphase()) * middleSymbol.inphase();
|
||||
|
||||
float errorQuadrature = (mPreviousSymbol.quadrature() -
|
||||
currentSymbol.quadrature()) * middleSymbol.quadrature();
|
||||
|
||||
float gardnerError = normalize(errorInphase + errorQuadrature, 1.0f);
|
||||
|
||||
if(mEyeDiagramDataTap != null)
|
||||
{
|
||||
mEyeDiagramDataTap.receive(
|
||||
new EyeDiagramData(
|
||||
Arrays.copyOfRange(mDelayLineInphase, 0, 20),
|
||||
Arrays.copyOfRange(mDelayLineQuadrature, 0, 20),
|
||||
mMu, (float)half_sps + half_mu, gardnerError));
|
||||
}
|
||||
|
||||
/* mOmega is samples per symbol and is constrained to floating
|
||||
* between +/- .005 of the nominal 10.0 samples per symbol */
|
||||
mOmega = mOmega + mGainOmega * gardnerError;
|
||||
mOmega = mOmegaMid + clip(mOmega - mOmegaMid, mOmegaRel);
|
||||
|
||||
/* Adjust sample timing based on error of current sample */
|
||||
mMu += mOmega + (mGainMu * gardnerError);
|
||||
|
||||
/* Store current samples/symbols to use for the next period */
|
||||
mPreviousSample.setValues(currentSampleInphase, currentSampleQuadrature);
|
||||
mPreviousMiddleSample.setValues(middleSampleInphase, middleSampleQuadrature);
|
||||
mPreviousSymbol = currentSymbol;
|
||||
|
||||
/* Update costas loop using phase error present in current
|
||||
* symbol. The symbol is rotated from star orientation to polar
|
||||
* orientation to simplify error calculation */
|
||||
mCostasLoop.receive(currentSymbol);
|
||||
// mCostasLoop.receive(
|
||||
// ComplexSample.multiply( currentSymbol, POINT_45_DEGREES ) );
|
||||
|
||||
/* Dispatch the differentiated symbol to the registered listener */
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSymbolListener.receive(currentSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Costas Loop - phase locked loop synchronized to the frequency offset of
|
||||
* the incoming signal to enable the signal to be mixed down to zero offset
|
||||
* for proper synchronization. The mFrequency value indicates the detected
|
||||
* frequency offset. We attempt to keep that value close to zero by issuing
|
||||
* frequency adjustments to the tuner channel source.
|
||||
*
|
||||
* Most of the costas loop code was ported from gnuradio/control_loop and
|
||||
* gnuradio/mpsk_receiver_cc using initialization values from KA1RBI's
|
||||
* OP25/cqpsk.py
|
||||
*/
|
||||
public class CostasLoop implements Listener<Complex>, IPhaseLockedLoop
|
||||
{
|
||||
public static final double TWO_PI = 2.0 * Math.PI;
|
||||
|
||||
private static final float MAXIMUM_FREQUENCY =
|
||||
(2400.0f * (float)TWO_PI) / 48000.0f;
|
||||
|
||||
/* http://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html */
|
||||
private float mDamping = (float)Math.sqrt(2.0) / 2.0f;
|
||||
|
||||
/* Use denominator between 100 (most) and 400 (least) to adjust costas
|
||||
* loop control level */
|
||||
private float mLoopBandwidth = (float)(TWO_PI / 400.0d);
|
||||
|
||||
private float mAlphaGain = (4.0f * mDamping * mLoopBandwidth) /
|
||||
(1.0f + (2.0f * mDamping * mLoopBandwidth) +
|
||||
(mLoopBandwidth * mLoopBandwidth));
|
||||
|
||||
private float mBetaGain = (4.0f * mLoopBandwidth * mLoopBandwidth) /
|
||||
(1.0f + (2.0f * mDamping * mLoopBandwidth) +
|
||||
(mLoopBandwidth * mLoopBandwidth));
|
||||
|
||||
private float mLoopPhase = 0.0f;
|
||||
|
||||
private float mLoopFrequency = 0.0f;
|
||||
|
||||
private FrequencyControl mFrequencyControl = new FrequencyControl();
|
||||
|
||||
public CostasLoop()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a phase correction value to the current phasor. Use this
|
||||
* method to apply correction when a +/- 90, or 180 degree phase error
|
||||
* is detected in the binary output stream.
|
||||
*
|
||||
* If the supplied correction value places the loop frequency outside
|
||||
* of the max frequency, then the frequency will be corrected 360
|
||||
* degrees in the opposite direction to maintain within the max
|
||||
* frequency bounds.
|
||||
*
|
||||
* @param correction - correction value in radians
|
||||
*/
|
||||
public void correctPhaseError(double correction)
|
||||
{
|
||||
mLoopFrequency += correction;
|
||||
|
||||
if(mLoopFrequency > MAXIMUM_FREQUENCY)
|
||||
{
|
||||
mLoopFrequency -= 2.0d * MAXIMUM_FREQUENCY;
|
||||
}
|
||||
|
||||
if(mLoopFrequency < -MAXIMUM_FREQUENCY)
|
||||
{
|
||||
mLoopFrequency += 2.0d * MAXIMUM_FREQUENCY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the phase of the loop
|
||||
*/
|
||||
public void increment()
|
||||
{
|
||||
mLoopPhase += mLoopFrequency;
|
||||
|
||||
/* Keep the loop phase in bounds */
|
||||
unwrapPhase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mLoopPhase = 0.0f;
|
||||
mLoopFrequency = 0.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void adjust(double error)
|
||||
{
|
||||
adjust(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void correctInversion(double correction)
|
||||
{
|
||||
correctPhaseError(correction);
|
||||
}
|
||||
|
||||
private void unwrapPhase()
|
||||
{
|
||||
while(mLoopPhase > TWO_PI)
|
||||
{
|
||||
mLoopPhase -= TWO_PI;
|
||||
}
|
||||
|
||||
while(mLoopPhase < -TWO_PI)
|
||||
{
|
||||
mLoopPhase += TWO_PI;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current vector of the loop rotated by THETA degrees.
|
||||
*/
|
||||
public Complex getCurrentVector()
|
||||
{
|
||||
// return ComplexSample.fromAngle( mLoopPhase + THETA );
|
||||
return Complex.fromAngle(mLoopPhase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Complex incrementAndGetCurrentVector()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Complex complex)
|
||||
{
|
||||
/* Calculate phase error */
|
||||
float phaseError = getPhaseError(complex);
|
||||
|
||||
Dibit d = mQPSKSymbolDecoder.decode(complex);
|
||||
// mLog.debug( "Dibit: " + d.name() + " Error: " + phaseError );
|
||||
|
||||
/* Adjust for phase error */
|
||||
adjust(phaseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the costas loop frequency and phase to adjust for the phase
|
||||
* error value
|
||||
*
|
||||
* @param phase_error - (-)= late and (+)= early
|
||||
*/
|
||||
private void adjust(float phase_error)
|
||||
{
|
||||
mLoopFrequency += mBetaGain * phase_error;
|
||||
mLoopPhase += mLoopFrequency + mAlphaGain * phase_error;
|
||||
|
||||
/* Maintain phase between +/- 2 * PI */
|
||||
unwrapPhase();
|
||||
|
||||
/* Limit frequency to +/- maximum loop frequency */
|
||||
limitFrequency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains the frequency within the bounds of +/- loop frequency
|
||||
*/
|
||||
private void limitFrequency()
|
||||
{
|
||||
/* Check for and issue tuner offset correction */
|
||||
mFrequencyControl.receive(mLoopFrequency);
|
||||
|
||||
if(mLoopFrequency > MAXIMUM_FREQUENCY)
|
||||
{
|
||||
mLoopFrequency = MAXIMUM_FREQUENCY;
|
||||
}
|
||||
|
||||
if(mLoopFrequency < -MAXIMUM_FREQUENCY)
|
||||
{
|
||||
mLoopFrequency = -MAXIMUM_FREQUENCY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects rotational error present when the costas loop is not aligned
|
||||
* with the carrier frequency. Provides error feedback to adjust
|
||||
* mixer frequency.
|
||||
*/
|
||||
public float getPhaseError(Complex complex)
|
||||
{
|
||||
float phase_error = 0;
|
||||
|
||||
if(Math.abs(complex.inphase()) > Math.abs(complex.quadrature()))
|
||||
{
|
||||
if(complex.inphase() > 0)
|
||||
{
|
||||
phase_error = -complex.quadrature();
|
||||
}
|
||||
else
|
||||
{
|
||||
phase_error = complex.quadrature();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(complex.quadrature() > 0)
|
||||
{
|
||||
phase_error = complex.inphase();
|
||||
}
|
||||
else
|
||||
{
|
||||
phase_error = -complex.inphase();
|
||||
}
|
||||
}
|
||||
|
||||
return phase_error;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if(mAvailableTaps == null)
|
||||
{
|
||||
mAvailableTaps = new ArrayList<>();
|
||||
|
||||
TapGroup group = new TapGroup("LSM Demodulator");
|
||||
|
||||
group.add(new EyeDiagramDataTap("Eye Diagram", 0, 4800));
|
||||
|
||||
mAvailableTaps.add(group);
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerTap(Tap tap)
|
||||
{
|
||||
if(tap instanceof EyeDiagramDataTap)
|
||||
{
|
||||
mEyeDiagramDataTap = (EyeDiagramDataTap)tap;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterTap(Tap tap)
|
||||
{
|
||||
if(tap instanceof EyeDiagramDataTap)
|
||||
{
|
||||
mEyeDiagramDataTap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
137
src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java
Normal file
137
src/main/java/io/github/dsheirer/dsp/psk/PSKDemodulator.java
Normal file
@@ -0,0 +1,137 @@
|
||||
/*******************************************************************************
|
||||
* 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.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
|
||||
|
||||
public abstract class PSKDemodulator<T> implements ComplexSampleListener
|
||||
{
|
||||
private InterpolatingSampleBuffer mInterpolatingSampleBuffer;
|
||||
private IPhaseLockedLoop mPLL;
|
||||
private Complex mReceivedSample = new Complex(0, 0);
|
||||
private Listener<T> mSymbolListener;
|
||||
|
||||
/**
|
||||
* Abstract Phase Shift Keyed (PSK) demodulator
|
||||
* @param interpolatingSampleBuffer to store complex samples and provide access to interpolated samples
|
||||
* @param phaseLockedLoop to track and remove frequency offset in the incoming samples
|
||||
*/
|
||||
public PSKDemodulator(InterpolatingSampleBuffer interpolatingSampleBuffer, IPhaseLockedLoop phaseLockedLoop)
|
||||
{
|
||||
mInterpolatingSampleBuffer = interpolatingSampleBuffer;
|
||||
mPLL = phaseLockedLoop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive symbol decisions from this demodulator
|
||||
*/
|
||||
public void setSymbolListener(Listener<T> listener)
|
||||
{
|
||||
mSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the symbol decision to the registered symbol listener
|
||||
* @param symbol
|
||||
*/
|
||||
protected void broadcast(T symbol)
|
||||
{
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSymbolListener.receive(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolating sample buffer for receiving the incoming complex sample stream and providing access to
|
||||
* indexed and interpolated samples
|
||||
*/
|
||||
protected InterpolatingSampleBuffer getInterpolatingSampleBuffer()
|
||||
{
|
||||
return mInterpolatingSampleBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase Locked Loop (PLL)
|
||||
*/
|
||||
protected IPhaseLockedLoop getPLL()
|
||||
{
|
||||
return mPLL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a (filtered) buffer containing complex samples for decoding
|
||||
* @param reusableComplexBuffer with complex samples
|
||||
*/
|
||||
public void receive(ReusableComplexBuffer reusableComplexBuffer)
|
||||
{
|
||||
float[] samples = reusableComplexBuffer.getSamples();
|
||||
|
||||
for(int x = 0; x < samples.length; x += 2)
|
||||
{
|
||||
receive(samples[x], samples[x + 1]);
|
||||
}
|
||||
|
||||
reusableComplexBuffer.decrementUserCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a complex sample for decoding. Once sufficient samples are buffered, a symbol decision is made.
|
||||
* @param inphase value for the sample
|
||||
* @param quadrature value for the sample
|
||||
*/
|
||||
@Override
|
||||
public void receive(float inphase, float quadrature)
|
||||
{
|
||||
//Update current sample with values
|
||||
mReceivedSample.setValues(inphase, quadrature);
|
||||
|
||||
//Mix current sample with costas loop to remove any rotation that is present from a mis-tuned carrier frequency
|
||||
mReceivedSample.multiply(mPLL.incrementAndGetCurrentVector());
|
||||
|
||||
//Store the sample in the interpolating buffer
|
||||
mInterpolatingSampleBuffer.receive(mReceivedSample);
|
||||
|
||||
//Calculate the symbol once we've stored enough samples
|
||||
if(mInterpolatingSampleBuffer.hasSymbol())
|
||||
{
|
||||
calculateSymbol();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void calculateSymbol();
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user