mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -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:
354
src/main/java/io/github/dsheirer/dsp/c4fm/C4FMEqualizer.java
Normal file
354
src/main/java/io/github/dsheirer/dsp/c4fm/C4FMEqualizer.java
Normal file
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.dsp.c4fm;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Arrays;
|
||||
import org.apache.commons.math3.stat.descriptive.moment.Variance;
|
||||
|
||||
/**
|
||||
* LMS adaptive equalizer for real-valued C4FM symbols. Trains taps on externally triggered sync detection combined
|
||||
* with dynamic tap updates for a fixed symbol count and then freezes the taps until the error variance between sync
|
||||
* detections exceeds a fixed threshold, whereby a tap retraining is enqueued for the next sync detection.
|
||||
*/
|
||||
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.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;
|
||||
private final Variance mVariance = new Variance();
|
||||
private final float[] mBuffer;
|
||||
private final float[] mReversedSyncSymbols;
|
||||
private final float[] mTaps;
|
||||
private final int mCenterTap;
|
||||
private final int mDelaySymbolCount;
|
||||
private final int mTrainingPeriodSymbolCount;
|
||||
private final int mTrainingSkipCountThreshold;
|
||||
private boolean mEnabled = false;
|
||||
private boolean mSyncDetected = false;
|
||||
private int mSymbolsUntilTraining = 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 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 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++)
|
||||
{
|
||||
mReversedSyncSymbols[syncSymbols.length - x - 1] = syncSymbols[x];
|
||||
}
|
||||
|
||||
//Make the symbol count odd length for equal delay on either side of the center tap
|
||||
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];
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of the current tap values
|
||||
*/
|
||||
public String getTapsDescription()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<html>EQ Taps<br>");
|
||||
for(int i = 0; i < mTaps.length; i++)
|
||||
{
|
||||
sb.append(i).append(": ").append(DF.format(mTaps[i])).append("<br>");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Symbol processing delay.
|
||||
* @return processing delay in symbols.
|
||||
*/
|
||||
public int getDelaySymbolCount()
|
||||
{
|
||||
return mDelaySymbolCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the equalizer in symbols. Note: the internal tap count is twice as long since this
|
||||
* is a fractional (x2) equalizer.
|
||||
* @return length in symbols.
|
||||
*/
|
||||
public int getLength()
|
||||
{
|
||||
return mTaps.length / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the equalizer when a valid sync pattern is detected
|
||||
*/
|
||||
public void enable()
|
||||
{
|
||||
mEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the enabled state.
|
||||
* @return enabled state
|
||||
*/
|
||||
public boolean isEnabled()
|
||||
{
|
||||
return mEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the equalizer when the signal is lost so that noise doesn't contaminate the equalizer taps.
|
||||
*/
|
||||
public void disable()
|
||||
{
|
||||
mEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the equalizer that a sync pattern is detected and queues a training sequence once a couple more
|
||||
* symbols arrive once the equalizer delay line has aligned to output the sync pattern.
|
||||
*/
|
||||
public void syncDetected()
|
||||
{
|
||||
mSyncDetected = true;
|
||||
mSymbolsUntilTraining = mDelaySymbolCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the sync patter is lost and the equalizer should reset.
|
||||
*/
|
||||
public void syncLost()
|
||||
{
|
||||
// System.out.println("C4FM Equalizer - sync lost triggered");
|
||||
reset();
|
||||
disable();
|
||||
}
|
||||
|
||||
/*
|
||||
* Resets the equalizer taps
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
Arrays.fill(mTaps, 0f);
|
||||
mTaps[mCenterTap] = 1.0f;
|
||||
mVariance.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the samples and produce a delayed, equalized soft symbol ready for symbol decision. Updates the
|
||||
* equalizer taps to correct for the error in the equalized sample relative to the hard symbol decision.
|
||||
*
|
||||
* @param fractional sample that falls halfway between symbols
|
||||
* @param symbol to equalize
|
||||
* @return equalized symbol.
|
||||
*/
|
||||
public float process(float fractional, float symbol)
|
||||
{
|
||||
//Shift buffer samples to the right two places and add new sample and symbol.
|
||||
System.arraycopy(mBuffer, 0, mBuffer, 2, mBuffer.length - 2);
|
||||
mBuffer[0] = fractional;
|
||||
mBuffer[1] = symbol;
|
||||
|
||||
//Since there's a buffer delay, count down from sync detection until the pattern is aligned with the buffer
|
||||
// contents and we can start training
|
||||
if(mSyncDetected)
|
||||
{
|
||||
if(--mSymbolsUntilTraining <= 0)
|
||||
{
|
||||
train();
|
||||
}
|
||||
}
|
||||
|
||||
float equalized = 0f;
|
||||
|
||||
if(mEnabled)
|
||||
{
|
||||
int tapIndex;
|
||||
|
||||
for(tapIndex = 0; tapIndex < mTaps.length; tapIndex++)
|
||||
{
|
||||
equalized += mTaps[tapIndex] * mBuffer[tapIndex];
|
||||
}
|
||||
|
||||
if(mTrainingSymbolsRemaining > 0)
|
||||
{
|
||||
mTrainingSymbolsRemaining--;
|
||||
float error = Dibit.getError(equalized);
|
||||
error = Math.clamp(error, -MAX_SYMBOL_ERROR, MAX_SYMBOL_ERROR);
|
||||
|
||||
//Step size (mu) in range 0.0001 to 0.01
|
||||
float stepSize = 0.01f;
|
||||
|
||||
for(tapIndex = 0; tapIndex < mTaps.length; tapIndex++)
|
||||
{
|
||||
if(tapIndex != mCenterTap)
|
||||
{
|
||||
mTaps[tapIndex] += stepSize * error * mBuffer[tapIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Track the residual error that remains after equalization
|
||||
mVariance.increment(Dibit.getError(equalized));
|
||||
}
|
||||
else
|
||||
{
|
||||
//Return the uncorrected, delayed symbol when we're disabled
|
||||
equalized = mBuffer[mCenterTap];
|
||||
}
|
||||
|
||||
return equalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trains the equalizer against the buffered detected sync pattern. Trains against the sync pattern sequence
|
||||
* multiple times while the per-iteration improvement continues to become smaller and stops training once the
|
||||
* improvement falls below a threshold goal or exceeds a maximum iterations. If the training converges to
|
||||
* below a threshold, we snapshot and use the trained taps, otherwise fallback to the previous training snapshot.
|
||||
*/
|
||||
private void train()
|
||||
{
|
||||
mSyncDetected = false;
|
||||
|
||||
//Skip retrain when we're still doing dynamic tap adjustments following the previous training session.
|
||||
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)
|
||||
{
|
||||
mSkippedTrainingCount = 0;
|
||||
//If the current variance tracked since the last training session using dynamic tap adjustments at each
|
||||
// symbol is excessive, fallback to the most recent training snapshot as a better starting point
|
||||
if(mVariance.getResult() > 0.225)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
float accumulator, error;
|
||||
double improvement, previousVariance;
|
||||
int symbolIndex, index, trainingIterationCount = 0;
|
||||
Variance variance = new Variance();
|
||||
|
||||
//Step size (mu) in range 0.0001 to 0.01
|
||||
float stepSize = 0.005f;
|
||||
|
||||
do
|
||||
{
|
||||
previousVariance = variance.getN() == 0 ? 20.0 : variance.getResult();
|
||||
variance.clear();
|
||||
|
||||
|
||||
for(symbolIndex = 0; symbolIndex < mReversedSyncSymbols.length; symbolIndex++)
|
||||
{
|
||||
accumulator = 0.0f;
|
||||
for(index = 0; index < mTaps.length; index++)
|
||||
{
|
||||
accumulator += mTaps[index] * mBuffer[index + (2 * symbolIndex)];
|
||||
}
|
||||
|
||||
error = mReversedSyncSymbols[symbolIndex] - accumulator;
|
||||
variance.increment(error);
|
||||
|
||||
for(index = 0; index < mTaps.length; index++)
|
||||
{
|
||||
if(index != mCenterTap)
|
||||
{
|
||||
mTaps[index] += error * stepSize * mBuffer[index + (2 * symbolIndex)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trainingIterationCount++;
|
||||
improvement = previousVariance - variance.getResult();
|
||||
}
|
||||
while(trainingIterationCount < TRAINING_ITERATIONS_MAX && improvement > TRAINING_IMPROVEMENT_GOAL_PER_ITERATION);
|
||||
|
||||
// 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)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.dsp.c4fm;
|
||||
|
||||
/**
|
||||
* Waveform representing the ideal sync pattern, resampled at the sample rate.
|
||||
* @param samples in samples
|
||||
* @param idealCorrelationScore as dot-product of the ideal multiplied by itself.
|
||||
* @param maxSample maximum allowable sample value
|
||||
*/
|
||||
public record ReferenceSyncWaveform(float[] samples, float idealCorrelationScore, float maxSample){};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user