mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
- 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:
@@ -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();
|
||||
@@ -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();
|
||||
}
|
||||
1291
src/main/java/io/github/dsheirer/dsp/c4fm/C4FMSymbolProcessor.java
Normal file
1291
src/main/java/io/github/dsheirer/dsp/c4fm/C4FMSymbolProcessor.java
Normal file
File diff suppressed because it is too large
Load Diff
203
src/main/java/io/github/dsheirer/dsp/c4fm/DibitSyncDetector.java
Normal file
203
src/main/java/io/github/dsheirer/dsp/c4fm/DibitSyncDetector.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user