#7 WIP - fixes critical error in pulse shaping filter design parameters to improve low-SNR signal decoding performance. Continues code cleanup.

This commit is contained in:
denny
2026-06-23 05:13:51 -04:00
parent a79ab1c607
commit 11da7c0dc7
4 changed files with 82 additions and 154 deletions

View File

@@ -607,6 +607,22 @@ public class FilterFactory
return cicArray;
}
/**
* Creates a Root Raised Cosine filter.
* @param samplesPerSymbol
* @param symbolCount
* @param alpha
* @return
*/
public static float[] getRRC(double samplesPerSymbol, int symbolCount, float alpha)
{
//TODO: this wraps the existing method which is reducing the samples per symbol value x2 internally and is likely
// a mistake. This method is used by NXDN to correct that error until the DMR and P25 decoders can be updated to
// ensure that the correction doesn't impact those decoders. Once the cleanup is complete, this method can be
// deleted and the NXDN implementation can revert to using the future corrected version.
return getRootRaisedCosine(samplesPerSymbol * 2, symbolCount, alpha);
}
/**
* Creates root raised cosine filter coefficients with a tap count equal to the symbols x samplesPerSymbol + 1.
*

View File

@@ -151,7 +151,7 @@ public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider,
int symbolLength = 26;
float rrcAlpha = 0.2f;
float[] rrcTaps = FilterFactory.getRootRaisedCosine(samplesPerSymbol, symbolLength, rrcAlpha);
float[] rrcTaps = FilterFactory.getRRC(samplesPerSymbol, symbolLength, rrcAlpha);
mPulseShapingFilter = new RealFIRFilter(rrcTaps);
float[] basebandTaps = getBasebandFilter(decimatedSampleRate, mConfig);
mBasebandFilterI = FilterFactory.getRealFilter(basebandTaps);
@@ -349,9 +349,9 @@ public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider,
//NXDN 4800 Channels
config = new DecodeConfigNXDN(TransmissionMode.M4800);
String file = directory + "20251128_052514_150845000_MobileTech-NXDN-48_Fulton_Control_47_baseband.wav";
// String file = directory + "20251128_052514_150845000_MobileTech-NXDN-48_Fulton_Control_47_baseband.wav";
//Lower SNR recordings
// String file = directory + "20260430_023716_150867500_Mobiletech-Communications-(NXDN)_Pulaski_Pulaski_11_baseband.wav";
String file = directory + "20260430_023716_150867500_Mobiletech-Communications-(NXDN)_Pulaski_Pulaski_11_baseband.wav";
// String file = directory + "20260430_023556_152982500_Mobiletech-Communications-(NXDN)_Manlius_Manlius_15_baseband.wav";
//This traffic channel sample Has Sync Detects between 1,096,430 - 1,111,430 samples (decimated sample rate: 12,500 Hz)

View File

@@ -43,17 +43,16 @@ import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
*/
public class NXDNSymbolProcessor
{
private static final float TIMING_LOOP_GAIN = 0.4f;
private static final float SOFT_SYMBOL_QUADRANT_BOUNDARY = (float)(Math.PI / 2.0);
private static final float SYNC_THRESHOLD_DETECTION = 37;
private static final float SYNC_THRESHOLD_OPTIMIZED = 40;
private static final float SYNC_THRESHOLD_EQUALIZED = 40;
private static final float TWO_PI = (float)(Math.PI * 2.0);
private static final float[] SYNC_SYMBOLS = NXDNSyncDetector.SYMBOLS;
private static final int BUFFER_WORKSPACE_LENGTH = 1024;
private static final int DIBIT_LENGTH_NID = 33; //32 dibits (64 bits) +1 status
private static final int DIBIT_LENGTH_SYNC = 10;
private static final Correction INVALID_SYNC_DETECTION = new Correction(0d, Double.MAX_VALUE, 0f, 0f, 0f, 0f, 0);
private ISyncResultsListener mSyncResultsViewer;
private static final DecimalFormat DF = new DecimalFormat("00.000");
private static final Dibit[] SYNC_DIBITS = NXDNSyncDetector.DIBITS;
private final DibitToByteBufferAssembler mDibitAssembler = new DibitToByteBufferAssembler(300);
private final FeedbackDecoder mFeedbackDecoder;
private final NXDNMessageFramer mMessageFramer;
@@ -61,12 +60,8 @@ public class NXDNSymbolProcessor
private final NXDNSoftSyncDetector mSyncDetectorLagging = NXDNSyncDetectorFactory.getStandardDetector();
private final SampleEqualizer mSampleEqualizer = new SampleEqualizer();
private final C4FMEqualizer mSymbolEqualizer;
private final SyncTracker mSyncTracker = new SyncTracker(2400, 3);
private double mNoiseStandardDeviationThreshold;
private double mSamplePoint;
private double mSamplePointAdjustment;
private double mSamplePointAdjustmentIncrement;
private double mSamplePointAdjustmentMax;
private double mSamplesPerSymbol;
private double mTimingCorrectionThreshold;
private float[] mBuffer;
@@ -80,11 +75,11 @@ public class NXDNSymbolProcessor
private boolean mSynchronized = false;
private boolean mDelayedSyncNotificationPending = false;
private int mDelayedSyncNotificationSymbolsRemaining = 0;
private ISyncResultsListener mSyncResultsViewer;
private ReferenceSyncWaveform mReferenceSyncWaveform;
private final boolean mVisualize = false;
private final int mDebugSymbolStart = 200;
private static final DecimalFormat DF = new DecimalFormat("00.000");
private final boolean mDebugVisualize = false;
private final int mDebugSymbolStart = 0;
/**
* Constructs an instance
@@ -98,6 +93,9 @@ public class NXDNSymbolProcessor
DF.setPositivePrefix(" ");
}
/**
* Resets the symbol equalizer.
*/
public void resetEqualizer()
{
mSymbolEqualizer.reset();
@@ -108,12 +106,6 @@ public class NXDNSymbolProcessor
return mSymbolEqualizer.getTapsDescription();
}
public void log()
{
System.out.println("SAMPLES: " + mDebugSampleCounter + " SYMBOLS: " + mDebugSymbolCounter);
}
/**
* Finalize and prepare for shutdown.
*/
@@ -220,24 +212,22 @@ public class NXDNSymbolProcessor
if(scorePrimary > SYNC_THRESHOLD_DETECTION && scorePrimary > scoreLag)
{
correctionCandidate = mSampleEqualizer.optimize(mSyncDetector, 0, scorePrimary,
(bufferPointer + samplePoint));
correctionCandidate = mSampleEqualizer.optimize(0, scorePrimary, (bufferPointer + samplePoint));
}
else if(scoreLag > SYNC_THRESHOLD_DETECTION)
{
correctionCandidate = mSampleEqualizer.optimize(mSyncDetectorLagging, -mLaggingSyncOffset,
scoreLag, (bufferPointer + samplePoint));
correctionCandidate = mSampleEqualizer.optimize(-mLaggingSyncOffset, scoreLag, (bufferPointer + samplePoint));
}
softSymbol = mSymbolEqualizer.process(softSymbolLag, softSymbol);
//Broadcast the decoded soft symbol to an optionally registered listener (ie Symbol view in channel panel).
Dibit symbol = toSymbol(softSymbol);
Dibit symbol = Dibit.fromSample(softSymbol);
//Equalizer processes current soft symbol and returns a delayed soft symbol that is equalized
mFeedbackDecoder.broadcast(softSymbol);
if(mVisualize)
if(mDebugVisualize)
{
getSyncResultsViewer().symbol(softSymbol);
}
@@ -390,13 +380,11 @@ public class NXDNSymbolProcessor
public void setSamplesPerSymbol(float samplesPerSymbol)
{
mSamplesPerSymbol = samplesPerSymbol;
mSamplePoint = samplesPerSymbol;
mTimingCorrectionThreshold = samplesPerSymbol / 30;
mNoiseStandardDeviationThreshold = .2;
mSamplePoint = samplesPerSymbol;
mSamplePointAdjustmentMax = samplesPerSymbol / 2;
mSamplePointAdjustmentIncrement = samplesPerSymbol / 100.0;
mOptimizeFineIncrement = samplesPerSymbol / 200f;
mLaggingSyncOffset = samplesPerSymbol / 2;
mOptimizeFineIncrement = samplesPerSymbol / 200.0f;
mLaggingSyncOffset = samplesPerSymbol / 2.0f;
int bufferLength = BUFFER_WORKSPACE_LENGTH + (int)(Math.ceil((DIBIT_LENGTH_SYNC + DIBIT_LENGTH_NID + 2) * samplesPerSymbol));
mBuffer = new float[bufferLength];
mBufferReloadThreshold = mBuffer.length - (int)Math.ceil(samplesPerSymbol * (DIBIT_LENGTH_NID + 1));
@@ -405,23 +393,6 @@ public class NXDNSymbolProcessor
mReferenceSyncWaveform = mSyncDetector.getReferenceWaveform(samplesPerSymbol);
}
/**
* Decodes the sample value to determine the correct QPSK quadrant and maps the value to a Dibit symbol.
* @param sample in radians.
* @return symbol decision.
*/
public static Dibit toSymbol(float sample)
{
if(sample > 0)
{
return sample > SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D01_PLUS_3 : Dibit.D00_PLUS_1;
}
else
{
return sample < -SOFT_SYMBOL_QUADRANT_BOUNDARY ? Dibit.D11_MINUS_3 : Dibit.D10_MINUS_1;
}
}
/**
* Debug viewer UI for visualizing sync detections in the sample stream/waveform.
*/
@@ -442,13 +413,11 @@ public class NXDNSymbolProcessor
*/
public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint)
{
if(!mVisualize)
if(!mDebugVisualize)
{
return;
}
// //Correct for the 1-symbol delay from the symbol equalizer
// samplePoint -= mSamplesPerSymbol;
int integral = (int)Math.floor(samplePoint);
samplePoint -= integral;
bufferPointer += integral;
@@ -508,7 +477,12 @@ public class NXDNSymbolProcessor
*/
public void visualizeSymbols(int bufferPointer, double samplePoint)
{
if(!mVisualize)
if(true)
{
return;
}
if(!mDebugVisualize)
{
return;
}
@@ -537,7 +511,7 @@ public class NXDNSymbolProcessor
for(int x = 0; x < 10; x++)
{
symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
decisions[x] = toSymbol(symbols[x]).getIdealPhase();
decisions[x] = Dibit.fromSample(symbols[x]).getIdealPhase();
symbolPointer += mSamplesPerSymbol;
symbolIntegral = (int)Math.floor(symbolPointer);
mu = symbolPointer - symbolIntegral;
@@ -711,7 +685,7 @@ public class NXDNSymbolProcessor
* Sample equalizer providing gain and balance (DC offset compensation).
*
* Note: equalizer is initialized with a gain value of 1.219, as determined through significant testing against
* both P25 and DMR C4FM modulated samples that employ pulse shaping filters. Gain value is adjusted and
* both P25 and DMR C4FM modulated samples that employ the same pulse shaping filters. Gain value is adjusted and
* optimized with each sync detection.
*/
class SampleEqualizer
@@ -788,7 +762,7 @@ public class NXDNSymbolProcessor
* offset for the lagging sync detector.
* @return optimized timing adjustment or NO_OPTIMIZATION sentinel value.
*/
public Correction optimize(NXDNSyncDetector syncDetector, double additionalOffset, float detectionScore, double bufferOffset)
public Correction optimize(double additionalOffset, float detectionScore, double bufferOffset)
{
//Offset points to the final symbol/sample in the buffer for the detected sync pattern.
double offset = bufferOffset + additionalOffset;
@@ -815,13 +789,13 @@ public class NXDNSymbolProcessor
double adjustmentMax = samplesPerSymbol / 2.0;
double candidate = offset;
float scoreCenter = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
float scoreCenter = score(candidate, samplesPerSymbol, calculatedBalanceCorrection);
candidate = offset - stepSize;
float scoreLeft = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
float scoreLeft = score(candidate, samplesPerSymbol, calculatedBalanceCorrection);
candidate = offset + stepSize;
float scoreRight = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
float scoreRight = score(candidate, samplesPerSymbol, calculatedBalanceCorrection);
while(stepSize > stepSizeMin && Math.abs(adjustment) <= adjustmentMax)
{
@@ -832,7 +806,7 @@ public class NXDNSymbolProcessor
scoreCenter = scoreLeft;
candidate = offset + adjustment - stepSize;
scoreLeft = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
scoreLeft = score(candidate, samplesPerSymbol, calculatedBalanceCorrection);
}
else if(scoreRight > scoreLeft && scoreRight > scoreCenter)
{
@@ -841,7 +815,7 @@ public class NXDNSymbolProcessor
scoreCenter = scoreRight;
candidate = offset + adjustment + stepSize;
scoreRight = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
scoreRight = score(candidate, samplesPerSymbol, calculatedBalanceCorrection);
}
else
{
@@ -850,10 +824,10 @@ public class NXDNSymbolProcessor
if(stepSize > stepSizeMin)
{
candidate = offset + adjustment - stepSize;
scoreLeft = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
scoreLeft = score(candidate, samplesPerSymbol, calculatedBalanceCorrection);
candidate = offset + adjustment + stepSize;
scoreRight = score(syncDetector, candidate, samplesPerSymbol, calculatedBalanceCorrection);
scoreRight = score(candidate, samplesPerSymbol, calculatedBalanceCorrection);
}
}
}
@@ -864,7 +838,7 @@ public class NXDNSymbolProcessor
return INVALID_SYNC_DETECTION;
}
return getCorrection(syncDetector, additionalOffset, adjustment, detectionScore, scoreCenter, bufferOffset,
return getCorrection(additionalOffset, adjustment, detectionScore, scoreCenter, bufferOffset,
calculatedBalanceCorrection);
}
@@ -878,45 +852,6 @@ public class NXDNSymbolProcessor
return symbol * mGain + mBalance;
}
/**
* Adjusts the symbol balance from the symbol error to address multi-path and fading between sync periods.
*/
public double getAdjustment(float softSymbol, Dibit symbol, int bufferPointer)
{
double adjustment;
if(softSymbol < symbol.getIdealPhase())
{
if(mBuffer[bufferPointer] > mBuffer[bufferPointer + 1])
{
adjustment = -mSamplePointAdjustmentIncrement;
}
else
{
adjustment = mSamplePointAdjustmentIncrement;
}
}
else
{
if(mBuffer[bufferPointer] > mBuffer[bufferPointer + 1])
{
adjustment = mSamplePointAdjustmentIncrement;
}
else
{
adjustment = -mSamplePointAdjustmentIncrement;
}
}
if(Math.abs(mSamplePointAdjustment + adjustment) <= mSamplePointAdjustmentMax)
{
mSamplePointAdjustment += adjustment;
return adjustment;
}
return 0;
}
/**
* Equalizes both samples with optional phase wrap detection and returns the interpolated value at the mu offset.
*
@@ -949,41 +884,6 @@ public class NXDNSymbolProcessor
return 0;
}
/**
* Equalizes both samples using the candidate correction values and returns the interpolated value at the
* mu offset.
* @param sample1 to equalize
* @param sample2 to equalize
* @param mu interpolation between samples 1 and 2
* @param correction to apply when equalizing samples 1 and 2
* @return interpolated value.
*/
public float getEqualizedSymbol(float sample1, float sample2, double mu, Correction correction)
{
sample1 = (sample1 + mBalance + correction.getBalanceAdjustment()) * (mGain + correction.getGainAdjustment());
sample2 = (sample2 + mBalance + correction.getBalanceAdjustment()) * (mGain + correction.getGainAdjustment());
if(sample1 > Math.PI)
{
sample1 -= TWO_PI;
}
else if(sample1 < -Math.PI)
{
sample1 += TWO_PI;
}
if(sample2 > Math.PI)
{
sample2 -= TWO_PI;
}
else if(sample2 < -Math.PI)
{
sample2 += TWO_PI;
}
return LinearInterpolator.calculate(sample1, sample2, mu);
}
/**
* Calculates the sync correlation score at the specified offset and samples per symbol interval using the
* current equalizer balance and gain values.
@@ -992,9 +892,9 @@ public class NXDNSymbolProcessor
* @param samplesPerSymbol spacing to test for.
* @return correlation score.
*/
public float score(NXDNSyncDetector syncDetector, double offset, double samplesPerSymbol, float balance)
public float score(double offset, double samplesPerSymbol, float balance)
{
return score(syncDetector, offset, samplesPerSymbol, balance, mGain);
return score(offset, samplesPerSymbol, balance, mGain);
}
/**
@@ -1003,19 +903,20 @@ public class NXDNSymbolProcessor
* @param samplesPerSymbol spacing to test for.
* @return correlation score.
*/
public float score(NXDNSyncDetector syncDetector, double offset, double samplesPerSymbol, float balance, float gain)
public float score(double offset, double samplesPerSymbol, float balance, float gain)
{
int maxPointer = mBuffer.length - 1;
float softSymbol;
double pointer = offset - (samplesPerSymbol * (syncDetector.getSyncPatternDibitLength() - 1));
double pointer = offset - (samplesPerSymbol * 9);
int bufferPointer = (int)Math.floor(pointer);
double fractional = pointer - bufferPointer;
float score = 0;
float sample1, sample2;
float idealSymbol;
for(int x = 0; x < syncDetector.getSyncPatternDibitLength(); x++)
for(int x = 0; x < 10; x++)
{
if(bufferPointer < maxPointer)
{
@@ -1028,7 +929,19 @@ public class NXDNSymbolProcessor
softSymbol = 0.0f;
}
score += softSymbol * syncDetector.getSyncSymbols()[x];
idealSymbol = SYNC_SYMBOLS[x];
//Constrain the contribution of the soft symbol to +/- the ideal symbol value.
if(idealSymbol > 0)
{
softSymbol = Math.clamp(softSymbol, -idealSymbol, idealSymbol);
}
else
{
softSymbol = Math.clamp(softSymbol, idealSymbol, -idealSymbol);
}
score += softSymbol * idealSymbol;
pointer += samplesPerSymbol;
bufferPointer = (int)Math.floor(pointer);
fractional = pointer - bufferPointer;
@@ -1079,25 +992,25 @@ public class NXDNSymbolProcessor
* sync detection, the equalizer settings are applied to the samples in the buffer allowing the symbols to be
* resampled during coarse sync acquisition.
*/
public Correction getCorrection(NXDNSyncDetector syncDetector, double additionalOffset, double timingCorrection,
public Correction getCorrection(double additionalOffset, double timingCorrection,
float detectionScore, float optimizationScore, double offset, float balanceCorrection)
{
double resampleStart = offset + additionalOffset + timingCorrection - (mSamplesPerSymbol * (syncDetector.getSyncPatternDibitLength() - 1));
double resampleStart = offset + additionalOffset + timingCorrection - (mSamplesPerSymbol * 9);
int resampleStartIntegral = (int)Math.floor(resampleStart);
float symbol, gainAccumulator = 0, resampledSoftSymbol;
Dibit resampledDibit;
int bitErrorCount = 0;
for(int x = 0; x < syncDetector.getSyncPatternDibitLength(); x++)
for(int x = 0; x < 10; x++)
{
if(resampleStartIntegral >= 0)
{
symbol = syncDetector.getSyncSymbols()[x];
symbol = SYNC_SYMBOLS[x];
resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral],
mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral);
resampledSoftSymbol = resampledSoftSymbol * mGain + balanceCorrection;
resampledDibit = toSymbol(resampledSoftSymbol);
bitErrorCount += syncDetector.getSyncDibits()[x].getBitErrorFrom(resampledDibit);
resampledDibit = Dibit.fromSample(resampledSoftSymbol);
bitErrorCount += SYNC_DIBITS[x].getBitErrorFrom(resampledDibit);
gainAccumulator += Math.abs(symbol) - Math.abs(resampledSoftSymbol);
}
@@ -1105,15 +1018,14 @@ public class NXDNSymbolProcessor
resampleStartIntegral = (int)Math.floor(resampleStart);
}
gainAccumulator /= syncDetector.getSyncPatternDibitLength();
gainAccumulator /= 10;
//Recalculate the optimization score using the adjusted equalizer settings
if(!mSynchronized)
{
double optimizedFinalSymbol = offset + additionalOffset + timingCorrection;
balanceCorrection = -getBalance(optimizedFinalSymbol);
optimizationScore = score(syncDetector, optimizedFinalSymbol, mSamplesPerSymbol,
balanceCorrection, mGain + gainAccumulator);
optimizationScore = score(optimizedFinalSymbol, mSamplesPerSymbol, balanceCorrection, mGain + gainAccumulator);
}
return new Correction(additionalOffset, timingCorrection, balanceCorrection, gainAccumulator, detectionScore,

View File

@@ -33,8 +33,8 @@ public abstract class NXDNSyncDetector
public static final long SYNC_PATTERN = 0xCDF59;
public static final long SYNC_MASK = 0xFFFFF;
public static final int SYNC_DIBIT_LENGTH = 10;
private static final Dibit[] DIBITS = toDibits(SYNC_PATTERN, SYNC_DIBIT_LENGTH);
protected static final float[] SYMBOLS = toSymbols(DIBITS);
public static final Dibit[] DIBITS = toDibits(SYNC_PATTERN, SYNC_DIBIT_LENGTH);
public static final float[] SYMBOLS = toSymbols(DIBITS);
private static Map<Integer,ReferenceSyncWaveform> sReferenceWaveformMap = new TreeMap<>();
/**