From 5635341d12ce87208fc534dd06e1e482ea543500 Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Sun, 25 Oct 2026 09:44:50 -0400 Subject: [PATCH] #2494 P25P1 and NXDN Decoder Enhancements. (#2514) - Updates P25P1 C4FM decoder to use same pattern developed for NXDN. - Both share the C4FM symbol processor and equalizer. - P25P1 updated with Garner style timing error detector - NXDN filters updated to use a combined RRC and inverse sync filter all in one. - P25P1 replaces RRC with band limiting filter - P25P1 enhanced with dynamic PLL balance updates to combat multi-path fading, in addition to the sync detection training window. - Decoder PPM error reports now only sent after 2x consecutive sync detections to prevent tuner PPM corrections from noisy/isolated false sync detects. - Soft error correction added to both P25P1 and IDEN. Co-authored-by: denny --- .../layer1 => dsp/c4fm}/C4FMEqualizer.java | 68 +- .../dsheirer/dsp/c4fm/C4FMMessageFramer.java | 34 + .../dsp/c4fm/C4FMSymbolProcessor.java | 1291 +++++++++++++++++ .../dsheirer/dsp/c4fm/DibitSyncDetector.java | 203 +++ .../dsp/c4fm/ISoftC4FMSyncDetector.java | 40 + .../c4fm}/ReferenceSyncWaveform.java | 2 +- .../dsheirer/dsp/filter/FilterFactory.java | 57 + .../dsheirer/edac/bch/BCH_63_16_23_P25.java | 27 + .../edac/trellis/ViterbiDecoder_1_2_P25.java | 144 ++ .../gui/viewer/sync/FMSyncVisualizer.java | 2 +- .../module/decode/nxdn/DecodeConfigNXDN.java | 3 - .../module/decode/nxdn/NXDNDecoder.java | 39 +- .../decode/nxdn/NXDNFilterDesigner.java | 358 +++++ .../module/decode/nxdn/NXDNMessageFramer.java | 59 +- .../decode/nxdn/NXDNSymbolProcessor.java | 1050 +------------- .../module/decode/nxdn/layer1/Frame.java | 192 ++- .../layer1/sync/NXDNHardSyncDetector.java | 6 +- .../layer1/sync/NXDNSoftSyncDetector.java | 9 +- .../sync/NXDNSoftSyncDetectorScalar.java | 4 +- .../sync/NXDNSoftSyncDetectorVector128.java | 8 +- .../sync/NXDNSoftSyncDetectorVector256.java | 6 +- .../sync/NXDNSoftSyncDetectorVector512.java | 7 +- .../sync/NXDNSoftSyncDetectorVector64.java | 4 +- .../nxdn/layer1/sync/NXDNSyncDetector.java | 147 +- .../decode/nxdn/layer2/Interleaver.java | 18 + .../nxdn/layer3/coding/Convolution.java | 118 ++ .../nxdn/layer3/coding/PunctureProvider.java | 26 + .../layer3/coding/PunctureProviderNone.java | 6 + .../nxdn/layer3/type/TransmissionMode.java | 22 +- .../decode/p25/phase1/P25P1DecoderC4FM.java | 150 +- .../p25/phase1/P25P1DemodulatorC4FM.java | 381 +++-- .../p25/phase1/P25P1FilterDesigner.java | 244 ++++ .../decode/p25/phase1/P25P1Interleave.java | 18 + .../p25/phase1/P25P1MessageAssembler.java | 52 +- .../decode/p25/phase1/P25P1MessageFramer.java | 226 ++- .../p25/phase1/P25P1SymbolProcessorC4FM.java | 328 +++++ .../phase1/message/pdu/PDUMessageFactory.java | 46 +- .../pdu/block/UnconfirmedDataBlock.java | 9 + .../message/tsbk/TSBKMessageFactory.java | 44 +- .../phase1/sync/P25P1HardSyncDetector.java | 4 +- .../phase1/sync/P25P1SoftSyncDetector.java | 9 +- .../sync/P25P1SoftSyncDetectorScalar.java | 2 +- .../sync/P25P1SoftSyncDetectorVector128.java | 2 +- .../sync/P25P1SoftSyncDetectorVector256.java | 2 +- .../sync/P25P1SoftSyncDetectorVector512.java | 4 +- .../sync/P25P1SoftSyncDetectorVector64.java | 2 +- .../p25/phase1/sync/P25P1SyncDetector.java | 53 +- .../SourceConfigTunerMultipleFrequency.java | 2 +- .../rotation/ChannelRotationMonitor.java | 2 +- .../ChannelFrequencyErrorManager.java | 7 +- .../source/wave/ComplexWaveSource.java | 2 +- 51 files changed, 4035 insertions(+), 1504 deletions(-) rename src/main/java/io/github/dsheirer/{module/decode/nxdn/layer1 => dsp/c4fm}/C4FMEqualizer.java (77%) create mode 100644 src/main/java/io/github/dsheirer/dsp/c4fm/C4FMMessageFramer.java create mode 100644 src/main/java/io/github/dsheirer/dsp/c4fm/C4FMSymbolProcessor.java create mode 100644 src/main/java/io/github/dsheirer/dsp/c4fm/DibitSyncDetector.java create mode 100644 src/main/java/io/github/dsheirer/dsp/c4fm/ISoftC4FMSyncDetector.java rename src/main/java/io/github/dsheirer/{module/decode/nxdn/layer1/sync => dsp/c4fm}/ReferenceSyncWaveform.java (95%) create mode 100644 src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNFilterDesigner.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1FilterDesigner.java create mode 100644 src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1SymbolProcessorC4FM.java diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/C4FMEqualizer.java b/src/main/java/io/github/dsheirer/dsp/c4fm/C4FMEqualizer.java similarity index 77% rename from src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/C4FMEqualizer.java rename to src/main/java/io/github/dsheirer/dsp/c4fm/C4FMEqualizer.java index d4e4a7cd..78b04a22 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/C4FMEqualizer.java +++ b/src/main/java/io/github/dsheirer/dsp/c4fm/C4FMEqualizer.java @@ -17,7 +17,7 @@ * **************************************************************************** */ -package io.github.dsheirer.module.decode.nxdn.layer1; +package io.github.dsheirer.dsp.c4fm; import io.github.dsheirer.dsp.symbol.Dibit; import java.text.DecimalFormat; @@ -34,7 +34,7 @@ public class C4FMEqualizer private static final DecimalFormat DF = new DecimalFormat("0.0000"); //Maximum possible error is PI/4 = 0.78, but we clip it to less than half as the allowable error for tap updates private static final float MAX_SYMBOL_ERROR = 0.3f; - private static final double TRAINING_IMPROVEMENT_GOAL_PER_ITERATION = 0.001f; + private static final double TRAINING_IMPROVEMENT_GOAL_PER_ITERATION = 0.0005f; private static final double TRAINING_CONVERGENCE_THRESHOLD = 0.16f; private static final double TRAINING_SKIP_VARIANCE_THRESHOLD = 0.05; private static final int TRAINING_ITERATIONS_MAX = 30; @@ -44,24 +44,43 @@ public class C4FMEqualizer private final float[] mTaps; private final int mCenterTap; private final int mDelaySymbolCount; - private final int mTapUpdateSymbolCount; + private final int mTrainingPeriodSymbolCount; private final int mTrainingSkipCountThreshold; private boolean mEnabled = false; private boolean mSyncDetected = false; private int mSymbolsUntilTraining = 0; - private int mTapUpdateSymbolsRemaining = 0; + private int mTrainingSymbolsRemaining = 0; private int mSkippedTrainingCount = 0; + /** + * Constructs an instance with an equalizer length derived from the sync pattern length. + * + * @param syncSymbols sync pattern that will be used to train the equalizer on sync detections + * @param trainingPeriodSymbolCount following sync detection to continue training. + * @param maxNoTrainSyncCount maximum number of consecutive sync detections and skipped trainings that can occur without retraining. + */ + public C4FMEqualizer(float[] syncSymbols, int trainingPeriodSymbolCount, int maxNoTrainSyncCount) + { + this(syncSymbols, getDefaultSymbolLength(syncSymbols.length), trainingPeriodSymbolCount, maxNoTrainSyncCount); + } + /** * Constructs an instance * * @param syncSymbols sync pattern that will be used to train the equalizer on sync detections - * @param symbolLength for the equalizer - will be made to odd length, increasing by one if necessary (5-11) - * @param tapUpdateSymbolCount symbol count following sync detection and training to continue tap updates. - * @param maxSkipTrainingCount maximum number of consecutive sync detections and skipped trainings that can occur without retraining. + * @param equalizerSymbolLength equalizer span in symbols. Even values are reduced by one so that the center tap + * has equal delay on either side. The tap count is twice this value since this is a fractional (T/2) equalizer. + * @param trainingPeriodSymbolCount following sync detection to continue training. + * @param maxNoTrainSyncCount maximum number of consecutive sync detections and skipped trainings that can occur without retraining. */ - public C4FMEqualizer(float[] syncSymbols, int symbolLength, int tapUpdateSymbolCount, int maxSkipTrainingCount) + public C4FMEqualizer(float[] syncSymbols, int equalizerSymbolLength, int trainingPeriodSymbolCount, + int maxNoTrainSyncCount) { + if(equalizerSymbolLength < 1) + { + throw new IllegalArgumentException("Equalizer symbol length must be at least 1"); + } + //Store the sync pattern in reverse order mReversedSyncSymbols = new float[syncSymbols.length]; for(int x = 0; x < syncSymbols.length; x++) @@ -70,15 +89,25 @@ public class C4FMEqualizer } //Make the symbol count odd length for equal delay on either side of the center tap - symbolLength += (symbolLength % 2 == 0) ? 1 : 0; + int symbolLength = (equalizerSymbolLength % 2 == 0) ? equalizerSymbolLength - 1 : equalizerSymbolLength; mCenterTap = symbolLength; //This works since we're going to double the buffer length mDelaySymbolCount = symbolLength / 2; //Delay until new symbol is aligned to center tap mTaps = new float[symbolLength * 2]; mTaps[mCenterTap] = 1.0f; int bufferLength = mReversedSyncSymbols.length * 2 + mTaps.length - 2; mBuffer = new float[bufferLength]; - mTapUpdateSymbolCount = tapUpdateSymbolCount; - mTrainingSkipCountThreshold = maxSkipTrainingCount; + mTrainingPeriodSymbolCount = trainingPeriodSymbolCount; + mTrainingSkipCountThreshold = maxNoTrainSyncCount; + } + + /** + * Default equalizer span in symbols derived from the sync pattern length (odd length). + * @param syncLength in symbols + * @return default equalizer symbol length + */ + public static int getDefaultSymbolLength(int syncLength) + { + return (syncLength % 2 == 0) ? syncLength - 1 : syncLength; } /** @@ -155,6 +184,7 @@ public class C4FMEqualizer */ public void syncLost() { +// System.out.println("C4FM Equalizer - sync lost triggered"); reset(); disable(); } @@ -205,9 +235,9 @@ public class C4FMEqualizer equalized += mTaps[tapIndex] * mBuffer[tapIndex]; } - if(mTapUpdateSymbolsRemaining > 0) + if(mTrainingSymbolsRemaining > 0) { - mTapUpdateSymbolsRemaining--; + mTrainingSymbolsRemaining--; float error = Dibit.getError(equalized); error = Math.clamp(error, -MAX_SYMBOL_ERROR, MAX_SYMBOL_ERROR); @@ -219,7 +249,6 @@ public class C4FMEqualizer if(tapIndex != mCenterTap) { mTaps[tapIndex] += stepSize * error * mBuffer[tapIndex]; -// mTaps[tapIndex] += stepSize * error * mBuffer[tapIndex] * TAP_LEAKAGE; } } } @@ -247,12 +276,14 @@ public class C4FMEqualizer mSyncDetected = false; //Skip retrain when we're still doing dynamic tap adjustments following the previous training session. - if(mTapUpdateSymbolsRemaining > 0) + if(mTrainingSymbolsRemaining > 0) { mVariance.clear(); return; } +// System.out.println("**C4FM EQ Training Triggered - skip count[" + mSkippedTrainingCount + "] variance [" + mVariance.getResult() + "] var threshold [" + TRAINING_SKIP_VARIANCE_THRESHOLD + "]"); + //Ensure we've collected enough variance samples in the buffer and we have sufficient variance if(mVariance.getN() <= mDelaySymbolCount || mVariance.getResult() > TRAINING_SKIP_VARIANCE_THRESHOLD || ++mSkippedTrainingCount > mTrainingSkipCountThreshold) @@ -304,7 +335,10 @@ public class C4FMEqualizer } while(trainingIterationCount < TRAINING_ITERATIONS_MAX && improvement > TRAINING_IMPROVEMENT_GOAL_PER_ITERATION); -// System.out.println("Training Iterations:" + trainingIterationCount + " End Variance:" + DF.format(variance.getResult())); +// System.out.println("**C4FM EQ - train complete - iterations [" + trainingIterationCount + +// "] end variance [" + DF.format(variance.getResult()) + +// "] final improvement [" + DF.format(improvement) + +// "] improvement threshold [" + DF.format(TRAINING_IMPROVEMENT_GOAL_PER_ITERATION) + "]"); //If we failed to converge, reset the taps to default and use blind training if(variance.getResult() > TRAINING_CONVERGENCE_THRESHOLD) @@ -312,7 +346,7 @@ public class C4FMEqualizer reset(); } - mTapUpdateSymbolsRemaining = mTapUpdateSymbolCount; + mTrainingSymbolsRemaining = mTrainingPeriodSymbolCount; } mVariance.clear(); diff --git a/src/main/java/io/github/dsheirer/dsp/c4fm/C4FMMessageFramer.java b/src/main/java/io/github/dsheirer/dsp/c4fm/C4FMMessageFramer.java new file mode 100644 index 00000000..bf4219c5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/c4fm/C4FMMessageFramer.java @@ -0,0 +1,34 @@ +package io.github.dsheirer.dsp.c4fm; + +import io.github.dsheirer.dsp.symbol.Dibit; + +/** + * C4FM message framer base class. + */ +public abstract class C4FMMessageFramer +{ + /** + * Process the symbol + * @param symbol in radians + * @return true if the framer emits a valid message that includes this symbol, so that corrective measurement + * can be sent to a feedback decoder. + */ + public abstract boolean process(Dibit symbol); + + /** + * Process the symbol decision along with the soft symbol it was decided from. Framers that can use soft symbols + * (ie soft decision decoding) override this method. Default implementation discards the soft symbol. + * @param symbol decision + * @param softSymbol equalized soft symbol in radians + * @return true if the framer emits a valid message that includes this symbol. + */ + public boolean process(Dibit symbol, float softSymbol) + { + return process(symbol); + } + + /** + * A sync pattern is detected + */ + public abstract void syncDetected(); +} diff --git a/src/main/java/io/github/dsheirer/dsp/c4fm/C4FMSymbolProcessor.java b/src/main/java/io/github/dsheirer/dsp/c4fm/C4FMSymbolProcessor.java new file mode 100644 index 00000000..6669da76 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/c4fm/C4FMSymbolProcessor.java @@ -0,0 +1,1291 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 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 + * **************************************************************************** + */ + +package io.github.dsheirer.dsp.c4fm; + +import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler; +import io.github.dsheirer.module.decode.FeedbackDecoder; +import io.github.dsheirer.sample.Listener; +import java.nio.ByteBuffer; +import java.text.DecimalFormat; +import org.apache.commons.math3.stat.descriptive.moment.Mean; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; + +/** + * C4FM symbol processor provides sync detection, symbol timing recovery and symbol decision stream. + * + * This processor employs two sync detectors staged 1/2 symbol period apart as a primary and lagging detector. When a + * sync is detected, it applies an optimization process to adjust symbol timing, constellation gain and constellation + * balance similar to a phase locked loop (PLL). For protocols that consistently transmit a sync pattern at routine + * intervals, the PLL only adjusts at each sync interval. For protocols (ie P25 Phase 1 C4FM) with variable length + * messaging and longer message lengths, the processor employs decision directed feedback to dynamically adjust the + * constellation balance like a dynamic PLL. + * + * Subclass implementations can override the default behavior to provide protocol specific implementations tuned to + * the features of the protocol. The general workflow is: + * + * 1. Detect the sync pattern + * 2. Optimize on the sync pattern and produce a Correction candidate + * 3. Verify the correction candidate and the waveform of the sync pattern with the correction applied. + * 4. Apply the correction. + * 5. Produce a stream of symbol decisions + * 6. (Optional) adjust constellation balance at each symbol interval between sync detections, similar to a PLL. + */ +public abstract class C4FMSymbolProcessor +{ + private static final DecimalFormat DEBUG_DF = new DecimalFormat("00.0000"); + protected final Correction INVALID_SYNC_DETECTION = new Correction(0d, Double.MAX_VALUE, 0f, 0f, 0f, 0f, 0); + protected final ISoftC4FMSyncDetector mSyncDetector; + protected final SampleEqualizer mSampleEqualizer; + private final DibitToByteBufferAssembler mDibitAssembler = new DibitToByteBufferAssembler(300); + private final FeedbackDecoder mFeedbackDecoder; + private final C4FMMessageFramer mMessageFramer; + private final ISoftC4FMSyncDetector mSyncDetectorLagging; + private final C4FMEqualizer mSymbolEqualizer; + private static final double SYMBOL_TIMING_GAIN = 0.25; + //Clip applied to the Gardner timing error (radians squared) to limit the impact of FM discriminator clicks + private static final float TIMING_ERROR_CLIP = 2.0f; + private float mPreviousTimingSymbol = 0.0f; + //Ideal phase of the outer (+/-3) symbols in the equalized symbol domain, as defined by Dibit.D01_PLUS_3 + 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; + private ReferenceSyncWaveform mReferenceSyncWaveform; + private double mNoiseStandardDeviationThreshold; + private double mSamplePoint; + protected double mSamplesPerSymbol; + protected float[] mBuffer; + private float mLaggingSyncOffset; + protected float mOptimizeFineIncrement; + private int mBufferPointer; + private int mBufferReloadThreshold; + private int mDelayedSyncNotificationSymbolsRemaining = 0; + private int mSymbolsSinceLastSync = 0; + private int mConsecutiveSyncPeriods = 0; + private int mSyncWorkspaceLength; + private int mBufferReloadLength; + protected boolean mSynchronized = false; + private boolean mDelayedSyncNotificationPending = false; + +// private final boolean mDebugVisualizeSync = true; +// private final boolean mDebugVisualizeSymbols = true; +// private final boolean mDebugLogDetectionScores = false; +// private final int mDebugSymbolStart = 0; +// private ISyncResultsListener mDebugSyncResultsViewer; +// private int mDebugSymbolCounter = 0; +// private int mDebugSampleCounter = 0; + + /** + * Constructs an instance + * @param messageFramer to receive symbol decisions (dibits) and sync notifications. + */ + public C4FMSymbolProcessor(FeedbackDecoder feedbackDecoder, C4FMMessageFramer messageFramer, + ISoftC4FMSyncDetector primary, ISoftC4FMSyncDetector lagging) + { + mMessageFramer = messageFramer; + mFeedbackDecoder = feedbackDecoder; + mSyncDetector = primary; + mSyncDetectorLagging = lagging; + mSymbolEqualizer = new C4FMEqualizer(mSyncDetector.getPatternSymbols(), getEqualizerSymbolLength(), + getEqualizerTrainingPeriodDurationSymbolCount(), getEqualizerSkipTrainingFrameCount()); + mSampleEqualizer = new SampleEqualizer(isDynamicBalanceTrackingRequired()); + DEBUG_DF.setPositivePrefix(" "); + } + + /** + * Indicates if the processor requires dynamic balance tracking using a decision directed PLL approach, to combat + * multi-path fading. + * @return true for dynamic tracking. + */ + public abstract boolean isDynamicBalanceTrackingRequired(); + + /** + * Equalizer tap training period symbol count. When an equalizer training is triggered by a sync detection, this + * defines the training period length in symbols. + * @return training period duration as a count of symbols. + */ + public abstract int getEqualizerTrainingPeriodDurationSymbolCount(); + + /** + * Symbol equalizer span in symbols (odd values; even values are reduced by one). Defaults to the sync pattern + * length. The equalizer is trained against the sync pattern, so a span much longer than the delay spread of the + * channel adds unknowns that are fit to noise in the sync pattern. + * + * Note: this is invoked from the constructor, so implementations must not depend on subclass instance state. + * @return equalizer span in symbols. + */ + public int getEqualizerSymbolLength() + { + return C4FMEqualizer.getDefaultSymbolLength(mSyncDetector.getDibitLength()); + } + + /** + * Continuous (per-symbol) Gardner symbol timing tracking loop gain, as a fraction of the samples per symbol per + * unit of timing error, applied between sync detections. Zero (default) disables continuous timing tracking, + * relying on the timing corrections applied at each sync detection. Protocols with long intervals between sync + * patterns (ie P25 LDU voice frames) can override this to track timing drift between sync detections. + * @return timing tracking gain + */ + public double getTimingTrackingGain() + { + return 0.0; + } + + /** + * Once trained, number of frames (sync detections) to skip until a full-retraining. Equalizer training can be + * skipped at each sync detection if the variance of the soft symbol values relative to the ideal symbol values + * doesn't exceed a threshold. This count defines how many sync detections can be skipped before triggering an + * updated equalizer training. + */ + public abstract int getEqualizerSkipTrainingFrameCount(); + + /** + * Maximum message dibit length + * @return length in dibits + */ + public abstract int getMaxMessageDibitLength(); + + /** + * Symbol rate of the protocol. + * @return symbols per second. + */ + public abstract int getSymbolRate(); + + /** + * Frequency deviation of the outer (+/-3) symbols as transmitted by the protocol. + * @return deviation in Hertz. + */ + public abstract double getOuterSymbolDeviation(); + + /** + * Nominal gain to apply to the FM demodulated (radians per sample) waveform, normalized to samples per symbol, + * that scales the outer (+/-3) symbols to the ideal 3*PI/4 phase used by the symbol slicer. + * + * The FM demodulator produces 2*PI*deviation/sampleRate radians per sample and the receive filter has unity gain + * at DC and at the (Nyquist) symbol sampling instants, so multiplying by samples-per-symbol yields + * 2*PI*deviation/symbolRate radians per symbol. The remaining scalar maps that to 3*PI/4. + * + * Examples: P25 (1800 Hz @ 4800) = 1.0, NXDN48 (1050 Hz @ 2400) = 0.857, NXDN96 (2400 Hz @ 4800) = 0.75 + * + * @return nominal gain scalar to apply to the samples per symbol value. + */ + public double getNominalGainScalar() + { + double outerSymbolPhase = 2.0 * Math.PI * getOuterSymbolDeviation() / getSymbolRate(); + return IDEAL_OUTER_SYMBOL_PHASE / outerSymbolPhase; + } + + /** + * Threshold score for a sync detection. + */ + public abstract float getSyncThresholdDetected(); + + /** + * Threshold score following a sync detection where the waveform is resampled at optimized slicer timing. + */ + public abstract float getSyncThresholdOptimized(); + + /** + * Threshold score following a sync detection and timing optimization where the waveform is optimized for gain + * and balance. + */ + public abstract float getSyncThresholdEqualized(); + + + /** + * Finalize and prepare for shutdown. + */ + public void dispose() + { + mDibitAssembler.flush(); + } + + /** + * Resets the balance + */ + public void resetBalance() + { + mSampleEqualizer.reset(); + } + + /** + * Primary input method for receiving a stream of demodulated samples to process into symbols. + * @param samples to process + */ + public void process(float[] samples) + { + //Establish buffer size based on incoming sample array length which should be consistent across decoder lifecycle + if(mBuffer == null) + { + mBuffer = new float[mSyncWorkspaceLength + samples.length]; + mBufferReloadThreshold = mBuffer.length - mBufferReloadLength; + mBufferPointer = mBufferReloadThreshold; + } + + //Create shadow copy of heap variables on stack + double samplePoint = mSamplePoint; + double samplesPerSymbol = mSamplesPerSymbol; + int bufferPointer = mBufferPointer; + int bufferReloadThreshold = mBufferReloadThreshold; + int symbolsSinceLastSync = mSymbolsSinceLastSync; + + int samplesPointer = 0; + float softSymbol, scorePrimary; + Correction correctionCandidate; + + if(!mSynchronized && samples.length > 400) + { + StandardDeviation sd = new StandardDeviation(); + + float accumulator = 0.0f; + + for(int i = 0; i < 400; i++) + { + accumulator += samples[i]; + sd.increment(samples[i]); + } + + //Only apply a balance correction when we have signal. Noise = .45-.55, Signal = 0.00 - 0.20 + if(sd.getResult() < 0.3) + { + //Convert the raw (radians per sample) mean into the equalized symbol domain where the balance is + //applied, and move the balance partway toward cancelling it. + float residualBalance = (accumulator / 400) * mSampleEqualizer.mGain + mSampleEqualizer.mBalance; + mSampleEqualizer.mBalance -= (residualBalance * .3f); + } + } + + while(samplesPointer < samples.length) + { + //Note: buffer pointer can become greater than reload threshold during timing optimization at sync detect + if(bufferPointer >= bufferReloadThreshold) + { + int copyLength = samples.length; + System.arraycopy(mBuffer, copyLength, mBuffer, 0, mBuffer.length - copyLength); + System.arraycopy(samples, samplesPointer, mBuffer, mBuffer.length - copyLength, copyLength); + samplesPointer += copyLength; + bufferPointer -= copyLength; + } + + while(bufferPointer < bufferReloadThreshold) + { + bufferPointer++; + samplePoint--; + +// mDebugSampleCounter++; + + if(samplePoint < 1) + { +// mDebugSymbolCounter++; +// +// if(mDebugSymbolCounter % mSyncDetector.getDibitLength() == 0) +// { +// visualizeSymbols(bufferPointer, samplePoint, symbolsSinceLastSync); +// } + + symbolsSinceLastSync++; + + //Clear synchronized state when we miss a sync period. + if(mSynchronized && symbolsSinceLastSync > getMaxMessageDibitLength()) + { + mSynchronized = false; + mSymbolEqualizer.syncLost(); + mConsecutiveSyncPeriods = 0; + } + + softSymbol = mSampleEqualizer.getEqualizedSymbol(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], samplePoint); + float timingSymbol = softSymbol; + + //Process dynamic updates to the PLL when supported by the decoder + softSymbol = mSampleEqualizer.correct(softSymbol); + + //Check for lagging sync pattern + float lag = (float)(bufferPointer + samplePoint - mLaggingSyncOffset); + int lagIntegral = (int)Math.floor(lag); + float softSymbolLag = mSampleEqualizer.getEqualizedSymbol(mBuffer[lagIntegral], mBuffer[lagIntegral + 1], lag - lagIntegral); + + //Gardner symbol timing error detector using the half-symbol (lagging) sample between the previous and + //current symbols. A positive error means the symbol is sampled late, so the next sample is advanced. + double timingAdjustment = 0.0; + + if(mSynchronized && getTimingTrackingGain() > 0) + { + float timingError = softSymbolLag * (mPreviousTimingSymbol - timingSymbol); + timingError = Math.clamp(timingError, -TIMING_ERROR_CLIP, TIMING_ERROR_CLIP); + timingAdjustment = getTimingTrackingGain() * samplesPerSymbol * timingError; + } + + mPreviousTimingSymbol = timingSymbol; + float scoreLag = mSyncDetectorLagging.process(softSymbolLag); + scorePrimary = mSyncDetector.process(softSymbol); + +// if(mDebugLogDetectionScores) +// { +// boolean exceeds = scorePrimary > 45 || scoreLag > 45; +// System.out.println("DETECT - Primary [" + DEBUG_DF.format(scorePrimary) + "] Lag [" + DEBUG_DF.format(scoreLag) + "] at [" + mDebugSymbolCounter + "]" + (exceeds ? " <<<<<<<<<<<<<<<<<<<<<<<<<<" : "")); +// } + + correctionCandidate = INVALID_SYNC_DETECTION; + + if(scorePrimary > getSyncThresholdDetected() && scorePrimary > scoreLag) + { + correctionCandidate = optimize(0, scorePrimary, (bufferPointer + samplePoint)); + } + else if(scoreLag > getSyncThresholdDetected() && symbolsSinceLastSync > 1) + { + correctionCandidate = optimize(-mLaggingSyncOffset, scoreLag, (bufferPointer + samplePoint)); + } + + //Equalizer processes current soft symbol and returns a delayed soft symbol that is equalized + softSymbol = mSymbolEqualizer.process(softSymbolLag, softSymbol); + + //Broadcast the decoded soft symbol to an optionally registered listener (ie Symbol view in channel panel). + Dibit symbol = Dibit.fromSample(softSymbol); + mFeedbackDecoder.broadcast(softSymbol); + +// if(mDebugVisualizeSync) +// { +// getDebugSyncResultsViewer().symbol(softSymbol); +// } + + //When the message framer emits at least one valid message (ie CRC check passes) and we have at least + // 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) + { + mFeedbackDecoder.processPLLError(mSampleEqualizer.balanceOptimized() / mSampleEqualizer.gain()); + } + + mDibitAssembler.receive(symbol); + + //Do a final validation against the reference sync waveform for the first two sync detections + //following a sync loss. + if(correctionCandidate.isValid() && mConsecutiveSyncPeriods < 3) + { + correctionCandidate = validateSyncWaveform(correctionCandidate, bufferPointer, samplePoint); + } + + if(correctionCandidate.isValid()) + { + mConsecutiveSyncPeriods = Math.min(3, mConsecutiveSyncPeriods + 1); + +// if(mDebugLogDetectionScores) +// { +// System.out.println("DETECT - Primary [" + DEBUG_DF.format(scorePrimary) + "] Lag [" + DEBUG_DF.format(scoreLag) + "] Optimized [" + correctionCandidate.mOptimizationScore + "] at samples [" + mDebugSampleCounter + "] symbols [" + mDebugSymbolCounter + "]"); +// } + + mSampleEqualizer.apply(correctionCandidate); + + if(mSynchronized) + { + samplePoint += (correctionCandidate.getTimingCorrection() * SYMBOL_TIMING_GAIN); + } + else + { + samplePoint += correctionCandidate.getTimingCorrection(); + } + //Notify the framer once the final sync symbol emerges from the equalizer delay line. The + //countdown only runs on subsequent symbols, so with zero delay (the current symbol is the final + //sync symbol and the framer already received it) notify immediately. + mDelayedSyncNotificationSymbolsRemaining = mSymbolEqualizer.getDelaySymbolCount(); + + if(mDelayedSyncNotificationSymbolsRemaining <= 0) + { + mMessageFramer.syncDetected(); + mDelayedSyncNotificationPending = false; + } + else + { + mDelayedSyncNotificationPending = true; + } + mSynchronized = true; + symbolsSinceLastSync = 0; + + //If the equalizer was disabled, it has bad samples contaminating the training buffer, and we + // need to prime that buffer with aligned samples prior to sync detection training. + if(!mSymbolEqualizer.isEnabled()) + { + int symbols = mSyncDetector.getPatternSymbols().length + mSymbolEqualizer.getLength() - mSymbolEqualizer.getDelaySymbolCount(); + double offset = bufferPointer + samplePoint - ((symbols - 1) * samplesPerSymbol) - mLaggingSyncOffset; + + float mid, sym; + for(int s = 0; s < symbols; s++) + { + //Mid symbol + + mid = mSampleEqualizer.getEqualizedSymbol(offset); + offset += mLaggingSyncOffset; + sym = mSampleEqualizer.getEqualizedSymbol(offset); + offset += mLaggingSyncOffset; + mSymbolEqualizer.process(mid, sym); + } + } + + mSymbolEqualizer.syncDetected(); + mSymbolEqualizer.enable(); +// visualizeSyncDetect(scorePrimary, true, "DETECT - POST", bufferPointer, samplePoint); + } + else if(mDelayedSyncNotificationPending) + { + mDelayedSyncNotificationSymbolsRemaining--; + + if(mDelayedSyncNotificationSymbolsRemaining <= 0) + { + mMessageFramer.syncDetected(); + mDelayedSyncNotificationPending = false; + } + } + + //Add another symbol's worth of samples to the counter for processing, including the continuous timing + //adjustment unless a sync detection just corrected the timing for this symbol. + if(correctionCandidate.isValid()) + { + timingAdjustment = 0.0; + } + + samplePoint += samplesPerSymbol + timingAdjustment; + } + } + } + + //Copy shadow variables back to the heap + mBufferPointer = bufferPointer; + mSamplePoint = samplePoint; + mSymbolsSinceLastSync = symbolsSinceLastSync; + } + + /** + * Validates the sync detection and candidate correction settings against a reference instance of the sync detection + * waveform. If the detected sync correlates well against the reference sync then the detected sync and candidate + * correction is valid. Otherwise, this method returns the static invalid sync correction to signal that the sync + * detection was invalid. + * @param candidate to evaluate + * @param bufferPointer for the final (optimized) symbol in the buffer + * @param samplePoint offset from the buffer pointer to the final symbol + * @return the original correction or the static invalid sync correction + */ + private Correction validateSyncWaveform(Correction candidate, int bufferPointer, double samplePoint) + { + float gain = mSampleEqualizer.mGain + candidate.getGainAdjustment(); + float balance = mSampleEqualizer.mBalance + candidate.getBalanceAdjustment(); + double offset = bufferPointer + samplePoint + candidate.getTimingCorrection() - mReferenceSyncWaveform.samples().length; + int integral = (int)Math.ceil(offset); + double mu = integral - offset; + double accumulator = 0.0; + int index; + + //Evaluate the reference waveform in reverse order since the final symbol is aligned to the final sample + for(int x = mReferenceSyncWaveform.samples().length - 1; x >= 0; x--) + { + index = integral + x; + float sample = LinearInterpolator.calculate(mBuffer[index], mBuffer[index + 1], mu); + sample *= gain; + sample += balance; + sample = Math.clamp(sample, -mReferenceSyncWaveform.maxSample(), mReferenceSyncWaveform.maxSample()); + accumulator += mReferenceSyncWaveform.samples()[x] * sample; + } + + double threshold = mReferenceSyncWaveform.idealCorrelationScore() * 0.85; //85% of the ideal correlation score + + //Return the correction as valid if the correlation score exceeds the threshold, otherwise return the static + // invalid sync detection to nullify the correction. + if(accumulator > threshold) + { + return candidate; + } + + return INVALID_SYNC_DETECTION; + } + + /** + * Registers the listener to receive demodulated bit stream buffers. + * @param listener to register + */ + public void setBufferListener(Listener listener) + { + mDibitAssembler.setBufferListener(listener); + } + + /** + * Indicates if there is a registered buffer listener + */ + public boolean hasBufferListener() + { + return mDibitAssembler.hasBufferListeners(); + } + + /** + * Sets or updates the samples per symbol + * @param samplesPerSymbol to apply. + */ + public void setSamplesPerSymbol(float samplesPerSymbol) + { + mSamplesPerSymbol = samplesPerSymbol; + mSamplePoint = samplesPerSymbol; + mNoiseStandardDeviationThreshold = .25; + mOptimizeFineIncrement = samplesPerSymbol / 200.0f; + mLaggingSyncOffset = samplesPerSymbol / 2.0f; + //Make the sync workspace long enough to hold the detected sync waveform +/- one symbol period for timing optimization + mSyncWorkspaceLength = (int)Math.ceil((mSyncDetector.getDibitLength() + 1) * samplesPerSymbol); + mBufferReloadLength = (int)Math.ceil(samplesPerSymbol); + mSampleEqualizer.configure(samplesPerSymbol); + mReferenceSyncWaveform = mSyncDetector.getReferenceWaveform(samplesPerSymbol); +// System.out.println("Samples Per Symbol: " + samplesPerSymbol); + } + +// /** +// * Debug viewer UI for visualizing sync detections in the sample stream/waveform. +// */ +// private ISyncResultsListener getDebugSyncResultsViewer() +// { +// if(mDebugSyncResultsViewer == null) +// { +// mDebugSyncResultsViewer = new FMSyncResultsViewer(); +// } +// +// return mDebugSyncResultsViewer; +// } +// +// /** +// * Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing. +// * @param score from the sync detector +// * @param primary sync detector (true) or secondary (false) +// */ +// public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint) +// { +// if(!mDebugVisualizeSync || mDebugSymbolCounter < mDebugSymbolStart) +// { +// return; +// } +// +// int integral = (int)Math.floor(samplePoint); +// samplePoint -= integral; +// bufferPointer += integral; +// +// int offset = 3; +// int length = (int)Math.ceil(mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1)) + (2 * offset); +// int end = bufferPointer + offset; +// int start = end - length; +// float[] symbols = new float[mSyncDetector.getDibitLength()]; +// float[] samples = Arrays.copyOfRange(mBuffer, start, end); +// for(int i = 0; i < samples.length; i++) +// { +// samples[i] = mSampleEqualizer.equalize(samples[i]); +// } +// +// double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1)); +// int symbolIntegral = (int)Math.floor(symbolPointer); +// double mu = symbolPointer - symbolIntegral; +// +// for(int x = 0; x < mSyncDetector.getDibitLength(); x++) +// { +// symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); +// symbolPointer += mSamplesPerSymbol; +// symbolIntegral = (int)Math.floor(symbolPointer); +// mu = symbolPointer - symbolIntegral; +// } +// +// float[] syncIntervals = new float[mSyncDetector.getDibitLength()]; +// int adjust = bufferPointer - length + offset; +// double pointer = bufferPointer + samplePoint - adjust; +// +// for(int x = (mSyncDetector.getDibitLength() - 1); x >= 0; x--) +// { +// syncIntervals[x] = (float)pointer; +// pointer -= mSamplesPerSymbol; +// } +// +// CountDownLatch countDownLatch = new CountDownLatch(1); +// getDebugSyncResultsViewer().receive(symbols, mSyncDetector.getPatternSymbols(), samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain, +// tag + " Score: " + score + (primary ? " PRIMARY " : " SECONDARY ") + (mSynchronized ? "FINE " : "COARSE ") + +// " EQ-BO:" + DEBUG_DF.format(mSampleEqualizer.mBalanceOptimized) + +// " EQ-B:" + DEBUG_DF.format(mSampleEqualizer.mBalance) + +// " EQ-G:" + DEBUG_DF.format(mSampleEqualizer.mGain) + " " + +// " SAM:" + mDebugSampleCounter + +// " SYM:" + mDebugSymbolCounter + +// " DUR:" + DEBUG_DF.format(mDebugSymbolCounter / 2400.0), +// countDownLatch); +// +// try +// { +// countDownLatch.await(); +// } +// catch(InterruptedException e) +// { +// throw new RuntimeException(e); +// } +// } +// +// /** +// * Debug method to visualize the symbol decisions +// */ +// public void visualizeSymbols(int bufferPointer, double samplePoint, int symbolsSinceLastSync) +// { +// if(!mDebugVisualizeSymbols || mDebugSymbolCounter < mDebugSymbolStart) +// { +// return; +// } +// +// int syncLength = mSyncDetector.getDibitLength(); +// +// int offset = 3; +// int length = (int)Math.ceil(mSamplesPerSymbol * (syncLength - 1)) + (2 * offset); +// int end = bufferPointer + offset; +// int start = end - length; +// float[] symbols = new float[syncLength]; +// float[] decisions = new float[syncLength]; +// float[] samples = Arrays.copyOfRange(mBuffer, start, end); +// for(int i = 0; i < samples.length; i++) +// { +// samples[i] = mSampleEqualizer.equalize(samples[i]); +// } +// +// double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * (syncLength - 1)); +// int symbolIntegral = (int)Math.floor(symbolPointer); +// double mu = symbolPointer - symbolIntegral; +// +// for(int x = 0; x < syncLength; x++) +// { +// symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); +// decisions[x] = Dibit.fromSample(symbols[x]).getIdealPhase(); +// symbolPointer += mSamplesPerSymbol; +// symbolIntegral = (int)Math.floor(symbolPointer); +// mu = symbolPointer - symbolIntegral; +// } +// +// float[] syncIntervals = new float[syncLength]; +// int adjust = bufferPointer - length + offset; +// double pointer = bufferPointer + samplePoint - adjust; +// +// for(int x = syncLength - 1; x >= 0; x--) +// { +// syncIntervals[x] = (float)pointer; +// pointer -= mSamplesPerSymbol; +// } +// +// CountDownLatch countDownLatch = new CountDownLatch(1); +// getDebugSyncResultsViewer().receive(symbols, decisions, samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain, +// "Balance:" + mSampleEqualizer.mBalance + " Gain:" + mSampleEqualizer.mGain + +// " SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter + +// " DUR:" + DEBUG_DF.format(mDebugSymbolCounter / 2400.0) + +// " ELAPSED:" + symbolsSinceLastSync, countDownLatch); +// +// try +// { +// countDownLatch.await(); +// } +// catch(InterruptedException e) +// { +// throw new RuntimeException(e); +// } +// } + + /** + * Calculates the standard deviation of the samples at the specified offset that aligns with a sync detection. + * @param offset to the sample representing the final symbol in the detected sync pattern. + * @return standard deviation. + */ + public double getStandardDeviation(double offset) + { + StandardDeviation standardDeviation = new StandardDeviation(); + int start = (int)Math.floor(offset - ((mSyncDetector.getDibitLength() - 1) * mSamplesPerSymbol)); + int end = (int)Math.ceil(offset); + end = Math.min(end, mBuffer.length - 1); + + for(int i = start; i < end; i++) + { + standardDeviation.increment(mBuffer[i] - mBuffer[i + 1]); + } + + return standardDeviation.getResult(); + } + + /** + * Symbol timing and equalizer correction candidate settings + */ + public class Correction + { + private final double mTimingAdjustment; + private final double mAdditionalOffset; + private final float mBalanceAdjustment; + private final float mGainAdjustment; + private final float mDetectionScore; + private final float mOptimizationScore; + private int mCorrectedBitCount = 0; + + /** + * Constructs an instance + * @param timingAdjustment correction for symbol sampling + * @param balanceAdjustment correction for equalizer + * @param gainAdjustment correction for equalizer + * @param bitErrorCount in the detected sync pattern + */ + public Correction(double additionalOffset, double timingAdjustment, float balanceAdjustment, float gainAdjustment, + float detectionScore, float optimizationScore, int bitErrorCount) + { + mAdditionalOffset = additionalOffset; + mTimingAdjustment = timingAdjustment; + mBalanceAdjustment = balanceAdjustment; + mGainAdjustment = gainAdjustment; + mDetectionScore = detectionScore; + mOptimizationScore = optimizationScore; + mCorrectedBitCount = bitErrorCount; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(isValid()) + { + sb.append("\tCORRECTION - Bit Errors [").append(mCorrectedBitCount).append("]"); + } + else + { + sb.append("CORRECTION - INVALID"); + } + + sb.append(" At Offset [").append(mAdditionalOffset); + sb.append("] Adjustments-Timing [").append(DEBUG_DF.format(mTimingAdjustment)); + sb.append("] Balance [").append(DEBUG_DF.format(mBalanceAdjustment)); + sb.append("] Gain [").append(DEBUG_DF.format(mGainAdjustment)); + sb.append("] Detect Score [").append(mDetectionScore); + sb.append("] Optimize Score [").append(mOptimizationScore); + sb.append("]"); + + if(!hasValidTiming()) + { + sb.append(" >> INVALID TIMING"); + } + + return sb.toString(); + } + + /** + * Candidate timing correction + * @return correction + */ + public double getTimingCorrection() + { + return mAdditionalOffset + mTimingAdjustment; + } + + /** + * Equalizer balance correction value. + * @return correction + */ + public float getBalanceAdjustment() + { + return mBalanceAdjustment; + } + + /** + * Equalizer gain correction value + * @return correction. + */ + public float getGainAdjustment() + { + return mGainAdjustment; + } + + /** + * Number of bits that were corrected across the SYNC and NID + * @return corrected bit count. + */ + public int getCorrectedBitCount() + { + return mCorrectedBitCount; + } + + /** + * Indicates if this correction has valid timing adjustment information. + * @return true if the timing correction is not the default (ie no timing) double max value. + */ + public boolean hasValidTiming() + { + return getTimingCorrection() != Double.MAX_VALUE; + } + + /** + * Indicates if this correction has a valid NID or the optimization (or detection) score is high quality. + * @return true if valid. + */ + public boolean isValid() + { + return mOptimizationScore > getSyncThresholdEqualized(); + } + } + + /** + * On sync detection, calculates the optimal timing adjustment that achieves the highest sync correlation score + * or returns an INVALID_SYNC_DETECTION sentinel value if the correlation score doesn't exceed the threshold. + * + * @param additionalOffset from current bufferPointer and samplePoint to adjust for which sync detector + * triggered the event. This can be zero offset for the primary sync detector or a lagging (ie half symbol) + * offset for the lagging sync detector. + * @return optimized timing adjustment or NO_OPTIMIZATION sentinel value. + */ + 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; + + //Reject any sync detections where the sample:sample standard deviation exceeds the noise threshold. + double standardDeviation = getStandardDeviation(offset); + + if(standardDeviation > mNoiseStandardDeviationThreshold) + { + return INVALID_SYNC_DETECTION; + } + + //Absolute balance (equalized symbol domain) that cancels the DC offset measured across the sync pattern + float measuredBalance = -getBalance(offset) * mSampleEqualizer.mGain; + + //Find the optimal symbol timing + double samplesPerSymbol = mSamplesPerSymbol; + double stepSize = samplesPerSymbol / (mSynchronized ? 16.0 : 8.0); //Start at 1/8th for coarse & 1/16th for fine + double stepSizeMin = mOptimizeFineIncrement; + double adjustment = 0.0; + + //In coarse sync mode, constrain max adjustment to half a symbol period so that a lagging sync detect doesn't + //preempt a primary sync detect prematurely and cause a significant timing correction. So, we constrain the + //max correction to a half symbol period for coarse sync mode. + double adjustmentMax = samplesPerSymbol / 2.0; + double candidate = offset; + + float scoreCenter = score(candidate, samplesPerSymbol, measuredBalance); + + candidate = offset - stepSize; + float scoreLeft = score(candidate, samplesPerSymbol, measuredBalance); + + candidate = offset + stepSize; + float scoreRight = score(candidate, samplesPerSymbol, measuredBalance); + + while(stepSize > stepSizeMin && Math.abs(adjustment) <= adjustmentMax) + { + if(scoreLeft > scoreRight && scoreLeft > scoreCenter) + { + adjustment -= stepSize; + scoreRight = scoreCenter; + scoreCenter = scoreLeft; + + candidate = offset + adjustment - stepSize; + scoreLeft = score(candidate, samplesPerSymbol, measuredBalance); + } + else if(scoreRight > scoreLeft && scoreRight > scoreCenter) + { + adjustment += stepSize; + scoreLeft = scoreCenter; + scoreCenter = scoreRight; + + candidate = offset + adjustment + stepSize; + scoreRight = score(candidate, samplesPerSymbol, measuredBalance); + } + else + { + stepSize *= 0.5f; + + if(stepSize > stepSizeMin) + { + candidate = offset + adjustment - stepSize; + scoreLeft = score(candidate, samplesPerSymbol, measuredBalance); + + candidate = offset + adjustment + stepSize; + scoreRight = score(candidate, samplesPerSymbol, measuredBalance); + } + } + } + + //If we didn't find an optimal correlation score above the threshold, return a false sync. + if(scoreCenter < getSyncThresholdOptimized()) + { + return INVALID_SYNC_DETECTION; + } + + return getCorrection(additionalOffset, adjustment, detectionScore, scoreCenter, bufferOffset, + measuredBalance); + } + + /** + * Sample equalizer providing gain and balance (DC offset compensation). + * + * Note: equalizer is initialized with a nominal gain derived from the samples per symbol and the protocol's outer + * symbol deviation and symbol rate (see getNominalGainScalar()), and is constrained to +/- 10% of that nominal + * value. Gain value is adjusted and optimized with each sync detection. + */ + public class SampleEqualizer + { + private static final float EQUALIZER_LOOP_GAIN = 0.05f; + //Balance values are in the equalized symbol domain (radians per symbol, same scale as the ideal symbol phases). + //PI/2 equates to +/- symbolRate / (4 * nominal gain scalar) Hz: P25 1200 Hz, NXDN48 700 Hz, NXDN96 1600 Hz + private static final float EQUALIZER_MAXIMUM_BALANCE = (float)(Math.PI / 2.0); + private static final float EQUALIZER_RECALIBRATE_THRESHOLD = (float)(Math.PI / 8.0); + private static final float DYNAMIC_BALANCE_ERROR_THRESHOLD = (float)(Math.PI / 8.0); + private static final float DYNAMIC_BALANCE_SWING = (float)(Math.PI / 12.0); + private static final float DYNAMIC_BALANCE_GAIN = 0.25f; + private float mBalanceOptimized; + private float mBalanceMinimum = -DYNAMIC_BALANCE_ERROR_THRESHOLD; + private float mBalanceMaximum = DYNAMIC_BALANCE_ERROR_THRESHOLD; + private float mBalance = 0.0f; + private float mGain = 1.0f; + private float mGainInitial = 1.0f; + private float mGainMax = 1.0f; + private float mGainMin = 1.0f; + private final boolean mDynamicBalanceTracking; + + /** + * Constructs an instance. Note: gain values are established when configure() is invoked with the + * samples per symbol. + * @param dynamicBalanceTracking to enable dynamic balance tracking + */ + public SampleEqualizer(boolean dynamicBalanceTracking) + { + mDynamicBalanceTracking = dynamicBalanceTracking; + } + + /** + * Applies decision directed correction to the constellation balance when dynamic balance tracking is enabled + * and the processor is synchronized following a valid sync detection. + * @param symbol sliced at the correct timing + */ + public float correct(float symbol) + { + if(mDynamicBalanceTracking && mSynchronized) + { + Dibit dibit = Dibit.fromSample(symbol); + float adjustment = (dibit.getIdealPhase() - symbol); + + if(Math.abs(adjustment) < DYNAMIC_BALANCE_ERROR_THRESHOLD) + { + adjustment *= DYNAMIC_BALANCE_GAIN; + mBalance += adjustment; + mBalance = Math.clamp(mBalance, mBalanceMinimum, mBalanceMaximum); + return symbol + adjustment; + } + else + { + //If symbols are too noisy, gradually restore balance to optimized level + mBalance += (mBalanceOptimized - mBalance) * DYNAMIC_BALANCE_GAIN; + } + } + + return symbol; + } + + /** + * Applies equalizer correction settings. + * + * @param correction settings to apply + */ + public void apply(Correction correction) + { + float balanceCorrection = correction.getBalanceAdjustment(); + +// System.out.println("Correction [" + DEBUG_DF.format(balanceCorrection) + +// "] Balance [" + DEBUG_DF.format(mBalance) + +// "] Applying [" + DEBUG_DF.format(balanceCorrection * EQUALIZER_LOOP_GAIN) + +// "] Synchronized [" + mSynchronized + "] " + correction); + + //Re-initialize the equalizer any time the balance correction value exceeds the threshold. + if(mSynchronized && Math.abs(balanceCorrection) > EQUALIZER_RECALIBRATE_THRESHOLD) + { + reset(); + } + + //Limit equalizer adjustments using a control loop at each sync detect after the initial equalizer setup or + //apply as new settings on initial sync detect or any time an excess balance correction is supplied. + if(mSynchronized) + { + mBalanceOptimized += (mBalance - mBalanceOptimized + balanceCorrection) * EQUALIZER_LOOP_GAIN; +// mBalanceOptimized = mBalance + (balanceCorrection * EQUALIZER_LOOP_GAIN); + mGain += (correction.getGainAdjustment() * EQUALIZER_LOOP_GAIN); + } + else + { + mBalanceOptimized = mBalance + balanceCorrection; + mGain += correction.getGainAdjustment(); + } + + mBalanceOptimized = Math.clamp(mBalanceOptimized, -EQUALIZER_MAXIMUM_BALANCE, EQUALIZER_MAXIMUM_BALANCE); + mBalanceMinimum = mBalanceOptimized - DYNAMIC_BALANCE_SWING; + mBalanceMaximum = mBalanceOptimized + DYNAMIC_BALANCE_SWING; + mBalance = mBalanceOptimized; + mGain = Math.clamp(mGain, mGainMin, mGainMax); + } + + /** + * Current balance that represents a traditional PLL signal offset value. + * @return balance. + */ + public float balance() + { + return mBalance; + } + + /** + * Balance as optimized at the last sync detection + * @return balance optimized + */ + public float balanceOptimized() + { + return mBalanceOptimized; + } + + /** + * Current gain + */ + public float gain() + { + return mGain; + } + + /** + * Updates the initial, min and max gain values from the samples per symbol and the protocol's nominal + * gain scalar. + * @param samplesPerSymbol for the incoming stream + */ + public void configure(double samplesPerSymbol) + { + double nominalGain = samplesPerSymbol * getNominalGainScalar(); + mGainInitial = (float)nominalGain; + mGain = mGainInitial; + + //Allow the gain to float up/down by a fraction of the nominal gain + float tolerance = (float)(nominalGain * SAMPLE_GAIN_TOLERANCE); + mGainMax = mGain + tolerance; + mGainMin = mGain - tolerance; + } + + /** + * Reset after center frequency change or PPM change. + */ + public void reset() + { + mSynchronized = false; + mGain = mGainInitial; + } + + /** + * Equalize the symbol by applying balance correction and multiplying with gain value. + * @param symbol to equalize. + * @return equalized symbol + */ + public float equalize(float symbol) + { + return symbol * mGain + mBalance; + } + + /** + * Equalizes both samples with optional phase wrap detection and returns the interpolated value at the mu offset. + * + * @param sample1 of the sample stream + * @param sample2 of the sample stream + * @param mu interpolation point between sample 1 and sample 2 + * @return equalized symbol + */ + 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); + } + + /** + * Gets the interpolated sample value at the specified offset and equalizes it with the current equalizer settings. + * @param offset to the sample + * @return equalized value. + */ + public float getEqualizedSymbol(double offset) + { + int integral = (int)Math.floor(offset); + + if(integral >= 0) + { + return getEqualizedSymbol(mBuffer[integral], mBuffer[integral + 1], offset - integral); + } + + return 0; + } + + } + + /** + * Calculates the sync correlation score at the specified offset and samples per symbol interval using the + * current equalizer gain and supplied balance value. + * + * @param offset to the final symbol in the soft symbol buffer + * @param samplesPerSymbol spacing to test for. * @param balance for the calculation + * @return correlation score. + */ + public float score(double offset, double samplesPerSymbol, float balance) + { + return score(offset, samplesPerSymbol, balance, mSampleEqualizer.mGain); + } + + /** + * Calculates the sync correlation score at the specified offset and samples per symbol interval. + * @param offset to the final symbol in the soft symbol buffer + * @param samplesPerSymbol spacing to test for. + * @return correlation score. + */ + public float score(double offset, double samplesPerSymbol, float balance, float gain) + { + int maxPointer = mBuffer.length - 1; + float softSymbol; + + double pointer = offset - (samplesPerSymbol * (mSyncDetector.getDibitLength() - 1)); + int bufferPointer = (int)Math.floor(pointer); + double fractional = pointer - bufferPointer; + + float score = 0; + float sample1, sample2; + float idealSymbol; + + for(int x = 0; x < mSyncDetector.getDibitLength(); x++) + { + if(bufferPointer < maxPointer) + { + sample1 = mBuffer[bufferPointer] * gain + balance; + sample2 = mBuffer[bufferPointer + 1] * gain + balance; + softSymbol = LinearInterpolator.calculate(sample1, sample2, fractional); + } + else + { + softSymbol = 0.0f; + } + + idealSymbol = mSyncDetector.getPatternSymbols()[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; + } + + return score; + } + + /** + * Converts the magnitude error measured across the sync pattern symbols into an (additive) gain adjustment for the + * sample equalizer. The soft symbols were measured with the current gain applied, so the gain that maps them to + * the ideal symbol magnitudes is currentGain * idealMagnitudeSum / softMagnitudeSum. + * + * @param idealMagnitudeSum sum of the absolute ideal symbol values across the sync pattern + * @param magnitudeErrorSum sum of (|ideal symbol| - |soft symbol|) across the sync pattern + * @return gain adjustment to add to the current gain value, or zero if the soft symbol magnitudes are degenerate. + */ + protected float getGainAdjustment(float idealMagnitudeSum, float magnitudeErrorSum) + { + float softMagnitudeSum = idealMagnitudeSum - magnitudeErrorSum; + + if(softMagnitudeSum <= 0 || idealMagnitudeSum <= 0) + { + return 0.0f; + } + + return mSampleEqualizer.mGain * (idealMagnitudeSum / softMagnitudeSum - 1.0f); + } + + /** + * Calculates the mean of sync pattern detection for the samples. + * @param offset for the sync detection which is just after the sample of the last symbol in the sync pattern + * @return mean for the samples in this range + */ + public float getBalance(double offset) + { + Mean mean = new Mean(); + int start = (int)Math.floor(offset - ((mSyncDetector.getDibitLength() - 1) * mSamplesPerSymbol)); + int end = (int)Math.ceil(offset); + + for(int i = start; i <= end; i++) + { + mean.increment(mBuffer[i]); + } + + return (float)mean.getResult(); + } + + /** + * Calculates a correction update for the equalizer balance and gain when the sync pattern is detected in the + * sample buffer, using the supplied timing correction argument. Resamples the sync symbols and compares each + * soft symbol to the ideal symbol phase to develop average error measurements for balance and gain. On initial + * 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(double additionalOffset, double timingCorrection, + float detectionScore, float optimizationScore, double offset, float measuredBalance) + { + double resampleStart = offset + additionalOffset + timingCorrection - (mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1)); + int resampleStartIntegral = (int)Math.floor(resampleStart); + float symbol, gainAccumulator = 0, idealMagnitudeAccumulator = 0, resampledSoftSymbol; + Dibit resampledDibit; + int bitErrorCount = 0; + + for(int x = 0; x < mSyncDetector.getDibitLength(); x++) + { + if(resampleStartIntegral >= 0) + { + symbol = mSyncDetector.getPatternSymbols()[x]; + resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral], + mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral); + resampledSoftSymbol = resampledSoftSymbol * mSampleEqualizer.mGain + measuredBalance; + resampledDibit = Dibit.fromSample(resampledSoftSymbol); + bitErrorCount += mSyncDetector.getPatternDibits()[x].getBitErrorFrom(resampledDibit); + gainAccumulator += Math.abs(symbol) - Math.abs(resampledSoftSymbol); + idealMagnitudeAccumulator += Math.abs(symbol); + } + + resampleStart += mSamplesPerSymbol; + resampleStartIntegral = (int)Math.floor(resampleStart); + } + + //Convert the accumulated magnitude error into a gain adjustment + gainAccumulator = getGainAdjustment(idealMagnitudeAccumulator, gainAccumulator); + + //Recalculate the optimization score using the adjusted equalizer settings + if(!mSynchronized) + { + double optimizedFinalSymbolOffset = offset + additionalOffset + timingCorrection; + float adjustedGain = mSampleEqualizer.mGain + gainAccumulator; + measuredBalance = -getBalance(optimizedFinalSymbolOffset) * adjustedGain; + optimizationScore = score(optimizedFinalSymbolOffset, mSamplesPerSymbol, measuredBalance, adjustedGain); + } + + //Correction carries the balance as an adjustment relative to the current equalizer balance + float balanceAdjustment = measuredBalance - mSampleEqualizer.mBalance; + + return new Correction(additionalOffset, timingCorrection, balanceAdjustment, gainAccumulator, detectionScore, + optimizationScore, bitErrorCount); + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/c4fm/DibitSyncDetector.java b/src/main/java/io/github/dsheirer/dsp/c4fm/DibitSyncDetector.java new file mode 100644 index 00000000..544c7a59 --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/c4fm/DibitSyncDetector.java @@ -0,0 +1,203 @@ +package io.github.dsheirer.dsp.c4fm; + +import io.github.dsheirer.dsp.symbol.Dibit; +import java.util.Map; +import java.util.TreeMap; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; + +/** + * Base sync detector for C4FM/QPSK dibit implementations. + */ +public class DibitSyncDetector +{ + private final long mPattern; + private final long mMask; + private final int mDibitLength; + private Dibit[] mPatternDibits; + private float[] mPatternSymbols; + private final Map mReferenceWaveformMap = new TreeMap<>(); + + /* + * Constructs an instance. + * @param pattern for the sync + * @param dibitLength of the sync pattern + */ + public DibitSyncDetector(long pattern, int dibitLength) + { + mPattern = pattern; + mMask = (long)(Math.pow(2, dibitLength * 2) - 1); + mDibitLength = dibitLength; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("SYNC: ").append(Long.toHexString(mPattern).toUpperCase()); + sb.append(" MASK: ").append(Long.toHexString(mMask).toUpperCase()); + sb.append(" LENGTH: ").append(mDibitLength); + return sb.toString(); + } + + /** + * Sync pattern + * @return pattern + */ + public long getPattern() + { + return mPattern; + } + + /** + * Sync mask + */ + public long getMask() + { + return mMask; + } + + /** + * Pattern length in dibits + */ + public int getDibitLength() + { + return mDibitLength; + } + + /** + * Sync pattern as an array of dibits. + * @return dibit array. + */ + public Dibit[] getPatternDibits() + { + if(mPatternDibits == null) + { + mPatternDibits = toDibits(mPattern, mDibitLength); + } + + return mPatternDibits; + } + + /** + * Sync pattern as an array of soft symbols + * @return symbols array in radians. + */ + public float[] getPatternSymbols() + { + if(mPatternSymbols == null) + { + mPatternSymbols = toSymbols(getPatternDibits()); + } + + return mPatternSymbols; + } + + /** + * Converts the sync pattern to dibits. + * @param pattern to convert + * @param dibitLength in dibits + * @return array of dibits for the sync pattern. + */ + public static Dibit[] toDibits(long pattern, int dibitLength) + { + Dibit[] dibits = new Dibit[dibitLength]; + long mask = 3; + int dibitValue; + + for(int x = 0; x < dibitLength; x++) + { + dibitValue = (int)((pattern & mask) >> (2 * x)); + + dibits[dibitLength - x - 1] = switch(dibitValue) + { + case 0 -> Dibit.D00_PLUS_1; + case 1 -> Dibit.D01_PLUS_3; + case 2 -> Dibit.D10_MINUS_1; + default -> Dibit.D11_MINUS_3; + }; + + mask = mask << 2; + } + + return dibits; + } + + /** + * Utility method to convert a dibit array into an array of ideal soft symbol values. + * @param dibits for the sync pattern + * @return ideal soft symbol array. + */ + public static float[] toSymbols(Dibit[] dibits) + { + float[] symbols = new float[dibits.length]; + + for(int x = 0; x < dibits.length; x++) + { + symbols[x] = dibits[x].getIdealPhase(); + } + + return symbols; + } + + /** + * Creates a reference waveform representation of the sync pattern that can be used to correlate against a + * candidate detected sync waveform to discriminate between a valid and invalid sync detection. The generated + * waveform will be aligned where the final sample is time-aligned to the final symbol. + * + * @param samplesPerSymbol count + * @return a reference waveform and ideal correlation score. + */ + public ReferenceSyncWaveform getReferenceWaveform(double samplesPerSymbol) + { + int key = (int)(samplesPerSymbol * 1000.0); + + if(mReferenceWaveformMap.containsKey(key)) + { + return mReferenceWaveformMap.get(key); + } + + float[] symbols = getPatternSymbols(); + int sampleCount = (int)Math.ceil(samplesPerSymbol * (symbols.length - 1)) + 1; + float[] samples = new float[sampleCount]; + double sampleInterval = 1.0 / samplesPerSymbol; + + double[] x = new double[symbols.length + 2]; + double[] y = new double[symbols.length + 2]; + + for(int i = -1; i < x.length - 1; i++) + { + x[i + 1] = i; + + if(0 <= i && i < symbols.length) + { + y[i + 1] = symbols[i]; + } + } + + double start = symbols.length - 1; + int samplePointer = sampleCount - 1; + SplineInterpolator interpolator = new SplineInterpolator(); + PolynomialSplineFunction function = interpolator.interpolate(x, y); + + float accumulator = 0; + float sample; + float max = 0; + while(samplePointer >= 0) + { + sample = (float)function.value(start); + + if(Math.abs(sample) > max) + { + max = Math.abs(sample); + } + samples[samplePointer--] = sample; + accumulator += sample * sample; + start -= sampleInterval; + } + + ReferenceSyncWaveform referenceWaveform = new ReferenceSyncWaveform(samples, accumulator, max); + mReferenceWaveformMap.put(key, referenceWaveform); + return referenceWaveform; + } +} diff --git a/src/main/java/io/github/dsheirer/dsp/c4fm/ISoftC4FMSyncDetector.java b/src/main/java/io/github/dsheirer/dsp/c4fm/ISoftC4FMSyncDetector.java new file mode 100644 index 00000000..339e6f2a --- /dev/null +++ b/src/main/java/io/github/dsheirer/dsp/c4fm/ISoftC4FMSyncDetector.java @@ -0,0 +1,40 @@ +package io.github.dsheirer.dsp.c4fm; + +import io.github.dsheirer.dsp.symbol.Dibit; + +/** + * Interface for a soft sync detector for C4FM/QPSK dibits. + */ +public interface ISoftC4FMSyncDetector +{ + /** + * Process the symbol and detect sync pattern + * @param softSymbol to process + * @return sync detection score + */ + float process(float softSymbol); + + /** + * Soft symbols that represent the sync pattern + * @return soft symbols array + */ + float[] getPatternSymbols(); + + /** + * Array of dibits for the sync pattern + * @return sync pattern dibit array + */ + Dibit[] getPatternDibits(); + + /** + * Length of the sync pattern in dibits + */ + int getDibitLength(); + + /** + * Provides a reference waveform that represents the sync pattern for sync detection validation + * @param samplesPerSymbol for each symbol + * @return reference waveform + */ + ReferenceSyncWaveform getReferenceWaveform(double samplesPerSymbol); +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/ReferenceSyncWaveform.java b/src/main/java/io/github/dsheirer/dsp/c4fm/ReferenceSyncWaveform.java similarity index 95% rename from src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/ReferenceSyncWaveform.java rename to src/main/java/io/github/dsheirer/dsp/c4fm/ReferenceSyncWaveform.java index 47ca4647..b9819e4e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/ReferenceSyncWaveform.java +++ b/src/main/java/io/github/dsheirer/dsp/c4fm/ReferenceSyncWaveform.java @@ -17,7 +17,7 @@ * **************************************************************************** */ -package io.github.dsheirer.module.decode.nxdn.layer1.sync; +package io.github.dsheirer.dsp.c4fm; /** * Waveform representing the ideal sync pattern, resampled at the sample rate. 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 a9d538ea..f7cc527e 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,63 @@ public class FilterFactory return cicArray; } + /** + * Designs an integrate-and-dump filter. + * + * @param sampleRate in Hertz + * @param symbolRate in symbols per second (4800 for P25 Phase 1) + * @return filter taps, symmetric, summing to 1.0 (unity DC gain) + * @throws IllegalArgumentException if either rate is non-positive or the sample rate is below the symbol rate + */ + public static float[] getIntegrateAndDump(double sampleRate, double symbolRate) + { + if(sampleRate <= 0 || symbolRate <= 0) + { + throw new IllegalArgumentException("Sample rate and symbol rate must be positive"); + } + + double samplesPerSymbol = sampleRate / symbolRate; + + if(samplesPerSymbol < 1.0) + { + throw new IllegalArgumentException("Sample rate must be at least the symbol rate"); + } + + double halfWidth = samplesPerSymbol / 2.0; + + //Candidate length: odd and long enough to span the window plus a partial tap on each edge + int length = 2 * (int)Math.ceil(halfWidth + 0.5) + 1; + double center = (length - 1) / 2.0; + double[] weights = new double[length]; + + for(int i = 0; i < length; i++) + { + //Each tap represents the unit interval [x - 0.5, x + 0.5] centered on its position + double x = i - center; + double low = Math.max(x - 0.5, -halfWidth); + double high = Math.min(x + 0.5, halfWidth); + weights[i] = Math.max(0.0, high - low); + } + + //Trim zero-weight taps from the edges (symmetric, so trim equally) + final double epsilon = 1e-9; + int start = 0; + while(start < length / 2 && weights[start] < epsilon) + { + start++; + } + int end = length - start; + + //Normalize to unity DC gain: the weights sum to samplesPerSymbol + float[] taps = new float[end - start]; + for(int i = start; i < end; i++) + { + taps[i - start] = (float)(weights[i] / samplesPerSymbol); + } + + return taps; + } + /** * Creates a Root Raised Cosine filter. * @param samplesPerSymbol diff --git a/src/main/java/io/github/dsheirer/edac/bch/BCH_63_16_23_P25.java b/src/main/java/io/github/dsheirer/edac/bch/BCH_63_16_23_P25.java index c83ec53c..b3be411a 100644 --- a/src/main/java/io/github/dsheirer/edac/bch/BCH_63_16_23_P25.java +++ b/src/main/java/io/github/dsheirer/edac/bch/BCH_63_16_23_P25.java @@ -31,6 +31,9 @@ public class BCH_63_16_23_P25 extends BCH_63 private static final int T = 11; // Error-correcting capability public static final IntField NAC_FIELD = IntField.length12(0); public static final IntField DUID_FIELD = IntField.length4(12); + //Generator polynomial g(x) for BCH(63,16,23), degree 47 (octal 6331 1413 6723 5453 per TIA-102.BAAA) + private static final long GENERATOR_POLYNOMIAL = 0xCD930BDD3B2BL; + private static final int PARITY_LENGTH = 47; /** * Constructs a BCH decoder instance for processing APCO25 BCH(63,16,23) protected NID codewords @@ -40,6 +43,30 @@ public class BCH_63_16_23_P25 extends BCH_63 super(K, T); } + /** + * Encodes the NAC and DUID as a systematic BCH(63,16,23) NID codeword. The returned value holds the 63 codeword + * bits with the first transmitted bit (NAC MSB) in bit position 62. Note: the 64th NID bit is not included. + * + * @param nac network access code (12 bits) + * @param duid data unit ID value (4 bits) + * @return 63-bit codeword. + */ + public static long encode(int nac, int duid) + { + long information = ((long)(nac & 0xFFF) << 4) | (duid & 0xF); + long remainder = information << PARITY_LENGTH; + + for(int bit = 62; bit >= PARITY_LENGTH; bit--) + { + if(((remainder >> bit) & 1) == 1) + { + remainder ^= GENERATOR_POLYNOMIAL << (bit - PARITY_LENGTH); + } + } + + return (information << PARITY_LENGTH) | remainder; + } + @Override public void decode(CorrectedBinaryMessage message) { diff --git a/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_1_2_P25.java b/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_1_2_P25.java index 56a9e8dc..8ef4256b 100644 --- a/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_1_2_P25.java +++ b/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_1_2_P25.java @@ -20,6 +20,7 @@ package io.github.dsheirer.edac.trellis; import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.dsp.symbol.Dibit; import java.util.List; @@ -28,6 +29,12 @@ import java.util.List; */ public class ViterbiDecoder_1_2_P25 extends ViterbiDecoder { + /** + * Maximum per-symbol error magnitude (radians) applied to the soft decision branch metric. FM discriminator noise + * is impulsive (clicks), and an unbounded squared error lets a single corrupted symbol dominate the path metric. + * Noise injection testing against a P25 control channel recording found a peak at 1.75 - 2.0 radians. + */ + private static final double SOFT_SYMBOL_ERROR_CLIP = 1.75; public ViterbiDecoder_1_2_P25() { super(2,4); @@ -49,6 +56,143 @@ public class ViterbiDecoder_1_2_P25 extends ViterbiDecoder return getMessage(mostLikelyPath); } + /** + * Soft decision Viterbi decoding of a deinterleaved 1/2 rate TCM encoded P25 block. Each 4-bit trellis output + * value is transmitted as two dibits/symbols, so the branch metric is the squared Euclidean distance between the + * two received soft symbols and the ideal symbol phases for the branch output value. + * + * The corrected bit count on the returned message is the Hamming distance between the hard decisions of the + * received soft symbols and the re-encoded output of the decoded path, consistent with hard decision decoding. + * + * @param softSymbols 98 deinterleaved soft symbols (radians, ideal values +/- PI/4 and +/- 3*PI/4). + * @return decoded 96-bit message + */ + public CorrectedBinaryMessage decodeSoft(float[] softSymbols) + { + if(softSymbols == null || softSymbols.length % 2 != 0 || softSymbols.length < 4) + { + throw new IllegalArgumentException("Soft symbols must contain an integral number of symbol pairs"); + } + + int steps = softSymbols.length / 2; + int states = getInputValueCount(); + double[] metrics = new double[states]; + double[] nextMetrics = new double[states]; + int[][] previousState = new int[steps][states]; + java.util.Arrays.fill(metrics, Double.MAX_VALUE); + metrics[0] = 0.0; //Encoder starts in state 0 + + //Branch metric for each of the 16 possible 4-bit output values at the current step + double[] branchMetrics = new double[getOutputValueCount()]; + + for(int step = 0; step < steps; step++) + { + float first = softSymbols[2 * step]; + float second = softSymbols[2 * step + 1]; + + for(int output = 0; output < branchMetrics.length; output++) + { + double errorFirst = Math.min(Math.abs(first - getIdealPhase(output >> 2)), SOFT_SYMBOL_ERROR_CLIP); + double errorSecond = Math.min(Math.abs(second - getIdealPhase(output & 3)), SOFT_SYMBOL_ERROR_CLIP); + branchMetrics[output] = errorFirst * errorFirst + errorSecond * errorSecond; + } + + java.util.Arrays.fill(nextMetrics, Double.MAX_VALUE); + + //The final step is the flushing step where the encoder input is zero + int inputCount = (step == steps - 1) ? 1 : states; + + for(int state = 0; state < states; state++) + { + if(metrics[state] == Double.MAX_VALUE) + { + continue; + } + + for(int input = 0; input < inputCount; input++) + { + double metric = metrics[state] + branchMetrics[P25_1_2_Node.TRANSITION_MATRIX[state][input]]; + + if(metric < nextMetrics[input]) + { + nextMetrics[input] = metric; + previousState[step][input] = state; + } + } + } + + double[] swap = metrics; + metrics = nextMetrics; + nextMetrics = swap; + } + + //Trace back from the final (flushed) state 0. The state after each step equals that step's input value. + int[] inputs = new int[steps]; + int state = 0; + + for(int step = steps - 1; step >= 0; step--) + { + inputs[step] = state; + state = previousState[step][state]; + } + + //Message bits are the inputs for all but the final flushing step + CorrectedBinaryMessage message = new CorrectedBinaryMessage((steps - 1) * getInputBitLength()); + int bitErrors = 0; + int previous = 0; + + for(int step = 0; step < steps; step++) + { + int input = inputs[step]; + + if(step < steps - 1) + { + int offset = step * getInputBitLength(); + + if((input & 2) == 2) + { + message.set(offset); + } + + if((input & 1) == 1) + { + message.set(offset + 1); + } + } + + int output = P25_1_2_Node.TRANSITION_MATRIX[previous][input]; + int received = (getDibitValue(softSymbols[2 * step]) << 2) | getDibitValue(softSymbols[2 * step + 1]); + bitErrors += P25_1_2_Node.HAMMING_ERROR_COUNT[output ^ received]; + previous = input; + } + + message.setCorrectedBitCount(bitErrors); + return message; + } + + /** + * Ideal soft symbol phase for a 2-bit dibit value where the MSB is the first transmitted bit. + */ + private static float getIdealPhase(int dibitValue) + { + return switch(dibitValue) + { + case 0 -> Dibit.D00_PLUS_1.getIdealPhase(); + case 1 -> Dibit.D01_PLUS_3.getIdealPhase(); + case 2 -> Dibit.D10_MINUS_1.getIdealPhase(); + default -> Dibit.D11_MINUS_3.getIdealPhase(); + }; + } + + /** + * Hard decision 2-bit dibit value (MSB is the first transmitted bit) for the soft symbol. + */ + private static int getDibitValue(float softSymbol) + { + Dibit dibit = Dibit.fromSample(softSymbol); + return (dibit.getBit1() ? 2 : 0) | (dibit.getBit2() ? 1 : 0); + } + /** * Extracts the decoded/corrected message from the surviving path argument. * diff --git a/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncVisualizer.java b/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncVisualizer.java index db235e57..d9455245 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncVisualizer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/sync/FMSyncVisualizer.java @@ -50,7 +50,7 @@ public class FMSyncVisualizer extends VBox implements ISyncResultsListener private final XYChart.Series mSymbolSeriesMarker = new XYChart.Series<>(); private final ScatterChart mConstellationChart = new ScatterChart<>(mConstellationI, mConstellationQ); private final XYChart.Series mConstellationSymbols = new XYChart.Series<>(); - private static final int CONSTELLATION_SYMBOL_COUNT = 50; + private static final int CONSTELLATION_SYMBOL_COUNT = 24; private int mConstellationSymbolPointer = 0; private final LineChart mSampleChart = new LineChart<>(mSampleTiming, mSamplePhase); diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/DecodeConfigNXDN.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/DecodeConfigNXDN.java index e40f81ba..7a30884d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/DecodeConfigNXDN.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/DecodeConfigNXDN.java @@ -36,9 +36,6 @@ import java.util.List; */ public class DecodeConfigNXDN extends DecodeConfigJMBE { - public static final int CHANNEL_ROTATION_DELAY_MINIMUM_MS = 200; - public static final int CHANNEL_ROTATION_DELAY_DEFAULT_MS = 500; - public static final int CHANNEL_ROTATION_DELAY_MAXIMUM_MS = 2000; private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT; private boolean mIgnoreDataCalls = false; private boolean mIgnoreEncryptedCalls = false; 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 9623bba6..6c2cf0d4 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 @@ -31,6 +31,7 @@ import io.github.dsheirer.dsp.fm.IDemodulator; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.FeedbackDecoder; +import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency; import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.buffer.IByteBufferProvider; @@ -43,9 +44,9 @@ import io.github.dsheirer.source.wave.ComplexWaveSource; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,7 +57,7 @@ public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider, ISourceEventListener, Listener { private static final Logger LOGGER = LoggerFactory.getLogger(NXDNDecoder.class); - private static final Map BASEBAND_FILTERS = new HashMap<>(); + private static final Map BASEBAND_FILTERS = new ConcurrentHashMap<>(); private final DecodeConfigNXDN mConfig; private final NXDNSymbolProcessor mSymbolProcessor; private final NXDNMessageFramer mMessageFramer; @@ -84,7 +85,7 @@ public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider, mMessageProcessor = new NXDNMessageProcessor(config); mMessageProcessor.setMessageListener(getMessageListener()); mMessageFramer = new NXDNMessageFramer(mMessageProcessor, config.getTransmissionMode()); - mSymbolProcessor = new NXDNSymbolProcessor(mMessageFramer, this); + mSymbolProcessor = new NXDNSymbolProcessor(this, mMessageFramer, config.getTransmissionMode()); } /** @@ -137,10 +138,15 @@ public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider, //Set the decimated sample rate to use for PLL error reporting. setDecimatedSampleRate(decimatedSampleRate); - int symbolLength = 26; - float rrcAlpha = 0.2f; - float[] rrcTaps = FilterFactory.getRRC(samplesPerSymbol, symbolLength, rrcAlpha); - mPulseShapingFilter = new RealFIRFilter(rrcTaps); + int rrcSymbolSpan = 12; + int inverseSyncSymbolSpan = 4; + //Create an RRC pulse shaping filter convolved with an inverse sync (ie restoration) filter. + float[] taps = NXDNFilterDesigner.designNXDN(decimatedSampleRate, mConfig.getTransmissionMode().getSymbolRate(), + rrcSymbolSpan, inverseSyncSymbolSpan); +// int symbolLength = 26; +// float rrcAlpha = 0.2f; +// taps = FilterFactory.getRRC(samplesPerSymbol, symbolLength, rrcAlpha); + mPulseShapingFilter = new RealFIRFilter(taps); float[] basebandTaps = getBasebandFilter(decimatedSampleRate, mConfig); mBasebandFilterI = FilterFactory.getRealFilter(basebandTaps); mBasebandFilterQ = FilterFactory.getRealFilter(basebandTaps); @@ -177,16 +183,12 @@ public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider, */ private float[] getBasebandFilter(double sampleRate, DecodeConfigNXDN config) { - String mapKey = config.getTransmissionMode().name() + sampleRate; - float[] coefficients = null; + //Filter cache is shared across decoder instances that may be started from different threads + float[] coefficients = BASEBAND_FILTERS.get(mapKey); - if(BASEBAND_FILTERS.containsKey(mapKey)) - { - coefficients = BASEBAND_FILTERS.get(mapKey); - } - else + if(coefficients == null) { ChannelSpecification channel = config.getChannelSpecification(); @@ -207,9 +209,10 @@ public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider, BASEBAND_FILTERS.put(mapKey, coefficients); } } - catch(Exception fde) //FilterDesignException + catch(Exception e) //FilterDesignException or runtime failures from the Remez designer { - System.out.println("Error"); + LOGGER.error("Error designing NXDN " + config.getTransmissionMode() + " baseband filter for sample rate [" + + sampleRate + "]", e); } } @@ -342,7 +345,7 @@ public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider, config = new DecodeConfigNXDN(TransmissionMode.M4800); // 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) @@ -350,7 +353,7 @@ public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider, // String file = directory + "20260104_065240_153582500_Mobiletech-Communications-(NXDN)_Fulton_LCN-3_50_baseband.wav"; // String file = directory + "AceVentura Sample 1/20260122_071724_154987500_SCFR_SITE_null_1_baseband.wav"; // String file = directory + "AceVentura Sample 2/20260122_073239_154987500_SCFR_SITE_null_1_baseband.wav"; -// String file = directory + "20260322_063443_150845000_Mobiletech-Communications-(NXDN)_Fulton_Fulton_2_baseband.wav"; + String file = directory + "20260322_063443_150845000_Mobiletech-Communications-(NXDN)_Fulton_Fulton_2_baseband.wav"; //Type-D Channels diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNFilterDesigner.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNFilterDesigner.java new file mode 100644 index 00000000..d8987db2 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNFilterDesigner.java @@ -0,0 +1,358 @@ +package io.github.dsheirer.module.decode.nxdn; + +/** + * Designs an inverse sinc (x / sin(x)) FIR filter for NXDN 4FSK receivers. + * + * The NXDN transmitter applies a sinc response, sin(pi*f*T)/(pi*f*T), to the symbol stream along with the root + * raised cosine filter. The receiver cascades this inverse sinc filter, H(f) = (pi*f*T)/sin(pi*f*T), with its own + * root raised cosine filter to restore an overall raised cosine (Nyquist) response. + * + * The inverse sinc grows without bound as f approaches the symbol rate, so the response is only matched over the + * occupied bandwidth of the signal, |f| <= (1 + alpha) * symbolRate / 2. Beyond that edge the cascaded RRC filter + * suppresses the signal, so that region is lightly weighted toward a flat gain equal to the band-edge value, which + * keeps the filter from boosting out-of-band noise. + * + * The taps are a weighted least-squares fit of a linear phase (symmetric, odd length) FIR filter to that target + * response on a dense frequency grid. + */ +public class NXDNFilterDesigner +{ + /** NXDN root raised cosine roll-off factor. */ + public static final double NXDN_ALPHA = 0.2; + + /** Default filter length in symbol periods. */ + private static final int DEFAULT_SYMBOL_SPAN = 4; + + /** Least-squares weight applied beyond the band edge, relative to a weight of 1.0 in band. */ + private static final double STOPBAND_WEIGHT = 0.01; + + /** Frequency grid points per filter coefficient used for the least-squares fit. */ + private static final int GRID_DENSITY = 16; + + /** + * Designs an inverse sinc filter using the NXDN roll-off factor (0.2) to define the matched bandwidth. + * + * @param sampleRate in Hertz + * @param symbolRate in symbols per second (2400 for NXDN48, 4800 for NXDN96) + * @return filter taps, odd length, linear phase, unity gain at DC + */ + public static float[] design(double sampleRate, double symbolRate) + { + return design(sampleRate, symbolRate, NXDN_ALPHA, DEFAULT_SYMBOL_SPAN); + } + + /** + * Designs an inverse sinc filter. + * + * @param sampleRate in Hertz + * @param symbolRate in symbols per second + * @param alpha roll-off factor of the companion RRC filter, defining the band edge (1 + alpha) * symbolRate / 2 + * @param symbolSpan filter length in symbol periods + * @return filter taps, odd length, linear phase, unity gain at DC + */ + public static float[] design(double sampleRate, double symbolRate, double alpha, int symbolSpan) + { + if(sampleRate <= 0 || symbolRate <= 0) + { + throw new IllegalArgumentException("Sample rate and symbol rate must be positive"); + } + + if(alpha < 0 || alpha > 1) + { + throw new IllegalArgumentException("Alpha must be in the range 0 - 1"); + } + + if(symbolSpan < 1) + { + throw new IllegalArgumentException("Symbol span must be at least 1"); + } + + double nyquist = sampleRate / 2.0; + double bandEdge = (1.0 + alpha) * symbolRate / 2.0; + + if(bandEdge >= nyquist) + { + throw new IllegalArgumentException("Sample rate [" + sampleRate + + "] is too low for symbol rate [" + symbolRate + "] - band edge exceeds Nyquist"); + } + + //Odd tap count so the filter has an integer group delay of (length - 1) / 2 samples + int length = (int)Math.ceil(symbolSpan * sampleRate / symbolRate); + if(length % 2 == 0) + { + length++; + } + length = Math.max(length, 9); + + int middle = (length - 1) / 2; + int coefficientCount = middle + 1; + double symbolPeriod = 1.0 / symbolRate; + double edgeGain = inverseSinc(bandEdge * symbolPeriod); + + //A symmetric filter has the zero-phase response A(f) = c[0] + sum(m=1..middle) c[m] * 2cos(2*pi*f*m/fs) + //where c[m] = h[middle - m] = h[middle + m]. Accumulate the weighted normal equations (B'WB)c = B'Wd over a + //dense frequency grid from DC to Nyquist. + double[][] normal = new double[coefficientCount][coefficientCount]; + double[] rhs = new double[coefficientCount]; + double[] basis = new double[coefficientCount]; + int gridSize = GRID_DENSITY * coefficientCount; + + for(int g = 0; g <= gridSize; g++) + { + double frequency = nyquist * g / gridSize; + boolean inBand = frequency <= bandEdge; + double desired = inBand ? inverseSinc(frequency * symbolPeriod) : edgeGain; + double weight = inBand ? 1.0 : STOPBAND_WEIGHT; + + basis[0] = 1.0; + for(int m = 1; m < coefficientCount; m++) + { + basis[m] = 2.0 * Math.cos(2.0 * Math.PI * frequency * m / sampleRate); + } + + for(int i = 0; i < coefficientCount; i++) + { + double weighted = weight * basis[i]; + rhs[i] += weighted * desired; + + for(int j = i; j < coefficientCount; j++) + { + normal[i][j] += weighted * basis[j]; + } + } + } + + //Fill in the lower triangle of the symmetric normal matrix + for(int i = 0; i < coefficientCount; i++) + { + for(int j = 0; j < i; j++) + { + normal[i][j] = normal[j][i]; + } + } + + double[] coefficients = solve(normal, rhs); + + //Expand to the full symmetric impulse response and normalize to unity gain at DC + double[] taps = new double[length]; + double sum = 0.0; + + for(int m = 0; m < coefficientCount; m++) + { + taps[middle - m] = coefficients[m]; + taps[middle + m] = coefficients[m]; + } + + for(double tap : taps) + { + sum += tap; + } + + float[] output = new float[length]; + for(int n = 0; n < length; n++) + { + output[n] = (float)(taps[n] / sum); + } + + return output; + } + + /** Default root raised cosine filter length in symbol periods for the combined NXDN receive filter. */ + private static final int DEFAULT_RRC_SYMBOL_SPAN = 8; + + /** + * Designs the complete NXDN receive filter: a root raised cosine filter (alpha 0.2) convolved with the inverse + * sinc filter. Cascaded with the NXDN transmit filter (RRC and sinc), the overall response is a raised cosine + * Nyquist filter with no intersymbol interference at the symbol sampling instants. + * + * @param sampleRate in Hertz + * @param symbolRate in symbols per second (2400 for NXDN48, 4800 for NXDN96) + * @return filter taps, odd length, linear phase, unity gain at DC + */ + public static float[] designNXDN(double sampleRate, double symbolRate) + { + return designNXDN(sampleRate, symbolRate, DEFAULT_RRC_SYMBOL_SPAN, DEFAULT_SYMBOL_SPAN); + } + + /** + * Designs the complete NXDN receive filter: a root raised cosine filter (alpha 0.2) convolved with the inverse + * sinc filter. + * + * @param sampleRate in Hertz + * @param symbolRate in symbols per second + * @param rrcSymbolSpan root raised cosine filter length in symbol periods + * @param inverseSincSymbolSpan inverse sinc filter length in symbol periods + * @return filter taps, odd length (sum of both filter lengths - 1), linear phase, unity gain at DC + */ + public static float[] designNXDN(double sampleRate, double symbolRate, int rrcSymbolSpan, int inverseSincSymbolSpan) + { + double[] rrc = designRootRaisedCosine(sampleRate, symbolRate, NXDN_ALPHA, rrcSymbolSpan); + float[] inverseSinc = design(sampleRate, symbolRate, NXDN_ALPHA, inverseSincSymbolSpan); + + //Full linear convolution - both filters are odd length and symmetric, so the result is too + double[] combined = new double[rrc.length + inverseSinc.length - 1]; + + for(int i = 0; i < rrc.length; i++) + { + for(int j = 0; j < inverseSinc.length; j++) + { + combined[i + j] += rrc[i] * inverseSinc[j]; + } + } + + //Normalize to unity gain at DC so the discriminator output keeps its deviation scaling + double sum = 0.0; + for(double tap : combined) + { + sum += tap; + } + + float[] output = new float[combined.length]; + for(int n = 0; n < combined.length; n++) + { + output[n] = (float)(combined[n] / sum); + } + + return output; + } + + /** + * Designs a root raised cosine filter from the closed form impulse response, normalized to unity gain at DC. + * + * @param sampleRate in Hertz + * @param symbolRate in symbols per second + * @param alpha roll-off factor (0 - 1) + * @param symbolSpan filter length in symbol periods + * @return filter taps, odd length, linear phase + */ + public static double[] designRootRaisedCosine(double sampleRate, double symbolRate, double alpha, int symbolSpan) + { + if(sampleRate <= 0 || symbolRate <= 0) + { + throw new IllegalArgumentException("Sample rate and symbol rate must be positive"); + } + + if(alpha <= 0 || alpha > 1) + { + throw new IllegalArgumentException("Alpha must be in the range (0 - 1]"); + } + + if(symbolSpan < 1) + { + throw new IllegalArgumentException("Symbol span must be at least 1"); + } + + int length = (int)Math.ceil(symbolSpan * sampleRate / symbolRate); + if(length % 2 == 0) + { + length++; + } + + int middle = (length - 1) / 2; + double[] taps = new double[length]; + double sum = 0.0; + + for(int n = 0; n < length; n++) + { + //Time in symbol periods relative to the filter center + double t = (n - middle) * symbolRate / sampleRate; + double value; + + if(Math.abs(t) < 1e-9) + { + value = 1.0 - alpha + 4.0 * alpha / Math.PI; + } + else if(Math.abs(Math.abs(t) - 1.0 / (4.0 * alpha)) < 1e-9) + { + //Limit at the singularity t = +/- T / (4 * alpha) + value = (alpha / Math.sqrt(2.0)) * + ((1.0 + 2.0 / Math.PI) * Math.sin(Math.PI / (4.0 * alpha)) + + (1.0 - 2.0 / Math.PI) * Math.cos(Math.PI / (4.0 * alpha))); + } + else + { + double numerator = Math.sin(Math.PI * t * (1.0 - alpha)) + + 4.0 * alpha * t * Math.cos(Math.PI * t * (1.0 + alpha)); + double denominator = Math.PI * t * (1.0 - Math.pow(4.0 * alpha * t, 2.0)); + value = numerator / denominator; + } + + taps[n] = value; + sum += value; + } + + for(int n = 0; n < length; n++) + { + taps[n] /= sum; + } + + return taps; + } + + /** + * Inverse sinc: x / sin(x) where x = pi * f * T. + * + * @param normalizedFrequency f * T (frequency divided by the symbol rate) + */ + private static double inverseSinc(double normalizedFrequency) + { + if(normalizedFrequency == 0.0) + { + return 1.0; + } + + double x = Math.PI * normalizedFrequency; + return x / Math.sin(x); + } + + /** + * Solves Ax = b using Gaussian elimination with partial pivoting. A and b are modified. + */ + private static double[] solve(double[][] a, double[] b) + { + int n = b.length; + + for(int column = 0; column < n; column++) + { + int pivot = column; + for(int row = column + 1; row < n; row++) + { + if(Math.abs(a[row][column]) > Math.abs(a[pivot][column])) + { + pivot = row; + } + } + + double[] swapRow = a[column]; + a[column] = a[pivot]; + a[pivot] = swapRow; + double swapValue = b[column]; + b[column] = b[pivot]; + b[pivot] = swapValue; + + for(int row = column + 1; row < n; row++) + { + double factor = a[row][column] / a[column][column]; + b[row] -= factor * b[column]; + + for(int k = column; k < n; k++) + { + a[row][k] -= factor * a[column][k]; + } + } + } + + double[] x = new double[n]; + for(int row = n - 1; row >= 0; row--) + { + double sum = b[row]; + for(int k = row + 1; k < n; k++) + { + sum -= a[row][k] * x[k]; + } + x[row] = sum / a[row][row]; + } + + return x; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageFramer.java index 456f3ff1..43c94112 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/NXDNMessageFramer.java @@ -20,6 +20,7 @@ package io.github.dsheirer.module.decode.nxdn; import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.dsp.c4fm.C4FMMessageFramer; import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.SyncLossMessage; @@ -39,13 +40,17 @@ import java.util.Arrays; * uncertain message to assemble and then we'll inspect before and after data unit IDs and the quantity of captured * dibits to make a best guess on what the assembled message represents. */ -public class NXDNMessageFramer +public class NXDNMessageFramer extends C4FMMessageFramer { private static final int FRAME_LENGTH = 192; private static final int SYNC_LENGTH = 10; private static final int PAYLOAD_LENGTH = FRAME_LENGTH - SYNC_LENGTH; private static final int SYNC_DETECTION_BIT_ERROR_THRESHOLD = 8; private final Dibit[] mDibitBuffer = new Dibit[192]; + //Soft symbols (radians) parallel to the dibit buffer, or NaN when the soft symbol wasn't provided + private final float[] mSoftSymbolBuffer = new float[192]; + private float mCurrentSoftSymbol = Float.NaN; + private static final float SOFT_SYMBOL_SCALE = (float)(4.0 / Math.PI); //Radians to symbol units (+/-1, +/-3) private final LICHTracker mLICHTracker; private final Listener mMessageListener; private final NXDNHardSyncDetector mHardSyncDetector = new NXDNHardSyncDetector(); @@ -69,6 +74,7 @@ public class NXDNMessageFramer mSymbolsPerSecond = transmissionMode.getSymbolRate(); mCoarseSyncLossThreshold = mSymbolsPerSecond + FRAME_LENGTH; Arrays.fill(mDibitBuffer, Dibit.D00_PLUS_1); + Arrays.fill(mSoftSymbolBuffer, Float.NaN); } /** @@ -90,7 +96,7 @@ public class NXDNMessageFramer { if(mReferenceTimestamp > 0) { - return mReferenceTimestamp + (long)(1000.0 * mDibitSinceTimestampCounter / 4800); + return mReferenceTimestamp + (long)(1000.0 * mDibitSinceTimestampCounter / mSymbolsPerSecond); } else { @@ -99,6 +105,24 @@ public class NXDNMessageFramer } } + /** + * Converts the soft symbol into a pair of soft bit values using max-log likelihood ratios for the Gray mapped + * 4-level symbol (D01=+3, D00=+1, D10=-1, D11=-3), where a positive soft bit value favors a 1 bit. + * @param softSymbol in radians + * @param softBits array to receive the two soft bit values + * @param index for the first bit (sign bit), followed by the second (magnitude) bit + */ + private static void setSoftBits(float softSymbol, float[] softBits, int index) + { + float u = softSymbol * SOFT_SYMBOL_SCALE; + float magnitude = Math.abs(u); + //First bit is 1 for negative symbols + float favorZero = magnitude <= 2.0f ? 4.0f * u : 8.0f * u - 8.0f * Math.signum(u); + softBits[index] = -favorZero; + //Second bit is 1 for the outer (+/-3) symbols + softBits[index + 1] = 4.0f * (magnitude - 2.0f); + } + /** * Dispatches a currently assembling message once it's complete. * @return true if any of the messages from the frame are valid. @@ -106,6 +130,8 @@ public class NXDNMessageFramer private boolean dispatchMessage() { CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(PAYLOAD_LENGTH * 2); + float[] softBits = new float[PAYLOAD_LENGTH * 2]; + boolean softAvailable = true; //The buffer pointer is the next symbol after the completed message, ie start of sync int bufferDibitPointer = mDibitBufferPointer + SYNC_LENGTH; bufferDibitPointer %= FRAME_LENGTH; @@ -114,14 +140,25 @@ public class NXDNMessageFramer Dibit dibit; while(messageDibitPointer < PAYLOAD_LENGTH) { + float softSymbol = mSoftSymbolBuffer[bufferDibitPointer]; dibit = mDibitBuffer[bufferDibitPointer++]; cbm.add(dibit.getBit1(), dibit.getBit2()); + + if(Float.isNaN(softSymbol)) + { + softAvailable = false; + } + else + { + setSoftBits(softSymbol, softBits, messageDibitPointer * 2); + } + bufferDibitPointer %= FRAME_LENGTH; messageDibitPointer++; } boolean validMessage = false; - Frame frame = new Frame(cbm, getTimestamp(), mLICHTracker); + Frame frame = new Frame(cbm, softAvailable ? softBits : null, getTimestamp(), mLICHTracker); for(NXDNMessage message: frame.getMessages()) { @@ -159,7 +196,7 @@ public class NXDNMessageFramer dibitBufferPointer %= FRAME_LENGTH; } - return Long.bitCount(value ^ NXDNSyncDetector.SYNC_PATTERN); + return Long.bitCount(value ^ mHardSyncDetector.getPattern()); } /** @@ -203,8 +240,22 @@ public class NXDNMessageFramer * Process symbol decision without sync detection. * @param symbol that was demodulated. */ + @Override + public boolean process(Dibit symbol, float softSymbol) + { + mCurrentSoftSymbol = softSymbol; + return process(symbol); + } + + /** + * Process symbol decision without sync detection or soft symbol. + * @param symbol that was demodulated. + */ public boolean process(Dibit symbol) { + mSoftSymbolBuffer[mDibitBufferPointer] = mCurrentSoftSymbol; + mCurrentSoftSymbol = Float.NaN; + //Check when we didn't get a sync detect, and count-wise, we're at the next frame's end-sync location if(!mSyncDetected && mDibitsSinceLastMessage == SYNC_LENGTH) { 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 da6644bf..87ea7e05 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 @@ -1,1007 +1,99 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2026 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 - * **************************************************************************** - */ - package io.github.dsheirer.module.decode.nxdn; -import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator; -import io.github.dsheirer.dsp.symbol.Dibit; -import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler; +import io.github.dsheirer.dsp.c4fm.C4FMMessageFramer; +import io.github.dsheirer.dsp.c4fm.C4FMSymbolProcessor; import io.github.dsheirer.module.decode.FeedbackDecoder; -import io.github.dsheirer.module.decode.nxdn.layer1.C4FMEqualizer; -import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSoftSyncDetector; -import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSyncDetector; import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSyncDetectorFactory; -import io.github.dsheirer.module.decode.nxdn.layer1.sync.ReferenceSyncWaveform; -import io.github.dsheirer.sample.Listener; -import java.nio.ByteBuffer; -import java.text.DecimalFormat; -import org.apache.commons.math3.stat.descriptive.moment.Mean; -import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; +import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode; /** - * NXDN symbol processor provides symbol timing recovery and sync detection. + * NXDN implementation of the symbol processor. */ -public class NXDNSymbolProcessor +public class NXDNSymbolProcessor extends C4FMSymbolProcessor { - private static final float SYNC_THRESHOLD_DETECTION = 37; + private static final int MAX_MESSAGE_DIBIT_LENGTH = 192; + private static final int EQUALIZER_TRAINING_PERIOD_SYMBOL_COUNT = 192 * 3; + private static final int EQUALIZER_SKIP_TRAINING_FRAME_COUNT = 10; + //Equalizer span in symbols (14x T/2 taps). Testing against NXDN recordings: 7 yields the most valid messages on + //low SNR and mistuned recordings compared to the default sync-length span of 9 symbols. + private static final int EQUALIZER_SYMBOL_LENGTH = 7; + private static final float SYNC_THRESHOLD_DETECTED = 37; private static final float SYNC_THRESHOLD_OPTIMIZED = 40; private static final float SYNC_THRESHOLD_EQUALIZED = 40; - 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 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; - private final NXDNSoftSyncDetector mSyncDetector = NXDNSyncDetectorFactory.getStandardDetector(); - private final NXDNSoftSyncDetector mSyncDetectorLagging = NXDNSyncDetectorFactory.getStandardDetector(); - private final SampleEqualizer mSampleEqualizer = new SampleEqualizer(); - private final C4FMEqualizer mSymbolEqualizer; - private ReferenceSyncWaveform mReferenceSyncWaveform; - private double mNoiseStandardDeviationThreshold; - private double mSamplePoint; - private double mSamplesPerSymbol; - private double mTimingCorrectionThreshold; - private float[] mBuffer; - private float mLaggingSyncOffset; - private float mOptimizeFineIncrement; - private int mBufferPointer; - private int mBufferReloadThreshold; - private int mDelayedSyncNotificationSymbolsRemaining = 0; - private int mSymbolsSinceLastSync = 0; - private boolean mSynchronized = false; - private boolean mDelayedSyncNotificationPending = false; - -// private final boolean mDebugVisualize = false; -// private final int mDebugSymbolStart = 0; -// private ISyncResultsListener mSyncResultsViewer; -// private int mDebugSymbolCounter = 0; -// private int mDebugSampleCounter = 0; + private final TransmissionMode mTransmissionMode; /** * Constructs an instance - * @param messageFramer to receive symbol decisions (dibits) and sync notifications. - */ - public NXDNSymbolProcessor(NXDNMessageFramer messageFramer, FeedbackDecoder feedbackDecoder) - { - mMessageFramer = messageFramer; - mFeedbackDecoder = feedbackDecoder; - mSymbolEqualizer = new C4FMEqualizer(mSyncDetector.getSyncSymbols(), 9, 192 * 3, 10); - DF.setPositivePrefix(" "); - } - - /** - * Finalize and prepare for shutdown. - */ - public void dispose() - { - mDibitAssembler.flush(); - } - - /** - * Resets the balance - */ - public void resetBalance() - { - mSampleEqualizer.reset(); - } - - /** - * Primary input method for receiving a stream of demodulated samples to process into symbols. - * @param samples to process - */ - public void process(float[] samples) - { - //Create shadow copy of heap variables on stack - double samplePoint = mSamplePoint; - double samplesPerSymbol = mSamplesPerSymbol; - int bufferPointer = mBufferPointer; - int bufferReloadThreshold = mBufferReloadThreshold; - int symbolsSinceLastSync = mSymbolsSinceLastSync; - - int samplesPointer = 0; - float softSymbol, scorePrimary; - Correction correctionCandidate; - - if(!mSynchronized && samples.length > 400) - { - StandardDeviation sd = new StandardDeviation(); - - float accumulator = 0.0f; - - for(int i = 0; i < 400; i++) - { - accumulator += samples[i]; - sd.increment(samples[i]); - } - - //Only apply a balance correction when we have signal. Noise = .45-.55, Signal = 0.00 - 0.20 - if(sd.getResult() < 0.3) - { - accumulator /= 400; - accumulator += mSampleEqualizer.mBalance; - mSampleEqualizer.mBalance -= (accumulator * .3f); - } - } - - while(samplesPointer < samples.length) - { - //Note: buffer pointer can become greater than reload threshold during timing optimization at sync detect - if(bufferPointer >= bufferReloadThreshold) - { - //Do reload - int copyLength = Math.min(BUFFER_WORKSPACE_LENGTH, samples.length - samplesPointer); - System.arraycopy(mBuffer, copyLength, mBuffer, 0, mBuffer.length - copyLength); - System.arraycopy(samples, samplesPointer, mBuffer, mBuffer.length - copyLength, copyLength); - samplesPointer += copyLength; - bufferPointer -= copyLength; - } - - while(bufferPointer < bufferReloadThreshold) - { - bufferPointer++; - samplePoint--; - -// mDebugSampleCounter++; - - if(samplePoint < 1) - { -// mDebugSymbolCounter++; -// -// if(mDebugSymbolCounter % 10 == 0) -// { -// visualizeSymbols(bufferPointer, samplePoint); -// } - - symbolsSinceLastSync++; - - //Clear synchronized state when we miss a sync period. - if(mSynchronized && symbolsSinceLastSync > 192) - { - mSampleEqualizer.reset(); - mSymbolEqualizer.syncLost(); - mSynchronized = false; - } - - softSymbol = mSampleEqualizer.getEqualizedSymbol(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], samplePoint); - - //Check for lagging sync pattern - float lag = (float)(bufferPointer + samplePoint - mLaggingSyncOffset); - int lagIntegral = (int)Math.floor(lag); - float softSymbolLag = mSampleEqualizer.getEqualizedSymbol(mBuffer[lagIntegral], mBuffer[lagIntegral + 1], lag - lagIntegral); - float scoreLag = mSyncDetectorLagging.process(softSymbolLag); - scorePrimary = mSyncDetector.process(softSymbol); - - correctionCandidate = INVALID_SYNC_DETECTION; - - if(scorePrimary > SYNC_THRESHOLD_DETECTION && scorePrimary > scoreLag) - { - correctionCandidate = mSampleEqualizer.optimize(0, scorePrimary, (bufferPointer + samplePoint)); - } - else if(scoreLag > SYNC_THRESHOLD_DETECTION) - { - correctionCandidate = mSampleEqualizer.optimize(-mLaggingSyncOffset, scoreLag, (bufferPointer + samplePoint)); - } - - //Equalizer processes current soft symbol and returns a delayed soft symbol that is equalized - softSymbol = mSymbolEqualizer.process(softSymbolLag, softSymbol); - - //Broadcast the decoded soft symbol to an optionally registered listener (ie Symbol view in channel panel). - Dibit symbol = Dibit.fromSample(softSymbol); - mFeedbackDecoder.broadcast(softSymbol); - -// if(mDebugVisualize) -// { -// getSyncResultsViewer().symbol(softSymbol); -// } - - //When the message framer emits at least one valid message (ie CRC check passes), send the current - // sample equalizer's balance value as a pseudo-PLL error measurement. - if(mMessageFramer.process(symbol)) - { - mFeedbackDecoder.processPLLError(mSampleEqualizer.balance()); - } - - mDibitAssembler.receive(symbol); - - //Do a final validation against the reference sync waveform - if(correctionCandidate.isValid()) - { - correctionCandidate = validateSyncWaveform(correctionCandidate, bufferPointer, samplePoint); - } - - if(correctionCandidate.isValid()) - { - mSampleEqualizer.apply(correctionCandidate); - - if(mSynchronized) - { - double adjustment = Math.clamp(correctionCandidate.getTimingCorrection(), - -mTimingCorrectionThreshold, mTimingCorrectionThreshold); - samplePoint += adjustment; - } - else - { - samplePoint += correctionCandidate.getTimingCorrection(); - } - - mDelayedSyncNotificationPending = true; - mDelayedSyncNotificationSymbolsRemaining = mSymbolEqualizer.getDelaySymbolCount(); - mSynchronized = true; - symbolsSinceLastSync = 0; - - //If the equalizer was disabled, it has bad samples contaminating the training buffer and we - // need to prime that buffer with aligned samples prior to sync detection training. - if(!mSymbolEqualizer.isEnabled()) - { - int symbols = mSyncDetector.getSyncSymbols().length + mSymbolEqualizer.getLength() - mSymbolEqualizer.getDelaySymbolCount(); - double offset = bufferPointer + samplePoint - ((symbols - 1) * samplesPerSymbol) - mLaggingSyncOffset; - - float mid, sym; - for(int s = 0; s < symbols; s++) - { - //Mid symbol - - mid = mSampleEqualizer.getEqualizedSymbol(offset); - offset += mLaggingSyncOffset; - sym = mSampleEqualizer.getEqualizedSymbol(offset); - offset += mLaggingSyncOffset; - mSymbolEqualizer.process(mid, sym); - } - } - - mSymbolEqualizer.syncDetected(); - mSymbolEqualizer.enable(); -// visualizeSyncDetect(scorePrimary, true, "DETECT - POST", bufferPointer, samplePoint); - } - else if(mDelayedSyncNotificationPending) - { - mDelayedSyncNotificationSymbolsRemaining--; - - if(mDelayedSyncNotificationSymbolsRemaining <= 0) - { - mMessageFramer.syncDetected(); - mDelayedSyncNotificationPending = false; - } - } - - //Add another symbol's worth of samples to the counter for processing - samplePoint += samplesPerSymbol; - } - } - } - - //Copy shadow variables back to the heap - mBufferPointer = bufferPointer; - mSamplePoint = samplePoint; - mSymbolsSinceLastSync = symbolsSinceLastSync; - } - - /** - * Validates the sync detection and candidate correction settings against a reference instance of the sync detection - * waveform. If the detected sync correlates well against the reference sync then the correction is valid. - * Otherwise, this method returns the static invalid sync correction to signal that the sync detection was invalid. - * @param correction to evaluate - * @param bufferPointer for the final (optimized) symbol in the buffer - * @param samplePoint offset from the buffer pointer to the final symbol - * @return the original correction or the static invalid sync correction - */ - private Correction validateSyncWaveform(Correction correction, int bufferPointer, double samplePoint) - { - float gain = mSampleEqualizer.mGain + correction.getGainAdjustment(); - float balance = mSampleEqualizer.mBalance + correction.getBalanceAdjustment(); - double offset = bufferPointer + samplePoint + correction.getTimingCorrection() - mReferenceSyncWaveform.samples().length; - int integral = (int)Math.ceil(offset); - double mu = integral - offset; - double accumulator = 0.0; - int index; - - //Evaluate the reference waveform in reverse order since the final symbol is aligned to the final sample - for(int x = mReferenceSyncWaveform.samples().length - 1; x >= 0; x--) - { - index = integral + x; - float sample = LinearInterpolator.calculate(mBuffer[index], mBuffer[index + 1], mu); - sample += balance; - sample *= gain; - sample = Math.clamp(sample, -mReferenceSyncWaveform.maxSample(), mReferenceSyncWaveform.maxSample()); - accumulator += mReferenceSyncWaveform.samples()[x] * sample; - } - - double threshold = mReferenceSyncWaveform.idealCorrelationScore() * 0.85; //85% of the ideal correlation score - - //Return the correction as valid if the correlation score exceeds the threshold, otherwise return the static - // invalid sync detection to nullify the correction. - if(accumulator > threshold) - { - return correction; - } - - return INVALID_SYNC_DETECTION; - } - - /** - * Registers the listener to receive demodulated bit stream buffers. - * @param listener to register - */ - public void setBufferListener(Listener listener) - { - mDibitAssembler.setBufferListener(listener); - } - - /** - * Indicates if there is a registered buffer listener - */ - public boolean hasBufferListener() - { - return mDibitAssembler.hasBufferListeners(); - } - - /** - * Sets or updates the samples per symbol - * @param samplesPerSymbol to apply. - */ - public void setSamplesPerSymbol(float samplesPerSymbol) - { - mSamplesPerSymbol = samplesPerSymbol; - mSamplePoint = samplesPerSymbol; - mTimingCorrectionThreshold = samplesPerSymbol / 30; - mNoiseStandardDeviationThreshold = .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)); - mBufferPointer = mBufferReloadThreshold; - mSampleEqualizer.configure(samplesPerSymbol); - mReferenceSyncWaveform = mSyncDetector.getReferenceWaveform(samplesPerSymbol); - } - -// /** -// * Debug viewer UI for visualizing sync detections in the sample stream/waveform. -// */ -// private ISyncResultsListener getSyncResultsViewer() -// { -// if(mSyncResultsViewer == null) -// { -// mSyncResultsViewer = new FMSyncResultsViewer(); -// } -// -// return mSyncResultsViewer; -// } -// -// /** -// * Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing. -// * @param score from the sync detector -// * @param primary sync detector (true) or secondary (false) -// */ -// public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint) -// { -// if(!mDebugVisualize) -// { -// return; -// } -// -// int integral = (int)Math.floor(samplePoint); -// samplePoint -= integral; -// bufferPointer += integral; -// -// int offset = 3; -// int length = (int)Math.ceil(mSamplesPerSymbol * 9) + (2 * offset); -// int end = bufferPointer + offset; -// int start = end - length; -// float[] symbols = new float[10]; -// float[] samples = Arrays.copyOfRange(mBuffer, start, end); -// for(int i = 0; i < samples.length; i++) -// { -// samples[i] = mSampleEqualizer.equalize(samples[i]); -// } -// -// double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 9); -// int symbolIntegral = (int)Math.floor(symbolPointer); -// double mu = symbolPointer - symbolIntegral; -// -// for(int x = 0; x < 10; x++) -// { -// symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); -// symbolPointer += mSamplesPerSymbol; -// symbolIntegral = (int)Math.floor(symbolPointer); -// mu = symbolPointer - symbolIntegral; -// } -// -// float[] syncIntervals = new float[10]; -// int adjust = bufferPointer - length + offset; -// double pointer = bufferPointer + samplePoint - adjust; -// -// for(int x = 9; x >= 0; x--) -// { -// syncIntervals[x] = (float)pointer; -// pointer -= mSamplesPerSymbol; -// } -// -// CountDownLatch countDownLatch = new CountDownLatch(1); -// getSyncResultsViewer().receive(symbols, mSyncDetector.getSyncSymbols(), samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain, -// tag + " Score: " + score + (primary ? " PRIMARY " : " SECONDARY ") + (mSynchronized ? "FINE " : "COARSE ") + -// " EQ-B:" + mSampleEqualizer.mBalance + " EQ-G:" + mSampleEqualizer.mGain + " " + -// " SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter + " DUR:" + DF.format(mDebugSymbolCounter / 2400.0), -// countDownLatch); -// -// try -// { -// countDownLatch.await(); -// } -// catch(InterruptedException e) -// { -// throw new RuntimeException(e); -// } -// } -// -// /** -// * Debug method to visualize the symbol decisions -// */ -// public void visualizeSymbols(int bufferPointer, double samplePoint) -// { -// if(!mDebugVisualize) -// { -// return; -// } -// -// if(mDebugSymbolCounter < mDebugSymbolStart) -// { -// return; -// } -// -// int offset = 3; -// int length = (int)Math.ceil(mSamplesPerSymbol * 9) + (2 * offset); -// int end = bufferPointer + offset; -// int start = end - length; -// float[] symbols = new float[10]; -// float[] decisions = new float[10]; -// float[] samples = Arrays.copyOfRange(mBuffer, start, end); -// for(int i = 0; i < samples.length; i++) -// { -// samples[i] = mSampleEqualizer.equalize(samples[i]); -// } -// -// double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 9); -// int symbolIntegral = (int)Math.floor(symbolPointer); -// double mu = symbolPointer - symbolIntegral; -// -// for(int x = 0; x < 10; x++) -// { -// symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); -// decisions[x] = Dibit.fromSample(symbols[x]).getIdealPhase(); -// symbolPointer += mSamplesPerSymbol; -// symbolIntegral = (int)Math.floor(symbolPointer); -// mu = symbolPointer - symbolIntegral; -// } -// -// float[] syncIntervals = new float[10]; -// int adjust = bufferPointer - length + offset; -// double pointer = bufferPointer + samplePoint - adjust; -// -// for(int x = 9; x >= 0; x--) -// { -// syncIntervals[x] = (float)pointer; -// pointer -= mSamplesPerSymbol; -// } -// -// CountDownLatch countDownLatch = new CountDownLatch(1); -// getSyncResultsViewer().receive(symbols, decisions, samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain, -// "Balance:" + mSampleEqualizer.mBalance + " Gain:" + mSampleEqualizer.mGain + -// " SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter + " DUR:" + DF.format(mDebugSymbolCounter / 2400.0), countDownLatch); -// -// try -// { -// countDownLatch.await(); -// } -// catch(InterruptedException e) -// { -// throw new RuntimeException(e); -// } -// } - - /** - * Calculates the standard deviation of the samples at the specified offset that aligns with a sync detection. - * @param offset to the sample representing the final symbol in the detected sync pattern. - * @return standard deviation. - */ - public double getStandardDeviation(double offset) - { - StandardDeviation standardDeviation = new StandardDeviation(); - int start = (int)Math.floor(offset - (9 * mSamplesPerSymbol)); - int end = (int)Math.ceil(offset); - end = Math.min(end, mBuffer.length - 1); - - for(int i = start; i < end; i++) - { - standardDeviation.increment(mBuffer[i] - mBuffer[i + 1]); - } - - return standardDeviation.getResult(); - } - - /** - * Symbol timing and equalizer correction candidate settings - */ - public static class Correction - { - private final double mTimingAdjustment; - private final double mAdditionalOffset; - private final float mBalanceAdjustment; - private final float mGainAdjustment; - private final float mDetectionScore; - private final float mOptimizationScore; - private int mCorrectedBitCount = 0; - - /** - * Constructs an instance - * @param timingAdjustment correction for symbol sampling - * @param balanceAdjustment correction for equalizer - * @param gainAdjustment correction for equalizer - * @param bitErrorCount in the detected sync pattern - */ - public Correction(double additionalOffset, double timingAdjustment, float balanceAdjustment, float gainAdjustment, - float detectionScore, float optimizationScore, int bitErrorCount) - { - mAdditionalOffset = additionalOffset; - mTimingAdjustment = timingAdjustment; - mBalanceAdjustment = balanceAdjustment; - mGainAdjustment = gainAdjustment; - mDetectionScore = detectionScore; - mOptimizationScore = optimizationScore; - mCorrectedBitCount = bitErrorCount; - } - - @Override - public String toString() - { - StringBuilder sb = new StringBuilder(); - - if(isValid()) - { - sb.append("\tCORRECTION - Bit Errors [").append(mCorrectedBitCount).append("]"); - } - else - { - sb.append("CORRECTION - INVALID"); - } - - sb.append(" At Offset [").append(mAdditionalOffset); - sb.append("] Adjustments-Timing [").append(mTimingAdjustment); - sb.append("] Balance [").append(mBalanceAdjustment); - sb.append("] Gain [").append(mGainAdjustment); - sb.append("] Detect Score [").append(mDetectionScore); - sb.append("] Optimize Score [").append(mOptimizationScore); - sb.append("]"); - - if(!hasValidTiming()) - { - sb.append(" >> INVALID TIMING"); - } - - return sb.toString(); - } - - /** - * Candidate timing correction - * @return correction - */ - public double getTimingCorrection() - { - return mAdditionalOffset + mTimingAdjustment; - } - - /** - * Equalizer balance correction value. - * @return correction - */ - public float getBalanceAdjustment() - { - return mBalanceAdjustment; - } - - /** - * Equalizer gain correction value - * @return correction. - */ - public float getGainAdjustment() - { - return mGainAdjustment; - } - - /** - * Number of bits that were corrected across the SYNC and NID - * @return corrected bit count. - */ - public int getCorrectedBitCount() - { - return mCorrectedBitCount; - } - - /** - * Indicates if this correction has valid timing adjustment information. - * @return true if the timing correction is not the default (ie no timing) double max value. - */ - public boolean hasValidTiming() - { - return getTimingCorrection() != Double.MAX_VALUE; - } - - /** - * Indicates if this correction has a valid NID or the optimization (or detection) score is high quality. - * @return true if valid. - */ - public boolean isValid() - { - return mOptimizationScore > SYNC_THRESHOLD_EQUALIZED; - } - } - - /** - * 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 the same pulse shaping filters. Gain value is adjusted and - * optimized with each sync detection. + * @param feedbackDecoder parent decoder + * @param messageFramer to receive symbol decisions (dibits) and sync notifications. + * @param transmissionMode to determine the symbol rate and symbol deviation. */ - class SampleEqualizer + public NXDNSymbolProcessor(FeedbackDecoder feedbackDecoder, C4FMMessageFramer messageFramer, + TransmissionMode transmissionMode) { - private static final float EQUALIZER_LOOP_GAIN = 0.15f; - private static final float EQUALIZER_MAXIMUM_BALANCE = (float)(Math.PI / 3.0); //+/- 800 Hz - private static final float EQUALIZER_RECALIBRATE_THRESHOLD = (float)(Math.PI / 8.0); - private float mBalance = 0.0f; - private float mGain = 5.7f; - private float mGainInitial = 5.7f; - private float mGainMax = 6.2f; - private float mGainMin = 5.2f; + super(feedbackDecoder, messageFramer, NXDNSyncDetectorFactory.getStandardDetector(), + NXDNSyncDetectorFactory.getStandardDetector()); + mTransmissionMode = transmissionMode; + } - /** - * Current balance that represents a traditional PLL signal offset value. - * @return balance. - */ - public float balance() - { - return mBalance; - } + @Override + public boolean isDynamicBalanceTrackingRequired() + { + return true; + } - /** - * Updates the initial, min and max gain values from the samples per symbol - * @param samplesPerSymbol for the incoming stream - */ - public void configure(double samplesPerSymbol) - { - //Testing across a range of sample rates (5.2 to 6.25) indicates gain has linear correlation with samples per symbol - mGainInitial = (float)(samplesPerSymbol * 1.185); - mGain = mGainInitial; - mGainMax = mGain + 0.1f; - mGainMin = mGain - 0.1f; - } + @Override + public int getMaxMessageDibitLength() + { + return MAX_MESSAGE_DIBIT_LENGTH; + } - /** - * Reset after center frequency change or PPM change. - */ - public void reset() - { - mSynchronized = false; - mGain = mGainInitial; - } + @Override + public int getEqualizerTrainingPeriodDurationSymbolCount() + { + return EQUALIZER_TRAINING_PERIOD_SYMBOL_COUNT; + } - /** - * Calculates the mean of sync pattern detection for the samples starting at 9 symbol periods before and ending - * at the tenth symbol. - * @param offset for the sync detection which is just after the sample of the tenth symbol in the sync pattern - * @return mean for the samples in this range - */ - public float getBalance(double offset) - { - Mean mean = new Mean(); - int start = (int)Math.floor(offset - (9 * mSamplesPerSymbol)); - int end = (int)Math.ceil(offset); + @Override + public int getEqualizerSymbolLength() + { + return EQUALIZER_SYMBOL_LENGTH; + } - for(int i = start; i <= end; i++) - { - mean.increment(mBuffer[i]); - } + @Override + public int getEqualizerSkipTrainingFrameCount() + { + return EQUALIZER_SKIP_TRAINING_FRAME_COUNT; + } - return (float)mean.getResult(); - } + @Override + public int getSymbolRate() + { + return mTransmissionMode.getSymbolRate(); + } - /** - * On sync detection, calculates the optimal timing adjustment that achieves the highest sync correlation score - * or returns an INVALID_SYNC_DETECTION sentinel value if the correlation score doesn't exceed the threshold. - * - * @param additionalOffset from current bufferPointer and samplePoint to adjust for which sync detector - * triggered the event. This can be zero offset for the primary sync detector or a lagging (ie half symbol) - * offset for the lagging sync detector. - * @return optimized timing adjustment or NO_OPTIMIZATION sentinel value. - */ - 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; + @Override + public double getOuterSymbolDeviation() + { + return mTransmissionMode.getOuterSymbolDeviation(); + } - //Reject any sync detections where the sample:sample standard deviation exceeds the noise threshold. - double standardDeviation = getStandardDeviation(offset); + @Override + public float getSyncThresholdDetected() + { + return SYNC_THRESHOLD_DETECTED; + } - if(standardDeviation > mNoiseStandardDeviationThreshold) - { - return INVALID_SYNC_DETECTION; - } + @Override + public float getSyncThresholdOptimized() + { + return SYNC_THRESHOLD_OPTIMIZED; + } - float calculatedBalanceCorrection = -getBalance(offset); - - //Find the optimal symbol timing - double samplesPerSymbol = mSamplesPerSymbol; - double stepSize = samplesPerSymbol / (mSynchronized ? 16.0 : 8.0); //Start at 1/8th for coarse & 1/16th for fine - double stepSizeMin = mOptimizeFineIncrement; - double adjustment = 0.0; - - //In coarse sync mode, constrain max adjustment to half a symbol period so that a lagging sync detect doesn't - //preempt a primary sync detect prematurely and cause a significant timing correction. So, we constrain the - //max correction to a half symbol period for coarse sync mode. - double adjustmentMax = samplesPerSymbol / 2.0; - double candidate = offset; - - float scoreCenter = score(candidate, samplesPerSymbol, calculatedBalanceCorrection); - - candidate = offset - stepSize; - float scoreLeft = score(candidate, samplesPerSymbol, calculatedBalanceCorrection); - - candidate = offset + stepSize; - float scoreRight = score(candidate, samplesPerSymbol, calculatedBalanceCorrection); - - while(stepSize > stepSizeMin && Math.abs(adjustment) <= adjustmentMax) - { - if(scoreLeft > scoreRight && scoreLeft > scoreCenter) - { - adjustment -= stepSize; - scoreRight = scoreCenter; - scoreCenter = scoreLeft; - - candidate = offset + adjustment - stepSize; - scoreLeft = score(candidate, samplesPerSymbol, calculatedBalanceCorrection); - } - else if(scoreRight > scoreLeft && scoreRight > scoreCenter) - { - adjustment += stepSize; - scoreLeft = scoreCenter; - scoreCenter = scoreRight; - - candidate = offset + adjustment + stepSize; - scoreRight = score(candidate, samplesPerSymbol, calculatedBalanceCorrection); - } - else - { - stepSize *= 0.5f; - - if(stepSize > stepSizeMin) - { - candidate = offset + adjustment - stepSize; - scoreLeft = score(candidate, samplesPerSymbol, calculatedBalanceCorrection); - - candidate = offset + adjustment + stepSize; - scoreRight = score(candidate, samplesPerSymbol, calculatedBalanceCorrection); - } - } - } - - //If we didn't find an optimal correlation score above the threshold, return a false sync. - if(scoreCenter < SYNC_THRESHOLD_OPTIMIZED) - { - return INVALID_SYNC_DETECTION; - } - - return getCorrection(additionalOffset, adjustment, detectionScore, scoreCenter, bufferOffset, - calculatedBalanceCorrection); - } - - /** - * Equalize the symbol by applying balance correction and multiplying with gain value. - * @param symbol to equalize. - * @return equalized symbol - */ - public float equalize(float symbol) - { - return symbol * mGain + mBalance; - } - - /** - * Equalizes both samples with optional phase wrap detection and returns the interpolated value at the mu offset. - * - * @param sample1 of the sample stream - * @param sample2 of the sample stream - * @param mu interpolation point between sample 1 and sample 2 - * @return equalized symbol - */ - public float getEqualizedSymbol(float sample1, float sample2, double mu) - { - sample1 = equalize(sample1); - sample2 = equalize(sample2); - return LinearInterpolator.calculate(sample1, sample2, mu); - } - - /** - * Gets the interpolated sample value at the specified offset and equalizes it with the current equalizer settings. - * @param offset to the sample - * @return equalized value. - */ - public float getEqualizedSymbol(double offset) - { - int integral = (int)Math.floor(offset); - - if(integral >= 0) - { - return getEqualizedSymbol(mBuffer[integral], mBuffer[integral + 1], offset - integral); - } - - return 0; - } - - /** - * Calculates the sync correlation score at the specified offset and samples per symbol interval using the - * current equalizer gain and supplied balance value. - * - * @param offset to the final symbol in the soft symbol buffer - * @param samplesPerSymbol spacing to test for. - * @param balance for the calculation - * @return correlation score. - */ - public float score(double offset, double samplesPerSymbol, float balance) - { - return score(offset, samplesPerSymbol, balance, mGain); - } - - /** - * Calculates the sync correlation score at the specified offset and samples per symbol interval. - * @param offset to the final symbol in the soft symbol buffer - * @param samplesPerSymbol spacing to test for. - * @return correlation score. - */ - public float score(double offset, double samplesPerSymbol, float balance, float gain) - { - int maxPointer = mBuffer.length - 1; - float softSymbol; - - 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 < 10; x++) - { - if(bufferPointer < maxPointer) - { - sample1 = mBuffer[bufferPointer] * gain + balance; - sample2 = mBuffer[bufferPointer + 1] * gain + balance; - softSymbol = LinearInterpolator.calculate(sample1, sample2, fractional); - } - else - { - softSymbol = 0.0f; - } - - 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; - } - - return score; - } - - /** - * Applies equalizer correction settings. - * - * @param correction settings to apply - */ - public void apply(Correction correction) - { - float remainingBalanceToCorrect = correction.getBalanceAdjustment() - mBalance; - - //Re-initialize the equalizer any time the balance correction value exceeds the threshold. - if(mSynchronized && Math.abs(remainingBalanceToCorrect) > EQUALIZER_RECALIBRATE_THRESHOLD) - { - reset(); - } - - //Limit equalizer adjustments using a control loop at each sync detect after the initial equalizer setup or - //apply as new settings on initial sync detect or any time an excess balance correction is supplied. - if(mSynchronized) - { - mBalance += (remainingBalanceToCorrect * EQUALIZER_LOOP_GAIN); - mGain += (correction.getGainAdjustment() * EQUALIZER_LOOP_GAIN); - } - else - { - mBalance += remainingBalanceToCorrect; - mGain += correction.getGainAdjustment(); - } - - mBalance = Math.min(mBalance, EQUALIZER_MAXIMUM_BALANCE); - mBalance = Math.max(mBalance, -EQUALIZER_MAXIMUM_BALANCE); - mGain = Math.min(mGain, mGainMax); - mGain = Math.max(mGain, mGainMin); - } - - /** - * Calculates a correction update for the equalizer balance and gain when the sync pattern is detected in the - * sample buffer, using the supplied timing correction argument. Resamples the sync symbols and compares each - * soft symbol to the ideal symbol phase to develop average error measurements for balance and gain. On initial - * 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(double additionalOffset, double timingCorrection, - float detectionScore, float optimizationScore, double offset, float balanceCorrection) - { - 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 < 10; x++) - { - if(resampleStartIntegral >= 0) - { - symbol = SYNC_SYMBOLS[x]; - resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral], - mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral); - resampledSoftSymbol = resampledSoftSymbol * mGain + balanceCorrection; - resampledDibit = Dibit.fromSample(resampledSoftSymbol); - bitErrorCount += SYNC_DIBITS[x].getBitErrorFrom(resampledDibit); - gainAccumulator += Math.abs(symbol) - Math.abs(resampledSoftSymbol); - } - - resampleStart += mSamplesPerSymbol; - resampleStartIntegral = (int)Math.floor(resampleStart); - } - - gainAccumulator /= 10; - - //Recalculate the optimization score using the adjusted equalizer settings - if(!mSynchronized) - { - double optimizedFinalSymbol = offset + additionalOffset + timingCorrection; - balanceCorrection = -getBalance(optimizedFinalSymbol); - optimizationScore = score(optimizedFinalSymbol, mSamplesPerSymbol, balanceCorrection, mGain + gainAccumulator); - } - - return new Correction(additionalOffset, timingCorrection, balanceCorrection, gainAccumulator, detectionScore, - optimizationScore, bitErrorCount); - } + @Override + public float getSyncThresholdEqualized() + { + return SYNC_THRESHOLD_EQUALIZED; } } diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/Frame.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/Frame.java index 99da374b..1778fed1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/Frame.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/Frame.java @@ -91,8 +91,24 @@ public class Frame * @param tracker for accessing tracked RF channel type and direction. */ public Frame(CorrectedBinaryMessage message, long timestamp, LICHTracker tracker) + { + this(message, null, timestamp, tracker); + } + + /** + * Constructs a frame using soft decision decoding of the convolutionally encoded payloads when soft bits are + * provided. Note: channel and direction are used as fallback values when we can't exactly match the LICH value + * for this frame due to decoding errors. + * @param message with frame bits + * @param softBits for each frame bit, where a positive value favors a 1 bit and the magnitude is the confidence, or + * null for hard decision decoding. Note: the soft bit values are modified (descrambled) by this constructor. + * @param timestamp for the frame. + * @param tracker for accessing tracked RF channel type and direction. + */ + public Frame(CorrectedBinaryMessage message, float[] softBits, long timestamp, LICHTracker tracker) { message.xor(SCRAMBLE_SEQUENCE_LICH); + xor(softBits, SCRAMBLE_SEQUENCE_LICH); boolean lichParityValid = checkLICHParity(message); int lichValue = message.getInt(LICH_FIELD); mLICH = LICH.fromValue(lichValue, tracker.isTypeD()); @@ -115,7 +131,8 @@ public class Frame if(mLICH.isOutbound()) { message.xor(SCRAMBLE_SEQUENCE_CONTROL_OUTBOUND); - CorrectedBinaryMessage payload = decodeCAC(message); + xor(softBits, SCRAMBLE_SEQUENCE_CONTROL_OUTBOUND); + CorrectedBinaryMessage payload = decodeCAC(message, softBits); int ran = payload.getInt(RADIO_ACCESS_NUMBER); CorrectedBinaryMessage cac = payload.getSubMessage(8, 152); cac.setCorrectedBitCount(payload.getCorrectedBitCount()); @@ -127,7 +144,8 @@ public class Frame else { message.xor(SCRAMBLE_SEQUENCE_CONTROL_INBOUND); - CorrectedBinaryMessage payload = mLICH.isLongCAC() ? decodeLongCAC(message) : decodeShortCAC(message); + xor(softBits, SCRAMBLE_SEQUENCE_CONTROL_INBOUND); + CorrectedBinaryMessage payload = mLICH.isLongCAC() ? decodeLongCAC(message, softBits) : decodeShortCAC(message, softBits); int ran = payload.getInt(RADIO_ACCESS_NUMBER); CorrectedBinaryMessage cac = mLICH.isLongCAC() ? payload.getSubMessage(8, 136) : payload.getSubMessage(8, 104); cac.setCorrectedBitCount(payload.getCorrectedBitCount()); @@ -140,10 +158,11 @@ public class Frame else { message.xor(SCRAMBLE_SEQUENCE_FULL); + xor(softBits, SCRAMBLE_SEQUENCE_FULL); if(mLICH.hasSACCH()) //SACCH + Voice/FACCH frames { - CorrectedBinaryMessage sacch = decodeSACCH(message); + CorrectedBinaryMessage sacch = decodeSACCH(message, softBits); SACCHFragment sacchFragment = new SACCHFragment(sacch, timestamp, mLICH); sacchFragment.setValid(NXDNCRC.checkSACCH(sacch)); mMessages.add(sacchFragment); @@ -171,7 +190,7 @@ public class Frame if(mLICH.isFACCH1First()) { - CorrectedBinaryMessage payload = decodeFACCH1First(message); + CorrectedBinaryMessage payload = decodeFACCH1First(message, softBits); NXDNMessageType type = NXDNMessageType.getTraffic(payload, mLICH); NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, payload, timestamp, sacchFragment.getRAN(), mLICH); layer3.setValid(NXDNCRC.checkFACCH1(payload)); @@ -180,7 +199,7 @@ public class Frame if(mLICH.isFACCH1Second()) { - CorrectedBinaryMessage payload = decodeFACCH1Second(message); + CorrectedBinaryMessage payload = decodeFACCH1Second(message, softBits); NXDNMessageType type = NXDNMessageType.getTraffic(payload, mLICH); NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, payload, timestamp, sacchFragment.getRAN(), mLICH); layer3.setValid(NXDNCRC.checkFACCH1(payload)); @@ -189,7 +208,7 @@ public class Frame } else if(mLICH.hasSCCH()) //Type-D segments with SCCH { - CorrectedBinaryMessage scch = decodeSCCH(message); + CorrectedBinaryMessage scch = decodeSCCH(message, softBits); NXDNMessageType typeSCCH = NXDNMessageType.getSCCH(scch, mLICH); NXDNLayer3Message scchMessage = NXDNMessageFactory.get(typeSCCH, scch, timestamp, RAN_TYPE_D, mLICH); scchMessage.setValid(NXDNCRC.checkSCCH(scch)); @@ -197,7 +216,7 @@ public class Frame if(mLICH.isFACCH1First()) { - CorrectedBinaryMessage payload = decodeFACCH1First(message); + CorrectedBinaryMessage payload = decodeFACCH1First(message, softBits); NXDNMessageType type = NXDNMessageType.getTypeD(payload, mLICH); NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, payload, timestamp, RAN_TYPE_D, mLICH); layer3.setValid(NXDNCRC.checkFACCH1(payload)); @@ -206,7 +225,7 @@ public class Frame if(mLICH.isFACCH1Second()) { - CorrectedBinaryMessage payload = decodeFACCH1Second(message); + CorrectedBinaryMessage payload = decodeFACCH1Second(message, softBits); NXDNMessageType type = NXDNMessageType.getTypeD(payload, mLICH); NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, payload, timestamp, RAN_TYPE_D, mLICH); layer3.setValid(NXDNCRC.checkFACCH1(payload)); @@ -215,8 +234,8 @@ public class Frame if(mLICH.isData()) //FACCH3 or UDCH2 { - CorrectedBinaryMessage payloadA = decodeUDCH2AndFACCH3A(message); - CorrectedBinaryMessage payloadB = decodeUDCH2AndFACCH3B(message); + CorrectedBinaryMessage payloadA = decodeUDCH2AndFACCH3A(message, softBits); + CorrectedBinaryMessage payloadB = decodeUDCH2AndFACCH3B(message, softBits); boolean validA = NXDNCRC.checkFACCH3(payloadA); boolean validB = NXDNCRC.checkFACCH3(payloadB); CorrectedBinaryMessage combined = new CorrectedBinaryMessage(160); @@ -247,7 +266,7 @@ public class Frame } else if(mLICH.getRFChannel() != RFChannel.RTCH2) //UDCH or FACCH2 frames on non-Type-D { - CorrectedBinaryMessage payload = decodeUDCHAndFACCH2(message); + CorrectedBinaryMessage payload = decodeUDCHAndFACCH2(message, softBits); int ran = payload.getInt(RADIO_ACCESS_NUMBER); CorrectedBinaryMessage facch2 = payload.getSubMessage(8, 184); facch2.setCorrectedBitCount(payload.getCorrectedBitCount()); @@ -284,6 +303,47 @@ public class Frame return Convolution.decode(punctureProvider.depuncture(deinterleaved), punctureProvider); } + /** + * Decodes the frame payload using soft decision Viterbi decoding when soft bits are available, otherwise hard + * decision decoding. + * @param message from the frame (descrambled) + * @param softBits for the frame (descrambled) or null + * @param offset to the start of the payload + * @param interleaver for the payload type + * @param punctureProvider for the payload type + * @return decoded message + */ + private static CorrectedBinaryMessage decode(CorrectedBinaryMessage message, float[] softBits, int offset, + Interleaver interleaver, PunctureProvider punctureProvider) + { + if(softBits == null) + { + return decode(message, offset, interleaver, punctureProvider); + } + + float[] deinterleaved = interleaver.deinterleave(softBits, offset); + return Convolution.decodeSoft(punctureProvider.depuncture(deinterleaved), punctureProvider); + } + + /** + * Applies a scrambling sequence to the soft bit values by inverting the sign of each soft bit where the sequence + * bit is set, mirroring an XOR of the corresponding message bits. + * @param softBits to descramble, or null + * @param sequence scrambling sequence + */ + private static void xor(float[] softBits, BinaryMessage sequence) + { + if(softBits != null) + { + int length = Math.min(softBits.length, sequence.size()); + + for(int x = sequence.nextSetBit(0); x >= 0 && x < length; x = sequence.nextSetBit(x + 1)) + { + softBits[x] = -softBits[x]; + } + } + } + /** * Decodes the first FACCH1 frame * @param message to decode @@ -294,6 +354,17 @@ public class Frame return decode(message, 76, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2); } + /** + * Soft decision variant of decodeFACCH1First(), or hard decision decoding when soft bits are null. + * @param message to decode + * @param softBits for the frame bits, or null + * @return decoded message + */ + public static CorrectedBinaryMessage decodeFACCH1First(CorrectedBinaryMessage message, float[] softBits) + { + return decode(message, softBits, 76, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2); + } + /** * Decodes the second FACCH1 frame * @param message to decode @@ -304,6 +375,17 @@ public class Frame return decode(message, 220, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2); } + /** + * Soft decision variant of decodeFACCH1Second(), or hard decision decoding when soft bits are null. + * @param message to decode + * @param softBits for the frame bits, or null + * @return decoded message + */ + public static CorrectedBinaryMessage decodeFACCH1Second(CorrectedBinaryMessage message, float[] softBits) + { + return decode(message, softBits, 220, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2); + } + /** * Decodes the FACCH2 frame * @param message to decode @@ -314,6 +396,17 @@ public class Frame return decode(message, 16, INTERLEAVER_12_29, PUNCTURE_PROVIDER_CAC_FACCH2_UDCH); } + /** + * Soft decision variant of decodeUDCHAndFACCH2(), or hard decision decoding when soft bits are null. + * @param message to decode + * @param softBits for the frame bits, or null + * @return decoded message + */ + public static CorrectedBinaryMessage decodeUDCHAndFACCH2(CorrectedBinaryMessage message, float[] softBits) + { + return decode(message, softBits, 16, INTERLEAVER_12_29, PUNCTURE_PROVIDER_CAC_FACCH2_UDCH); + } + /** * Decodes the first half of the FACCH3 and UDCH2 frame on Type-D systems * @param message to decode @@ -324,6 +417,17 @@ public class Frame return decode(message, 76, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2); } + /** + * Soft decision variant of decodeUDCH2AndFACCH3A(), or hard decision decoding when soft bits are null. + * @param message to decode + * @param softBits for the frame bits, or null + * @return decoded message + */ + public static CorrectedBinaryMessage decodeUDCH2AndFACCH3A(CorrectedBinaryMessage message, float[] softBits) + { + return decode(message, softBits, 76, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2); + } + /** * Decodes the second half of the FACCH3 and UDCH2 frame on Type-D systems * @param message to decode @@ -334,6 +438,17 @@ public class Frame return decode(message, 220, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2); } + /** + * Soft decision variant of decodeUDCH2AndFACCH3B(), or hard decision decoding when soft bits are null. + * @param message to decode + * @param softBits for the frame bits, or null + * @return decoded message + */ + public static CorrectedBinaryMessage decodeUDCH2AndFACCH3B(CorrectedBinaryMessage message, float[] softBits) + { + return decode(message, softBits, 220, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2); + } + /** * Decodes the SACCH frame * @param message to decode @@ -344,6 +459,17 @@ public class Frame return decode(message, 16, INTERLEAVER_12_5, PUNCTURE_PROVIDER_SACCH_SCCH); } + /** + * Soft decision variant of decodeSACCH(), or hard decision decoding when soft bits are null. + * @param message to decode + * @param softBits for the frame bits, or null + * @return decoded message + */ + public static CorrectedBinaryMessage decodeSACCH(CorrectedBinaryMessage message, float[] softBits) + { + return decode(message, softBits, 16, INTERLEAVER_12_5, PUNCTURE_PROVIDER_SACCH_SCCH); + } + /** * Decodes the SCCH frame in a Type-D system * @param message to decode @@ -354,6 +480,17 @@ public class Frame return decode(message, 16, INTERLEAVER_12_5, PUNCTURE_PROVIDER_SACCH_SCCH); } + /** + * Soft decision variant of decodeSCCH(), or hard decision decoding when soft bits are null. + * @param message to decode + * @param softBits for the frame bits, or null + * @return decoded message + */ + public static CorrectedBinaryMessage decodeSCCH(CorrectedBinaryMessage message, float[] softBits) + { + return decode(message, softBits, 16, INTERLEAVER_12_5, PUNCTURE_PROVIDER_SACCH_SCCH); + } + /** * Decodes the outbound control frame * @param message to decode @@ -364,6 +501,17 @@ public class Frame return decode(message, 16, INTERLEAVER_12_25, PUNCTURE_PROVIDER_CAC_FACCH2_UDCH); } + /** + * Soft decision variant of decodeCAC(), or hard decision decoding when soft bits are null. + * @param message to decode + * @param softBits for the frame bits, or null + * @return decoded message + */ + public static CorrectedBinaryMessage decodeCAC(CorrectedBinaryMessage message, float[] softBits) + { + return decode(message, softBits, 16, INTERLEAVER_12_25, PUNCTURE_PROVIDER_CAC_FACCH2_UDCH); + } + /** * Decodes the inbound long control frame * @param message to decode @@ -374,6 +522,17 @@ public class Frame return decode(message, 16, INTERLEAVER_12_21, PUNCTURE_PROVIDER_LONG_CAC); } + /** + * Soft decision variant of decodeLongCAC(), or hard decision decoding when soft bits are null. + * @param message to decode + * @param softBits for the frame bits, or null + * @return decoded message + */ + public static CorrectedBinaryMessage decodeLongCAC(CorrectedBinaryMessage message, float[] softBits) + { + return decode(message, softBits, 16, INTERLEAVER_12_21, PUNCTURE_PROVIDER_LONG_CAC); + } + /** * Decodes the inbound short control frame * @param message to decode @@ -384,6 +543,17 @@ public class Frame return decode(message, 16, INTERLEAVER_12_21, PUNCTURE_PROVIDER_NONE); } + /** + * Soft decision variant of decodeShortCAC(), or hard decision decoding when soft bits are null. + * @param message to decode + * @param softBits for the frame bits, or null + * @return decoded message + */ + public static CorrectedBinaryMessage decodeShortCAC(CorrectedBinaryMessage message, float[] softBits) + { + return decode(message, softBits, 16, INTERLEAVER_12_21, PUNCTURE_PROVIDER_NONE); + } + /** * Message(s( from this frame. */ diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNHardSyncDetector.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNHardSyncDetector.java index 6cde69c3..bb57a020 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNHardSyncDetector.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNHardSyncDetector.java @@ -44,8 +44,8 @@ public class NXDNHardSyncDetector extends NXDNSyncDetector */ public boolean process(Dibit dibit) { - mValue = (Long.rotateLeft(mValue, 2) & SYNC_MASK) + dibit.getValue(); - return Long.bitCount(mValue ^ SYNC_PATTERN) <= MAXIMUM_BIT_ERROR; + mValue = (Long.rotateLeft(mValue, 2) & getMask()) + dibit.getValue(); + return Long.bitCount(mValue ^ getPattern()) <= MAXIMUM_BIT_ERROR; } @@ -54,6 +54,6 @@ public class NXDNHardSyncDetector extends NXDNSyncDetector */ public int getCurrentBitErrors() { - return Long.bitCount(mValue ^ SYNC_PATTERN); + return Long.bitCount(mValue ^ getPattern()); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetector.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetector.java index ff868735..5c32d463 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetector.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetector.java @@ -19,15 +19,16 @@ package io.github.dsheirer.module.decode.nxdn.layer1.sync; +import io.github.dsheirer.dsp.c4fm.ISoftC4FMSyncDetector; import java.util.Arrays; /** * NXDN Sync Detector. Uses soft demodulated symbols to detect sync patterns in the demodulated symbol stream. */ -public abstract class NXDNSoftSyncDetector extends NXDNSyncDetector +public abstract class NXDNSoftSyncDetector extends NXDNSyncDetector implements ISoftC4FMSyncDetector { //symbol buffer is extended by 6x to support SIMD 512 calculations - protected float[] mSymbols = new float[SYNC_DIBIT_LENGTH * 2 + 6]; + protected float[] mSymbols = new float[getDibitLength() * 2 + 6]; protected int mSymbolPointer = 0; /** @@ -63,9 +64,9 @@ public abstract class NXDNSoftSyncDetector extends NXDNSyncDetector public float process(float dibitSymbol) { mSymbols[mSymbolPointer] = dibitSymbol; - mSymbols[mSymbolPointer + SYNC_DIBIT_LENGTH] = dibitSymbol; + mSymbols[mSymbolPointer + getDibitLength()] = dibitSymbol; mSymbolPointer++; - mSymbolPointer %= SYNC_DIBIT_LENGTH; + mSymbolPointer %= getDibitLength(); return calculate(); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorScalar.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorScalar.java index ff0aeee9..dfb468cd 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorScalar.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorScalar.java @@ -35,10 +35,10 @@ public class NXDNSoftSyncDetectorScalar extends NXDNSoftSyncDetector float symbol; float score = 0; - for(int x = 0; x < SYNC_DIBIT_LENGTH; x++) + for(int x = 0; x < getDibitLength(); x++) { symbol = mSymbols[mSymbolPointer + x]; - score += SYMBOLS[x] * symbol; + score += getPatternSymbols()[x] * symbol; } return score; diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector128.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector128.java index 29de82ee..6351bea6 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector128.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector128.java @@ -34,8 +34,8 @@ public class NXDNSoftSyncDetectorVector128 extends NXDNSoftSyncDetector public NXDNSoftSyncDetectorVector128() { //Create an array of 4x elements containing the final 2x symbols for SIMD loading. - SYNC_EXTENDED[2] = SYMBOLS[8]; - SYNC_EXTENDED[3] = SYMBOLS[9]; + SYNC_EXTENDED[2] = getPatternSymbols()[8]; + SYNC_EXTENDED[3] = getPatternSymbols()[9]; } /** @@ -47,9 +47,9 @@ public class NXDNSoftSyncDetectorVector128 extends NXDNSoftSyncDetector public float calculate() { FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES); - accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYMBOLS, 0) + accumulator = FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 0) .fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer), accumulator); - accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYMBOLS, 4) + accumulator = FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 4) .fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 4), accumulator); accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYNC_EXTENDED, 0) .fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 6), accumulator); diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector256.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector256.java index a46613c5..b81e03e2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector256.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector256.java @@ -34,8 +34,8 @@ public class NXDNSoftSyncDetectorVector256 extends NXDNSoftSyncDetector public NXDNSoftSyncDetectorVector256() { //Create an array of 8x elements containing the final 2x symbols for SIMD loading. - SYNC_EXTENDED[2] = SYMBOLS[8]; - SYNC_EXTENDED[3] = SYMBOLS[9]; + SYNC_EXTENDED[2] = getPatternSymbols()[8]; + SYNC_EXTENDED[3] = getPatternSymbols()[9]; } /** @@ -47,7 +47,7 @@ public class NXDNSoftSyncDetectorVector256 extends NXDNSoftSyncDetector public float calculate() { FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES); - accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYMBOLS, 0) + accumulator = FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 0) .fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer), accumulator); accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYNC_EXTENDED, 0) .fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 6), accumulator); diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector512.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector512.java index 9255d5be..16fec417 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector512.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector512.java @@ -31,7 +31,12 @@ public class NXDNSoftSyncDetectorVector512 extends NXDNSoftSyncDetector { private static final VectorSpecies VECTOR_SPECIES = FloatVector.SPECIES_512; //Extend the symbols out to 16x using zero padding after the 10th symbol - private static final float[] SYNC_EXTENDED = Arrays.copyOf(SYMBOLS, 16); + private static float[] SYNC_EXTENDED; + + public NXDNSoftSyncDetectorVector512() + { + SYNC_EXTENDED = Arrays.copyOf(getPatternSymbols(), 16);; + } /** * Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector64.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector64.java index 0f830b3e..c0db88c9 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector64.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer1/sync/NXDNSoftSyncDetectorVector64.java @@ -40,9 +40,9 @@ public class NXDNSoftSyncDetectorVector64 extends NXDNSoftSyncDetector { FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES); - for(int x = 0; x < SYNC_DIBIT_LENGTH; x += VECTOR_SPECIES.length()) + for(int x = 0; x < getDibitLength(); x += VECTOR_SPECIES.length()) { - accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYMBOLS, x) + accumulator = FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), x) .fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + x), accumulator); } 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 e9ee7678..73e01019 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 @@ -19,155 +19,18 @@ package io.github.dsheirer.module.decode.nxdn.layer1.sync; -import io.github.dsheirer.dsp.symbol.Dibit; -import java.util.Map; -import java.util.TreeMap; -import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; -import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; +import io.github.dsheirer.dsp.c4fm.DibitSyncDetector; /** * Base implementation of NXDN sync detector. */ -public abstract class NXDNSyncDetector +public abstract class NXDNSyncDetector extends DibitSyncDetector { - public static final long SYNC_PATTERN = 0xCDF59; - public static final long SYNC_MASK = 0xFFFFF; - public static final int SYNC_DIBIT_LENGTH = 10; - public static final Dibit[] DIBITS = toDibits(SYNC_PATTERN, SYNC_DIBIT_LENGTH); - public static final float[] SYMBOLS = toSymbols(DIBITS); - private static Map sReferenceWaveformMap = new TreeMap<>(); - /** - * Sync pattern length in dibits. + * Constructs an instance */ - public int getSyncPatternDibitLength() + public NXDNSyncDetector() { - return SYNC_DIBIT_LENGTH; - } - - /** - * Dibits array for the sync pattern - * @return dibits array. - */ - public Dibit[] getSyncDibits() - { - return DIBITS; - } - - /** - * Symbols array for the sync pattern. - * @return array of (ideal) soft symbol values. - */ - public float[] getSyncSymbols() - { - return SYMBOLS; - } - - /** - * Converts the sync pattern to dibits. - * @param pattern to convert - * @param dibitLength in dibits - * @return array of dibits for the sync pattern. - */ - protected static Dibit[] toDibits(long pattern, int dibitLength) - { - Dibit[] dibits = new Dibit[dibitLength]; - long mask = 3; - int dibitValue; - - for(int x = 0; x < dibitLength; x++) - { - dibitValue = (int)((pattern & mask) >> (2 * x)); - - dibits[dibitLength - x - 1] = switch(dibitValue) - { - case 0 -> Dibit.D00_PLUS_1; - case 1 -> Dibit.D01_PLUS_3; - case 2 -> Dibit.D10_MINUS_1; - default -> Dibit.D11_MINUS_3; - }; - - mask = mask << 2; - } - - return dibits; - } - - /** - * Utility method to convert a dibit array into an array of ideal soft symbol values. - * @param dibits for the sync pattern - * @return ideal soft symbol array. - */ - protected static float[] toSymbols(Dibit[] dibits) - { - float[] symbols = new float[dibits.length]; - - for(int x = 0; x < dibits.length; x++) - { - symbols[x] = dibits[x].getIdealPhase(); - } - - return symbols; - } - - /** - * Creates a reference waveform representation of the sync pattern that can be used to correlate against a - * candidate detected sync waveform to discriminate between a valid and invalid sync detection. The generated - * waveform will be aligned where the final sample is time-aligned to the final symbol. - * - * @param samplesPerSymbol count - * @return a reference waveform and ideal correlation score. - */ - public ReferenceSyncWaveform getReferenceWaveform(double samplesPerSymbol) - { - int key = (int)(samplesPerSymbol * 1000.0); - - if(sReferenceWaveformMap.containsKey(key)) - { - return sReferenceWaveformMap.get(key); - } - - float[] symbols = getSyncSymbols(); - int sampleCount = (int)Math.ceil(samplesPerSymbol * (symbols.length - 1)) + 1; - float[] samples = new float[sampleCount]; - double sampleInterval = 1.0 / samplesPerSymbol; - - double[] x = new double[symbols.length + 2]; - double[] y = new double[symbols.length + 2]; - - for(int i = -1; i < x.length - 1; i++) - { - x[i + 1] = i; - - if(0 <= i && i < symbols.length) - { - y[i + 1] = symbols[i]; - } - } - - double start = symbols.length - 1; - int samplePointer = sampleCount - 1; - SplineInterpolator interpolator = new SplineInterpolator(); - PolynomialSplineFunction function = interpolator.interpolate(x, y); - - float accumulator = 0; - float sample; - float max = 0; - while(samplePointer >= 0) - { - sample = (float)function.value(start); - - if(Math.abs(sample) > max) - { - max = Math.abs(sample); - } - samples[samplePointer--] = sample; - accumulator += sample * sample; - start -= sampleInterval; - } - - ReferenceSyncWaveform referenceWaveform = new ReferenceSyncWaveform(samples, accumulator, max); - sReferenceWaveformMap.put(key, referenceWaveform); - return referenceWaveform; + super(0xCDF59, 10); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/Interleaver.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/Interleaver.java index 51202387..1e3dce2d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/Interleaver.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer2/Interleaver.java @@ -74,6 +74,24 @@ public class Interleaver return interleaved; } + /** + * Performs deinterleave of the interleaved soft bit values. + * @param interleaved soft bit values containing the interleaved payload starting at the offset + * @param offset into the soft bit values to the start of the interleaved payload. + * @return deinterleaved soft bit values starting at index 0 + */ + public float[] deinterleave(float[] interleaved, int offset) + { + float[] deinterleaved = new float[mInterleaveIndexes.length]; + + for(int index = 0; index < mInterleaveIndexes.length; index++) + { + deinterleaved[mInterleaveIndexes[index]] = interleaved[index + offset]; + } + + return deinterleaved; + } + /** * Performs deinterleave of the interleaved message * @param interleaved containing the interleaved payload starting at the offset diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/Convolution.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/Convolution.java index f430e6da..65b73adc 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/Convolution.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/Convolution.java @@ -113,6 +113,124 @@ public class Convolution return paths.getFirst().getMessage(); } + /** + * Maximum per-bit soft decision cost. FM discriminator noise is impulsive (clicks) and an unbounded cost lets a + * single corrupted symbol dominate the path metric. A value of 4 is the sign bit confidence of an ideal inner + * (+/-1) symbol. Noise injection testing against NXDN48 and NXDN96 recordings: 3 - 4 performs best, versus + * 2, 6, 8, 12, 16 and unclipped. + */ + private static final float SOFT_BIT_COST_CLIP = 4.0f; + + /** + * Soft decision Viterbi decoder (16 states) for the depunctured, deinterleaved, descrambled soft bit values. + * + * Soft bit values are signed confidences where a positive value favors a 1 bit, a negative value favors a 0 bit + * and zero is an erasure (ie punctured bit). The branch metric is the sum of the clipped soft bit magnitudes for + * the encoded bits that disagree with the received soft bit sign. The final 4 message bits are flushing (0) bits. + * + * The corrected bit count on the returned message is the count of non-punctured encoded bits where the hard + * decision of the received soft bit disagrees with the re-encoded decoded message, consistent with hard decision + * decoding. + * + * @param softBits depunctured soft bit values, two per message bit (G1, G2) + * @param provider to identify punctured bits + * @return decoded message, including the final 4 flushing bits, with the bit error count set. + */ + public static CorrectedBinaryMessage decodeSoft(float[] softBits, PunctureProvider provider) + { + int steps = softBits.length / 2; + int states = 16; + float[] metrics = new float[states]; + float[] nextMetrics = new float[states]; + byte[][] survivors = new byte[steps][states]; //Previous state for each state at each step + java.util.Arrays.fill(metrics, Float.MAX_VALUE); + metrics[0] = 0.0f; + + for(int step = 0; step < steps; step++) + { + float soft1 = softBits[2 * step]; + float soft2 = softBits[2 * step + 1]; + float cost1 = Math.min(Math.abs(soft1), SOFT_BIT_COST_CLIP); + float cost2 = Math.min(Math.abs(soft2), SOFT_BIT_COST_CLIP); + boolean hard1 = soft1 > 0; + boolean hard2 = soft2 > 0; + int inputs = (step >= steps - 4) ? 1 : 2; //Final 4 bits are flushing zero bits + java.util.Arrays.fill(nextMetrics, Float.MAX_VALUE); + + for(int state = 0; state < states; state++) + { + if(metrics[state] == Float.MAX_VALUE) + { + continue; + } + + for(int input = 0; input < inputs; input++) + { + int register = (state << 1) | input; + float metric = metrics[state]; + + if(G1[register] != hard1) + { + metric += cost1; + } + + if(G2[register] != hard2) + { + metric += cost2; + } + + int nextState = register & 0xF; + + if(metric < nextMetrics[nextState]) + { + nextMetrics[nextState] = metric; + survivors[step][nextState] = (byte)state; + } + } + } + + float[] swap = metrics; + metrics = nextMetrics; + nextMetrics = swap; + } + + //Trace back from the flushed (zero) state. The input bit for each step is the LSB of the state after the step. + CorrectedBinaryMessage message = new CorrectedBinaryMessage(steps); + int state = 0; + + for(int step = steps - 1; step >= 0; step--) + { + if((state & 1) == 1) + { + message.set(step); + } + + state = survivors[step][state]; + } + + //Count the non-punctured bit errors between the received hard decisions and the re-encoded message + int bitErrors = 0; + int register = 0; + + for(int step = 0; step < steps; step++) + { + register = ((register << 1) & 0x1F) | (message.get(step) ? 1 : 0); + + if(G1[register] != (softBits[2 * step] > 0) && provider.isPreserved(2 * step)) + { + bitErrors++; + } + + if(G2[register] != (softBits[2 * step + 1] > 0) && provider.isPreserved(2 * step + 1)) + { + bitErrors++; + } + } + + message.setCorrectedBitCount(bitErrors); + return message; + } + /** * Encodes the message using NXDN convolutional encoder * @param message to be encoded diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProvider.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProvider.java index d226f3cc..41e0f0b2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProvider.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProvider.java @@ -115,6 +115,32 @@ public abstract class PunctureProvider return depunctured; } + /** + * De-punctures the punctured soft bit values, inserting zero-valued (erasure) soft bits at the punctured positions. + * Mirrors the depuncture(CorrectedBinaryMessage) bit placement. + * @param punctured soft bit values to be de-punctured + * @return de-punctured soft bit values + */ + public float[] depuncture(float[] punctured) + { + float[] depunctured = new float[calculateDepuncturedLength(punctured.length)]; + + int depuncturedPointer = 0; + + for(int puncturedPointer = 0; puncturedPointer < punctured.length && depuncturedPointer < depunctured.length; + puncturedPointer++) + { + depunctured[depuncturedPointer++] = punctured[puncturedPointer]; + + if(isPunctured(depuncturedPointer)) + { + depuncturedPointer++; + } + } + + return depunctured; + } + /** * Calculates the length of punctured message from the original depunctured length. * @param depunctured message length diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProviderNone.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProviderNone.java index 87e0a320..c445a720 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProviderNone.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/coding/PunctureProviderNone.java @@ -46,6 +46,12 @@ public class PunctureProviderNone extends PunctureProvider return punctured; } + @Override + public float[] depuncture(float[] punctured) + { + return punctured; + } + @Override public boolean isPreserved(int index) { diff --git a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/TransmissionMode.java b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/TransmissionMode.java index 6d6ab10f..0aea50ed 100644 --- a/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/TransmissionMode.java +++ b/src/main/java/io/github/dsheirer/module/decode/nxdn/layer3/type/TransmissionMode.java @@ -20,26 +20,30 @@ package io.github.dsheirer.module.decode.nxdn.layer3.type; /** - * Transmission mode and symbol rate + * Transmission mode, symbol rate and outer (+/-3) symbol deviation per NXDN TS 1-A Table 3.3-1. */ public enum TransmissionMode { - M4800("4800", 2400), - M9600("9600", 4800), - TYPE_D("TYPE D", 2400); + M4800("4800", 2400, 1050), + M9600("9600", 4800, 2400), + TYPE_D("TYPE D", 2400, 1050); private final String mLabel; private final int mSymbolRate; + private final int mOuterSymbolDeviation; /** * Constructs an instance * * @param label to display + * @param symbolRate in symbols per second + * @param outerSymbolDeviation frequency deviation of the +/-3 symbols in Hertz */ - TransmissionMode(String label, int symbolRate) + TransmissionMode(String label, int symbolRate, int outerSymbolDeviation) { mLabel = label; mSymbolRate = symbolRate; + mOuterSymbolDeviation = outerSymbolDeviation; } public String getLabel() @@ -55,6 +59,14 @@ public enum TransmissionMode return mSymbolRate; } + /** + * Frequency deviation of the outer (+/-3) symbols in Hertz. + */ + public int getOuterSymbolDeviation() + { + return mOuterSymbolDeviation; + } + /** * Indicates if this mode is Type D */ diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java index cd570c10..d143bfde 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderC4FM.java @@ -26,18 +26,12 @@ import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter; import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification; import io.github.dsheirer.dsp.filter.fir.real.IRealFilter; import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter; -import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFactory; -import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFloat; +import io.github.dsheirer.dsp.fm.FmDemodulatorFactory; +import io.github.dsheirer.dsp.fm.IDemodulator; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.SyncLossMessage; import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.FeedbackDecoder; -import io.github.dsheirer.module.decode.dmr.message.DMRBurst; -import io.github.dsheirer.module.decode.dmr.message.data.DataMessageWithLinkControl; -import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage; -import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; -import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ShortLCMessage; -import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator; import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule; import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message; import io.github.dsheirer.preference.UserPreferences; @@ -49,12 +43,13 @@ import io.github.dsheirer.source.ISourceEventListener; import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.wave.ComplexWaveSource; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import java.text.DecimalFormat; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -74,18 +69,21 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv private static final Logger LOGGER = LoggerFactory.getLogger(P25P1DecoderC4FM.class); private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##"); private static final int SYMBOL_RATE = 4800; - private static final Map BASEBAND_FILTERS = new HashMap<>(); + //Band limited integrate and dump receive filter settings. Noise injection testing against P25 C4FM recordings: + //stopband starts of 3300 - 4000 Hz perform similarly, a 5-symbol span is too short, and 12 symbols adds nothing. + private static final double RECEIVE_FILTER_STOP_BAND_START = 3600.0; //Hertz + private static final int RECEIVE_FILTER_SYMBOL_SPAN = 8; + private static final Map BASEBAND_FILTERS = new ConcurrentHashMap<>(); - private final P25P1DemodulatorC4FM mSymbolProcessor; + private final P25P1SymbolProcessorC4FM mSymbolProcessor; private final P25P1MessageFramer mMessageFramer = new P25P1MessageFramer(); private final P25P1MessageProcessor mMessageProcessor = new P25P1MessageProcessor(); - private DifferentialDemodulatorFloat mDemodulator; + private IDemodulator mDemodulator; private IRealDecimationFilter mDecimationFilterI; private IRealDecimationFilter mDecimationFilterQ; private IRealFilter mBasebandFilterI; private IRealFilter mBasebandFilterQ; - private IRealFilter mPulseShapingFilterI; - private IRealFilter mPulseShapingFilterQ; + private IRealFilter mReceiveFilter; @Override public DecoderType getDecoderType() @@ -96,7 +94,7 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv public P25P1DecoderC4FM() { mMessageProcessor.setMessageListener(getMessageListener()); - mSymbolProcessor = new P25P1DemodulatorC4FM(mMessageFramer, this); + mSymbolProcessor = new P25P1SymbolProcessorC4FM(this, mMessageFramer); } @Override @@ -111,18 +109,20 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv */ public void setSampleRate(double sampleRate) { - if(sampleRate <= SYMBOL_RATE * 2) + float samplesPerSymbol = (float)sampleRate / SYMBOL_RATE; + + if(samplesPerSymbol <= 4.0f) { - throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >9600 (2 * " + - SYMBOL_RATE + " symbol rate)"); + throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be at least " + (SYMBOL_RATE * 4) + " Hz"); } int decimation = 1; - //Identify decimation that gets us as close to 4.0 Samples Per Symbol as possible (19.2 kHz) - while((sampleRate / decimation) >= 38400) + //Identify decimation that gets us as close to 5.0 Samples Per Symbol as possible + while(samplesPerSymbol >= 10.0) { decimation *= 2; + samplesPerSymbol /= 2; } mDecimationFilterI = DecimationFilterFactory.getRealDecimationFilter(decimation); @@ -133,19 +133,21 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv //Set the decimated sample rate to use for PLL error reporting. setDecimatedSampleRate(decimatedSampleRate); - int symbolLength = 16; - float rrcAlpha = 0.2f; - - float[] taps = FilterFactory.getRootRaisedCosine(decimatedSampleRate / SYMBOL_RATE, - symbolLength, rrcAlpha); - mPulseShapingFilterI = new RealFIRFilter(taps); - mPulseShapingFilterQ = new RealFIRFilter(taps); + //Receive filter: integrate and dump (sinc) response across the signal band with a stopband above the signal band + //to reject FM discriminator noise that an integrate and dump filter's sidelobes would otherwise pass. + float[] receiveFilterTaps = P25P1FilterDesigner.designReceiveFilter(decimatedSampleRate, SYMBOL_RATE, + RECEIVE_FILTER_STOP_BAND_START, RECEIVE_FILTER_SYMBOL_SPAN); + mReceiveFilter = new RealFIRFilter(receiveFilterTaps); mBasebandFilterI = FilterFactory.getRealFilter(getBasebandFilter(decimatedSampleRate)); mBasebandFilterQ = FilterFactory.getRealFilter(getBasebandFilter(decimatedSampleRate)); - mDemodulator = DifferentialDemodulatorFactory.getFloatDemodulator(decimatedSampleRate, SYMBOL_RATE); - mSymbolProcessor.setSamplesPerSymbol(mDemodulator.getSamplesPerSymbol()); + + mDemodulator = FmDemodulatorFactory.getFmDemodulator(); + mSymbolProcessor.setSamplesPerSymbol(samplesPerSymbol); + + //TODO: update P25 message Framer and Processor like we do in NXDN constructor mMessageFramer.setListener(mMessageProcessor); + mMessageProcessor.setMessageListener(getMessageListener()); } @@ -165,11 +167,8 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv i = mBasebandFilterI.filter(i); q = mBasebandFilterQ.filter(q); - i = mPulseShapingFilterI.filter(i); - q = mPulseShapingFilterQ.filter(q); - - // PI/4 DQPSK differential demodulation float[] demodulated = mDemodulator.demodulate(i, q); + demodulated = mReceiveFilter.filter(demodulated); //Process demodulated samples into symbols and apply message sync detection and framing. mSymbolProcessor.process(demodulated); @@ -180,9 +179,12 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv */ private float[] getBasebandFilter(double sampleRate) { - if(BASEBAND_FILTERS.containsKey(sampleRate)) + //Filter cache is shared across decoder instances that may be started from different threads + float[] cached = BASEBAND_FILTERS.get(sampleRate); + + if(cached != null) { - return BASEBAND_FILTERS.get(sampleRate); + return cached; } FIRFilterSpecification specification = FIRFilterSpecification @@ -198,11 +200,15 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv try { coefficients = FilterFactory.getTaps(specification); - BASEBAND_FILTERS.put(sampleRate, coefficients); + + if(coefficients != null) + { + BASEBAND_FILTERS.put(sampleRate, coefficients); + } } - catch(Exception fde) //FilterDesignException + catch(Exception e) //FilterDesignException or runtime failures from the Remez designer { - System.out.println("Error"); + LOGGER.error("Error designing P25 Phase 1 C4FM baseband filter for sample rate [" + sampleRate + "]", e); } if(coefficients == null) @@ -258,6 +264,7 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv { super.stop(); mMessageFramer.stop(); + mSymbolProcessor.dispose(); } /** @@ -269,7 +276,7 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv { case NOTIFICATION_FREQUENCY_CHANGE: case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE: - mSymbolProcessor.resetPLL(); + mSymbolProcessor.resetBalance(); break; case NOTIFICATION_SAMPLE_RATE_CHANGE: setSampleRate(sourceEvent.getValue().doubleValue()); @@ -288,10 +295,11 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv LOGGER.info("Starting ..."); // String directory = "D:\\DQPSK Equalizer Research - P25\\"; //Windows - String directory = "/media/denny/T9/DQPSK Equalizer Research - P25/"; //Linux + String directory = "/run/media/denny/T9/DQPSK Equalizer Research - P25/"; //Linux - String file = directory + "P25-S1-Conventional-repeater-20241115_212221_469325000_QPS_Digital_Kynoch_Kynoch_Digital_59_baseband.wav"; -// String file = directory + "P25-S3-C4FM-20241225_040459_152517500_NYSEG_Onondaga_Control_30_baseband.wav"; + //Note: this file has lots of dropped sample periods across each burst, with otherwise high SNR good decodes +// String file = directory + "P25-S1-Conventional-repeater-20241115_212221_469325000_QPS_Digital_Kynoch_Kynoch_Digital_59_baseband.wav"; + String file = directory + "P25-S3-C4FM-20241225_040459_152517500_NYSEG_Onondaga_Control_30_baseband.wav"; boolean autoReplay = false; @@ -336,23 +344,9 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv mValidMessageCounter++; } } - else if(iMessage instanceof LCMessage lcw) - { - mTotalMessageCounter++; - errors = lcw.getMessage().getCorrectedBitCount(); - if(lcw.isValid()) - { - mBitErrorCounter += errors; - mValidMessageCounter++; - } - } double bitErrorRate = (double)mBitErrorCounter / (double)mBitCounter * 100.0; - boolean logEverything = true; - boolean logFLC = true; - boolean logSLC = true; - boolean logCACH = true; boolean logIdles = true; if(logEverything) @@ -365,32 +359,6 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv } else { - if (logFLC) - { - if(iMessage instanceof FullLCMessage) - { - System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]"); - } - else if(iMessage instanceof Terminator terminator) - { - System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]"); - } - else if(iMessage instanceof DataMessageWithLinkControl) - { - System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]"); - } - } - - if(logCACH && iMessage instanceof DMRBurst burst && burst.hasCACH()) - { - System.out.println("CACH:" + burst.getCACH()); - } - - if(logSLC && iMessage instanceof ShortLCMessage) - { - System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]"); - } - if(logIdles && iMessage instanceof SyncLossMessage) { System.out.println(">>MESSAGE: TS" + iMessage.getTimeslot() + " " + iMessage + " \t\t[" + errors + " | " + mBitErrorCounter + " | Valid:" + mValidMessageCounter + " Total:" + mTotalMessageCounter + " Msgs] Rate [" + DECIMAL_FORMAT.format(bitErrorRate) + " %]"); @@ -412,11 +380,25 @@ public class P25P1DecoderC4FM extends FeedbackDecoder implements IByteBufferProv source.start(); decoder.setSampleRate(source.getSampleRate()); - while(true) + boolean hasData = true; + + while(hasData) { - source.next(2048, true); + try + { + source.next(2048, true); + } + catch(IOException e) + { + LOGGER.info("End of file: " + e.getLocalizedMessage()); + hasData = false; + } } } + catch(FileNotFoundException e) + { + LOGGER.error("File not found: " + file); + } catch(IOException ioe) { LOGGER.error("Error", ioe); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DemodulatorC4FM.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DemodulatorC4FM.java index 75e42613..0ed6ebba 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DemodulatorC4FM.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DemodulatorC4FM.java @@ -26,11 +26,11 @@ import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.dsp.symbol.DibitDelayLine; import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler; import io.github.dsheirer.edac.bch.BCH_63_16_23_P25; -import io.github.dsheirer.gui.viewer.sync.SyncResultsViewer; +import io.github.dsheirer.gui.viewer.sync.FMSyncResultsViewer; +import io.github.dsheirer.gui.viewer.sync.ISyncResultsListener; import io.github.dsheirer.module.decode.FeedbackDecoder; import io.github.dsheirer.module.decode.p25.phase1.sync.P25P1SoftSyncDetector; import io.github.dsheirer.module.decode.p25.phase1.sync.P25P1SoftSyncDetectorFactory; -import io.github.dsheirer.module.decode.p25.phase1.sync.P25P1SyncDetector; import io.github.dsheirer.sample.Listener; import java.nio.ByteBuffer; import java.util.Arrays; @@ -58,16 +58,13 @@ public class P25P1DemodulatorC4FM private static final float SYNC_THRESHOLD_OPTIMIZED = 80; private static final float SYNC_THRESHOLD_EQUALIZED = 110; private static final float TWO_PI = (float)(Math.PI * 2.0); - private static final float[] SYNC_PATTERN_SYMBOLS = P25P1SyncDetector.syncPatternToSymbols(); 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 = 24; private static final Correction INVALID_SYNC_DETECTION = new Correction(0d, Double.MAX_VALUE, 0f, 0f, 0f, 0f, 0); - private static final Dibit[] SYNC_PATTERN_DIBITS = P25P1SyncDetector.syncPatternToDibits(); private static final IntField NAC_FIELD = IntField.length12(0); private static final IntField DUID_FIELD = IntField.length4(12); - private SyncResultsViewer mSyncResultsViewer; private final BCH_63_16_23_P25 mBCHDecoder = new BCH_63_16_23_P25(); private final DibitDelayLine mSymbolDelayLine = new DibitDelayLine(DIBIT_LENGTH_SYNC); private final DibitToByteBufferAssembler mDibitAssembler = new DibitToByteBufferAssembler(300); @@ -92,6 +89,13 @@ public class P25P1DemodulatorC4FM private int mBufferReloadThreshold; private int mSymbolsSinceLastSync = 0; + //Debug for waveform and symbol analysis + private ISyncResultsListener mSyncResultsViewer; + private final boolean mDebugVisualize = false; + private final int mDebugSymbolStart = 0; + private int mDebugSymbolCounter = 0; + private int mDebugSampleCounter = 0; + /** * Constructs an instance * @param messageFramer to receive symbol decisions (dibits) and sync notifications. @@ -272,7 +276,7 @@ public class P25P1DemodulatorC4FM symbolsSinceLastSync = 0; //Overwrite the sync symbols in the symbol delay line to eliminate any decoding errors - for(Dibit syncSymbol: SYNC_PATTERN_DIBITS) + for(Dibit syncSymbol: mSyncDetector.getPatternDibits()) { mSymbolDelayLine.insert(syncSymbol); } @@ -416,127 +420,114 @@ public class P25P1DemodulatorC4FM } } - /** - * Debug viewer UI for visualizing sync detections in the sample stream/waveform. - */ - private SyncResultsViewer getSyncResultsViewer() - { - if(mSyncResultsViewer == null) - { - mSyncResultsViewer = new SyncResultsViewer(); - } - - return mSyncResultsViewer; - } - /** * Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing. * @param score from the sync detector * @param primary sync detector (true) or secondary (false) */ - public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint) - { - int offset = 3; - int length = (int)Math.ceil(mSamplesPerSymbol * 23) + (2 * offset); - int end = bufferPointer + offset; - int start = end - length; - float[] symbols = new float[24]; - float[] samples = Arrays.copyOfRange(mBuffer, start, end); - for(int i = 0; i < samples.length; i++) - { - samples[i] = mEqualizer.equalize(samples[i]); - } - - double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 23); - int symbolIntegral = (int)Math.floor(symbolPointer); - double mu = symbolPointer - symbolIntegral; - - for(int x = 0; x < 24; x++) - { - symbols[x] = mEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); - symbolPointer += mSamplesPerSymbol; - symbolIntegral = (int)Math.floor(symbolPointer); - mu = symbolPointer - symbolIntegral; - } - - float[] syncIntervals = new float[24]; - int adjust = bufferPointer - length + offset; - double pointer = bufferPointer + samplePoint - adjust; - - for(int x = 23; x >= 0; x--) - { - syncIntervals[x] = (float)pointer; - pointer -= mSamplesPerSymbol; - } - - CountDownLatch countDownLatch = new CountDownLatch(1); - getSyncResultsViewer().receive(symbols, SYNC_PATTERN_SYMBOLS, samples, syncIntervals, mEqualizer.mPll, mEqualizer.mGain, - "Score: " + score + (primary ? " PRIMARY " : " SECONDARY ") + (mFineSync ? "FINE " : "COARSE ") + - " EQ-B:" + mEqualizer.mPll + " EQ-G:" + mEqualizer.mGain + " " + tag, countDownLatch); - - try - { - countDownLatch.await(); - } - catch(InterruptedException e) - { - throw new RuntimeException(e); - } - } +// public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint) +// { +// int offset = 3; +// int length = (int)Math.ceil(mSamplesPerSymbol * 23) + (2 * offset); +// int end = bufferPointer + offset; +// int start = end - length; +// float[] symbols = new float[24]; +// float[] samples = Arrays.copyOfRange(mBuffer, start, end); +// for(int i = 0; i < samples.length; i++) +// { +// samples[i] = mEqualizer.equalize(samples[i]); +// } +// +// double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 23); +// int symbolIntegral = (int)Math.floor(symbolPointer); +// double mu = symbolPointer - symbolIntegral; +// +// for(int x = 0; x < 24; x++) +// { +// symbols[x] = mEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); +// symbolPointer += mSamplesPerSymbol; +// symbolIntegral = (int)Math.floor(symbolPointer); +// mu = symbolPointer - symbolIntegral; +// } +// +// float[] syncIntervals = new float[24]; +// int adjust = bufferPointer - length + offset; +// double pointer = bufferPointer + samplePoint - adjust; +// +// for(int x = 23; x >= 0; x--) +// { +// syncIntervals[x] = (float)pointer; +// pointer -= mSamplesPerSymbol; +// } +// +// CountDownLatch countDownLatch = new CountDownLatch(1); +// getSyncResultsViewer().receive(symbols, mSyncDetector.getSymbols(), samples, syncIntervals, mEqualizer.mPll, mEqualizer.mGain, +// "Score: " + score + (primary ? " PRIMARY " : " SECONDARY ") + (mFineSync ? "FINE " : "COARSE ") + +// " EQ-B:" + mEqualizer.mPll + " EQ-G:" + mEqualizer.mGain + " " + tag, countDownLatch); +// +// try +// { +// countDownLatch.await(); +// } +// catch(InterruptedException e) +// { +// throw new RuntimeException(e); +// } +// } /** * Debug method to visualize the symbol decisions */ - public void visualizeSymbols(int bufferPointer, double samplePoint) - { - int offset = 3; - int length = (int)Math.ceil(mSamplesPerSymbol * 23) + (2 * offset); - int end = bufferPointer + offset; - int start = end - length; - float[] symbols = new float[24]; - float[] decisions = new float[24]; - float[] samples = Arrays.copyOfRange(mBuffer, start, end); - for(int i = 0; i < samples.length; i++) - { - samples[i] = mEqualizer.equalize(samples[i]); - } - - double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 23); - int symbolIntegral = (int)Math.floor(symbolPointer); - double mu = symbolPointer - symbolIntegral; - - for(int x = 0; x < 24; x++) - { - symbols[x] = mEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); - decisions[x] = toSymbol(symbols[x]).getIdealPhase(); - symbolPointer += mSamplesPerSymbol; - symbolIntegral = (int)Math.floor(symbolPointer); - mu = symbolPointer - symbolIntegral; - } - - float[] syncIntervals = new float[24]; - int adjust = bufferPointer - length + offset; - double pointer = bufferPointer + samplePoint - adjust; - - for(int x = 23; x >= 0; x--) - { - syncIntervals[x] = (float)pointer; - pointer -= mSamplesPerSymbol; - } - - CountDownLatch countDownLatch = new CountDownLatch(1); - getSyncResultsViewer().receive(symbols, decisions, samples, syncIntervals, mEqualizer.mPll, mEqualizer.mGain, - "PLL:" + mEqualizer.mPll + " Gain:" + mEqualizer.mGain, countDownLatch); - - try - { - countDownLatch.await(); - } - catch(InterruptedException e) - { - throw new RuntimeException(e); - } - } +// public void visualizeSymbols(int bufferPointer, double samplePoint) +// { +// int offset = 3; +// int length = (int)Math.ceil(mSamplesPerSymbol * 23) + (2 * offset); +// int end = bufferPointer + offset; +// int start = end - length; +// float[] symbols = new float[24]; +// float[] decisions = new float[24]; +// float[] samples = Arrays.copyOfRange(mBuffer, start, end); +// for(int i = 0; i < samples.length; i++) +// { +// samples[i] = mEqualizer.equalize(samples[i]); +// } +// +// double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 23); +// int symbolIntegral = (int)Math.floor(symbolPointer); +// double mu = symbolPointer - symbolIntegral; +// +// for(int x = 0; x < 24; x++) +// { +// symbols[x] = mEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); +// decisions[x] = toSymbol(symbols[x]).getIdealPhase(); +// symbolPointer += mSamplesPerSymbol; +// symbolIntegral = (int)Math.floor(symbolPointer); +// mu = symbolPointer - symbolIntegral; +// } +// +// float[] syncIntervals = new float[24]; +// int adjust = bufferPointer - length + offset; +// double pointer = bufferPointer + samplePoint - adjust; +// +// for(int x = 23; x >= 0; x--) +// { +// syncIntervals[x] = (float)pointer; +// pointer -= mSamplesPerSymbol; +// } +// +// CountDownLatch countDownLatch = new CountDownLatch(1); +// getSyncResultsViewer().receive(symbols, decisions, samples, syncIntervals, mEqualizer.mPll, mEqualizer.mGain, +// "PLL:" + mEqualizer.mPll + " Gain:" + mEqualizer.mGain, countDownLatch); +// +// try +// { +// countDownLatch.await(); +// } +// catch(InterruptedException e) +// { +// throw new RuntimeException(e); +// } +// } /** * Indicates if the samples in the sample buffer that contain a detected sync pattern have a standard deviation @@ -1011,7 +1002,7 @@ public class P25P1DemodulatorC4FM softSymbol = 0.0f; } - score += softSymbol * SYNC_PATTERN_SYMBOLS[x]; + score += softSymbol * mSyncDetector.getPatternSymbols()[x]; pointer += samplesPerSymbol; bufferPointer = (int)Math.floor(pointer); fractional = pointer - bufferPointer; @@ -1079,7 +1070,7 @@ public class P25P1DemodulatorC4FM { double resampleStart = offset + additionalOffset + timingCorrection; int resampleStartIntegral = (int)Math.floor(resampleStart); - float symbol = SYNC_PATTERN_SYMBOLS[23]; + float symbol = mSyncDetector.getPatternSymbols()[23]; float resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral], mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral); resampledSoftSymbol = (resampledSoftSymbol + mPll) * mGain; @@ -1088,7 +1079,7 @@ public class P25P1DemodulatorC4FM float balanceMinus3Symbols = resampledSoftSymbol - symbol; float gainAccumulator = Math.abs(symbol) - Math.abs(resampledSoftSymbol); Dibit resampledDibit = toSymbol(resampledSoftSymbol); - int bitErrorCount = SYNC_PATTERN_DIBITS[23].getBitErrorFrom(resampledDibit); + int bitErrorCount = mSyncDetector.getPatternDibits()[23].getBitErrorFrom(resampledDibit); resampleStart -= (23 * mSamplesPerSymbol); resampleStartIntegral = (int)Math.floor(resampleStart); @@ -1097,12 +1088,12 @@ public class P25P1DemodulatorC4FM { if(resampleStartIntegral >= 0) { - symbol = SYNC_PATTERN_SYMBOLS[x]; + symbol = mSyncDetector.getPatternSymbols()[x]; resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral], mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral); resampledSoftSymbol = (resampledSoftSymbol + mPll) * mGain; resampledDibit = toSymbol(resampledSoftSymbol); - bitErrorCount += SYNC_PATTERN_DIBITS[x].getBitErrorFrom(resampledDibit); + bitErrorCount += mSyncDetector.getPatternDibits()[x].getBitErrorFrom(resampledDibit); if(symbol > 0) { @@ -1140,4 +1131,148 @@ public class P25P1DemodulatorC4FM optimizationScore, bitErrorCount); } } + + /** + * Debug viewer UI for visualizing sync detections in the sample stream/waveform. + */ + private ISyncResultsListener getSyncResultsViewer() + { + if(mSyncResultsViewer == null) + { + mSyncResultsViewer = new FMSyncResultsViewer(); + } + + return mSyncResultsViewer; + } + + /** + * Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing. + * @param score from the sync detector + * @param primary sync detector (true) or secondary (false) + */ + public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint) + { + if(!mDebugVisualize) + { + return; + } + + int integral = (int)Math.floor(samplePoint); + samplePoint -= integral; + bufferPointer += integral; + + int offset = 3; + int length = (int)Math.ceil(mSamplesPerSymbol * 9) + (2 * offset); + int end = bufferPointer + offset; + int start = end - length; + float[] symbols = new float[10]; + float[] samples = Arrays.copyOfRange(mBuffer, start, end); + for(int i = 0; i < samples.length; i++) + { +// samples[i] = mSampleEqualizer.equalize(samples[i]); + } + + double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 9); + int symbolIntegral = (int)Math.floor(symbolPointer); + double mu = symbolPointer - symbolIntegral; + + for(int x = 0; x < 10; x++) + { +// symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); + symbolPointer += mSamplesPerSymbol; + symbolIntegral = (int)Math.floor(symbolPointer); + mu = symbolPointer - symbolIntegral; + } + + float[] syncIntervals = new float[10]; + int adjust = bufferPointer - length + offset; + double pointer = bufferPointer + samplePoint - adjust; + + for(int x = 9; x >= 0; x--) + { + syncIntervals[x] = (float)pointer; + pointer -= mSamplesPerSymbol; + } + + CountDownLatch countDownLatch = new CountDownLatch(1); +// getSyncResultsViewer().receive(symbols, mSyncDetector.getSyncSymbols(), samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain, +// tag + " Score: " + score + (primary ? " PRIMARY " : " SECONDARY ") + (mSynchronized ? "FINE " : "COARSE ") + +// " EQ-B:" + mSampleEqualizer.mBalance + " EQ-G:" + mSampleEqualizer.mGain + " " + +// " SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter + " DUR:" + DF.format(mDebugSymbolCounter / 2400.0), +// countDownLatch); + + try + { + countDownLatch.await(); + } + catch(InterruptedException e) + { + throw new RuntimeException(e); + } + } + + /** + * Debug method to visualize the symbol decisions + */ + public void visualizeSymbols(int bufferPointer, double samplePoint) + { + if(!mDebugVisualize) + { + return; + } + + if(mDebugSymbolCounter < mDebugSymbolStart) + { + return; + } + + int offset = 3; + int length = (int)Math.ceil(mSamplesPerSymbol * 9) + (2 * offset); + int end = bufferPointer + offset; + int start = end - length; + float[] symbols = new float[10]; + float[] decisions = new float[10]; + float[] samples = Arrays.copyOfRange(mBuffer, start, end); + for(int i = 0; i < samples.length; i++) + { +// samples[i] = mSampleEqualizer.equalize(samples[i]); + } + + double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * 9); + int symbolIntegral = (int)Math.floor(symbolPointer); + double mu = symbolPointer - symbolIntegral; + + for(int x = 0; x < 10; x++) + { +// symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu); + decisions[x] = Dibit.fromSample(symbols[x]).getIdealPhase(); + symbolPointer += mSamplesPerSymbol; + symbolIntegral = (int)Math.floor(symbolPointer); + mu = symbolPointer - symbolIntegral; + } + + float[] syncIntervals = new float[10]; + int adjust = bufferPointer - length + offset; + double pointer = bufferPointer + samplePoint - adjust; + + for(int x = 9; x >= 0; x--) + { + syncIntervals[x] = (float)pointer; + pointer -= mSamplesPerSymbol; + } + + CountDownLatch countDownLatch = new CountDownLatch(1); +// getSyncResultsViewer().receive(symbols, decisions, samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain, +// "Balance:" + mSampleEqualizer.mBalance + " Gain:" + mSampleEqualizer.mGain + +// " SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter + " DUR:" + DF.format(mDebugSymbolCounter / 2400.0), countDownLatch); + + try + { + countDownLatch.await(); + } + catch(InterruptedException e) + { + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1FilterDesigner.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1FilterDesigner.java new file mode 100644 index 00000000..91ede962 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1FilterDesigner.java @@ -0,0 +1,244 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 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 + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase1; + +/** + * Designs the P25 Phase 1 C4FM receive filter applied to the FM discriminator output. + * + * TIA-102.BAAA specifies a raised cosine (alpha 0.2) Nyquist filter cascaded with an inverse sinc (x / sin(x)) shaping + * filter at the transmitter, and an integrate and dump filter (sinc response) at the receiver, so that the overall + * response is the raised cosine with no intersymbol interference at the symbol sampling instants. + * + * The transmitted signal is band limited to (1 + alpha) * symbolRate / 2 (2880 Hz), but an integrate and dump filter + * has slowly decaying sidelobes that pass the FM discriminator noise above the signal band, where the discriminator + * noise power spectral density rises with the square of frequency. This designer produces a linear phase FIR filter + * with the integrate and dump (sinc) response across the signal band and a stopband above it, which preserves the zero + * ISI property while rejecting the out of band noise. + */ +public class P25P1FilterDesigner +{ + /** P25 C4FM raised cosine roll-off factor. */ + public static final double P25_ALPHA = 0.2; + + /** Least-squares weight applied in the stopband, relative to a weight of 1.0 in the passband. */ + private static final double STOPBAND_WEIGHT = 1.0; + + /** Frequency grid points per filter coefficient used for the least-squares fit. */ + private static final int GRID_DENSITY = 16; + + /** + * Designs the band limited integrate and dump receive filter. + * + * @param sampleRate in Hertz + * @param symbolRate in symbols per second + * @param stopBandStart frequency in Hertz where the stopband starts. The passband extends to the signal band + * edge of (1 + alpha) * symbolRate / 2 and the region between the passband edge and the stopband start is a + * transition band that is excluded from the fit. + * @param symbolSpan filter length in symbol periods + * @return filter taps, odd length, linear phase, unity gain at DC + */ + public static float[] designReceiveFilter(double sampleRate, double symbolRate, double stopBandStart, int symbolSpan) + { + if(sampleRate <= 0 || symbolRate <= 0) + { + throw new IllegalArgumentException("Sample rate and symbol rate must be positive"); + } + + if(symbolSpan < 1) + { + throw new IllegalArgumentException("Symbol span must be at least 1"); + } + + double nyquist = sampleRate / 2.0; + double passBandEdge = (1.0 + P25_ALPHA) * symbolRate / 2.0; + + if(stopBandStart <= passBandEdge || stopBandStart >= nyquist) + { + throw new IllegalArgumentException("Stop band start [" + stopBandStart + "] must be between the pass band " + + "edge [" + passBandEdge + "] and Nyquist [" + nyquist + "]"); + } + + //Odd tap count so the filter has an integer group delay of (length - 1) / 2 samples + int length = (int)Math.ceil(symbolSpan * sampleRate / symbolRate); + + if(length % 2 == 0) + { + length++; + } + + int middle = (length - 1) / 2; + int coefficientCount = middle + 1; + double symbolPeriod = 1.0 / symbolRate; + + //A symmetric filter has the zero-phase response A(f) = c[0] + sum(m=1..middle) c[m] * 2cos(2*pi*f*m/fs) + //where c[m] = h[middle - m] = h[middle + m]. Accumulate the weighted normal equations (B'WB)c = B'Wd over a + //dense frequency grid from DC to Nyquist, skipping the transition band. + double[][] normal = new double[coefficientCount][coefficientCount]; + double[] rhs = new double[coefficientCount]; + double[] basis = new double[coefficientCount]; + int gridSize = GRID_DENSITY * coefficientCount; + + for(int g = 0; g <= gridSize; g++) + { + double frequency = nyquist * g / gridSize; + double desired; + double weight; + + if(frequency <= passBandEdge) + { + desired = sinc(frequency * symbolPeriod); + weight = 1.0; + } + else if(frequency >= stopBandStart) + { + desired = 0.0; + weight = STOPBAND_WEIGHT; + } + else + { + continue; //Transition band + } + + basis[0] = 1.0; + + for(int m = 1; m < coefficientCount; m++) + { + basis[m] = 2.0 * Math.cos(2.0 * Math.PI * frequency * m / sampleRate); + } + + for(int i = 0; i < coefficientCount; i++) + { + double weighted = weight * basis[i]; + rhs[i] += weighted * desired; + + for(int j = i; j < coefficientCount; j++) + { + normal[i][j] += weighted * basis[j]; + } + } + } + + //Fill in the lower triangle of the symmetric normal matrix + for(int i = 0; i < coefficientCount; i++) + { + for(int j = 0; j < i; j++) + { + normal[i][j] = normal[j][i]; + } + } + + double[] coefficients = solve(normal, rhs); + + //Expand to the full symmetric impulse response and normalize to unity gain at DC + double[] taps = new double[length]; + double sum = 0.0; + + for(int m = 0; m < coefficientCount; m++) + { + taps[middle - m] = coefficients[m]; + taps[middle + m] = coefficients[m]; + } + + for(double tap : taps) + { + sum += tap; + } + + float[] output = new float[length]; + + for(int n = 0; n < length; n++) + { + output[n] = (float)(taps[n] / sum); + } + + return output; + } + + /** + * Sinc: sin(x) / x where x = pi * f * T, the frequency response of an integrate and dump filter of length T. + * + * @param normalizedFrequency f * T (frequency divided by the symbol rate) + */ + private static double sinc(double normalizedFrequency) + { + if(normalizedFrequency == 0.0) + { + return 1.0; + } + + double x = Math.PI * normalizedFrequency; + return Math.sin(x) / x; + } + + /** + * Solves Ax = b using Gaussian elimination with partial pivoting. A and b are modified. + */ + private static double[] solve(double[][] a, double[] b) + { + int n = b.length; + + for(int column = 0; column < n; column++) + { + int pivot = column; + + for(int row = column + 1; row < n; row++) + { + if(Math.abs(a[row][column]) > Math.abs(a[pivot][column])) + { + pivot = row; + } + } + + double[] swapRow = a[column]; + a[column] = a[pivot]; + a[pivot] = swapRow; + double swapValue = b[column]; + b[column] = b[pivot]; + b[pivot] = swapValue; + + for(int row = column + 1; row < n; row++) + { + double factor = a[row][column] / a[column][column]; + b[row] -= factor * b[column]; + + for(int k = column; k < n; k++) + { + a[row][k] -= factor * a[column][k]; + } + } + } + + double[] x = new double[n]; + + for(int row = n - 1; row >= 0; row--) + { + double sum = b[row]; + + for(int k = row + 1; k < n; k++) + { + sum -= a[row][k] * x[k]; + } + + x[row] = sum / a[row][row]; + } + + return x; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Interleave.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Interleave.java index fe1cbe38..812ea518 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Interleave.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1Interleave.java @@ -124,6 +124,24 @@ public class P25P1Interleave * @param interleaved bitset from the original message * @return binary message with length equal to the pattern size */ + /** + * Deinterleaves the soft symbols for a 196-bit (98 dibit) data block (TSBK, PDU header, unconfirmed data block). + * @param interleaved 98 soft symbols in transmitted order + * @return 98 soft symbols in deinterleaved order. + */ + public static float[] deinterleaveDataSoftSymbols(float[] interleaved) + { + //The data interleave pattern moves 4-bit groups, so each dibit (bit pair) maps to a dibit position + float[] deinterleaved = new float[DATA_DEINTERLEAVE.length / 2]; + + for(int dibit = 0; dibit < deinterleaved.length; dibit++) + { + deinterleaved[DATA_DEINTERLEAVE[2 * dibit] / 2] = interleaved[dibit]; + } + + return deinterleaved; + } + public static CorrectedBinaryMessage deinterleaveChunk(int[] pattern, BitSet interleaved) { CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(pattern.length); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageAssembler.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageAssembler.java index fd2aadc9..30acfb4e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageAssembler.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageAssembler.java @@ -46,6 +46,9 @@ public class P25P1MessageAssembler implements Listener private CorrectedBinaryMessage mMessage; private P25P1DataUnitID mDataUnitID; private int mNac; + //Soft symbols (radians) for each assembled dibit, in received order, or NaN when not provided. + private float[] mSoftSymbols = new float[256]; + private int mSoftSymbolCount = 0; /** * Constructs an instance @@ -138,7 +141,7 @@ public class P25P1MessageAssembler implements Listener if(length >= 195) { //Determine if this is a TSBK or an AMBTC PDU - CorrectedBinaryMessage candidate = TSBKMessageFactory.deinterleaveViterbiAndCrc(getMessage().getSubMessage(0, 195)); + CorrectedBinaryMessage candidate = TSBKMessageFactory.deinterleaveViterbiAndCrc(getMessage().getSubMessage(0, 196)); if(candidate != null && candidate.getCorrectedBitCount() >= 0) { @@ -217,6 +220,17 @@ public class P25P1MessageAssembler implements Listener * @param dibit to add to the message. */ public void receive(Dibit dibit) + { + receive(dibit, Float.NaN); + } + + /** + * Primary input method for demodulated dibits/symbols and their soft symbol values to be appended to the message + * under assembly. Soft symbols support soft decision decoding. + * @param dibit to add to the message. + * @param softSymbol the dibit was decided from (radians) or NaN if unavailable. + */ + public void receive(Dibit dibit, float softSymbol) { if(mMessage.isFull()) { @@ -226,9 +240,45 @@ public class P25P1MessageAssembler implements Listener else { mMessage.add(dibit.getBit1(), dibit.getBit2()); + + if(mSoftSymbolCount >= mSoftSymbols.length) + { + mSoftSymbols = java.util.Arrays.copyOf(mSoftSymbols, mSoftSymbols.length * 2); + } + + mSoftSymbols[mSoftSymbolCount++] = softSymbol; } } + /** + * Soft symbols for the assembled message bit range. + * @param startBit of the range (must be even/dibit aligned) + * @param endBit of the range, exclusive + * @return soft symbols for the range or null if the range isn't fully assembled or any soft symbol is unavailable. + */ + public float[] getSoftSymbols(int startBit, int endBit) + { + int startDibit = startBit / 2; + int endDibit = endBit / 2; + + if(startBit % 2 != 0 || endBit % 2 != 0 || startDibit < 0 || endDibit > mSoftSymbolCount || endDibit <= startDibit) + { + return null; + } + + float[] softSymbols = java.util.Arrays.copyOfRange(mSoftSymbols, startDibit, endDibit); + + for(float softSymbol : softSymbols) + { + if(Float.isNaN(softSymbol)) + { + return null; + } + } + + return softSymbols; + } + /** * Indicates if the message is fully assembled meaning that the quantity of assembled dibits matches the data unit * ID message length. diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java index 0265c324..d101968b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java @@ -21,6 +21,7 @@ package io.github.dsheirer.module.decode.p25.phase1; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.IntField; +import io.github.dsheirer.dsp.c4fm.C4FMMessageFramer; import io.github.dsheirer.dsp.symbol.Dibit; import io.github.dsheirer.edac.bch.BCH_63_16_23_P25; import io.github.dsheirer.message.DroppedSamplesMessage; @@ -46,7 +47,7 @@ import io.github.dsheirer.sample.Listener; * uncertain message to assemble and then we'll inspect before and after data unit IDs and the quantity of captured * dibits to make a best guess on what the assembled message represents. */ -public class P25P1MessageFramer +public class P25P1MessageFramer extends C4FMMessageFramer { private static final int DIBIT_LENGTH_NID = 33; //32 dibits (64 bits) +1 status private static final float SYNC_DETECTION_THRESHOLD = 60; @@ -55,12 +56,26 @@ public class P25P1MessageFramer private static final IntField DUID_FIELD = IntField.length4(12); private final NACTracker mNACTracker = new NACTracker(); private Dibit[] mNIDBuffer = new Dibit[DIBIT_LENGTH_NID]; + //Soft symbols (radians) for the NID dibits, or NaN when the soft symbol wasn't provided + private float[] mNIDSoftBuffer = new float[DIBIT_LENGTH_NID]; + private float mCurrentSoftSymbol = Float.NaN; + //NID dibit index that carries the embedded status symbol + private static final int NID_STATUS_DIBIT_INDEX = 11; + //Soft decision NID decoding: maximum hard bit disagreements (of 63) allowed for the best candidate codeword. + //With 7 candidate codewords for the tracked NAC, a random word falls within 15 bits of one with probability + //~1.3e-4, lower than the NAC-assisted BCH decode (~4.6e-4). + private static final int SOFT_NID_MAX_BIT_ERRORS = 15; + //Valid data unit ID values used as soft decision NID candidates. + private static final int[] SOFT_NID_CANDIDATE_DUIDS = {0, 3, 5, 7, 10, 12, 15}; + private final long[] mSoftNIDCandidateCodewords = new long[SOFT_NID_CANDIDATE_DUIDS.length]; + private int mSoftNIDCandidateNAC = -1; private int mNIDPointer = 0; private final P25P1SoftSyncDetector mSoftSyncDetector = P25P1SoftSyncDetectorFactory.getDetector(); private final P25P1HardSyncDetector mHardSyncDetector = new P25P1HardSyncDetector(); private boolean mSyncDetected = false; + //Indicates the NID under capture follows a sync detected while a message was still assembling + private boolean mPreemptiveNIDCheck = false; - private static final double MILLISECONDS_PER_SYMBOL = 1.0 / 4800.0 / 1000.0; private Listener mMessageListener; private boolean mMessageAssemblyRequired = false; private boolean mRunning = false; @@ -92,7 +107,7 @@ public class P25P1MessageFramer */ public boolean processWithSoftSyncDetect(float softSymbol, Dibit symbol) { - boolean validNIDDetected = process(symbol); + boolean validNIDDetected = process(symbol, softSymbol); if(mSoftSyncDetector.process(softSymbol) > SYNC_DETECTION_THRESHOLD) { @@ -123,12 +138,28 @@ public class P25P1MessageFramer */ public void syncDetected() { - //Only allow sync detection processing if we're not currently assembling a message - if(mMessageAssembler == null) - { - mSyncDetected = true; - mNIDPointer = 0; - } + //When we're currently assembling a message, still capture the NID but only allow it to preempt the assembling + //message when it passes strict validation (see checkNID()) to avoid false sync detections in voice frames + //truncating the message under assembly (ie audio squeaks - #2305). This allows a valid sync/NID to recover + //framing when a PLACEHOLDER (failed NID) assembly, or a message with incorrect length expectation, would + //otherwise cause the framer to ignore subsequent sync detections. + mPreemptiveNIDCheck = mMessageAssembler != null; + mSyncDetected = true; + mNIDPointer = 0; + } + + /** + * Process symbol decision and the soft symbol it was decided from, without sync detection. The soft symbol + * enables soft decision NID decoding. + * @param symbol that was demodulated. + * @param softSymbol equalized soft symbol in radians. + */ + @Override + public boolean process(Dibit symbol, float softSymbol) + { + mCurrentSoftSymbol = softSymbol; + boolean validNIDDetected = process(symbol); + return validNIDDetected; } /** @@ -138,6 +169,8 @@ public class P25P1MessageFramer public boolean process(Dibit symbol) { boolean validNIDDetected = false; + float softSymbol = mCurrentSoftSymbol; + mCurrentSoftSymbol = Float.NaN; mDebugSymbolCount++; mDibitSinceTimestampCounter++; @@ -148,6 +181,7 @@ public class P25P1MessageFramer if(mSyncDetected) { + mNIDSoftBuffer[mNIDPointer] = softSymbol; mNIDBuffer[mNIDPointer++] = symbol; if(mNIDPointer >= DIBIT_LENGTH_NID) @@ -181,12 +215,12 @@ public class P25P1MessageFramer //If we still have an assembler, feed it the current dibit (e.g. TSBK and PDU continuation block assembly) if(mMessageAssembler != null) { - mMessageAssembler.receive(symbol); + mMessageAssembler.receive(symbol, softSymbol); } } else { - mMessageAssembler.receive(symbol); + mMessageAssembler.receive(symbol, softSymbol); } } //Start a message assembler after ignoring 24x Sync, 32x NID, and 1x status dibits. Trigger assembler @@ -372,7 +406,7 @@ public class P25P1MessageFramer { case TRUNKING_SIGNALING_BLOCK_1: CorrectedBinaryMessage message1 = mMessageAssembler.getMessage().getSubMessage(0, 196); - TSBKMessage tsbk1 = TSBKMessageFactory.create(mChannelStatusProcessor.getDirection(), mMessageAssembler.getDataUnitID(), message1, mMessageAssembler.getNAC(), getTimestamp()); + TSBKMessage tsbk1 = TSBKMessageFactory.create(mChannelStatusProcessor.getDirection(), mMessageAssembler.getDataUnitID(), message1, mMessageAssembler.getSoftSymbols(0, 196), mMessageAssembler.getNAC(), getTimestamp()); if(tsbk1 != null) { @@ -403,7 +437,7 @@ public class P25P1MessageFramer break; case TRUNKING_SIGNALING_BLOCK_2: CorrectedBinaryMessage message2 = mMessageAssembler.getMessage().getSubMessage(196, 392); - TSBKMessage tsbk2 = TSBKMessageFactory.create(mChannelStatusProcessor.getDirection(), mMessageAssembler.getDataUnitID(), message2, mMessageAssembler.getNAC(), getTimestamp()); + TSBKMessage tsbk2 = TSBKMessageFactory.create(mChannelStatusProcessor.getDirection(), mMessageAssembler.getDataUnitID(), message2, mMessageAssembler.getSoftSymbols(196, 392), mMessageAssembler.getNAC(), getTimestamp()); if(tsbk2 != null) { @@ -432,7 +466,7 @@ public class P25P1MessageFramer break; case TRUNKING_SIGNALING_BLOCK_3: CorrectedBinaryMessage message3 = mMessageAssembler.getMessage().getSubMessage(392, 588); - TSBKMessage tsbk3 = TSBKMessageFactory.create(mChannelStatusProcessor.getDirection(), mMessageAssembler.getDataUnitID(), message3, mMessageAssembler.getNAC(), getTimestamp()); + TSBKMessage tsbk3 = TSBKMessageFactory.create(mChannelStatusProcessor.getDirection(), mMessageAssembler.getDataUnitID(), message3, mMessageAssembler.getSoftSymbols(392, 588), mMessageAssembler.getNAC(), getTimestamp()); broadcast(tsbk3); adjustDibitCounterFromMessageAssembler(); @@ -477,7 +511,7 @@ public class P25P1MessageFramer { case PACKET_DATA_UNIT: CorrectedBinaryMessage message = mMessageAssembler.getMessage().getSubMessage(0, 196); - PDUHeader header = PDUMessageFactory.createHeader(message); + PDUHeader header = PDUMessageFactory.createHeader(message, mMessageAssembler.getSoftSymbols(0, 196)); if(header != null) { @@ -512,7 +546,7 @@ public class P25P1MessageFramer } else { - mPDUSequence.addDataBlock(PDUMessageFactory.createUnconfirmedDataBlock(messageB1)); + mPDUSequence.addDataBlock(PDUMessageFactory.createUnconfirmedDataBlock(messageB1, mMessageAssembler.getSoftSymbols(196, 392))); } if(mPDUSequence.isComplete()) @@ -543,7 +577,7 @@ public class P25P1MessageFramer } else { - mPDUSequence.addDataBlock(PDUMessageFactory.createUnconfirmedDataBlock(messageB2)); + mPDUSequence.addDataBlock(PDUMessageFactory.createUnconfirmedDataBlock(messageB2, mMessageAssembler.getSoftSymbols(392, 588))); } if(mPDUSequence.isComplete()) @@ -574,7 +608,7 @@ public class P25P1MessageFramer } else { - mPDUSequence.addDataBlock(PDUMessageFactory.createUnconfirmedDataBlock(messageB3)); + mPDUSequence.addDataBlock(PDUMessageFactory.createUnconfirmedDataBlock(messageB3, mMessageAssembler.getSoftSymbols(588, 784))); } if(mPDUSequence.isComplete()) @@ -605,7 +639,7 @@ public class P25P1MessageFramer } else { - mPDUSequence.addDataBlock(PDUMessageFactory.createUnconfirmedDataBlock(messageB4)); + mPDUSequence.addDataBlock(PDUMessageFactory.createUnconfirmedDataBlock(messageB4, mMessageAssembler.getSoftSymbols(784, 980))); } if(mPDUSequence.isComplete()) @@ -636,7 +670,7 @@ public class P25P1MessageFramer } else { - mPDUSequence.addDataBlock(PDUMessageFactory.createUnconfirmedDataBlock(messageB5)); + mPDUSequence.addDataBlock(PDUMessageFactory.createUnconfirmedDataBlock(messageB5, mMessageAssembler.getSoftSymbols(980, 1176))); } adjustDibitCounterFromMessageAssembler(); @@ -829,13 +863,37 @@ public class P25P1MessageFramer int trackedNAC = mNACTracker.getTrackedNAC(); - mBCHDecoder.decode(nid, trackedNAC); + boolean preemptive = mPreemptiveNIDCheck; + mPreemptiveNIDCheck = false; + + if(preemptive) + { + //Strict validation for a NID that would preempt a message under assembly. Hard BCH(63,16,23) decoding + //accepts ~0.5% of random words and the NAC-assisted second attempt ~0.05%, so require an unassisted decode + //that also matches the tracked NAC (~1.3e-6 false acceptance) and a known data unit ID. + if(trackedNAC <= 0) + { + return false; + } + + mBCHDecoder.decode(nid); + } + else + { + mBCHDecoder.decode(nid, trackedNAC); + } int nac = nid.getInt(NAC_FIELD); P25P1DataUnitID duid = P25P1DataUnitID.fromValue(nid.getInt(DUID_FIELD)); - //If error correction fails, return the original correction candidate + //If error correction fails, attempt soft decision decoding against the tracked NAC. Not used for a NID that + //would preempt a message under assembly, which requires the strict validation above. if(nid.getCorrectedBitCount() < 0) + { + return !preemptive && softDecodeNID(trackedNAC); + } + + if(preemptive && (nac != trackedNAC || duid == P25P1DataUnitID.UNKNOWN)) { return false; } @@ -848,4 +906,128 @@ public class P25P1MessageFramer nidDetected(nac, duid, nid.getCorrectedBitCount()); return true; } + + /** + * Soft decision (maximum likelihood) NID decoding for a NID that failed hard decision BCH decoding. With a known + * (tracked) NAC there are only a handful of valid NID codewords, one per data unit ID. Selects the candidate + * codeword with the minimum squared Euclidean distance to the received soft symbols, and accepts it when it is the + * unique best candidate and it's within SOFT_NID_MAX_BIT_ERRORS of the hard symbol decisions. + * + * Note: the decoded NAC is not fed to the NAC tracker since the tracked NAC is an input to this decoder. + * + * @param trackedNAC currently tracked NAC value, or zero when no NAC has been tracked yet. + * @return true if a NID was decoded and nidDetected() was invoked. + */ + private boolean softDecodeNID(int trackedNAC) + { + if(trackedNAC <= 0) + { + return false; + } + + for(float softSymbol : mNIDSoftBuffer) + { + if(Float.isNaN(softSymbol)) + { + return false; + } + } + + if(mSoftNIDCandidateNAC != trackedNAC) + { + for(int x = 0; x < SOFT_NID_CANDIDATE_DUIDS.length; x++) + { + mSoftNIDCandidateCodewords[x] = BCH_63_16_23_P25.encode(trackedNAC, SOFT_NID_CANDIDATE_DUIDS[x]); + } + + mSoftNIDCandidateNAC = trackedNAC; + } + + int bestIndex = -1; + double bestDistance = Double.MAX_VALUE; + double secondBestDistance = Double.MAX_VALUE; + int bestBitErrors = 0; + + for(int candidate = 0; candidate < mSoftNIDCandidateCodewords.length; candidate++) + { + long codeword = mSoftNIDCandidateCodewords[candidate]; + double distance = 0.0; + int bitErrors = 0; + int bitIndex = 0; //Codeword bit index, where bit 0 is the first transmitted bit (codeword bit position 62) + + for(int dibitIndex = 0; dibitIndex < DIBIT_LENGTH_NID; dibitIndex++) + { + if(dibitIndex == NID_STATUS_DIBIT_INDEX) + { + continue; + } + + boolean bit1 = ((codeword >> (62 - bitIndex)) & 1) == 1; + Dibit received = mNIDBuffer[dibitIndex]; + float softSymbol = mNIDSoftBuffer[dibitIndex]; + + if(bit1 != received.getBit1()) + { + bitErrors++; + } + + if(bitIndex + 1 <= 62) + { + boolean bit2 = ((codeword >> (62 - bitIndex - 1)) & 1) == 1; + double error = softSymbol - getIdealPhase(bit1, bit2); + distance += error * error; + + if(bit2 != received.getBit2()) + { + bitErrors++; + } + } + else + { + //The 64th NID bit is not part of the BCH codeword, so use the nearest symbol for the first bit + double errorA = softSymbol - getIdealPhase(bit1, false); + double errorB = softSymbol - getIdealPhase(bit1, true); + distance += Math.min(errorA * errorA, errorB * errorB); + } + + bitIndex += 2; + } + + if(distance < bestDistance) + { + secondBestDistance = bestDistance; + bestDistance = distance; + bestIndex = candidate; + bestBitErrors = bitErrors; + } + else if(distance < secondBestDistance) + { + secondBestDistance = distance; + } + } + + if(bestIndex < 0 || bestDistance >= secondBestDistance || bestBitErrors > SOFT_NID_MAX_BIT_ERRORS) + { + return false; + } + + nidDetected(trackedNAC, P25P1DataUnitID.fromValue(SOFT_NID_CANDIDATE_DUIDS[bestIndex]), bestBitErrors); + return true; + } + + /** + * Ideal soft symbol phase for the dibit bits. + * @param bit1 first (sign) bit + * @param bit2 second (magnitude) bit + * @return ideal phase in radians. + */ + private static float getIdealPhase(boolean bit1, boolean bit2) + { + if(bit1) + { + return bit2 ? Dibit.D11_MINUS_3.getIdealPhase() : Dibit.D10_MINUS_1.getIdealPhase(); + } + + return bit2 ? Dibit.D01_PLUS_3.getIdealPhase() : Dibit.D00_PLUS_1.getIdealPhase(); + } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1SymbolProcessorC4FM.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1SymbolProcessorC4FM.java new file mode 100644 index 00000000..88793125 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1SymbolProcessorC4FM.java @@ -0,0 +1,328 @@ +package io.github.dsheirer.module.decode.p25.phase1; + +import io.github.dsheirer.dsp.c4fm.C4FMMessageFramer; +import io.github.dsheirer.dsp.c4fm.C4FMSymbolProcessor; +import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator; +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.module.decode.FeedbackDecoder; +import io.github.dsheirer.module.decode.p25.phase1.sync.P25P1SoftSyncDetectorFactory; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; + +/** + * P25 Phase 1 C4FM implementation of the symbol processor. + */ +public class P25P1SymbolProcessorC4FM extends C4FMSymbolProcessor +{ + private static final int MAX_MESSAGE_DIBIT_LENGTH = P25P1DataUnitID.LOGICAL_LINK_DATA_UNIT_1.getElapsedDibitLength(); + + //TODO: adjust each of these values to the P25 waveform + private static final int EQUALIZER_TRAINING_PERIOD_SYMBOL_COUNT = 600; + private static final int EQUALIZER_SKIP_TRAINING_FRAME_COUNT = 5; + //Equalizer span in symbols (14x T/2 taps). The default (sync length = 23 symbols / 46 taps) has more taps than the + //24 training symbols. Testing against C4FM recordings: 3-13 symbols all outperform 23, with 7 offering the best + //combination of valid message count and zero CRC failures. + private static final int EQUALIZER_SYMBOL_LENGTH = 7; + //Continuous Gardner symbol timing tracking gain between sync detections. LDU voice frames have 864 symbols between + //sync patterns. Testing: 0.002 reduced bit errors 4-7% on a conventional voice recording (clean and with injected + //noise) with ~1% fewer valid messages on a control channel at high noise; higher gains were less consistent. + private static final double TIMING_TRACKING_GAIN = 0.002; + private static final int SYMBOL_RATE = 4800; + private static final double OUTER_SYMBOL_DEVIATION = 1800.0; //Hertz + private static final float SYNC_THRESHOLD_DETECTED = 80; + private static final float SYNC_THRESHOLD_OPTIMIZED = 100; + private static final float SYNC_THRESHOLD_EQUALIZED = 100; + private static final float SOFT_SYMBOL_QUADRANT_BOUNDARY = (float)(Math.PI / 2.0); + private final int mPlusSymbolCount; + private final int mMinusSymbolCount; + + /** + * Constructs an instance + * + * @param feedbackDecoder parent decoder + * @param messageFramer to receive symbol decisions (dibits) and sync notifications. + */ + public P25P1SymbolProcessorC4FM(FeedbackDecoder feedbackDecoder, C4FMMessageFramer messageFramer) + { + super(feedbackDecoder, messageFramer, P25P1SoftSyncDetectorFactory.getDetector(), + P25P1SoftSyncDetectorFactory.getDetector()); + + //Count the +3 and -3 symbols in the sync pattern for balance averaging (11x Plus 3 and 13x Minus 3) + int plusCount = 0; + for(float patternSymbol : mSyncDetector.getPatternSymbols()) + { + if(patternSymbol > 0) + { + plusCount++; + } + } + mPlusSymbolCount = plusCount; + mMinusSymbolCount = mSyncDetector.getDibitLength() - plusCount; + } + + /** + * Dynamic balance tracking (ie decision directed PLL adjustments) is enabled for this decoder. + */ + @Override + public boolean isDynamicBalanceTrackingRequired() + { + return true; + } + + @Override + public int getMaxMessageDibitLength() + { + return MAX_MESSAGE_DIBIT_LENGTH; + } + + @Override + public int getEqualizerTrainingPeriodDurationSymbolCount() + { + return EQUALIZER_TRAINING_PERIOD_SYMBOL_COUNT; + } + + @Override + public double getTimingTrackingGain() + { + return TIMING_TRACKING_GAIN; + } + + @Override + public int getEqualizerSymbolLength() + { + return EQUALIZER_SYMBOL_LENGTH; + } + + @Override + public int getEqualizerSkipTrainingFrameCount() + { + return EQUALIZER_SKIP_TRAINING_FRAME_COUNT; + } + + @Override + public int getSymbolRate() + { + return SYMBOL_RATE; + } + + @Override + public double getOuterSymbolDeviation() + { + return OUTER_SYMBOL_DEVIATION; + } + + @Override + public float getSyncThresholdDetected() + { + return SYNC_THRESHOLD_DETECTED; + } + + @Override + public float getSyncThresholdOptimized() + { + return SYNC_THRESHOLD_OPTIMIZED; + } + + @Override + public float getSyncThresholdEqualized() + { + return SYNC_THRESHOLD_EQUALIZED; + } + + /** + * On sync detection, calculates the optimal timing adjustment that achieves the highest sync correlation score + * or returns an INVALID_SYNC_DETECTION sentinel value if the correlation score doesn't exceed the threshold. + * + * @param additionalOffset from current bufferPointer and samplePoint to adjust for which sync detector + * triggered the event. This can be zero offset for the primary sync detector or a lagging (ie half symbol) + * offset for the lagging sync detector. + * @return optimized timing adjustment or NO_OPTIMIZATION sentinel value. + */ + @Override + 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; + + //Reject any sync detections where the sample:sample standard deviation exceeds the noise threshold. + if(isNoisy(offset)) + { + return INVALID_SYNC_DETECTION; + } + + //Find the optimal symbol timing + double samplesPerSymbol = mSamplesPerSymbol; + double stepSize = samplesPerSymbol / (mSynchronized ? 16.0 : 8.0); //Start at 1/8th for coarse & 1/16th for fine + double stepSizeMin = mOptimizeFineIncrement; + double adjustment = 0.0; + + //In coarse sync mode, constrain max adjustment to half a symbol period so that a lagging sync detect doesn't + //preempt a primary sync detect prematurely and cause a significant timing correction. So, we constrain the + //max correction to a half symbol period for coarse sync mode. + double adjustmentMax = samplesPerSymbol / 2.0; + double candidate = offset; + + float scoreCenter = score(candidate, samplesPerSymbol); + + candidate = offset - stepSize; + float scoreLeft = score(candidate, samplesPerSymbol); + + candidate = offset + stepSize; + float scoreRight = score(candidate, samplesPerSymbol); + + while(stepSize > stepSizeMin && Math.abs(adjustment) <= adjustmentMax) + { + if(scoreLeft > scoreRight && scoreLeft > scoreCenter) + { + adjustment -= stepSize; + scoreRight = scoreCenter; + scoreCenter = scoreLeft; + + candidate = offset + adjustment - stepSize; + scoreLeft = score(candidate, samplesPerSymbol); + } + else if(scoreRight > scoreLeft && scoreRight > scoreCenter) + { + adjustment += stepSize; + scoreLeft = scoreCenter; + scoreCenter = scoreRight; + + candidate = offset + adjustment + stepSize; + scoreRight = score(candidate, samplesPerSymbol); + } + else + { + stepSize *= 0.5f; + + if(stepSize > stepSizeMin) + { + candidate = offset + adjustment - stepSize; + scoreLeft = score(candidate, samplesPerSymbol); + + candidate = offset + adjustment + stepSize; + scoreRight = score(candidate, samplesPerSymbol); + } + } + } + + //If we didn't find an optimal correlation score above the threshold, return a false sync. + if(scoreCenter < SYNC_THRESHOLD_OPTIMIZED) + { + return INVALID_SYNC_DETECTION; + } + + return getCorrection(additionalOffset, adjustment, detectionScore, scoreCenter, bufferOffset); + } + + /** + * Indicates if the samples in the sample buffer that contain a detected sync pattern have a standard deviation + * that is lower than the expected sample to sample deviation for a modulated signal. False detects against + * noise tend to have a standard deviation that is 2x the expected value. + * @param offset to the sample representing the final symbol in the detected sync pattern. + * @return true if the standard deviation is more than expected. + */ + public boolean isNoisy(double offset) + { + StandardDeviation standardDeviation = new StandardDeviation(); + int start = (int)Math.floor(offset - ((mSyncDetector.getDibitLength() - 1) * mSamplesPerSymbol)); + int end = (int)Math.ceil(offset); + end = Math.min(end, mBuffer.length - 1); + + for(int i = start; i < end; i++) + { + standardDeviation.increment(mBuffer[i] - mBuffer[i + 1]); + } + + double threshold = Dibit.D01_PLUS_3.getIdealPhase() * 2 / mSamplesPerSymbol * 1.2; //120% of optimal + return standardDeviation.getResult() > threshold; + } + + /** + * Calculates a correction update for the equalizer balance and gain when the sync pattern is detected in the + * sample buffer, using the supplied timing correction argument. Resamples the sync symbols and compares each + * soft symbol to the ideal symbol phase to develop average error measurements for balance and gain. On initial + * 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(double additionalOffset, double timingCorrection, float detectionScore, + float optimizationScore, double offset) + { + int syncLength = mSyncDetector.getDibitLength(); + float[] patternSymbols = mSyncDetector.getPatternSymbols(); + Dibit[] patternDibits = mSyncDetector.getPatternDibits(); + + //Resample each sync symbol, starting with the first symbol and ending with the final (detected) symbol. + double resampleStart = offset + additionalOffset + timingCorrection - ((syncLength - 1) * mSamplesPerSymbol); + int resampleStartIntegral = (int)Math.floor(resampleStart); + float symbol, resampledSoftSymbol; + float balancePlus3Symbols = 0; + float balanceMinus3Symbols = 0; + float gainAccumulator = 0; + Dibit resampledDibit; + int bitErrorCount = 0; + + for(int x = 0; x < syncLength; x++) + { + if(resampleStartIntegral >= 0) + { + symbol = patternSymbols[x]; + resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral], + mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral); + resampledSoftSymbol = resampledSoftSymbol * mSampleEqualizer.gain() + mSampleEqualizer.balance(); + resampledDibit = Dibit.fromSample(resampledSoftSymbol); + bitErrorCount += patternDibits[x].getBitErrorFrom(resampledDibit); + + if(symbol > 0) + { + balancePlus3Symbols += (resampledSoftSymbol - symbol); + } + else + { + balanceMinus3Symbols += (resampledSoftSymbol - symbol); + } + + gainAccumulator += Math.abs(symbol) - Math.abs(resampledSoftSymbol); + } + + resampleStart += mSamplesPerSymbol; + resampleStartIntegral = (int)Math.floor(resampleStart); + } + + //Average the balance error separately for the +3 and -3 symbols so that a gain error doesn't bias the balance + balancePlus3Symbols /= -mPlusSymbolCount; + balanceMinus3Symbols /= -mMinusSymbolCount; + float balanceCorrection = (balancePlus3Symbols + balanceMinus3Symbols) / 2f; + balanceCorrection = Math.min(balanceCorrection, SOFT_SYMBOL_QUADRANT_BOUNDARY); + balanceCorrection = Math.max(balanceCorrection, -SOFT_SYMBOL_QUADRANT_BOUNDARY); + + // System.out.println("Balance [" + balanceAverage + "] Plus3 [" + balancePlus3Symbols + "] Minus3 [" + balanceMinus3Symbols + "]"); + //Convert the accumulated magnitude error into a gain adjustment. All P25 sync symbols are +/-3. + gainAccumulator = getGainAdjustment(mSyncDetector.getDibitLength() * Dibit.D01_PLUS_3.getIdealPhase(), + gainAccumulator); + + //Recalculate the optimization score using the adjusted equalizer settings + if(!mSynchronized) + { + optimizationScore = score(offset + timingCorrection, mSamplesPerSymbol, + mSampleEqualizer.balance() + balanceCorrection, mSampleEqualizer.gain() + gainAccumulator); + } + + return new Correction(additionalOffset, timingCorrection, balanceCorrection, gainAccumulator, detectionScore, + optimizationScore, bitErrorCount); + } + + + /** + * Calculates the sync correlation score at the specified offset and samples per symbol interval using the + * current equalizer balance and gain values. + * + * @param offset to the final symbol in the soft symbol buffer + * @param samplesPerSymbol spacing to test for. + * @return correlation score. + */ + public float score(double offset, double samplesPerSymbol) + { + return score(offset, samplesPerSymbol, mSampleEqualizer.balance(), mSampleEqualizer.gain()); + } + + +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/PDUMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/PDUMessageFactory.java index 09694f34..0b7b0931 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/PDUMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/PDUMessageFactory.java @@ -96,6 +96,35 @@ public class PDUMessageFactory return null; } + /** + * Creates a PDU header message + * @param message containing an interleaved, viterbi encoded message + * @return header or null. + */ + /** + * Creates a PDU header message using soft decision Viterbi decoding when soft symbols are provided. + * @param message containing an interleaved, viterbi encoded message + * @param softSymbols for the 98 interleaved dibits of the header, or null. + * @return header or null. + */ + public static PDUHeader createHeader(CorrectedBinaryMessage message, float[] softSymbols) + { + if(softSymbols == null) + { + return createHeader(message); + } + + CorrectedBinaryMessage decoded = TSBKMessageFactory.deinterleaveViterbiAndCrc(message.getSubMessage(0, 196), + softSymbols); + + if(decoded != null) + { + return PDUHeaderFactory.getPDUHeader(decoded); + } + + return null; + } + /** * Creates a PDU header message * @param message containing an interleaved, viterbi encoded message @@ -103,7 +132,7 @@ public class PDUMessageFactory */ public static PDUHeader createHeader(CorrectedBinaryMessage message) { - CorrectedBinaryMessage decoded = TSBKMessageFactory.deinterleaveViterbiAndCrc(message.getSubMessage(0, 195)); + CorrectedBinaryMessage decoded = TSBKMessageFactory.deinterleaveViterbiAndCrc(message.getSubMessage(0, 196)); if(decoded != null) { @@ -148,6 +177,21 @@ public class PDUMessageFactory /** * Creates an unconfirmed data block for a packet sequence */ + /** + * Creates an unconfirmed data block using soft decision Viterbi decoding when soft symbols are provided. + * @param interleaved data block + * @param softSymbols for the 98 interleaved dibits of the block, or null. + */ + public static DataBlock createUnconfirmedDataBlock(CorrectedBinaryMessage interleaved, float[] softSymbols) + { + if(softSymbols == null || softSymbols.length != P25P1Interleave.DATA_DEINTERLEAVE.length / 2) + { + return createUnconfirmedDataBlock(interleaved); + } + + return new UnconfirmedDataBlock(P25P1Interleave.deinterleaveDataSoftSymbols(softSymbols)); + } + public static DataBlock createUnconfirmedDataBlock(CorrectedBinaryMessage interleaved) { CorrectedBinaryMessage deinterleaved = P25P1Interleave.deinterleaveChunk(P25P1Interleave.DATA_DEINTERLEAVE, interleaved); diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/block/UnconfirmedDataBlock.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/block/UnconfirmedDataBlock.java index c98da4b9..25b45dfc 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/block/UnconfirmedDataBlock.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/pdu/block/UnconfirmedDataBlock.java @@ -41,6 +41,15 @@ public class UnconfirmedDataBlock extends DataBlock mDecodedMessage.incrementCorrectedBitCount(correctedBinaryMessage.getCorrectedBitCount()); } + /** + * Constructs an unconfirmed data block using soft decision Viterbi decoding. + * @param deinterleavedSoftSymbols 98 deinterleaved soft symbols for the 196-bit data block. + */ + public UnconfirmedDataBlock(float[] deinterleavedSoftSymbols) + { + mDecodedMessage = VITERBI_HALF_RATE_DECODER.decodeSoft(deinterleavedSoftSymbols); + } + /** * Message payload */ diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/TSBKMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/TSBKMessageFactory.java index 4f022380..26941f72 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/TSBKMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/tsbk/TSBKMessageFactory.java @@ -145,11 +145,34 @@ public class TSBKMessageFactory */ public static CorrectedBinaryMessage deinterleaveViterbiAndCrc(CorrectedBinaryMessage raw) { - //Get deinterleaved header chunk - CorrectedBinaryMessage deinterleaved = P25P1Interleave.deinterleaveChunk(P25P1Interleave.DATA_DEINTERLEAVE, raw); + return deinterleaveViterbiAndCrc(raw, null); + } - //Decode 1/2 rate trellis encoded PDU header - CorrectedBinaryMessage message = VITERBI_HALF_RATE_DECODER.decode(deinterleaved); + /** + * Performs deinterleave, Viterbi decoding, and applies the CRC-CCITT error detection/correction. Uses soft + * decision Viterbi decoding when the soft symbols are provided, otherwise hard decision decoding. + * @param raw interleaved message + * @param softSymbols for the 98 interleaved dibits of the raw message, or null. + * @return null or the decoded message with CRC results set in the message where 0 or 1 bit errors is valid and + * 2 bit errors is invalid. + */ + public static CorrectedBinaryMessage deinterleaveViterbiAndCrc(CorrectedBinaryMessage raw, float[] softSymbols) + { + CorrectedBinaryMessage message; + + if(softSymbols != null && softSymbols.length == P25P1Interleave.DATA_DEINTERLEAVE.length / 2) + { + //Soft decision decode 1/2 rate trellis encoded block from the deinterleaved soft symbols + message = VITERBI_HALF_RATE_DECODER.decodeSoft(P25P1Interleave.deinterleaveDataSoftSymbols(softSymbols)); + } + else + { + //Get deinterleaved header chunk + CorrectedBinaryMessage deinterleaved = P25P1Interleave.deinterleaveChunk(P25P1Interleave.DATA_DEINTERLEAVE, raw); + + //Decode 1/2 rate trellis encoded PDU header + message = VITERBI_HALF_RATE_DECODER.decode(deinterleaved); + } if(message == null) { @@ -171,7 +194,18 @@ public class TSBKMessageFactory public static TSBKMessage create(Direction direction, P25P1DataUnitID dataUnitID, CorrectedBinaryMessage correctedBinaryMessage, int nac, long timestamp) { - CorrectedBinaryMessage message = deinterleaveViterbiAndCrc(correctedBinaryMessage); + return create(direction, dataUnitID, correctedBinaryMessage, null, nac, timestamp); + } + + /** + * Creates a TSBK message, using soft decision Viterbi decoding when soft symbols are provided. + * @param softSymbols for the 98 interleaved dibits of the message, or null for hard decision decoding. + */ + public static TSBKMessage create(Direction direction, P25P1DataUnitID dataUnitID, + CorrectedBinaryMessage correctedBinaryMessage, float[] softSymbols, int nac, + long timestamp) + { + CorrectedBinaryMessage message = deinterleaveViterbiAndCrc(correctedBinaryMessage, softSymbols); if(message == null) { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1HardSyncDetector.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1HardSyncDetector.java index b612754a..9dcf93d4 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1HardSyncDetector.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1HardSyncDetector.java @@ -44,7 +44,7 @@ public class P25P1HardSyncDetector extends P25P1SyncDetector */ public boolean process(Dibit dibit) { - mValue = (Long.rotateLeft(mValue, 2) & SYNC_MASK) + dibit.getValue(); - return Long.bitCount(mValue ^ SYNC_PATTERN) <= MAXIMUM_BIT_ERROR; + mValue = (Long.rotateLeft(mValue, 2) & getMask()) + dibit.getValue(); + return Long.bitCount(mValue ^ getPattern()) <= MAXIMUM_BIT_ERROR; } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetector.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetector.java index 85282347..e764b8e5 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetector.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetector.java @@ -19,18 +19,15 @@ package io.github.dsheirer.module.decode.p25.phase1.sync; -import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.dsp.c4fm.ISoftC4FMSyncDetector; import java.util.Arrays; /** * P25P1 Sync Detector. Uses soft demodulated symbols to detect sync patterns in the demodulated symbol stream. */ -public abstract class P25P1SoftSyncDetector extends P25P1SyncDetector +public abstract class P25P1SoftSyncDetector extends P25P1SyncDetector implements ISoftC4FMSyncDetector { - protected static final float MAX_POSITIVE = Dibit.D01_PLUS_3.getIdealPhase(); - protected static final float MAX_NEGATIVE = Dibit.D11_MINUS_3.getIdealPhase(); - protected static final float[] SYNC_PATTERN_SYMBOLS = syncPatternToSymbols(); - protected float[] mSymbols = new float[48]; //2x longer than 48-bit/24-dibit DMR sync patterns. + protected float[] mSymbols = new float[48]; protected int mSymbolPointer = 0; public P25P1SoftSyncDetector() diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorScalar.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorScalar.java index 3a379e3f..1160ea19 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorScalar.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorScalar.java @@ -38,7 +38,7 @@ public class P25P1SoftSyncDetectorScalar extends P25P1SoftSyncDetector for(int x = 0; x < 24; x++) { symbol = mSymbols[mSymbolPointer + x]; - score += SYNC_PATTERN_SYMBOLS[x] * symbol; + score += getPatternSymbols()[x] * symbol; } return score; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector128.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector128.java index 71624d08..c54d71b1 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector128.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector128.java @@ -42,7 +42,7 @@ public class P25P1SoftSyncDetectorVector128 extends P25P1SoftSyncDetector for(int x = 0; x < 24; x += VECTOR_SPECIES.length()) { - accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYNC_PATTERN_SYMBOLS, x) + accumulator = FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), x) .fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + x), accumulator); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector256.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector256.java index df683f13..50a7879f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector256.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector256.java @@ -42,7 +42,7 @@ public class P25P1SoftSyncDetectorVector256 extends P25P1SoftSyncDetector for(int x = 0; x < 24; x += VECTOR_SPECIES.length()) { - accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYNC_PATTERN_SYMBOLS, x) + accumulator = FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), x) .fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + x), accumulator); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector512.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector512.java index e469f67b..93aee1a8 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector512.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector512.java @@ -47,9 +47,9 @@ public class P25P1SoftSyncDetectorVector512 extends P25P1SoftSyncDetector VectorMask mask = VectorMask.fromArray(VECTOR_SPECIES, VECTOR_MASK, 0); FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES); accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer) - .fma(FloatVector.fromArray(VECTOR_SPECIES, SYNC_PATTERN_SYMBOLS, 0), accumulator); + .fma(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 0), accumulator); accumulator = FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 8, mask) - .fma(FloatVector.fromArray(VECTOR_SPECIES, SYNC_PATTERN_SYMBOLS, 8), accumulator); + .fma(FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), 8), accumulator); return accumulator.reduceLanes(VectorOperators.ADD); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector64.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector64.java index b0f4edf7..7e5ef78f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector64.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SoftSyncDetectorVector64.java @@ -42,7 +42,7 @@ public class P25P1SoftSyncDetectorVector64 extends P25P1SoftSyncDetector for(int x = 0; x < 24; x += VECTOR_SPECIES.length()) { - accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYNC_PATTERN_SYMBOLS, x) + accumulator = FloatVector.fromArray(VECTOR_SPECIES, getPatternSymbols(), x) .fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + x), accumulator); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SyncDetector.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SyncDetector.java index dec02354..9ec7767a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SyncDetector.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/sync/P25P1SyncDetector.java @@ -19,61 +19,18 @@ package io.github.dsheirer.module.decode.p25.phase1.sync; -import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.dsp.c4fm.DibitSyncDetector; /** * Base implementation of P25 Phase 1 sync detector. */ -public abstract class P25P1SyncDetector +public abstract class P25P1SyncDetector extends DibitSyncDetector { - public static final long SYNC_MASK = 0xFFFFFFFFFFFFL; - public static final long SYNC_PATTERN = 0x5575F5FF77FFL; - /** - * Converts the P25P1 sync pattern to a float array of ideal phase values for each Dibit to use for correlation - * against a stream of transmitted symbol phases. - * @return symbols array representing the sync pattern. + * Constructs an instance */ - public static float[] syncPatternToSymbols() + public P25P1SyncDetector() { - float[] symbols = new float[24]; - Dibit[] dibits = syncPatternToDibits(); - - for(int x = 0; x < 24; x++) - { - symbols[x] = dibits[x].getIdealPhase(); - } - - return symbols; - } - - /** - * Converts the P25P1 sync pattern to an array of dibit symbols. Note: only the +3 and -3 dibit symbols are used in - * sync patterns. - * @return dibit symbols array representing the sync pattern. - */ - public static Dibit[] syncPatternToDibits() - { - Dibit[] dibits = new Dibit[24]; - long mask = 3; - long dibitValue; - - for(int x = 0; x < 24; x++) - { - dibitValue = (SYNC_PATTERN & mask) >> (2 * x); - - if(dibitValue == 1) - { - dibits[23 - x] = Dibit.D01_PLUS_3; - } - else - { - dibits[23 - x] = Dibit.D11_MINUS_3; - } - - mask = mask << 2; - } - - return dibits; + super(0x5575F5FF77FFL, 24); } } diff --git a/src/main/java/io/github/dsheirer/source/config/SourceConfigTunerMultipleFrequency.java b/src/main/java/io/github/dsheirer/source/config/SourceConfigTunerMultipleFrequency.java index f2cab64c..16f9ce00 100644 --- a/src/main/java/io/github/dsheirer/source/config/SourceConfigTunerMultipleFrequency.java +++ b/src/main/java/io/github/dsheirer/source/config/SourceConfigTunerMultipleFrequency.java @@ -39,7 +39,7 @@ public class SourceConfigTunerMultipleFrequency extends SourceConfiguration private List mFrequencies = new ArrayList<>(); private String mPreferredTuner; private Long mPreferredFrequency; - private int mFrequencyRotationDelay = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MINIMUM; + private int mFrequencyRotationDelay = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_DEFAULT; public SourceConfigTunerMultipleFrequency() { diff --git a/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/ChannelRotationMonitor.java b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/ChannelRotationMonitor.java index f9509a06..e4837444 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/ChannelRotationMonitor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/channel/rotation/ChannelRotationMonitor.java @@ -45,7 +45,7 @@ public class ChannelRotationMonitor extends Module implements ISourceEventProvid Listener { public static final int CHANNEL_ROTATION_DELAY_MINIMUM = 200; - public static final int CHANNEL_ROTATION_DELAY_DEFAULT = 500; + public static final int CHANNEL_ROTATION_DELAY_DEFAULT = 1000; public static final int CHANNEL_ROTATION_DELAY_MAXIMUM = 2000; private final static Logger mLog = LoggerFactory.getLogger(ChannelRotationMonitor.class); diff --git a/src/main/java/io/github/dsheirer/source/tuner/frequency/ChannelFrequencyErrorManager.java b/src/main/java/io/github/dsheirer/source/tuner/frequency/ChannelFrequencyErrorManager.java index 7517b447..aa780651 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/frequency/ChannelFrequencyErrorManager.java +++ b/src/main/java/io/github/dsheirer/source/tuner/frequency/ChannelFrequencyErrorManager.java @@ -36,8 +36,9 @@ import java.util.concurrent.TimeUnit; public class ChannelFrequencyErrorManager implements ISourceEventListener, Listener { private static final long MAXIMUM_CHANNEL_ERROR_CORRECTION_PER_INTERVAL = 10; //Hertz - private static final long TIMER_INTERVAL_MILLISECONDS = 500; - private final FloatAveragingBuffer mRequestedCorrectAveragingBuffer = new FloatAveragingBuffer(6, 2); + private static final long TIMER_INTERVAL_MILLISECONDS = 1000; + private static final float ERROR_PROCESSING_GAIN = 0.3f; + private final FloatAveragingBuffer mRequestedCorrectAveragingBuffer = new FloatAveragingBuffer(12, 2); private final TunerChannelSource mTunerChannelSource; private final TunerFrequencyErrorManager mParent; private ScheduledFuture mScheduledFuture; @@ -135,7 +136,7 @@ public class ChannelFrequencyErrorManager implements ISourceEventListener, Liste if(partialCorrection != 0) { - mAppliedFrequencyCorrection += partialCorrection; + mAppliedFrequencyCorrection += (long)(partialCorrection * ERROR_PROCESSING_GAIN); mTunerChannelSource.setFrequencyCorrection(mAppliedFrequencyCorrection); mRequestedCorrectAveragingBuffer.reset(); } diff --git a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java index 990f37e9..620a2c4e 100644 --- a/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java +++ b/src/main/java/io/github/dsheirer/source/wave/ComplexWaveSource.java @@ -66,7 +66,7 @@ public class ComplexWaveSource extends Source implements IControllableFileSource { if(file == null || !file.exists() || !supports(file)) { - throw new IOException("Empty or null file"); + throw new IOException("Empty or null file: " + file); } if(!supports(file))