#2518 P25P1 LSM Decoder enhancements. (#2523)

* Uses same shared architecture and symbol processor pattern as other C4FM decoders.
* Adds soft symbol processing and sync detection.
* Removes RRC filter and replaces with improved channel filter.
* Viterbi soft symbol decoding with demodulator reliability weighting
* Improved sync reacquisition in low SNR
* Improved handling for phase wrapping and interpolation
* NAC Tracker updated to track by NAC count
* Native buffers returning partial buffers
* Message processor uses only valid LDU1 words
* Reduced phantom TDU messages
* Tuner/Channe frequency error corrections don't reset the symbol trackers and fix possible divide by zero error
* LSM timing error detector now updates per symbol block size instead of sample buffer size - faster convergence
* New unit tests
* Resolves several issues, including a lagging sync detect offset issue in the recently updated P25P1 C4FM decoder.
* Consolidate filter designers for each protocol back into the FilterFactory class.
* DMR, P25 Phase 1 C4FM and LSM, and P25 Phase 2 decoders design the baseband filter once and share the taps across the I and Q filters.

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-10-29 04:17:47 -04:00
committed by GitHub
parent f2c98cdb40
commit 6b9cd2d317
42 changed files with 3586 additions and 2767 deletions

View File

@@ -47,6 +47,20 @@ public abstract class C4FMMessageFramer
return process(symbol);
}
/**
* Process the symbol decision along with the soft symbol it was decided from and the soft symbol reliability (ie
* inverse of the soft symbol noise variance). Framers that can weight soft decision decoding override this method.
* Default implementation discards the reliability.
* @param symbol decision
* @param softSymbol equalized soft symbol in radians
* @param reliability of the soft symbol
* @return true if the framer emits a valid message that includes this symbol.
*/
public boolean process(Dibit symbol, float softSymbol, float reliability)
{
return process(symbol, softSymbol);
}
/**
* A sync pattern is detected
*/

View File

