diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java index 410b942b..a9d538ea 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java @@ -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. * diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoder.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoder.java index edff4637..5ed98291 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoder.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNDecoder.java @@ -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) diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNSymbolProcessor.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNSymbolProcessor.java index c008d6e2..d2046125 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNSymbolProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNSymbolProcessor.java @@ -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, diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSyncDetector.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSyncDetector.java index e706935e..e9ee7678 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSyncDetector.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSyncDetector.java @@ -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 sReferenceWaveformMap = new TreeMap<>(); /**