#2494 P25P1 and NXDN Decoder Enhancements. (#2514)

- Updates P25P1 C4FM decoder to use same pattern developed for NXDN.
- Both share the C4FM symbol processor and equalizer.
- P25P1 updated with Garner style timing error detector
- NXDN filters updated to use a combined RRC and inverse sync filter all in one.
- P25P1 replaces RRC with band limiting filter
- P25P1 enhanced with dynamic PLL balance updates to combat multi-path fading, in addition to the sync detection training window.
- Decoder PPM error reports now only sent after 2x consecutive sync detections to prevent tuner PPM corrections from noisy/isolated false sync detects.
- Soft error correction added to both P25P1 and IDEN.

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-10-25 09:44:50 -04:00
committed by GitHub
parent df739f525f
commit 5635341d12
51 changed files with 4035 additions and 1504 deletions

View File

@@ -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();

View File

@@ -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();
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<Integer, ReferenceSyncWaveform> 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;
}
}

View File

@@ -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);
}

View File

@@ -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.

View File

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

View File

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

View File

@@ -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.
*

View File

@@ -50,7 +50,7 @@ public class FMSyncVisualizer extends VBox implements ISyncResultsListener
private final XYChart.Series<Number, Number> mSymbolSeriesMarker = new XYChart.Series<>();
private final ScatterChart<Number, Number> mConstellationChart = new ScatterChart<>(mConstellationI, mConstellationQ);
private final XYChart.Series<Number, Number> 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<Number, Number> mSampleChart = new LineChart<>(mSampleTiming, mSamplePhase);

View File

@@ -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;

View File

@@ -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<ComplexSamples>
{
private static final Logger LOGGER = LoggerFactory.getLogger(NXDNDecoder.class);
private static final Map<String,float[]> BASEBAND_FILTERS = new HashMap<>();
private static final Map<String,float[]> 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

View File

@@ -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;
}
}

View File

@@ -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<IMessage> 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)
{

View File

@@ -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.
*/

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

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

View File

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

View File

@@ -31,7 +31,12 @@ public class NXDNSoftSyncDetectorVector512 extends NXDNSoftSyncDetector
{
private static final VectorSpecies<Float> 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

View File

@@ -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);
}

View File

@@ -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<Integer,ReferenceSyncWaveform> 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
*/

View File

@@ -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<Double,float[]> 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<Double,float[]> 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);

View File

@@ -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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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;
}
}

View File

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

View File

@@ -46,6 +46,9 @@ public class P25P1MessageAssembler implements Listener<Dibit>
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<Dibit>
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<Dibit>
* @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<Dibit>
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.

View File

@@ -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<IMessage> 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();
}
}

View File

@@ -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());
}
}

View File

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

View File

@@ -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
*/

View File

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

View File

@@ -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;
}
}

View File

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

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -47,9 +47,9 @@ public class P25P1SoftSyncDetectorVector512 extends P25P1SoftSyncDetector
VectorMask<Float> 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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -39,7 +39,7 @@ public class SourceConfigTunerMultipleFrequency extends SourceConfiguration
private List<Long> 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()
{

View File

@@ -45,7 +45,7 @@ public class ChannelRotationMonitor extends Module implements ISourceEventProvid
Listener<DecoderStateEvent>
{
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);

View File

@@ -36,8 +36,9 @@ import java.util.concurrent.TimeUnit;
public class ChannelFrequencyErrorManager implements ISourceEventListener, Listener<SourceEvent>
{
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();
}

View File

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