@@ -75,10 +75,17 @@ public abstract class C4FMSymbolProcessor
private static final double IDEAL_OUTER_SYMBOL_PHASE = 3.0 * Math.PI / 4.0;
//Allowable gain deviation from the nominal gain, as a fraction of the nominal gain.
private static final double SAMPLE_GAIN_TOLERANCE = 0.1;
//Reliability delay line length, which must exceed the symbol equalizer delay.
private static final int RELIABILITY_DELAY_LINE_LENGTH = 32;
private double mNoiseStandardDeviationThreshold;
private double mSamplePoint;
protected double mSamplesPerSymbol;
protected float[] mBuffer;
//Optional soft symbol reliabilities for each sample, parallel to the sample buffer (see process(float[], float[]))
private float[] mReliabilityBuffer;
//Delays the sampled reliabilities to align with the soft symbols emerging from the symbol equalizer delay line
private final float[] mReliabilityDelayLine = new float[RELIABILITY_DELAY_LINE_LENGTH];
private int mReliabilityDelayLinePointer = 0;
private float mLaggingSyncOffset;
protected float mOptimizeFineIncrement;
private int mBufferPointer;
@@ -216,6 +223,16 @@ public abstract class C4FMSymbolProcessor
*/
public abstract float getSyncThresholdEqualized();
/**
* Minimum correlation against the reference sync waveform, as a fraction of the ideal correlation score, for the
* first two sync detections following a sync loss (see validateSyncWaveform()). Default is 85%.
* @return validation threshold as a fraction of the ideal correlation score.
*/
public double getSyncWaveformValidationThreshold()
{
return 0.85;
}
/**
* Notification that a sync detection was validated and the correction applied. Subclasses can override this to
* capture protocol specific details (ie the detected sync pattern) from the sync detector that triggered the
@@ -269,6 +286,19 @@ public abstract class C4FMSymbolProcessor
* @param samples to process
*/
public void process(float[] samples)
{
process(samples, null);
}
/**
* Primary input method for receiving a stream of demodulated samples, with an optional parallel stream of soft
* symbol reliabilities (ie inverse of the soft symbol noise variance) for each sample, to process into symbols.
* When provided, each soft symbol's reliability is sampled at the symbol sampling instant and is provided to the
* message framer with the soft symbol to weight soft decision decoding.
* @param samples to process
* @param reliabilities for each sample, or null.
*/
public void process(float[] samples, float[] reliabilities)
{
//Establish buffer size based on incoming sample array length which should be consistent across decoder lifecycle
if(mBuffer == null)
@@ -278,6 +308,11 @@ public abstract class C4FMSymbolProcessor
mBufferPointer = mBufferReloadThreshold;
}
if(reliabilities != null && mReliabilityBuffer == null)
{
mReliabilityBuffer = new float[mBuffer.length];
}
//Create shadow copy of heap variables on stack
double samplePoint = mSamplePoint;
double samplesPerSymbol = mSamplesPerSymbol;
@@ -319,6 +354,22 @@ public abstract class C4FMSymbolProcessor
int copyLength = samples.length;
System.arraycopy(mBuffer, copyLength, mBuffer, 0, mBuffer.length - copyLength);
System.arraycopy(samples, samplesPointer, mBuffer, mBuffer.length - copyLength, copyLength);
if(mReliabilityBuffer != null)
{
System.arraycopy(mReliabilityBuffer, copyLength, mReliabilityBuffer, 0, mBuffer.length - copyLength);
if(reliabilities != null && reliabilities.length == samples.length)
{
System.arraycopy(reliabilities, samplesPointer, mReliabilityBuffer, mBuffer.length - copyLength,
copyLength);
}
else
{
Arrays.fill(mReliabilityBuffer, mBuffer.length - copyLength, mBuffer.length, Float.NaN);
}
}
samplesPointer += copyLength;
bufferPointer -= copyLength;
}
@@ -352,6 +403,13 @@ public abstract class C4FMSymbolProcessor
softSymbol = mSampleEqualizer.getEqualizedSymbol(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], samplePoint);
float timingSymbol = softSymbol;
if(mReliabilityBuffer != null)
{
mReliabilityDelayLinePointer = (mReliabilityDelayLinePointer + 1) % RELIABILITY_DELAY_LINE_LENGTH;
mReliabilityDelayLine[mReliabilityDelayLinePointer] = (float)(mReliabilityBuffer[bufferPointer] +
(mReliabilityBuffer[bufferPointer + 1] - mReliabilityBuffer[bufferPointer]) * samplePoint);
}
//Process dynamic updates to the PLL when supported by the decoder
softSymbol = mSampleEqualizer.correct(softSymbol);
@@ -413,7 +471,21 @@ public abstract class C4FMSymbolProcessor
// 2 consecutive sync detections, send the current sample equalizer's balance value as a pseudo-PLL
// error measurement so the tuner can self adjust to correct PPM. The balance is applied in the
// equalized symbol domain, so divide by gain to convert to radians per sample for the decoder.
if(mMessageFramer.process(symbol, softSymbol) && mConsecutiveSyncPeriods > 1)
boolean validMessage;
if(mReliabilityBuffer != null)
{
int delay = Math.min(mSymbolEqualizer.getDelaySymbolCount(), RELIABILITY_DELAY_LINE_LENGTH - 1);
float reliability = mReliabilityDelayLine[(mReliabilityDelayLinePointer - delay +
RELIABILITY_DELAY_LINE_LENGTH) % RELIABILITY_DELAY_LINE_LENGTH];
validMessage = mMessageFramer.process(symbol, softSymbol, reliability);
}
else
{
validMessage = mMessageFramer.process(symbol, softSymbol);
}
if(validMessage && mConsecutiveSyncPeriods > 1)
{
mFeedbackDecoder.processPLLError(mSampleEqualizer.balanceOptimized() / mSampleEqualizer.gain());
}
@@ -542,14 +614,14 @@ public abstract class C4FMSymbolProcessor
for(int x = referenceSyncWaveform.samples().length - 1; x >= 0; x--)
{
index = integral + x;
float sample = LinearInterpolator.calculate(mBuffer[index], mBuffer[index + 1], mu);
float sample = interpolate(mBuffer[index], mBuffer[index + 1], mu);
sample *= gain;
sample += balance;
sample = Math.clamp(sample, -referenceSyncWaveform.maxSample(), referenceSyncWaveform.maxSample());
accumulator += referenceSyncWaveform.samples()[x] * sample;
}
double threshold = referenceSyncWaveform.idealCorrelationScore() * 0.85; //85% of the ideal correlation score
double threshold = referenceSyncWaveform.idealCorrelationScore() * getSyncWaveformValidationThreshold();
//Return the correction as valid if the correlation score exceeds the threshold, otherwise return the static
// invalid sync detection to nullify the correction.
@@ -1145,7 +1217,7 @@ public abstract class C4FMSymbolProcessor
}
/**
* Equalizes both samples with optional phase wrap detection and returns the interpolated value at the mu offset.
* Interpolates between the samples at the mu offset and equalizes the interpolated value.
*
* @param sample1 of the sample stream
* @param sample2 of the sample stream
@@ -1154,10 +1226,7 @@ public abstract class C4FMSymbolProcessor
*/
public float getEqualizedSymbol(float sample1, float sample2, double mu)
{
sample1 = equalize(sample1);
sample2 = equalize(sample2);
//Calculate interpolated sample and pass through the dynamic balance correction method.
return LinearInterpolator.calculate(sample1, sample2, mu);
return equalize(interpolate(sample1, sample2, mu));
}
/**
@@ -1179,6 +1248,21 @@ public abstract class C4FMSymbolProcessor
}
/**
* Interpolates the (unequalized) sample value between two consecutive samples of the demodulated sample stream.
* Default implementation is linear interpolation. Protocols with a phase wrapped sample stream (ie P25 LSM) can
* override this to interpolate across the phase wrap.
*
* @param sample1 of the sample stream
* @param sample2 of the sample stream
* @param mu interpolation point between sample 1 and sample 2, in the range 0.0 to 1.0
* @return interpolated sample
*/
protected float interpolate(float sample1, float sample2, double mu)
{
return LinearInterpolator.calculate(sample1, sample2, mu);
}
/**
* Calculates the sync correlation score at the specified offset and samples per symbol interval using the
* current equalizer gain and supplied balance value.
@@ -1209,16 +1293,13 @@ public abstract class C4FMSymbolProcessor
double fractional = pointer - bufferPointer;
float score = 0;
float sample1, sample2;
float idealSymbol;
for(int x = 0; x < patternSymbols.length; x++)
{
if(bufferPointer < maxPointer)
{
sample1 = mBuffer[bufferPointer] * gain + balance;
sample2 = mBuffer[bufferPointer + 1] * gain + balance;
softSymbol = LinearInterpolator.calculate(sample1, sample2, fractional);
softSymbol = interpolate(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], fractional) * gain + balance;
}
else
{
@@ -1309,8 +1390,8 @@ public abstract class C4FMSymbolProcessor
if(resampleStartIntegral >= 0)
{
symbol = patternSymbols[x];
resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral],
mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral);
resampledSoftSymbol = interpolate(mBuffer[resampleStartIntegral], mBuffer[resampleStartIntegral + 1],
resampleStart - resampleStartIntegral);
resampledSoftSymbol = resampledSoftSymbol * mSampleEqualizer.mGain + measuredBalance;
resampledDibit = Dibit.fromSample(resampledSoftSymbol);
bitErrorCount += patternDibits[x].getBitErrorFrom(resampledDibit);