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))