39 - Adds P25/LSM support to the DemodulatorViewerFX application and

adds Feed forward gain support to the C4FM DQPSK demodulator which seems
to improve output from the interpolating sample buffer at each symbol
period.  Updates the symbol timing gain for C4FM to decrease the gain a
bit since the feed forward gain is providing a better interpolated symbol.
This commit is contained in:
Denny
2018-03-24 11:03:18 -04:00
parent bfaacb309d
commit c36f6901af
16 changed files with 130 additions and 76 deletions

View File

@@ -23,7 +23,7 @@ import io.github.dsheirer.sample.complex.reusable.ReusableComplexBuffer;
public class ComplexFIRFilter extends FIRFilter
{
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue();
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("Complex FIR Filter");
private RealFIRFilter mIFilter;
private RealFIRFilter mQFilter;
@@ -84,13 +84,13 @@ public class ComplexFIRFilter extends FIRFilter
*/
public ReusableComplexBuffer filter(ReusableComplexBuffer originalBuffer)
{
ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(originalBuffer.getSampleLength());
ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(originalBuffer.getSamples().length);
filteredBuffer.setTimestamp(originalBuffer.getTimestamp());
float[] samples = originalBuffer.getSamples();
float[] filteredSamples = filteredBuffer.getSamples();
for(int x = 0; x < originalBuffer.getSampleLength(); x += 2)
for(int x = 0; x < originalBuffer.getSamples().length; x += 2)
{
filteredSamples[x] = filterInphase(samples[x]);
filteredSamples[x + 1] = filterQuadrature(samples[x + 1]);

View File

@@ -29,7 +29,7 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener
private ComplexSampleListener mListener;
private FloatCircularBuffer mEnvelopeHistory;
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue();
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("ComplexFeedForwardGainControl");
private float mMaxEnvelope = 0.0f;
private float mGain = 1.0f;
@@ -155,11 +155,36 @@ public class ComplexFeedForwardGainControl implements ComplexSampleListener
*/
public ReusableComplexBuffer filter(ReusableComplexBuffer buffer)
{
float[] filtered = filter(buffer.getSamples());
float[] samples = buffer.getSamples();
ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(filtered.length);
ReusableComplexBuffer filteredBuffer = mReusableBufferQueue.getBuffer(samples.length);
filteredBuffer.setTimestamp(buffer.getTimestamp());
filteredBuffer.reloadFrom(filtered, buffer.getTimestamp());
float[] filtered = filteredBuffer.getSamples();
mMaxEnvelope = MINIMUM_ENVELOPE;
float currentEnvelope;
for(int x = 0; x < samples.length; x += 2)
{
currentEnvelope = Complex.envelope(samples[x], samples[x + 1]);
if(currentEnvelope > mMaxEnvelope)
{
mMaxEnvelope = currentEnvelope;
}
}
adjustGain();
for(int x = 0; x < samples.length; x += 2)
{
filtered[x] = samples[x] * mGain;
filtered[x + 1] = samples[x + 1] * mGain;
}
buffer.decrementUserCount();
filteredBuffer.incrementUserCount();
return filteredBuffer;

View File

@@ -25,7 +25,7 @@ public class DQPSKDecisionDirectedSymbolEvaluator implements IPSKSymbolEvaluator
private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * Math.PI / 4.0);
private Complex mSymbolError = new Complex(0, 0);
private Complex mEvaluationSymbol = new Complex(0, 0);
private float mPhaseError = 0.0f;
private float mTimingError = 0.0f;
private float mTimingErrorPolarity = 1.0f;
@@ -59,46 +59,46 @@ public class DQPSKDecisionDirectedSymbolEvaluator implements IPSKSymbolEvaluator
*/
public void setSymbol(Complex preceding, Complex current)
{
mSymbolError.setValues(current);
mEvaluationSymbol.setValues(current);
if(mSymbolError.quadrature() > 0.0f)
if(mEvaluationSymbol.quadrature() > 0.0f)
{
if(mSymbolError.inphase() > 0.0f)
if(mEvaluationSymbol.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D00_PLUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_PLUS_45);
mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_45);
}
else
{
mSymbolDecision = Dibit.D01_PLUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_PLUS_135);
mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_135);
}
}
else
{
if(mSymbolError.inphase() > 0.0f)
if(mEvaluationSymbol.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D10_MINUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_MINUS_45);
mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_45);
}
else
{
mSymbolDecision = Dibit.D11_MINUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mSymbolError.multiply(ROTATE_FROM_MINUS_135);
mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_135);
}
}
//Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the
//arctan of the error angle relative to 0 radians and this provides our error value
mPhaseError = -mSymbolError.quadrature();
mPhaseError = -mEvaluationSymbol.quadrature();
//Timing error is the same as phase error with the sign corrected according to the vector's angular rotation
mTimingError = mSymbolError.quadrature() * mTimingErrorPolarity;
mTimingError = mEvaluationSymbol.quadrature() * mTimingErrorPolarity;
}
/**

View File

@@ -20,10 +20,16 @@ import io.github.dsheirer.sample.complex.Complex;
public class DQPSKGardnerSymbolEvaluator 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);
private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * Math.PI / 4.0);
private float mPhaseError = 0.0f;
private float mTimingError = 0.0f;
private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
private Complex mPreviousSymbol = new Complex(0, 0);
private Complex mEvaluationSymbol = new Complex(0, 0);
/**
* Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer.
@@ -53,40 +59,48 @@ public class DQPSKGardnerSymbolEvaluator implements IPSKSymbolEvaluator<Dibit>
*/
public void setSymbols(Complex middle, Complex current)
{
/* Gardner timing error calculations */
//Gardner timing error calculation
float errorInphase = (mPreviousSymbol.inphase() - current.inphase()) * middle.inphase();
float errorQuadrature = (mPreviousSymbol.quadrature() - current.quadrature()) * middle.quadrature();
mTimingError = normalize(errorInphase + errorQuadrature, 1.0f);
mTimingError = normalize(errorInphase + errorQuadrature, .3f);
//Calculate phase error and determine the transmitted dibit from the symbol
if(Math.abs(current.inphase()) > Math.abs(current.quadrature()))
//Store the current symbol to use in the next symbol calculation
mPreviousSymbol.setValues(current);
//Phase error and symbol decision calculations ...
mEvaluationSymbol.setValues(current);
if(mEvaluationSymbol.quadrature() > 0.0f)
{
if(current.inphase() > 0)
if(mEvaluationSymbol.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D10_MINUS_1;
mPhaseError = -current.quadrature();
mSymbolDecision = Dibit.D00_PLUS_1;
mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_45);
}
else
{
mSymbolDecision = Dibit.D01_PLUS_3;
mPhaseError = current.quadrature();
mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_135);
}
}
else
{
if(current.quadrature() > 0)
if(mEvaluationSymbol.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D00_PLUS_1;
mPhaseError = current.inphase();
mSymbolDecision = Dibit.D10_MINUS_1;
mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_45);
}
else
{
mSymbolDecision = Dibit.D11_MINUS_3;
mPhaseError = -current.inphase();
mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_135);
}
}
mPreviousSymbol.setValues(current);
//Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the
//arctan of the error angle relative to 0 radians and this provides our error value
mPhaseError = -mEvaluationSymbol.quadrature();
}
/**

View File

@@ -32,7 +32,7 @@ public class InterpolatingSampleBuffer
private int mTwiceSamplesPerSymbol;
private float mSamplingPoint;
private float mSampleCounterGain = 1.0f;
private float mSampleCounterGain = 0.5f;
private float mDetectedSamplesPerSymbol;
private float mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain;
private float mMaximumSamplesPerSymbol;
@@ -43,9 +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
* @param sampleCounterGain for the symbol timing error adjustments
*/
public InterpolatingSampleBuffer(float samplesPerSymbol, float symbolTimingGain)
public InterpolatingSampleBuffer(float samplesPerSymbol, float sampleCounterGain)
{
mSamplingPoint = samplesPerSymbol;
mDetectedSamplesPerSymbol = samplesPerSymbol;
@@ -54,7 +54,9 @@ public class InterpolatingSampleBuffer
mTwiceSamplesPerSymbol = (int)Math.floor(2.0 * samplesPerSymbol);
mDelayLineInphase = new float[2 * mTwiceSamplesPerSymbol];
mDelayLineQuadrature = new float[2 * mTwiceSamplesPerSymbol];
mDetectedSamplesPerSymbolGain = symbolTimingGain * mSampleCounterGain * mSampleCounterGain;
mSampleCounterGain = sampleCounterGain;
mDetectedSamplesPerSymbolGain = 0.1f * mSampleCounterGain * mSampleCounterGain;
}
/**

View File

@@ -19,7 +19,7 @@ import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.dsp.gain.ComplexGain;
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.reusable.ReusableComplexBuffer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
@@ -31,7 +31,7 @@ import javafx.scene.chart.NumberAxis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ComplexPhaseLineChart extends LineChart implements Listener<ComplexBuffer>
public class ComplexPhaseLineChart extends LineChart implements Listener<ReusableComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexPhaseLineChart.class);
@@ -81,7 +81,7 @@ public class ComplexPhaseLineChart extends LineChart implements Listener<Complex
}
@Override
public void receive(ComplexBuffer complexBuffer)
public void receive(ReusableComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
@@ -95,6 +95,8 @@ public class ComplexPhaseLineChart extends LineChart implements Listener<Complex
mComplexCircularBuffer.put(copy);
}
complexBuffer.decrementUserCount();
updateChart();
}

View File

@@ -34,9 +34,9 @@ public class EyeDiagramChart extends LineChart implements Listener<SymbolDecisio
private int mSeriesPointer;
private int mSeriesLength;
public EyeDiagramChart(int seriesCount)
public EyeDiagramChart(int seriesCount, String legend)
{
super(new NumberAxis("Symbol Timing (5 = center)", 1.0, 10.0, 1.0),
super(new NumberAxis("Symbol Timing (" + legend + ")", 1.0, 10.0, 1.0),
new NumberAxis("Value", -1.25, 1.25, 0.25));
mSeriesCount = seriesCount * 2;

View File

@@ -19,7 +19,7 @@ import io.github.dsheirer.buffer.DoubleCircularBuffer;
import io.github.dsheirer.dsp.fm.FMDemodulator;
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.reusable.ReusableComplexBuffer;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.LineChart;
@@ -27,7 +27,7 @@ import javafx.scene.chart.NumberAxis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FMDemodulatedLineChart extends LineChart implements Listener<ComplexBuffer>
public class FMDemodulatedLineChart extends LineChart implements Listener<ReusableComplexBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(FMDemodulatedLineChart.class);
@@ -73,7 +73,7 @@ public class FMDemodulatedLineChart extends LineChart implements Listener<Comple
}
@Override
public void receive(ComplexBuffer complexBuffer)
public void receive(ReusableComplexBuffer complexBuffer)
{
float[] samples = complexBuffer.getSamples();
@@ -97,6 +97,8 @@ public class FMDemodulatedLineChart extends LineChart implements Listener<Comple
mPreviousSample = sample;
updateChart(demodulated);
}
complexBuffer.decrementUserCount();
}
private void updateChart(double value)

View File

@@ -156,7 +156,7 @@ public class P25Phase1C4FMPane extends DecoderPane
{
if(mEyeDiagramChart == null)
{
mEyeDiagramChart = new EyeDiagramChart(10);
mEyeDiagramChart = new EyeDiagramChart(10, "Symbol: 3-4");
}
return mEyeDiagramChart;

View File

@@ -156,7 +156,7 @@ public class P25Phase1LSMPane extends DecoderPane
{
if(mEyeDiagramChart == null)
{
mEyeDiagramChart = new EyeDiagramChart(10);
mEyeDiagramChart = new EyeDiagramChart(10, "Middle: 3-4 / Symbol: 8-9");
}
return mEyeDiagramChart;

View File

@@ -20,6 +20,7 @@ import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter;
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator;
import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
@@ -34,11 +35,12 @@ import java.util.Map;
public class P25DecoderC4FM extends P25Decoder
{
private final static Logger mLog = LoggerFactory.getLogger(P25DecoderC4FM.class);
protected static final float SYMBOL_TIMING_GAIN = 0.1f;
protected static final float SAMPLE_COUNTER_GAIN = 0.5f;
protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator;
protected CostasLoop mCostasLoop;
protected P25MessageFramer mMessageFramer;
private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
private ComplexFIRFilter mBasebandFilter;
@@ -61,7 +63,7 @@ public class P25DecoderC4FM extends P25Decoder
mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN);
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
@@ -79,10 +81,13 @@ public class P25DecoderC4FM extends P25Decoder
public void receive(ReusableComplexBuffer reusableComplexBuffer)
{
//User accounting of the incoming buffer is handled by the filter
ReusableComplexBuffer filtered = filter(reusableComplexBuffer);
ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer);
//User accounting of the incoming buffer is handled by the gain filter
ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered);
//User accounting of the filtered buffer is handled by the demodulator
mQPSKDemodulator.receive(filtered);
mQPSKDemodulator.receive(gainApplied);
}
/**

View File

@@ -50,9 +50,6 @@ public class P25DecoderC4FMInstrumented extends P25DecoderC4FM
{
ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer);
//Increment user count before we hand to another listener or return from this method
filtered.incrementUserCount();
if(mFilteredSymbolListener != null)
{
filtered.incrementUserCount();
@@ -70,7 +67,7 @@ public class P25DecoderC4FMInstrumented extends P25DecoderC4FM
super.setSampleRate(sampleRate);
InterpolatingSampleBufferInstrumented instrumentedBuffer =
new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN);
new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mInterpolatingSampleBuffer = instrumentedBuffer;
DQPSKDecisionDirectedDemodulatorInstrumented instrumented = new DQPSKDecisionDirectedDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate());

View File

@@ -34,7 +34,7 @@ import java.util.Map;
public class P25DecoderLSM extends P25Decoder
{
private final static Logger mLog = LoggerFactory.getLogger(P25DecoderLSM.class);
protected static final float SYMBOL_TIMING_GAIN = 0.1f;
protected static final float SAMPLE_COUNTER_GAIN = 0.5f;
private Map<Double,float[]> mBasebandFilters = new HashMap<>();
private ComplexFIRFilter mBasebandFilter;
@@ -67,7 +67,7 @@ public class P25DecoderLSM extends P25Decoder
mBasebandFilter = new ComplexFIRFilter(getBasebandFilter(), 1.0f);
mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN);
mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mQPSKDemodulator = new DQPSKGardnerDemodulator(mCostasLoop, mInterpolatingSampleBuffer);

View File

@@ -52,9 +52,6 @@ public class P25DecoderLSMInstrumented extends P25DecoderLSM
{
ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer);
//Increment user count before we hand to another listener or return from this method
filtered.incrementUserCount();
if(mFilteredSymbolListener != null)
{
filtered.incrementUserCount();
@@ -71,7 +68,7 @@ public class P25DecoderLSMInstrumented extends P25DecoderLSM
{
super.setSampleRate(sampleRate);
InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SYMBOL_TIMING_GAIN);
InterpolatingSampleBufferInstrumented instrumentedBuffer = new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
mInterpolatingSampleBuffer = instrumentedBuffer;
DQPSKGardnerDemodulatorInstrumented instrumented = new DQPSKGardnerDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate());

View File

@@ -248,12 +248,35 @@ public class Complex implements Serializable
}
/**
* Returns the vector length to 1 (unit circle),
* avoiding square root multiplication
* Maintains the vector magnitude close to 1.0 (unit circle) when the magnitude is already
* close to 1.0, avoiding square root multiplication. If you need a faster way to set the
* magnitude to close to 1.0 without the square root of normalize(), use fastNormalize2().
*/
public void fastNormalize()
{
multiply((float)(1.95f - magnitudeSquared()));
multiply((float)(1.9999f - magnitudeSquared()));
}
/**
* Sets vector magnitude to close to 1.0f without the magnitude square root operation
*/
public void normalizeFast()
{
float inphaseAbsolute = Math.abs(inphase());
float quadratureAbsolute = Math.abs(quadrature());
float gain;
if(inphaseAbsolute > quadratureAbsolute)
{
gain = 1.0f / (inphaseAbsolute + (0.4f * quadratureAbsolute));
}
else
{
gain = 1.0f / (quadratureAbsolute + (0.4f * inphaseAbsolute));
}
multiply(gain);
}
public float left()
@@ -434,17 +457,4 @@ public class Complex implements Serializable
return quadratureAbsolute + (0.4f * inphaseAbsolute);
}
}
public static void main(String[] args)
{
Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * Math.PI / 4.0);
Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * Math.PI / 4.0);
mLog.debug("REF: " + ROTATE_FROM_MINUS_45.toString());
Complex symbol = new Complex(0.7665009f, -0.6422432f);
mLog.debug("SYM: " + symbol.toString());
symbol.multiply(ROTATE_FROM_MINUS_45);
mLog.debug("DER: " + symbol.toString());
}
}

View File

@@ -45,7 +45,7 @@ public class ComplexWaveSource extends ComplexSource implements IControllableFil
private Listener<ReusableComplexBuffer> mListener;
private AudioInputStream mInputStream;
private File mFile;
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue();
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("Complex Wave Source");
public ComplexWaveSource(File file) throws IOException
{