#2516 DMR Decoder enhancements. Architecture now shares common C4FM framework like P25P1 and NXDN. Adds soft decoding and enhanced/corrected error detection and correction. (#2517)

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-10-26 06:34:57 -04:00
committed by GitHub
parent 5635341d12
commit f2c98cdb40
32 changed files with 4044 additions and 1282 deletions

View File

@@ -179,6 +179,27 @@ public class C4FMEqualizer
mSymbolsUntilTraining = mDelaySymbolCount;
}
/**
* Notifies the equalizer that a sync pattern is detected and updates the sync pattern used for training. This
* supports protocols (ie DMR) that employ multiple sync patterns of the same length.
* @param syncSymbols for the detected sync pattern, which must be the same length as the constructor sync pattern.
*/
public void syncDetected(float[] syncSymbols)
{
if(syncSymbols.length != mReversedSyncSymbols.length)
{
throw new IllegalArgumentException("Sync symbols length [" + syncSymbols.length +
"] must match the equalizer sync pattern length [" + mReversedSyncSymbols.length + "]");
}
for(int x = 0; x < syncSymbols.length; x++)
{
mReversedSyncSymbols[syncSymbols.length - x - 1] = syncSymbols[x];
}
syncDetected();
}
/**
* Indicates that the sync patter is lost and the equalizer should reset.
*/

View File

@@ -7,6 +7,26 @@ import io.github.dsheirer.dsp.symbol.Dibit;
*/
public abstract class C4FMMessageFramer
{
private static final float SOFT_SYMBOL_SCALE = (float)(4.0 / Math.PI); //Radians to symbol units (+/-1, +/-3)
/**
* 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
*/
protected 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);
}
/**
* Process the symbol
* @param symbol in radians

View File

@@ -22,10 +22,14 @@ package io.github.dsheirer.dsp.c4fm;
import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
import io.github.dsheirer.gui.viewer.sync.FMSyncResultsViewer;
import io.github.dsheirer.gui.viewer.sync.ISyncResultsListener;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.sample.Listener;
import java.nio.ByteBuffer;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import org.apache.commons.math3.stat.descriptive.moment.Mean;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
@@ -59,6 +63,9 @@ public abstract class C4FMSymbolProcessor
private final FeedbackDecoder mFeedbackDecoder;
private final C4FMMessageFramer mMessageFramer;
private final ISoftC4FMSyncDetector mSyncDetectorLagging;
//Detector (primary or lagging) that triggered the current sync detection. For multi-pattern sync detectors, this
//detector's most recently detected sync pattern is used for optimization, validation and equalizer training.
private ISoftC4FMSyncDetector mActiveSyncDetector;
private final C4FMEqualizer mSymbolEqualizer;
private static final double SYMBOL_TIMING_GAIN = 0.25;
//Clip applied to the Gardner timing error (radians squared) to limit the impact of FM discriminator clicks
@@ -68,7 +75,6 @@ public abstract class C4FMSymbolProcessor
private static final double IDEAL_OUTER_SYMBOL_PHASE = 3.0 * Math.PI / 4.0;
//Allowable gain deviation from the nominal gain, as a fraction of the nominal gain.
private static final double SAMPLE_GAIN_TOLERANCE = 0.1;
private ReferenceSyncWaveform mReferenceSyncWaveform;
private double mNoiseStandardDeviationThreshold;
private double mSamplePoint;
protected double mSamplesPerSymbol;
@@ -85,13 +91,13 @@ public abstract class C4FMSymbolProcessor
protected boolean mSynchronized = false;
private boolean mDelayedSyncNotificationPending = false;
// private final boolean mDebugVisualizeSync = true;
// private final boolean mDebugVisualizeSymbols = true;
// private final boolean mDebugLogDetectionScores = false;
// private final int mDebugSymbolStart = 0;
// private ISyncResultsListener mDebugSyncResultsViewer;
// private int mDebugSymbolCounter = 0;
// private int mDebugSampleCounter = 0;
private final boolean mDebugVisualizeSync = false;
private final boolean mDebugVisualizeSymbols = false;
private final boolean mDebugLogDetectionScores = false;
private final int mDebugSymbolStart = 0;
private ISyncResultsListener mDebugSyncResultsViewer;
private int mDebugSymbolCounter = 0;
private int mDebugSampleCounter = 0;
/**
* Constructs an instance
@@ -104,6 +110,7 @@ public abstract class C4FMSymbolProcessor
mFeedbackDecoder = feedbackDecoder;
mSyncDetector = primary;
mSyncDetectorLagging = lagging;
mActiveSyncDetector = primary;
mSymbolEqualizer = new C4FMEqualizer(mSyncDetector.getPatternSymbols(), getEqualizerSymbolLength(),
getEqualizerTrainingPeriodDurationSymbolCount(), getEqualizerSkipTrainingFrameCount());
mSampleEqualizer = new SampleEqualizer(isDynamicBalanceTrackingRequired());
@@ -209,6 +216,37 @@ public abstract class C4FMSymbolProcessor
*/
public abstract float getSyncThresholdEqualized();
/**
* Notification that a sync detection was validated and the correction applied. Subclasses can override this to
* capture protocol specific details (ie the detected sync pattern) from the sync detector that triggered the
* detection. The message framer is notified of the sync detection once the final sync symbol emerges from the
* equalizer delay line, which can be after this method is invoked.
* @param syncDetector that triggered the sync detection.
*/
protected void syncDetected(ISoftC4FMSyncDetector syncDetector)
{
//No-op default implementation
}
/**
* Indicates if a sync detection is allowed at the current symbol. Protocols with fixed length framing (ie DMR)
* can override this to only allow sync detections at the expected frame boundaries once synchronized, to prevent
* false sync detections from the frame payload. Default implementation always allows sync detection.
* @param symbolsSinceLastSync count of symbols since the final symbol of the previous sync detection.
* @return true if a sync detection is allowed.
*/
protected boolean isSyncDetectionAllowed(int symbolsSinceLastSync)
{
return true;
}
/**
* Number of consecutive sync periods (capped at 3) where a sync was detected within the max message length.
*/
protected int getConsecutiveSyncPeriods()
{
return mConsecutiveSyncPeriods;
}
/**
* Finalize and prepare for shutdown.
@@ -290,16 +328,16 @@ public abstract class C4FMSymbolProcessor
bufferPointer++;
samplePoint--;
// mDebugSampleCounter++;
mDebugSampleCounter++;
if(samplePoint < 1)
{
// mDebugSymbolCounter++;
//
// if(mDebugSymbolCounter % mSyncDetector.getDibitLength() == 0)
// {
// visualizeSymbols(bufferPointer, samplePoint, symbolsSinceLastSync);
// }
mDebugSymbolCounter++;
if(mDebugSymbolCounter % mSyncDetector.getDibitLength() == 0)
{
visualizeSymbols(bufferPointer, samplePoint, symbolsSinceLastSync);
}
symbolsSinceLastSync++;
@@ -337,21 +375,26 @@ public abstract class C4FMSymbolProcessor
float scoreLag = mSyncDetectorLagging.process(softSymbolLag);
scorePrimary = mSyncDetector.process(softSymbol);
// if(mDebugLogDetectionScores)
// {
// boolean exceeds = scorePrimary > 45 || scoreLag > 45;
// System.out.println("DETECT - Primary [" + DEBUG_DF.format(scorePrimary) + "] Lag [" + DEBUG_DF.format(scoreLag) + "] at [" + mDebugSymbolCounter + "]" + (exceeds ? " <<<<<<<<<<<<<<<<<<<<<<<<<<" : ""));
// }
if(mDebugLogDetectionScores)
{
boolean exceeds = scorePrimary > 45 || scoreLag > 45;
System.out.println("DETECT - Primary [" + DEBUG_DF.format(scorePrimary) + "] Lag [" + DEBUG_DF.format(scoreLag) + "] at [" + mDebugSymbolCounter + "]" + (exceeds ? " <<<<<<<<<<<<<<<<<<<<<<<<<<" : ""));
}
correctionCandidate = INVALID_SYNC_DETECTION;
if(scorePrimary > getSyncThresholdDetected() && scorePrimary > scoreLag)
if(isSyncDetectionAllowed(symbolsSinceLastSync))
{
correctionCandidate = optimize(0, scorePrimary, (bufferPointer + samplePoint));
}
else if(scoreLag > getSyncThresholdDetected() && symbolsSinceLastSync > 1)
{
correctionCandidate = optimize(-mLaggingSyncOffset, scoreLag, (bufferPointer + samplePoint));
if(scorePrimary > getSyncThresholdDetected() && scorePrimary > scoreLag)
{
mActiveSyncDetector = mSyncDetector;
correctionCandidate = optimize(0, scorePrimary, (bufferPointer + samplePoint));
}
else if(scoreLag > getSyncThresholdDetected() && symbolsSinceLastSync > 1)
{
mActiveSyncDetector = mSyncDetectorLagging;
correctionCandidate = optimize(-mLaggingSyncOffset, scoreLag, (bufferPointer + samplePoint));
}
}
//Equalizer processes current soft symbol and returns a delayed soft symbol that is equalized
@@ -361,10 +404,10 @@ public abstract class C4FMSymbolProcessor
Dibit symbol = Dibit.fromSample(softSymbol);
mFeedbackDecoder.broadcast(softSymbol);
// if(mDebugVisualizeSync)
// {
// getDebugSyncResultsViewer().symbol(softSymbol);
// }
if(mDebugVisualizeSync)
{
getDebugSyncResultsViewer().symbol(softSymbol);
}
//When the message framer emits at least one valid message (ie CRC check passes) and we have at least
// 2 consecutive sync detections, send the current sample equalizer's balance value as a pseudo-PLL
@@ -388,12 +431,13 @@ public abstract class C4FMSymbolProcessor
{
mConsecutiveSyncPeriods = Math.min(3, mConsecutiveSyncPeriods + 1);
// if(mDebugLogDetectionScores)
// {
// System.out.println("DETECT - Primary [" + DEBUG_DF.format(scorePrimary) + "] Lag [" + DEBUG_DF.format(scoreLag) + "] Optimized [" + correctionCandidate.mOptimizationScore + "] at samples [" + mDebugSampleCounter + "] symbols [" + mDebugSymbolCounter + "]");
// }
if(mDebugLogDetectionScores)
{
System.out.println("DETECT - Primary [" + DEBUG_DF.format(scorePrimary) + "] Lag [" + DEBUG_DF.format(scoreLag) + "] Optimized [" + correctionCandidate.mOptimizationScore + "] at samples [" + mDebugSampleCounter + "] symbols [" + mDebugSymbolCounter + "]");
}
mSampleEqualizer.apply(correctionCandidate);
syncDetected(mActiveSyncDetector);
if(mSynchronized)
{
@@ -424,7 +468,7 @@ public abstract class C4FMSymbolProcessor
// need to prime that buffer with aligned samples prior to sync detection training.
if(!mSymbolEqualizer.isEnabled())
{
int symbols = mSyncDetector.getPatternSymbols().length + mSymbolEqualizer.getLength() - mSymbolEqualizer.getDelaySymbolCount();
int symbols = mSyncDetector.getDibitLength() + mSymbolEqualizer.getLength() - mSymbolEqualizer.getDelaySymbolCount();
double offset = bufferPointer + samplePoint - ((symbols - 1) * samplesPerSymbol) - mLaggingSyncOffset;
float mid, sym;
@@ -440,9 +484,9 @@ public abstract class C4FMSymbolProcessor
}
}
mSymbolEqualizer.syncDetected();
mSymbolEqualizer.syncDetected(mActiveSyncDetector.getPatternSymbols());
mSymbolEqualizer.enable();
// visualizeSyncDetect(scorePrimary, true, "DETECT - POST", bufferPointer, samplePoint);
visualizeSyncDetect(scorePrimary, true, "DETECT - POST", bufferPointer, samplePoint);
}
else if(mDelayedSyncNotificationPending)
{
@@ -485,26 +529,27 @@ public abstract class C4FMSymbolProcessor
*/
private Correction validateSyncWaveform(Correction candidate, int bufferPointer, double samplePoint)
{
ReferenceSyncWaveform referenceSyncWaveform = mActiveSyncDetector.getReferenceWaveform(mSamplesPerSymbol);
float gain = mSampleEqualizer.mGain + candidate.getGainAdjustment();
float balance = mSampleEqualizer.mBalance + candidate.getBalanceAdjustment();
double offset = bufferPointer + samplePoint + candidate.getTimingCorrection() - mReferenceSyncWaveform.samples().length;
double offset = bufferPointer + samplePoint + candidate.getTimingCorrection() - referenceSyncWaveform.samples().length;
int integral = (int)Math.ceil(offset);
double mu = integral - offset;
double accumulator = 0.0;
int index;
//Evaluate the reference waveform in reverse order since the final symbol is aligned to the final sample
for(int x = mReferenceSyncWaveform.samples().length - 1; x >= 0; x--)
for(int x = referenceSyncWaveform.samples().length - 1; x >= 0; x--)
{
index = integral + x;
float sample = LinearInterpolator.calculate(mBuffer[index], mBuffer[index + 1], mu);
sample *= gain;
sample += balance;
sample = Math.clamp(sample, -mReferenceSyncWaveform.maxSample(), mReferenceSyncWaveform.maxSample());
accumulator += mReferenceSyncWaveform.samples()[x] * sample;
sample = Math.clamp(sample, -referenceSyncWaveform.maxSample(), referenceSyncWaveform.maxSample());
accumulator += referenceSyncWaveform.samples()[x] * sample;
}
double threshold = mReferenceSyncWaveform.idealCorrelationScore() * 0.85; //85% of the ideal correlation score
double threshold = referenceSyncWaveform.idealCorrelationScore() * 0.85; //85% of the ideal correlation score
//Return the correction as valid if the correlation score exceeds the threshold, otherwise return the static
// invalid sync detection to nullify the correction.
@@ -548,156 +593,157 @@ public abstract class C4FMSymbolProcessor
mSyncWorkspaceLength = (int)Math.ceil((mSyncDetector.getDibitLength() + 1) * samplesPerSymbol);
mBufferReloadLength = (int)Math.ceil(samplesPerSymbol);
mSampleEqualizer.configure(samplesPerSymbol);
mReferenceSyncWaveform = mSyncDetector.getReferenceWaveform(samplesPerSymbol);
//Pre-generate the reference sync waveform for the primary sync pattern
mSyncDetector.getReferenceWaveform(samplesPerSymbol);
// System.out.println("Samples Per Symbol: " + samplesPerSymbol);
}
// /**
// * Debug viewer UI for visualizing sync detections in the sample stream/waveform.
// */
// private ISyncResultsListener getDebugSyncResultsViewer()
// {
// if(mDebugSyncResultsViewer == null)
// {
// mDebugSyncResultsViewer = new FMSyncResultsViewer();
// }
//
// return mDebugSyncResultsViewer;
// }
//
// /**
// * Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing.
// * @param score from the sync detector
// * @param primary sync detector (true) or secondary (false)
// */
// public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint)
// {
// if(!mDebugVisualizeSync || mDebugSymbolCounter < mDebugSymbolStart)
// {
// return;
// }
//
// int integral = (int)Math.floor(samplePoint);
// samplePoint -= integral;
// bufferPointer += integral;
//
// int offset = 3;
// int length = (int)Math.ceil(mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1)) + (2 * offset);
// int end = bufferPointer + offset;
// int start = end - length;
// float[] symbols = new float[mSyncDetector.getDibitLength()];
// float[] samples = Arrays.copyOfRange(mBuffer, start, end);
// for(int i = 0; i < samples.length; i++)
// {
// samples[i] = mSampleEqualizer.equalize(samples[i]);
// }
//
// double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1));
// int symbolIntegral = (int)Math.floor(symbolPointer);
// double mu = symbolPointer - symbolIntegral;
//
// for(int x = 0; x < mSyncDetector.getDibitLength(); x++)
// {
// symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
// symbolPointer += mSamplesPerSymbol;
// symbolIntegral = (int)Math.floor(symbolPointer);
// mu = symbolPointer - symbolIntegral;
// }
//
// float[] syncIntervals = new float[mSyncDetector.getDibitLength()];
// int adjust = bufferPointer - length + offset;
// double pointer = bufferPointer + samplePoint - adjust;
//
// for(int x = (mSyncDetector.getDibitLength() - 1); x >= 0; x--)
// {
// syncIntervals[x] = (float)pointer;
// pointer -= mSamplesPerSymbol;
// }
//
// CountDownLatch countDownLatch = new CountDownLatch(1);
// getDebugSyncResultsViewer().receive(symbols, mSyncDetector.getPatternSymbols(), samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain,
// tag + " Score: " + score + (primary ? " PRIMARY " : " SECONDARY ") + (mSynchronized ? "FINE " : "COARSE ") +
// " EQ-BO:" + DEBUG_DF.format(mSampleEqualizer.mBalanceOptimized) +
// " EQ-B:" + DEBUG_DF.format(mSampleEqualizer.mBalance) +
// " EQ-G:" + DEBUG_DF.format(mSampleEqualizer.mGain) + " " +
// " SAM:" + mDebugSampleCounter +
// " SYM:" + mDebugSymbolCounter +
// " DUR:" + DEBUG_DF.format(mDebugSymbolCounter / 2400.0),
// countDownLatch);
//
// try
// {
// countDownLatch.await();
// }
// catch(InterruptedException e)
// {
// throw new RuntimeException(e);
// }
// }
//
// /**
// * Debug method to visualize the symbol decisions
// */
// public void visualizeSymbols(int bufferPointer, double samplePoint, int symbolsSinceLastSync)
// {
// if(!mDebugVisualizeSymbols || mDebugSymbolCounter < mDebugSymbolStart)
// {
// return;
// }
//
// int syncLength = mSyncDetector.getDibitLength();
//
// int offset = 3;
// int length = (int)Math.ceil(mSamplesPerSymbol * (syncLength - 1)) + (2 * offset);
// int end = bufferPointer + offset;
// int start = end - length;
// float[] symbols = new float[syncLength];
// float[] decisions = new float[syncLength];
// float[] samples = Arrays.copyOfRange(mBuffer, start, end);
// for(int i = 0; i < samples.length; i++)
// {
// samples[i] = mSampleEqualizer.equalize(samples[i]);
// }
//
// double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * (syncLength - 1));
// int symbolIntegral = (int)Math.floor(symbolPointer);
// double mu = symbolPointer - symbolIntegral;
//
// for(int x = 0; x < syncLength; x++)
// {
// symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
// decisions[x] = Dibit.fromSample(symbols[x]).getIdealPhase();
// symbolPointer += mSamplesPerSymbol;
// symbolIntegral = (int)Math.floor(symbolPointer);
// mu = symbolPointer - symbolIntegral;
// }
//
// float[] syncIntervals = new float[syncLength];
// int adjust = bufferPointer - length + offset;
// double pointer = bufferPointer + samplePoint - adjust;
//
// for(int x = syncLength - 1; x >= 0; x--)
// {
// syncIntervals[x] = (float)pointer;
// pointer -= mSamplesPerSymbol;
// }
//
// CountDownLatch countDownLatch = new CountDownLatch(1);
// getDebugSyncResultsViewer().receive(symbols, decisions, samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain,
// "Balance:" + mSampleEqualizer.mBalance + " Gain:" + mSampleEqualizer.mGain +
// " SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter +
// " DUR:" + DEBUG_DF.format(mDebugSymbolCounter / 2400.0) +
// " ELAPSED:" + symbolsSinceLastSync, countDownLatch);
//
// try
// {
// countDownLatch.await();
// }
// catch(InterruptedException e)
// {
// throw new RuntimeException(e);
// }
// }
/**
* Debug viewer UI for visualizing sync detections in the sample stream/waveform.
*/
private ISyncResultsListener getDebugSyncResultsViewer()
{
if(mDebugSyncResultsViewer == null)
{
mDebugSyncResultsViewer = new FMSyncResultsViewer();
}
return mDebugSyncResultsViewer;
}
/**
* Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing.
* @param score from the sync detector
* @param primary sync detector (true) or secondary (false)
*/
public void visualizeSyncDetect(float score, boolean primary, String tag, int bufferPointer, double samplePoint)
{
if(!mDebugVisualizeSync || mDebugSymbolCounter < mDebugSymbolStart)
{
return;
}
int integral = (int)Math.floor(samplePoint);
samplePoint -= integral;
bufferPointer += integral;
int offset = 3;
int length = (int)Math.ceil(mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1)) + (2 * offset);
int end = bufferPointer + offset;
int start = end - length;
float[] symbols = new float[mSyncDetector.getDibitLength()];
float[] samples = Arrays.copyOfRange(mBuffer, start, end);
for(int i = 0; i < samples.length; i++)
{
samples[i] = mSampleEqualizer.equalize(samples[i]);
}
double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1));
int symbolIntegral = (int)Math.floor(symbolPointer);
double mu = symbolPointer - symbolIntegral;
for(int x = 0; x < mSyncDetector.getDibitLength(); x++)
{
symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
symbolPointer += mSamplesPerSymbol;
symbolIntegral = (int)Math.floor(symbolPointer);
mu = symbolPointer - symbolIntegral;
}
float[] syncIntervals = new float[mSyncDetector.getDibitLength()];
int adjust = bufferPointer - length + offset;
double pointer = bufferPointer + samplePoint - adjust;
for(int x = (mSyncDetector.getDibitLength() - 1); x >= 0; x--)
{
syncIntervals[x] = (float)pointer;
pointer -= mSamplesPerSymbol;
}
CountDownLatch countDownLatch = new CountDownLatch(1);
getDebugSyncResultsViewer().receive(symbols, mSyncDetector.getPatternSymbols(), samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain,
tag + " Score: " + score + (primary ? " PRIMARY " : " SECONDARY ") + (mSynchronized ? "FINE " : "COARSE ") +
" EQ-BO:" + DEBUG_DF.format(mSampleEqualizer.mBalanceOptimized) +
" EQ-B:" + DEBUG_DF.format(mSampleEqualizer.mBalance) +
" EQ-G:" + DEBUG_DF.format(mSampleEqualizer.mGain) + " " +
" SAM:" + mDebugSampleCounter +
" SYM:" + mDebugSymbolCounter +
" DUR:" + DEBUG_DF.format(mDebugSymbolCounter / 2400.0),
countDownLatch);
try
{
countDownLatch.await();
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
}
/**
* Debug method to visualize the symbol decisions
*/
public void visualizeSymbols(int bufferPointer, double samplePoint, int symbolsSinceLastSync)
{
if(!mDebugVisualizeSymbols || mDebugSymbolCounter < mDebugSymbolStart)
{
return;
}
int syncLength = mSyncDetector.getDibitLength();
int offset = 3;
int length = (int)Math.ceil(mSamplesPerSymbol * (syncLength - 1)) + (2 * offset);
int end = bufferPointer + offset;
int start = end - length;
float[] symbols = new float[syncLength];
float[] decisions = new float[syncLength];
float[] samples = Arrays.copyOfRange(mBuffer, start, end);
for(int i = 0; i < samples.length; i++)
{
samples[i] = mSampleEqualizer.equalize(samples[i]);
}
double symbolPointer = bufferPointer + samplePoint - (mSamplesPerSymbol * (syncLength - 1));
int symbolIntegral = (int)Math.floor(symbolPointer);
double mu = symbolPointer - symbolIntegral;
for(int x = 0; x < syncLength; x++)
{
symbols[x] = mSampleEqualizer.getEqualizedSymbol(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
decisions[x] = Dibit.fromSample(symbols[x]).getIdealPhase();
symbolPointer += mSamplesPerSymbol;
symbolIntegral = (int)Math.floor(symbolPointer);
mu = symbolPointer - symbolIntegral;
}
float[] syncIntervals = new float[syncLength];
int adjust = bufferPointer - length + offset;
double pointer = bufferPointer + samplePoint - adjust;
for(int x = syncLength - 1; x >= 0; x--)
{
syncIntervals[x] = (float)pointer;
pointer -= mSamplesPerSymbol;
}
CountDownLatch countDownLatch = new CountDownLatch(1);
getDebugSyncResultsViewer().receive(symbols, decisions, samples, syncIntervals, mSampleEqualizer.mBalance, mSampleEqualizer.mGain,
"Balance:" + mSampleEqualizer.mBalance + " Gain:" + mSampleEqualizer.mGain +
" SAM:" + mDebugSampleCounter + " SYM:" + mDebugSymbolCounter +
" DUR:" + DEBUG_DF.format(mDebugSymbolCounter / 2400.0) +
" ELAPSED:" + symbolsSinceLastSync, countDownLatch);
try
{
countDownLatch.await();
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
}
/**
* Calculates the standard deviation of the samples at the specified offset that aligns with a sync detection.
@@ -965,7 +1011,10 @@ public abstract class C4FMSymbolProcessor
/**
* Applies decision directed correction to the constellation balance when dynamic balance tracking is enabled
* and the processor is synchronized following a valid sync detection.
* and the processor is synchronized following a valid sync detection. In periods of high noise or when the
* signal has stopped but we haven't yet detected it, we apply a correction that gradually restores the balance
* back towards the optimized balance decided at the last valid sync detection. This ensures that when the
* signal returns, the balance can be reestablished quickly.
* @param symbol sliced at the correct timing
*/
public float correct(float symbol)
@@ -1153,8 +1202,9 @@ public abstract class C4FMSymbolProcessor
{
int maxPointer = mBuffer.length - 1;
float softSymbol;
float[] patternSymbols = mActiveSyncDetector.getPatternSymbols();
double pointer = offset - (samplesPerSymbol * (mSyncDetector.getDibitLength() - 1));
double pointer = offset - (samplesPerSymbol * (patternSymbols.length - 1));
int bufferPointer = (int)Math.floor(pointer);
double fractional = pointer - bufferPointer;
@@ -1162,7 +1212,7 @@ public abstract class C4FMSymbolProcessor
float sample1, sample2;
float idealSymbol;
for(int x = 0; x < mSyncDetector.getDibitLength(); x++)
for(int x = 0; x < patternSymbols.length; x++)
{
if(bufferPointer < maxPointer)
{
@@ -1175,7 +1225,7 @@ public abstract class C4FMSymbolProcessor
softSymbol = 0.0f;
}
idealSymbol = mSyncDetector.getPatternSymbols()[x];
idealSymbol = patternSymbols[x];
//Constrain the contribution of the soft symbol to +/- the ideal symbol value.
if(idealSymbol > 0)
@@ -1246,22 +1296,24 @@ public abstract class C4FMSymbolProcessor
public Correction getCorrection(double additionalOffset, double timingCorrection,
float detectionScore, float optimizationScore, double offset, float measuredBalance)
{
double resampleStart = offset + additionalOffset + timingCorrection - (mSamplesPerSymbol * (mSyncDetector.getDibitLength() - 1));
float[] patternSymbols = mActiveSyncDetector.getPatternSymbols();
Dibit[] patternDibits = mActiveSyncDetector.getPatternDibits();
double resampleStart = offset + additionalOffset + timingCorrection - (mSamplesPerSymbol * (patternSymbols.length - 1));
int resampleStartIntegral = (int)Math.floor(resampleStart);
float symbol, gainAccumulator = 0, idealMagnitudeAccumulator = 0, resampledSoftSymbol;
Dibit resampledDibit;
int bitErrorCount = 0;
for(int x = 0; x < mSyncDetector.getDibitLength(); x++)
for(int x = 0; x < patternSymbols.length; x++)
{
if(resampleStartIntegral >= 0)
{
symbol = mSyncDetector.getPatternSymbols()[x];
symbol = patternSymbols[x];
resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral],
mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral);
resampledSoftSymbol = resampledSoftSymbol * mSampleEqualizer.mGain + measuredBalance;
resampledDibit = Dibit.fromSample(resampledSoftSymbol);
bitErrorCount += mSyncDetector.getPatternDibits()[x].getBitErrorFrom(resampledDibit);
bitErrorCount += patternDibits[x].getBitErrorFrom(resampledDibit);
gainAccumulator += Math.abs(symbol) - Math.abs(resampledSoftSymbol);
idealMagnitudeAccumulator += Math.abs(symbol);
}

View File

@@ -15,13 +15,14 @@ public interface ISoftC4FMSyncDetector
float process(float softSymbol);
/**
* Soft symbols that represent the sync pattern
* Soft symbols that represent the sync pattern. For detectors that monitor multiple sync patterns (ie DMR), this
* is the sync pattern that produced the score from the most recent call to process().
* @return soft symbols array
*/
float[] getPatternSymbols();
/**
* Array of dibits for the sync pattern
* Array of dibits for the sync pattern (or most recently detected sync pattern for multi-pattern detectors).
* @return sync pattern dibit array
*/
Dibit[] getPatternDibits();
@@ -32,7 +33,8 @@ public interface ISoftC4FMSyncDetector
int getDibitLength();
/**
* Provides a reference waveform that represents the sync pattern for sync detection validation
* Provides a reference waveform that represents the sync pattern (or most recently detected sync pattern for
* multi-pattern detectors) for sync detection validation
* @param samplesPerSymbol for each symbol
* @return reference waveform
*/

View File

@@ -0,0 +1,288 @@
/*
* *****************************************************************************
* 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.edac.soft;
import java.util.Arrays;
import java.util.function.IntPredicate;
/**
* Soft decision decoder for short binary block codes, defined by the full set of valid codewords.
*
* Codewords are represented as integers where bit i (LSB = 0) holds codeword bit position i. Soft bit values are
* log-likelihood ratios where a positive value favors a 1 bit and the magnitude indicates the reliability.
*
* Provides Chase-Pyndiah soft-in/soft-out decoding: tests the codewords nearest to the hard decision with the least
* reliable bits flipped and produces extrinsic soft information for iterative (ie product code) decoding.
*/
public class SoftBlockCode
{
private static final int MAX_LENGTH = 17;
private final int mLength;
private final int[] mCodewords;
private final int[] mDecodeTable;
/**
* Constructs an instance.
* @param length of the code in bits (maximum 17).
* @param codewords set of all valid codewords.
*/
public SoftBlockCode(int length, int[] codewords)
{
if(length < 1 || length > MAX_LENGTH)
{
throw new IllegalArgumentException("Code length must be in range 1 - " + MAX_LENGTH);
}
mLength = length;
mCodewords = codewords.clone();
//Single bit error correction table: maps each word within distance 1 of a codeword to that codeword, or -1.
mDecodeTable = new int[1 << length];
Arrays.fill(mDecodeTable, -1);
for(int codeword: codewords)
{
mDecodeTable[codeword] = codeword;
for(int i = 0; i < length; i++)
{
mDecodeTable[codeword ^ (1 << i)] = codeword;
}
}
}
/**
* Creates an instance by testing every possible word of the specified length to identify the valid codewords.
* @param length of the code in bits (maximum 17).
* @param isCodeword test that indicates if the word (bit i = codeword position i) is a valid codeword.
* @return soft block code.
*/
public static SoftBlockCode fromCodewordTest(int length, IntPredicate isCodeword)
{
int count = 0;
int[] candidates = new int[1 << length];
for(int word = 0; word < (1 << length); word++)
{
if(isCodeword.test(word))
{
candidates[count++] = word;
}
}
return new SoftBlockCode(length, Arrays.copyOf(candidates, count));
}
/**
* Code length in bits
*/
public int getLength()
{
return mLength;
}
/**
* Valid codewords
*/
public int[] getCodewords()
{
return mCodewords;
}
/**
* Indicates if the word is a valid codeword.
*/
public boolean isCodeword(int word)
{
return mDecodeTable[word] == word;
}
/**
* Hard decision of the soft bits.
* @param softBits array
* @param indices of the code bits in the soft bits array
* @return hard decision word.
*/
public static int getHardDecision(float[] softBits, int[] indices)
{
int word = 0;
for(int i = 0; i < indices.length; i++)
{
if(softBits[indices[i]] > 0)
{
word |= (1 << i);
}
}
return word;
}
/**
* Calculates the reliability metric for the candidate codeword: the sum of the soft bit magnitudes for each bit
* where the candidate differs from the hard decision. Lower values are more likely.
*/
private static float getMetric(int candidate, int hard, float[] magnitudes)
{
float metric = 0.0f;
int difference = candidate ^ hard;
while(difference != 0)
{
int bit = Integer.numberOfTrailingZeros(difference);
metric += magnitudes[bit];
difference &= difference - 1;
}
return metric;
}
/**
* Chase-Pyndiah soft-in/soft-out decode. Generates test patterns by flipping each combination of the least
* reliable bits in the hard decision, decodes each with the single bit error correction table and selects the
* most likely candidate codeword as the decision. For each bit, the extrinsic value is the metric difference
* between the decision and the best competing candidate that differs at that bit, less the soft input, or beta
* (signed by the decision) when there is no competing candidate. When no test pattern decodes to a codeword, the
* extrinsic values are zero.
*
* @param input soft bits array
* @param indices of the code bits in the input and output arrays
* @param output array to receive the extrinsic soft values at the code bit indices
* @param leastReliableCount number of least reliable bits to use for test patterns
* @param beta reliability to assign to a bit when there is no competing candidate codeword.
* @return decision codeword, or -1 if no test pattern decoded to a codeword.
*/
public int decodeChase(float[] input, int[] indices, float[] output, int leastReliableCount, float beta)
{
float[] magnitudes = new float[mLength];
for(int i = 0; i < mLength; i++)
{
magnitudes[i] = Math.abs(input[indices[i]]);
}
int hard = getHardDecision(input, indices);
//Identify the least reliable bit positions using selection sort over the small code length
int p = Math.min(leastReliableCount, mLength);
int[] leastReliable = new int[p];
int used = 0;
for(int x = 0; x < p; x++)
{
int minIndex = -1;
for(int i = 0; i < mLength; i++)
{
if((used & (1 << i)) == 0 && (minIndex < 0 || magnitudes[i] < magnitudes[minIndex]))
{
minIndex = i;
}
}
leastReliable[x] = minIndex;
used |= (1 << minIndex);
}
//Decode each test pattern into a unique candidate codeword
int[] candidates = new int[1 << p];
float[] metrics = new float[1 << p];
int candidateCount = 0;
int decision = -1;
float decisionMetric = Float.MAX_VALUE;
for(int pattern = 0; pattern < (1 << p); pattern++)
{
int test = hard;
for(int b = 0; b < p; b++)
{
if((pattern & (1 << b)) != 0)
{
test ^= (1 << leastReliable[b]);
}
}
int candidate = mDecodeTable[test];
if(candidate < 0)
{
continue;
}
boolean duplicate = false;
for(int c = 0; c < candidateCount; c++)
{
if(candidates[c] == candidate)
{
duplicate = true;
break;
}
}
if(!duplicate)
{
float metric = getMetric(candidate, hard, magnitudes);
candidates[candidateCount] = candidate;
metrics[candidateCount] = metric;
candidateCount++;
if(metric < decisionMetric)
{
decisionMetric = metric;
decision = candidate;
}
}
}
for(int i = 0; i < mLength; i++)
{
if(decision < 0)
{
output[indices[i]] = 0.0f;
continue;
}
int mask = 1 << i;
float sign = (decision & mask) != 0 ? 1.0f : -1.0f;
float competitorMetric = Float.MAX_VALUE;
for(int c = 0; c < candidateCount; c++)
{
if(((candidates[c] ^ decision) & mask) != 0 && metrics[c] < competitorMetric)
{
competitorMetric = metrics[c];
}
}
if(competitorMetric < Float.MAX_VALUE)
{
output[indices[i]] = sign * (competitorMetric - decisionMetric) - input[indices[i]];
}
else
{
output[indices[i]] = sign * beta;
}
}
return decision;
}
}

View File

@@ -0,0 +1,278 @@
/*
* *****************************************************************************
* 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.edac.soft;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* Iterative soft decision decoder for product codes where each row is a codeword of a short block code and each
* column has even parity (ie the final row is the column parity row). Bits are ordered row by row.
*
* Alternately decodes the rows using Chase-Pyndiah decoding and the even parity columns using min-sum decoding,
* exchanging extrinsic soft information, until the decisions form a valid product codeword. Rejects decisions
* where the correction cost (sum of the normalized channel soft bit magnitudes for the corrected bits) exceeds the
* maximum, since a low correction cost indicates a reliable decision.
*/
public class SoftProductCodeDecoder
{
private static final int CHASE_LEAST_RELIABLE_BITS = 4;
//Each half iteration decodes either all rows or all columns, starting with the rows. Alpha scales the extrinsic
//information fed to the next half iteration and beta is the reliability assigned to row bits without a competing
//candidate codeword.
private static final float[] SOFT_ALPHA = new float[]{0.0f, 0.2f, 0.3f, 0.5f, 0.7f, 0.9f, 1.0f, 1.0f};
private static final float[] SOFT_BETA = new float[]{0.2f, 0.4f, 0.6f, 0.8f, 1.0f, 1.0f, 1.0f, 1.0f};
private final SoftBlockCode mRowCode;
private final int mRowCount;
private final int mColumnCount;
private final int mLength;
private final int[][] mRowIndexes;
private final float mMaxCorrectionCost;
/**
* Constructs an instance
* @param rowCode for each row, where the code length is the column count
* @param rowCount number of rows, including the column parity row
* @param maxCorrectionCost maximum correction cost for a decision, or Float.MAX_VALUE for no limit
*/
public SoftProductCodeDecoder(SoftBlockCode rowCode, int rowCount, float maxCorrectionCost)
{
mRowCode = rowCode;
mRowCount = rowCount;
mColumnCount = rowCode.getLength();
mLength = mRowCount * mColumnCount;
mMaxCorrectionCost = maxCorrectionCost;
mRowIndexes = new int[mRowCount][mColumnCount];
for(int row = 0; row < mRowCount; row++)
{
for(int column = 0; column < mColumnCount; column++)
{
mRowIndexes[row][column] = row * mColumnCount + column;
}
}
}
/**
* Decodes the (de-interleaved) soft bits.
* @param channel soft bits ordered row by row where a positive value favors a 1 bit.
* @return decoded message with the corrected bit count set, or null if decoding doesn't converge or the correction
* cost exceeds the maximum.
*/
public CorrectedBinaryMessage decode(float[] channel)
{
if(channel == null || channel.length != mLength)
{
return null;
}
//Normalize the channel soft bits to a mean magnitude of 1 for the alpha and beta settings
float sum = 0.0f;
for(float softBit: channel)
{
sum += Math.abs(softBit);
}
if(sum <= 0.0f)
{
return null;
}
float scale = mLength / sum;
float[] received = new float[mLength];
for(int i = 0; i < mLength; i++)
{
received[i] = channel[i] * scale;
}
float[] extrinsic = new float[mLength];
float[] input = new float[mLength];
float[] output = new float[mLength];
for(int halfIteration = 0; halfIteration < SOFT_ALPHA.length; halfIteration++)
{
float alpha = SOFT_ALPHA[halfIteration];
for(int i = 0; i < mLength; i++)
{
input[i] = received[i] + alpha * extrinsic[i];
}
CorrectedBinaryMessage decoded = new CorrectedBinaryMessage(mLength);
if(halfIteration % 2 == 0)
{
boolean allDecoded = true;
for(int row = 0; row < mRowCount; row++)
{
int word = mRowCode.decodeChase(input, mRowIndexes[row], output, CHASE_LEAST_RELIABLE_BITS,
SOFT_BETA[halfIteration]);
allDecoded &= word >= 0;
for(int column = 0; column < mColumnCount; column++)
{
if((word & (1 << column)) != 0)
{
decoded.set(mRowIndexes[row][column]);
}
}
}
if(!allDecoded)
{
decoded = null;
}
}
else
{
decodeColumns(input, output);
for(int i = 0; i < mLength; i++)
{
if(input[i] + output[i] > 0)
{
decoded.set(i);
}
}
}
//Check for a valid product codeword from the decisions of this half iteration
if(decoded != null && isValid(decoded))
{
int corrected = 0;
float cost = 0.0f;
for(int i = 0; i < mLength; i++)
{
if(decoded.get(i) != (received[i] > 0))
{
corrected++;
cost += Math.abs(received[i]);
}
}
if(cost > mMaxCorrectionCost)
{
return null;
}
decoded.setCorrectedBitCount(corrected);
return decoded;
}
//Normalize the extrinsic information to a mean magnitude of 1 for the next half iteration
float extrinsicSum = 0.0f;
for(float value: output)
{
extrinsicSum += Math.abs(value);
}
float extrinsicScale = extrinsicSum > 0.0f ? mLength / extrinsicSum : 0.0f;
for(int i = 0; i < mLength; i++)
{
extrinsic[i] = output[i] * extrinsicScale;
}
}
return null;
}
/**
* Indicates if each row is a row codeword and each column has even parity.
*/
private boolean isValid(CorrectedBinaryMessage message)
{
int parity = 0;
for(int row = 0; row < mRowCount; row++)
{
int word = 0;
for(int column = 0; column < mColumnCount; column++)
{
if(message.get(mRowIndexes[row][column]))
{
word |= (1 << column);
}
}
if(!mRowCode.isCodeword(word))
{
return false;
}
parity ^= word;
}
return parity == 0;
}
/**
* Min-sum (max-log) soft-in/soft-out decoding of the even parity columns. The extrinsic value for each bit is
* the product of the signs and the minimum magnitude of the other bits in the column.
* @param input soft bits
* @param output to receive the extrinsic values.
*/
private void decodeColumns(float[] input, float[] output)
{
for(int column = 0; column < mColumnCount; column++)
{
float sign = 1.0f;
float minimum1 = Float.MAX_VALUE;
float minimum2 = Float.MAX_VALUE;
int minimumRow = -1;
for(int row = 0; row < mRowCount; row++)
{
float value = input[mRowIndexes[row][column]];
if(value < 0)
{
sign = -sign;
}
float magnitude = Math.abs(value);
if(magnitude < minimum1)
{
minimum2 = minimum1;
minimum1 = magnitude;
minimumRow = row;
}
else if(magnitude < minimum2)
{
minimum2 = magnitude;
}
}
for(int row = 0; row < mRowCount; row++)
{
int index = mRowIndexes[row][column];
float value = input[index];
float otherSign = value < 0 ? -sign : sign;
output[index] = otherSign * (row == minimumRow ? minimum2 : minimum1);
}
}
}
}

View File

@@ -26,8 +26,8 @@ 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;
@@ -40,6 +40,7 @@ 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.nxdn.NXDNFilterDesigner;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
@@ -52,25 +53,24 @@ import java.io.File;
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;
/**
* DMR DQPSK decoder.
* DMR 4FSK decoder.
*
* Processes the sample stream using an optimal (scalar or vector) DQPSK demodulator. The demodulated sample stream
* is processed by the DMRSoftSymbolProcessor at each symbol time interval using a primary and two lagging soft sync
* detectors. The sync detectors use correlation of the demodulated soft symbols with all known sync patterns to
* detect the sync and align the symbol timing and symbol period to the optimal location and symbol period as
* calculated across each detected burst. Once the symbol processor has fingerprinted the channel type, it optimizes
* sync detection for one of three detected modes (BASE, MOBILE or DIRECT) to reduce sync correlation workload.
* Processes the sample stream using an FM demodulator and a root raised cosine receive filter matched to the DMR
* transmit filter. The demodulated sample stream is processed by the DMRSymbolProcessor (C4FM symbol processor)
* at each symbol time interval using a primary and a lagging soft sync detector. The sync detectors use correlation
* of the demodulated soft symbols with all known sync patterns to detect the sync and align the symbol timing and
* constellation gain and balance. Once the symbol processor has fingerprinted the channel type, it optimizes sync
* detection for one of three detected modes (BASE, MOBILE or DIRECT) to reduce sync correlation workload.
*
* The DMRSoftSymbolMessageFramer receives sync detections from the soft symbol processor and buffers symbols from the
* start of the burst through the sync so that the symbols can be resampled when the sync is detected, ensuring that
* the burst symbols are accurate using the symbol timing and period values calculated on the sync detection.
* The DMRMessageFramer receives sync detections from the soft symbol processor and buffers symbols from the start of
* the burst through the sync so that the full burst can be framed once the sync is detected.
*
* For Direct Mode where one timeslot is empty, the symbol message framer uses two message burst buffers that alternate
* for each burst so that one buffer tracks the active timeslot and the other buffer tracks the empty timeslot to
@@ -85,18 +85,19 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
private static final Logger LOGGER = LoggerFactory.getLogger(DMRDecoder.class);
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
private static final int SYMBOL_RATE = 4800;
private static final float MAXIMUM_CARRIER_OFFSET = 5000.0f; //Threshold for retuning the signal.
private static final Map<Double,float[]> BASEBAND_FILTERS = new HashMap<>();
private DifferentialDemodulatorFloat mDemodulator;
//DMR transmit filter is a root raised cosine with 0.2 roll-off, so the receive filter is a matched RRC filter.
private static final double RECEIVE_FILTER_ALPHA = 0.2;
private static final int RECEIVE_FILTER_SYMBOL_SPAN = 12;
private static final Map<Double,float[]> BASEBAND_FILTERS = new ConcurrentHashMap<>();
private IDemodulator mDemodulator;
private final DMRMessageFramer mMessageFramer;
private final DMRSoftSymbolProcessor mSymbolProcessor;
private final DMRSymbolProcessor mSymbolProcessor;
private final DMRMessageProcessor mMessageProcessor;
private IRealFilter mIBasebandFilter;
private IRealFilter mQBasebandFilter;
private IRealFilter mBasebandFilterI;
private IRealFilter mBasebandFilterQ;
private IRealDecimationFilter mDecimationFilterI;
private IRealDecimationFilter mDecimationFilterQ;
private RealFIRFilter mRRCFilterI;
private RealFIRFilter mRRCFilterQ;
private IRealFilter mReceiveFilter;
/**
* Constructs an instance
@@ -106,7 +107,7 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
{
DMRCrcMaskManager crcMaskManager = new DMRCrcMaskManager(config.getIgnoreCRCChecksums());
mMessageFramer = new DMRMessageFramer(crcMaskManager);
mSymbolProcessor = new DMRSoftSymbolProcessor(mMessageFramer, this);
mSymbolProcessor = new DMRSymbolProcessor(this, mMessageFramer);
mMessageProcessor = new DMRMessageProcessor(config, crcMaskManager);
mMessageProcessor.setMessageListener(getMessageListener());
setSampleRate(25000.0);
@@ -129,21 +130,20 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
*/
public void setSampleRate(double sampleRate)
{
if(sampleRate <= SYMBOL_RATE * 2)
{
throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >9600 (2 * " +
SYMBOL_RATE + " symbol rate)");
}
float samplesPerSymbol = (float)sampleRate / SYMBOL_RATE;
mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
if(samplesPerSymbol <= 4.0f)
{
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);
@@ -154,21 +154,21 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
//Set the decimated sample rate to use for PLL error reporting.
setDecimatedSampleRate(decimatedSampleRate);
float rrcAlpha = Math.abs((float)(5760.0 / decimatedSampleRate));
int symbolLength = (int)Math.floor((-44 * rrcAlpha) + 33);
symbolLength += symbolLength % 2; //Make the symbol length even
if(symbolLength < 0)
//Receive filter: root raised cosine matched to the DMR transmit filter, normalized to unity gain at DC.
double[] receiveFilterTaps = NXDNFilterDesigner.designRootRaisedCosine(decimatedSampleRate, SYMBOL_RATE,
RECEIVE_FILTER_ALPHA, RECEIVE_FILTER_SYMBOL_SPAN);
float[] taps = new float[receiveFilterTaps.length];
for(int x = 0; x < receiveFilterTaps.length; x++)
{
symbolLength = 2;
taps[x] = (float)receiveFilterTaps[x];
}
mReceiveFilter = new RealFIRFilter(taps);
float[] taps = FilterFactory.getRootRaisedCosine(decimatedSampleRate / SYMBOL_RATE, symbolLength, rrcAlpha);
mRRCFilterI = new RealFIRFilter(taps);
mRRCFilterQ = new RealFIRFilter(taps);
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);
mMessageFramer.setListener(mMessageProcessor);
mMessageProcessor.setMessageListener(getMessageListener());
}
@@ -181,19 +181,20 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
public void receive(ComplexSamples samples)
{
//Update the message framer with the timestamp from the incoming sample buffer.
mMessageFramer.setTimestamp(samples.timestamp());
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
float[] i = mDecimationFilterI.decimateReal(samples.i());
float[] q = mDecimationFilterQ.decimateReal(samples.q());
i = mDecimationFilterI.decimateReal(i);
q = mDecimationFilterQ.decimateReal(q);
i = mRRCFilterI.filter(i);
q = mRRCFilterQ.filter(q);
i = mBasebandFilterI.filter(i);
q = mBasebandFilterQ.filter(q);
float[] demodulated = mDemodulator.demodulate(i, q);
mSymbolProcessor.receive(demodulated);
demodulated = mReceiveFilter.filter(demodulated);
//Process demodulated samples into symbols and apply message sync detection and framing.
mSymbolProcessor.process(demodulated);
}
/**
@@ -201,9 +202,12 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
*/
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
@@ -219,11 +223,15 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
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 DMR baseband filter for sample rate [" + sampleRate + "]", e);
}
if(coefficients == null)
@@ -270,7 +278,7 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
@Override
public String getProtocolDescription()
{
return "DMR C4FM";
return "DMR";
}
@Override
@@ -285,6 +293,7 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
{
super.stop();
mMessageFramer.stop();
mSymbolProcessor.dispose();
}
/**
@@ -296,6 +305,10 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
{
switch(sourceEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
mSymbolProcessor.resetBalance();
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
setSampleRate(sourceEvent.getValue().doubleValue());
break;
@@ -314,8 +327,8 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
LOGGER.info("Starting ...");
// String directory = "D:\\DQPSK Equalizer Research\\"; //Windows
String directory = "/media/denny/T9/DQPSK Equalizer Research/"; //Linux
String file = directory + "DMR_1_CAPPLUS.wav";
String directory = "/run/media/denny/T9/DQPSK Equalizer Research/"; //Linux
// String file = directory + "DMR_1_CAPPLUS.wav";
// String file = directory + "DMR_2_CAPPLUS.wav";
// String file = directory + "DMR_3_CAPPLUS.wav";
// String file = directory + "20230819_064211_451250000_SaiaNet_Syracuse_Control_29_baseband.wav";
@@ -325,7 +338,7 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
// String file = directory + "DMR_6_20241217_031511_451425000_SaiaNet_Onondaga_SaiaNet_Control_50_baseband.wav";
// String file = directory + "DMR_7_20241217_031651_451250000_SaiaNet_Onondaga_SaiaNet_Control_50_baseband.wav";
// String file = directory + "DMR_8_20241217_031845_461662500_SaiaNet_(Tier_III)_Onondaga_Control_25_baseband.wav";
// String file = directory + "DMR_9_CAPPLUS_encrypted_American_Airlines_Maricopa_Control_29_baseband.wav";
String file = directory + "DMR_9_CAPPLUS_encrypted_American_Airlines_Maricopa_Control_29_baseband.wav";
// String file = directory + "DMR_10_CAP_ENCRYPTED_20241222_035408_935487500_American_Airlines_Maricopa_Control_1_baseband.wav";
// String file = directory + "DMR_12_20250420_061639_451250000_SaiaNet_Onondaga_SaiaNet-Control_1_baseband.wav";
// String file = directory + "DMR_17_20250516_053150_451425000_SaiaNet_Onondaga_SaiaNet-Control_1_baseband.wav";

View File

@@ -20,7 +20,9 @@
package io.github.dsheirer.module.decode.dmr;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.dsp.c4fm.C4FMMessageFramer;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitDelayLine;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
import io.github.dsheirer.module.decode.dmr.message.CACH;
@@ -38,14 +40,34 @@ import java.util.Arrays;
* <p>
* This framer is able to detect DMR Direct mode and track one active and one inactive timeslot across each voice
* super frame in addition to framing base and mobile station data and voice bursts.
* <p>
* Supports two symbol stream inputs:
* <ul>
* <li>Listener receive(Dibit) and syncDetected(DMRSyncPattern) where the sync detection is signalled when the
* next dibit is the first dibit of the burst (ie the symbol processor maintains a burst delay line)</li>
* <li>C4FM message framer process(Dibit) and syncDetected() where the sync detection is signalled when the most
* recent dibit is the final dibit of the sync pattern. This framer maintains a delay line to retain the burst
* dibits from CACH through the sync pattern and the sync pattern must be provided via setSyncPattern() prior to
* the sync detection notification.</li>
* </ul>
*/
public class DMRMessageFramer implements Listener<Dibit>
public class DMRMessageFramer extends C4FMMessageFramer implements Listener<Dibit>
{
private static final int DIBIT_CACH_START = 0;
private static final int DIBIT_BURST_END = 144;
//Dibit delay line sizing: CACH(12) + MESSAGE_PREFIX(54) + SYNC(24)
private static final int DELAY_LINE_LENGTH = 90;
private final DibitDelayLine mDibitDelayLine = new DibitDelayLine(DELAY_LINE_LENGTH);
//Soft symbols (radians) delay line parallel to the dibit delay line, or NaN when the soft symbol wasn't provided
private final float[] mSoftSymbolDelayLine = new float[DELAY_LINE_LENGTH];
private int mSoftSymbolDelayLinePointer = 0;
private DMRSyncPattern mSyncPattern = DMRSyncPattern.UNKNOWN;
private Listener<IMessage> mMessageListener;
private final Dibit[] mBufferA = new Dibit[144];
private final Dibit[] mBufferB = new Dibit[144];
//Soft symbols (radians) parallel to the dibit buffers, or NaN when the soft symbol wasn't provided
private final float[] mSoftBufferA = new float[144];
private final float[] mSoftBufferB = new float[144];
private int mBufferAPointer = 0;
private int mBufferBPointer = 0;
private DMRSyncPattern mBufferAPattern = DMRSyncPattern.UNKNOWN;
@@ -67,15 +89,80 @@ public class DMRMessageFramer implements Listener<Dibit>
{
Arrays.fill(mBufferA, Dibit.D00_PLUS_1);
Arrays.fill(mBufferB, Dibit.D00_PLUS_1);
Arrays.fill(mSoftSymbolDelayLine, Float.NaN);
Arrays.fill(mSoftBufferA, Float.NaN);
Arrays.fill(mSoftBufferB, Float.NaN);
mMessageFactory = new DMRMessageFactory(crcMaskManager);
}
/**
* Sets the sync pattern for the next C4FM sync detection notification via syncDetected().
* @param pattern that was detected.
*/
public void setSyncPattern(DMRSyncPattern pattern)
{
mSyncPattern = pattern;
}
/**
* C4FM symbol stream input method. Dibits are delayed via the burst delay line before framing.
* @param symbol to process
* @return true if a valid message was dispatched.
*/
@Override
public boolean process(Dibit symbol)
{
return process(symbol, Float.NaN);
}
/**
* C4FM symbol stream input method. Dibits and soft symbols are delayed via the burst delay lines before framing.
* @param symbol decision
* @param softSymbol equalized soft symbol in radians
* @return true if a valid message was dispatched.
*/
@Override
public boolean process(Dibit symbol, float softSymbol)
{
float ejectedSoftSymbol = mSoftSymbolDelayLine[mSoftSymbolDelayLinePointer];
mSoftSymbolDelayLine[mSoftSymbolDelayLinePointer++] = softSymbol;
mSoftSymbolDelayLinePointer %= DELAY_LINE_LENGTH;
return processDibit(mDibitDelayLine.insert(symbol), ejectedSoftSymbol);
}
/**
* C4FM sync detection notification where the most recently processed dibit is the final dibit of the sync pattern
* that was set via setSyncPattern(). Overwrites the sync dibits in the delay line with the detected sync pattern
* to eliminate any sync bit errors and signals that the next dibit ejected from the delay line is the first dibit
* of the burst.
*/
@Override
public void syncDetected()
{
if(DMRSyncPattern.SYNC_PATTERNS.contains(mSyncPattern))
{
mDibitDelayLine.update(mSyncPattern.toDibits());
syncDetected(mSyncPattern);
}
}
/**
* Primary input method for receiving the stream of demodulated dibit symbols.
* @param dibit to process.
*/
@Override
public void receive(Dibit dibit)
{
processDibit(dibit, Float.NaN);
}
/**
* Processes the dibit
* @param dibit to process
* @param softSymbol for the dibit in radians, or NaN.
* @return true if a valid message was dispatched.
*/
private boolean processDibit(Dibit dibit, float softSymbol)
{
mDibitCounter++;
mDibitSinceTimestampCounter++;
@@ -93,23 +180,27 @@ public class DMRMessageFramer implements Listener<Dibit>
{
if(mBufferAActive)
{
mSoftBufferA[mBufferAPointer] = softSymbol;
mBufferA[mBufferAPointer++] = dibit;
if(mBufferAPointer >= mBufferA.length)
{
dispatchBufferA();
return dispatchBufferA();
}
}
else
{
mSoftBufferB[mBufferBPointer] = softSymbol;
mBufferB[mBufferBPointer++] = dibit;
if(mBufferBPointer >= mBufferB.length)
{
dispatchBufferB();
return dispatchBufferB();
}
}
}
return false;
}
/**
@@ -121,7 +212,11 @@ public class DMRMessageFramer implements Listener<Dibit>
(!mBufferAActive && mBufferBPattern.isVoicePattern()));
}
private void dispatchBufferA()
/**
* Dispatches the burst assembled in buffer A.
* @return true if the dispatched burst message is valid.
*/
private boolean dispatchBufferA()
{
mDibitCounter -= 144;
@@ -150,7 +245,8 @@ public class DMRMessageFramer implements Listener<Dibit>
mDibitCounter = 0;
CorrectedBinaryMessage message = getMessage(mBufferA);
CACH cach = CACH.getCACH(message);
float[] softBits = getSoftBits(mSoftBufferA);
CACH cach = CACH.getCACH(message, softBits);
if(mBufferAPattern.hasCACH() && cach.isValid() && mBufferATimeslot != cach.getTimeslot())
{
@@ -167,7 +263,9 @@ public class DMRMessageFramer implements Listener<Dibit>
}
}
dispatch(mMessageFactory.create(mBufferAPattern, message, cach, getTimestamp(), mBufferATimeslot));
IMessage burstMessage = mMessageFactory.create(mBufferAPattern, message, softBits, cach,
getTimestamp(), mBufferATimeslot);
dispatch(burstMessage);
//Since voice frames only have sync on the first burst, we use pseudo-patterns to track the rest of the bursts
//across the voice super-frame. If the current pattern is a voice frame, set it to the next pseudo voice sync.
@@ -187,9 +285,15 @@ public class DMRMessageFramer implements Listener<Dibit>
{
mAssemblingBurst = false;
}
return burstMessage != null && burstMessage.isValid();
}
private void dispatchBufferB()
/**
* Dispatches the burst assembled in buffer B.
* @return true if the dispatched burst message is valid.
*/
private boolean dispatchBufferB()
{
mDibitCounter -= 144;
@@ -218,7 +322,8 @@ public class DMRMessageFramer implements Listener<Dibit>
mDibitCounter = 0;
CorrectedBinaryMessage burst = getMessage(mBufferB);
CACH cach = CACH.getCACH(burst);
float[] softBits = getSoftBits(mSoftBufferB);
CACH cach = CACH.getCACH(burst, softBits);
if(mBufferBPattern.hasCACH() && cach.isValid() && mBufferBTimeslot != cach.getTimeslot())
{
@@ -236,7 +341,9 @@ public class DMRMessageFramer implements Listener<Dibit>
}
}
dispatch(mMessageFactory.create(mBufferBPattern, burst, cach, getTimestamp(), mBufferBTimeslot));
IMessage burstMessage = mMessageFactory.create(mBufferBPattern, burst, softBits, cach,
getTimestamp(), mBufferBTimeslot);
dispatch(burstMessage);
//Since voice frames only have sync on the first burst, we use pseudo-patterns to track the rest of the bursts
//across the voice super-frame. If the current pattern is a voice frame, set it to the next pseudo voice sync.
@@ -256,6 +363,8 @@ public class DMRMessageFramer implements Listener<Dibit>
{
mAssemblingBurst = false;
}
return burstMessage != null && burstMessage.isValid();
}
/**
@@ -295,6 +404,28 @@ public class DMRMessageFramer implements Listener<Dibit>
mMessageListener = listener;
}
/**
* Converts the soft symbol buffer into soft bits (one per message bit) where a positive value favors a 1 bit.
* @param softSymbols buffer
* @return soft bits or null if any of the soft symbols were not provided.
*/
private static float[] getSoftBits(float[] softSymbols)
{
float[] softBits = new float[softSymbols.length * 2];
for(int x = 0; x < softSymbols.length; x++)
{
if(Float.isNaN(softSymbols[x]))
{
return null;
}
setSoftBits(softSymbols[x], softBits, x * 2);
}
return softBits;
}
/**
* Creates a binary message from the dibit buffer.
*

View File

@@ -187,20 +187,21 @@ public class DMRMessageProcessor implements Listener<IMessage>
if(message.getTimeslot() == 1)
{
FullLCMessage flco = mFLCAssemblerTimeslot1.process(voice.getEMB().getLCSS(),
voice.getFLCFragment(), message.getTimestamp());
voice.getFLCFragment(), voice.getFLCFragmentSoftBits(), message.getTimestamp());
receive(flco);
}
else
{
FullLCMessage flco = mFLCAssemblerTimeslot2.process(voice.getEMB().getLCSS(),
voice.getFLCFragment(), message.getTimestamp());
voice.getFLCFragment(), voice.getFLCFragmentSoftBits(), message.getTimestamp());
receive(flco);
}
if(voice.hasCACH())
{
CACH cach = voice.getCACH();
ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(), message.getTimestamp());
ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(),
cach.getPayloadSoftBits(), message.getTimestamp());
receive(slco);
}
}
@@ -210,7 +211,8 @@ public class DMRMessageProcessor implements Listener<IMessage>
if(dmrBurst.hasCACH())
{
CACH cach = dmrBurst.getCACH();
ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(), message.getTimestamp());
ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(),
cach.getPayloadSoftBits(), message.getTimestamp());
receive(slco);
}

View File

@@ -1,766 +0,0 @@
/*
* *****************************************************************************
* 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.dmr;
import io.github.dsheirer.dsp.filter.interpolator.LinearInterpolator;
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.gui.viewer.sync.SyncResultsViewer;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.dmr.sync.DMRSoftSyncDetector;
import io.github.dsheirer.module.decode.dmr.sync.DMRSoftSyncDetectorFactory;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncDetectMode;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncModeMonitor;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
import io.github.dsheirer.sample.Listener;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
/**
* DMR soft symbol processor. Processes a stream of differentially decoded samples to align to the symbol timing and
* symbol period and extract symbols from the sample stream.
*
* Uses two soft sync correlation detectors to achieve initial fine sync with a primary sync detector and a lagging
* sync detector sampling at 1/2 the symbol period to ensure the sync is detected while only calculating at each symbol
* period interval. Once a sync is detected, fine sync is declared and the lagging sync detector is disabled. Fine sync
* disables whenever the quantity of processed symbols exceeds one DMR burst length and the lagging detector
* re-enables.
*
* The sync mode monitor processes the sync detections to identify the dominant sync mode for the channel and then
* disables processing of the two unused sync modes (BASE STATION, MOBILE STATION, or DIRECT MODE) for the remainder
* of the channel.
*
* This processor uses a symbol delay buffer (dibit delay line) to retain a partial DMR burst from CACH up through the
* sync field of the burst. This allows the optimize method to resample the burst symbols once the sync is detected and
* optimal symbol timing and symbol period are calculated. This ensures the best symbol decisions are then passed to
* the message framer along with the sync detection.
*
* Buffer Structure & Size = 129 Dibits: [2 pad][12 CACH][54 Message Prefix][24 Sync][24 workspace][1 pad]
*
* The equalizer calculates balance and gain values to apply to the differentially decoded sample stream to optimize
* the constellation skew and openness. The equalizer is updated at each quality sync detection and then the equalizer
* values are applied across the entire burst through to the next sync detection. DMR sync patterns are made up of
* equal quantities of +3 and -3 symbols which support the equalizer update approach.
*/
public class DMRSoftSymbolProcessor
{
private static final int BUFFER_PROTECTED_REGION_DIBITS = 92;
private static final int BUFFER_WORKSPACE_LENGTH_DIBITS = 25; //This can be adjusted for efficiency
private static final int BUFFER_LENGTH_DIBITS = BUFFER_PROTECTED_REGION_DIBITS + BUFFER_WORKSPACE_LENGTH_DIBITS;
private static final float EQUALIZER_LOOP_GAIN = 0.15f;
private static final float MAXIMUM_EQUALIZER_BALANCE = (float)(Math.PI / 3.0);
private static final float MAXIMUM_EQUALIZER_GAIN = 1.25f;
private static final float MAXIMUM_POSITIVE_SAMPLE_PHASE = 3.5f;
private static final float MAXIMUM_NEGATIVE_SAMPLE_PHASE = -3.5f;
private static final float SAMPLES_PER_SYMBOL_ALLOWABLE_DEVIATION = 0.005f; //.5%
private static final float SYMBOL_QUADRANT_BOUNDARY = (float)(Math.PI / 2.0);
private static final float SYNC_DETECTION_THRESHOLD = 60;
private static final float SYNC_OPTIMIZED_THRESHOLD = 80;
private static final float SYNC_EQUALIZED_THRESHOLD = 100;
private static final float TWO_PI = (float)(Math.PI * 2.0);
private DMRSyncModeMonitor mSyncModeMonitor = new DMRSyncModeMonitor();
private DMRSoftSyncDetector mSyncDetector = DMRSoftSyncDetectorFactory.getDetector();
private DMRSoftSyncDetector mSyncDetectorSecondary = DMRSoftSyncDetectorFactory.getDetector();
private DibitToByteBufferAssembler mDibitAssembler = new DibitToByteBufferAssembler(300);
//Dibit delay line sizing: CACH(12) + MESSAGE_PREFIX(54) + SYNC(24)
private DibitDelayLine mDibitDelayLine = new DibitDelayLine(90);
private DMRMessageFramer mMessageFramer;
private boolean mFineSync = false;
private boolean mEqualizerInitialized = false;
private double mNoiseStandardDeviationThreshold;
private double mSecondarySyncOffset;
private double mSamplesPerSymbol;
private double mObservedSamplesPerSymbol;
private double mSamplePoint;
private float mEqualizerBalance = 0.0f;
private float mEqualizerGain = 1.0f;
private float mOptimizeFineIncrement;
private float[] mBuffer;
private int mBufferInterpolatorReservedRegion;
private int mBufferLoadPointer;
private int mBufferPointer;
private int mBufferWorkspaceLength;
private int mSymbolsSinceLastSync = 0;
private SyncResultsViewer mSyncResultsViewer;
private FeedbackDecoder mFeedbackDecoder;
/**
* Constructs an instance
* @param messageFramer to receive demodulated symbols/dibits and sync detect notifications
*/
public DMRSoftSymbolProcessor(DMRMessageFramer messageFramer, FeedbackDecoder feedbackDecoder)
{
if(messageFramer == null)
{
throw new IllegalArgumentException("Message framer cannot be null");
}
mMessageFramer = messageFramer;
mFeedbackDecoder = feedbackDecoder;
mSyncModeMonitor.add(mSyncDetector);
mSyncModeMonitor.add(mSyncDetectorSecondary);
}
/**
* Sets base station sync detection mode. This disables detection for MOBILE and DIRECT sync patterns. This can
* be used for traffic channels that are known to be part of a base station.
* @param enabled true for base mode only, false otherwise.
*/
public void setBaseStationMode(boolean enabled)
{
mSyncModeMonitor.setMode(enabled ? DMRSyncDetectMode.BASE_ONLY : DMRSyncDetectMode.AUTOMATIC);
}
/**
* Registers the listener to receive demodulated bit stream buffers.
* @param listener to register
*/
public void setBufferListener(Listener<ByteBuffer> listener)
{
mDibitAssembler.setBufferListener(listener);
}
/**
* Indicates if there is a registered buffer listener
*/
public boolean hasBufferListener()
{
return mDibitAssembler.hasBufferListeners();
}
/**
* Sets the reference timestamp from the incoming sample stream to use for assigning a timestamp to framed messages.
* @param timestamp to set for reference
*/
public void setTimestamp(long timestamp)
{
mMessageFramer.setTimestamp(timestamp);
}
/**
* Primary input method for receiving a stream of demodulated samples to process into symbols.
* @param samples to process
*/
public void receive(float[] samples)
{
int samplesPointer = 0;
while(samplesPointer < samples.length)
{
if(mBufferLoadPointer == mBuffer.length)
{
System.arraycopy(mBuffer, mBufferWorkspaceLength, mBuffer, 0, mBuffer.length - mBufferWorkspaceLength);
mBufferLoadPointer -= mBufferWorkspaceLength;
mBufferPointer -= mBufferWorkspaceLength;
}
int copyLength = Math.min(mBuffer.length - mBufferLoadPointer, samples.length - samplesPointer);
System.arraycopy(samples, samplesPointer, mBuffer, mBufferLoadPointer, copyLength);
samplesPointer += copyLength;
mBufferLoadPointer += copyLength;
for(int x = mBufferPointer; x < mBufferLoadPointer; x++)
{
//Unwrap phases
if(mBuffer[x - 1] > 1.5f && mBuffer[x] < -1.5f)
{
mBuffer[x] += TWO_PI;
}
else if(mBuffer[x - 1] < -1.5f && mBuffer[x] > 1.5f)
{
mBuffer[x] -= TWO_PI;
}
//Apply equalizer adjustments
mBuffer[x] += mEqualizerBalance;
mBuffer[x] *= mEqualizerGain;
//Allow the equalized buffer samples to exceed PI (3.14) by a small factor (3.5) to ensure the optimize
// and equalizer update functions work correctly. The toSymbol() method will correctly map any symbols
// that exceed +/- PI into the correct quadrant.
mBuffer[x] = Math.min(mBuffer[x], MAXIMUM_POSITIVE_SAMPLE_PHASE);
mBuffer[x] = Math.max(mBuffer[x], MAXIMUM_NEGATIVE_SAMPLE_PHASE);
}
float softSymbol, primaryScore, secondaryScore;
double secondary;
int secondaryIntegral;
while(mBufferPointer < (mBufferLoadPointer - mBufferInterpolatorReservedRegion))
{
mBufferPointer++;
mSamplePoint--;
if(mSamplePoint < 1)
{
if(mSymbolsSinceLastSync > 144)
{
mFineSync = false;
}
softSymbol = LinearInterpolator.calculate(mBuffer[mBufferPointer], mBuffer[mBufferPointer + 1], mSamplePoint);
mFeedbackDecoder.broadcast(softSymbol);
mSyncDetector.process(softSymbol);
Dibit symbol = toSymbol(softSymbol);
//Store the symbol in the delay line and broadcast the delayed ejected symbol to the message framer
//and the bitstream assembler.
Dibit ejected = mDibitDelayLine.insert(symbol);
mMessageFramer.receive(ejected);
mDibitAssembler.receive(ejected);
mSymbolsSinceLastSync++;
if(mFineSync)
{
if(mSymbolsSinceLastSync >= 144)
{
if(mMessageFramer.isVoiceSuperFrame())
{
mSymbolsSinceLastSync -= 144;
}
else
{
primaryScore = mSyncDetector.calculate();
//Debug - visualize the sync to show samples and symbol timing
// if(primaryScore > SYNC_DETECTION_THRESHOLD)
// {
// visualizeSyncDetect(mSyncDetector.getDetectedPattern(), primaryScore, true);
// }
if(primaryScore > SYNC_DETECTION_THRESHOLD && optimizeFine(mSyncDetector.getDetectedPattern()))
{
//Update the sync mode monitor and message framer
mSyncModeMonitor.detected(mSyncDetector.getDetectedPattern());
mMessageFramer.syncDetected(mSyncDetector.getDetectedPattern());
mSymbolsSinceLastSync = 0;
}
else
{
mFineSync = false;
mSyncDetectorSecondary.reset();
secondary = mBufferPointer + mSamplePoint - mSecondarySyncOffset;
secondaryIntegral = (int)Math.floor(secondary);
softSymbol = LinearInterpolator.calculate(mBuffer[secondaryIntegral], mBuffer[secondaryIntegral + 1], secondary - secondaryIntegral);
mSyncDetectorSecondary.process(softSymbol);
}
}
}
}
else
{
primaryScore = mSyncDetector.calculate();
secondary = mBufferPointer + mSamplePoint - mSecondarySyncOffset;
secondaryIntegral = (int)Math.floor(secondary);
softSymbol = LinearInterpolator.calculate(mBuffer[secondaryIntegral], mBuffer[secondaryIntegral + 1], secondary - secondaryIntegral);
secondaryScore = mSyncDetectorSecondary.processAndCalculate(softSymbol);
if(primaryScore > SYNC_DETECTION_THRESHOLD && optimizeCoarse(mSyncDetector.getDetectedPattern(), 0))
{
mMessageFramer.syncDetected(mSyncDetector.getDetectedPattern());
mFineSync = true;
mSymbolsSinceLastSync = 0;
}
else if(secondaryScore > SYNC_DETECTION_THRESHOLD && optimizeCoarse(mSyncDetectorSecondary.getDetectedPattern(), -mSecondarySyncOffset))
{
mMessageFramer.syncDetected(mSyncDetectorSecondary.getDetectedPattern());
mFineSync = true;
mSymbolsSinceLastSync = 0;
}
}
//Add another symbol's worth of samples to the counter
mSamplePoint += mObservedSamplesPerSymbol;
}
}
}
}
/**
* 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.
*/
private boolean isNoisy(double offset)
{
StandardDeviation standardDeviation = new StandardDeviation();
int start = (int)Math.floor(offset - (23 * mObservedSamplesPerSymbol));
int end = (int)Math.ceil(offset);
end = Math.min(end, mBuffer.length - 1);
for(int i = start; i < end; i++)
{
standardDeviation.increment(mBuffer[i] - mBuffer[i + 1]);
}
return standardDeviation.getResult() > mNoiseStandardDeviationThreshold;
}
/**
* Adjusts the symbol timing and symbol spacing to identify the best achievable sync correlation score and apply
* those adjustments when the correlation score exceeds a positive sync detection threshold.
* @param pattern that was detected
* @param additionalOffset from current mBufferPointer and mSamplePoint. This can be zero offset for the primary
* sync detector or an offset for the lagging sync detectors.
* @return true if there is a positive sync detection.
*/
private boolean optimizeCoarse(DMRSyncPattern pattern, double additionalOffset)
{
//Offset is the start of the first sample of the first symbol of the sync pattern calculated from the current
//buffer pointer and sample point which should be the final sample of the final symbol of the detected sync.
double offset = mBufferPointer + mSamplePoint + additionalOffset;
//Reject any sync detections where the sample:sample standard deviation exceeds the noise threshold.
if(isNoisy(offset))
{
return false;
}
//Find the optimal symbol timing
double stepSize = mSamplesPerSymbol / 8.0; //Start at 1/8th of the samples per symbol
double stepSizeMin = mOptimizeFineIncrement;
double adjustment = 0.0;
double adjustmentMax = mSamplesPerSymbol / 4.0;
double candidate = offset;
float scoreCenter = score(candidate, mObservedSamplesPerSymbol, pattern);
candidate = offset - stepSize;
float scoreLeft = score(candidate, mObservedSamplesPerSymbol, pattern);
candidate = offset + stepSize;
float scoreRight = score(candidate, mObservedSamplesPerSymbol, pattern);
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, mObservedSamplesPerSymbol, pattern);
}
else if(scoreRight > scoreLeft && scoreRight > scoreCenter)
{
adjustment += stepSize;
scoreLeft = scoreCenter;
scoreCenter = scoreRight;
candidate = offset + adjustment + stepSize;
scoreRight = score(candidate, mObservedSamplesPerSymbol, pattern);
}
else
{
stepSize *= 0.5f;
if(stepSize > stepSizeMin)
{
candidate = offset + adjustment - stepSize;
scoreLeft = score(candidate, mObservedSamplesPerSymbol, pattern);
candidate = offset + adjustment + stepSize;
scoreRight = score(candidate, mObservedSamplesPerSymbol, pattern);
}
}
}
//If we didn't find an optimal correlation score above the 95 threshold, return a false sync.
if(scoreCenter < 95)
{
return false;
}
adjustment += additionalOffset;
mSamplePoint += adjustment;
while(mSamplePoint < 0)
{
mSamplePoint++;
mBufferPointer--;
}
while(mSamplePoint > 1)
{
mSamplePoint--;
mBufferPointer++;
}
boolean resample = !mEqualizerInitialized || (Math.abs(adjustment) > 0.25);
updateEqualizer(pattern);
// visualizeSyncDetect(pattern, scoreCenter, true);
//If the equalizer was just initialized or the timing error adjustment is high enough, resample the symbols.
// Otherwise, overwrite the captured sync pattern in the delay buffer with the detected sync pattern to
// eliminate any sync bit errors.
if(resample)
{
double resamplePointer = mBufferPointer + mSamplePoint;
resamplePointer -= (89 * mObservedSamplesPerSymbol); //Start at 89 (+ 1 current = 90) symbols
int integral;
for(int x = 0; x < 66; x++)
{
integral = (int)Math.floor(resamplePointer);
if(integral >= 0)
{
float resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[integral], mBuffer[integral + 1], resamplePointer - integral);
mDibitDelayLine.insert(toSymbol(resampledSoftSymbol));
}
else
{
//This shouldn't happen since there's 2x dibits of padding on the front side, but just in case.
mDibitDelayLine.insert(Dibit.D01_PLUS_3);
}
resamplePointer += mObservedSamplesPerSymbol;
}
//We don't need to resample the sync region ... just use the actual sync dibit values.
for(Dibit dibit: pattern.toDibits())
{
mDibitDelayLine.insert(dibit);
}
}
else
{
//Overwrite the most recent 24 dibits with the detected sync so there's no sync bit errors
mDibitDelayLine.update(pattern.toDibits());
}
return true;
}
/**
* Performs fine-grained symbol timing optimization of +/- .4% of the samples per symbol as needed.
* @param pattern that was detected
* @return true if detection score exceeds the optimized and equalized threshold for a quality sync detect.
*/
private boolean optimizeFine(DMRSyncPattern pattern)
{
//Offset is the sample for the last symbol of the detected sync pattern.
double offset = mBufferPointer + mSamplePoint;
float currentScore = score(offset, mObservedSamplesPerSymbol, pattern);
double candidate = offset - mOptimizeFineIncrement;
float candidateScore = score(candidate, mObservedSamplesPerSymbol, pattern);
boolean adjusted = false;
if(candidateScore > currentScore)
{
mSamplePoint -= mOptimizeFineIncrement;
adjusted = true;
}
else
{
candidate = offset + mOptimizeFineIncrement;
candidateScore = score(candidate, mObservedSamplesPerSymbol, pattern);
if(candidateScore > currentScore)
{
mSamplePoint += mOptimizeFineIncrement;
adjusted = true;
}
}
if(adjusted)
{
while(mSamplePoint < 0)
{
mSamplePoint++;
mBufferPointer--;
}
while(mSamplePoint > 1)
{
mSamplePoint--;
mBufferPointer++;
}
}
if(candidateScore > SYNC_OPTIMIZED_THRESHOLD)
{
updateEqualizer(pattern);
}
return candidateScore > SYNC_EQUALIZED_THRESHOLD;
}
/**
* Update the equalizer balance and gain when the sync pattern is detected in the sample buffer. This method
* resamples the 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.
* @param pattern that was detected.
*/
private void updateEqualizer(DMRSyncPattern pattern)
{
float[] symbols = pattern.toSymbols();
double resampleStart = mBufferPointer + mSamplePoint;
int resampleStartIntegral = (int)Math.floor(resampleStart);
float resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral],
mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral);
float balanceAccumulator = resampledSoftSymbol - symbols[23];
float gainAccumulator = Math.abs(symbols[23]) - Math.abs(resampledSoftSymbol);
resampleStart -= (23 * mObservedSamplesPerSymbol); //Start at 89 (+ 1 current = 90) symbols
resampleStartIntegral = (int)Math.floor(resampleStart);
for(int x = 0; x < 23; x++)
{
if(resampleStartIntegral >= 0)
{
resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[resampleStartIntegral],
mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral);
balanceAccumulator += resampledSoftSymbol - symbols[x];
gainAccumulator += Math.abs(symbols[x]) - Math.abs(resampledSoftSymbol);
}
resampleStart += mObservedSamplesPerSymbol;
resampleStartIntegral = (int)Math.floor(resampleStart);
}
//Average the accumulated error over 24 sync symbols
balanceAccumulator /= -24.0f;
gainAccumulator /= (24.0f * Dibit.D01_PLUS_3.getIdealPhase());
if(mEqualizerInitialized)
{
//Limit equalizer adjustments at each sync after the initial equalizer setup.
mEqualizerBalance += (balanceAccumulator * EQUALIZER_LOOP_GAIN);
mEqualizerGain += (gainAccumulator * EQUALIZER_LOOP_GAIN);
}
else
{
mEqualizerBalance += balanceAccumulator;
mEqualizerGain += gainAccumulator;
}
//Constrain balance to +/- PI/4
mEqualizerBalance = Math.min(mEqualizerBalance, MAXIMUM_EQUALIZER_BALANCE);
mEqualizerBalance = Math.max(mEqualizerBalance, -MAXIMUM_EQUALIZER_BALANCE);
mFeedbackDecoder.processPLLError(mEqualizerBalance);
//Constrain gain between 1.0f and 1.35f
mEqualizerGain = Math.min(mEqualizerGain, MAXIMUM_EQUALIZER_GAIN);
mEqualizerGain = Math.max(mEqualizerGain, 1.0f);
if(!mEqualizerInitialized)
{
//Apply the initial gain settings to the samples in the buffer so that the symbols can be resampled.
for(int x = 0; x < mBufferPointer; x++)
{
mBuffer[x] = (mBuffer[x] + mEqualizerBalance) * mEqualizerGain;
}
mEqualizerInitialized = true;
}
}
/**
* Debug method to visualize the contents of the sample buffer.
*/
public void visualizeBufferContents()
{
//This will block until the viewer is constructed and showing.
if(mSyncResultsViewer == null)
{
mSyncResultsViewer = new SyncResultsViewer();
}
int offset = 3;
int length = (int)Math.ceil(mObservedSamplesPerSymbol * 90) + (2 * offset);
int end = mBufferPointer + offset;
int start = end - length;
float[] symbols = new float[90];
float[] samples = Arrays.copyOfRange(mBuffer, start, end);
float[] intervals = new float[90];
int adjust = mBufferPointer - length + offset;
double pointer = mBufferPointer + mSamplePoint - adjust;
for(int x = 89; x >= 0; x--)
{
intervals[x] = (float)pointer;
pointer -= mObservedSamplesPerSymbol;
}
float[] sync = new float[90];
Arrays.fill(sync, -1.57f); //Cutoff between -1 & -1 to show symbol timings
CountDownLatch countDownLatch = new CountDownLatch(1);
mSyncResultsViewer.receive(symbols, sync, samples, intervals, mEqualizerBalance, mEqualizerGain,
"BUFFER CONTENTS", countDownLatch);
try
{
countDownLatch.await();
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
}
/**
* Debug method to visualize the contents of the sync detection for the samples and symbols and symbol timing.
* @param pattern that was detected.
* @param score from the sync detector
* @param primary sync detector (true) or secondary (false)
*/
public void visualizeSyncDetect(DMRSyncPattern pattern, float score, boolean primary)
{
//This will block until the viewer is constructed and showing.
if(mSyncResultsViewer == null)
{
mSyncResultsViewer = new SyncResultsViewer();
}
int offset = 3;
int length = (int)Math.ceil(mObservedSamplesPerSymbol * 23) + (2 * offset);
int end = mBufferPointer + offset;
int start = end - length;
float[] symbols = new float[24];
float[] samples = Arrays.copyOfRange(mBuffer, start, end);
float[] intervals = new float[24];
int adjust = mBufferPointer - length + offset;
double pointer = mBufferPointer + mSamplePoint - adjust;
double symbolPointer = mBufferPointer + mSamplePoint - (mObservedSamplesPerSymbol * 23);
int symbolIntegral = (int)Math.floor(symbolPointer);
double mu = symbolPointer - symbolIntegral;
for(int x = 0; x < 24; x++)
{
symbols[x] = LinearInterpolator.calculate(mBuffer[symbolIntegral], mBuffer[symbolIntegral + 1], mu);
symbolPointer += mObservedSamplesPerSymbol;
symbolIntegral = (int)Math.floor(symbolPointer);
mu = symbolPointer - symbolIntegral;
}
for(int x = 23; x >= 0; x--)
{
intervals[x] = (float)pointer;
pointer -= mObservedSamplesPerSymbol;
}
CountDownLatch countDownLatch = new CountDownLatch(1);
mSyncResultsViewer.receive(symbols, pattern.toSymbols(), samples, intervals, mEqualizerBalance, mEqualizerGain,
pattern + " Score: " + score + (primary ? " PRIMARY" : " SECONDARY"), countDownLatch);
try
{
countDownLatch.await();
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
}
/**
* Calculates the sync correlation score for sync pattern ending at the specified offset and samples per symbol interval.
* @param offset to the final symbol in the soft symbol buffer
* @param samplesPerSymbol spacing to test for.
* @param pattern to correlate against.
* @return correlation score.
*/
public float score(double offset, double samplesPerSymbol, DMRSyncPattern pattern)
{
float[] symbols = pattern.toSymbols();
int maxPointer = mBuffer.length - 1;
float softSymbol;
double pointer = offset - (samplesPerSymbol * 23.0);
int bufferPointer = (int)Math.floor(pointer);
double fractional = pointer - bufferPointer;
float score = 0;
for(int x = 0; x < 24; x++)
{
if(bufferPointer < maxPointer)
{
softSymbol = LinearInterpolator.calculate(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], fractional);
}
else
{
softSymbol = 0.0f;
}
score += softSymbol * symbols[x];
pointer += samplesPerSymbol;
bufferPointer = (int)Math.floor(pointer);
fractional = pointer - bufferPointer;
}
return score;
}
/**
* Sets or updates the samples per symbol
* @param samplesPerSymbol to apply.
*/
public void setSamplesPerSymbol(float samplesPerSymbol)
{
mSamplesPerSymbol = samplesPerSymbol;
float maxSamplesPerSymbol = samplesPerSymbol * (1.0f + SAMPLES_PER_SYMBOL_ALLOWABLE_DEVIATION);
mOptimizeFineIncrement = samplesPerSymbol * .004f; //Adjust at .4%
//Protected region for score() method to avoid IndexOutOfBounds = Max Adjustment + Max Step Size for Max SPS
mBufferInterpolatorReservedRegion = (int)Math.ceil((maxSamplesPerSymbol / 2.0f) + (maxSamplesPerSymbol / 10.0f));
mObservedSamplesPerSymbol = mSamplesPerSymbol;
mNoiseStandardDeviationThreshold = Dibit.D01_PLUS_3.getIdealPhase() * 2 / mSamplesPerSymbol * 1.2; //120% of optimal
mSamplePoint = mSamplesPerSymbol;
mSecondarySyncOffset = mSamplesPerSymbol / 2.0;
mBufferWorkspaceLength = (int)Math.ceil(BUFFER_WORKSPACE_LENGTH_DIBITS * mSamplesPerSymbol);
int bufferLength = (int)(Math.ceil(BUFFER_LENGTH_DIBITS * mSamplesPerSymbol));
mBuffer = new float[bufferLength];
mBufferLoadPointer = (int)Math.ceil(BUFFER_PROTECTED_REGION_DIBITS * mSamplesPerSymbol);
mBufferPointer = mBufferLoadPointer;
}
/**
* Decodes the sample value to determine the correct QPSK quadrant and maps the value to a Dibit symbol.
* @param sample in radians.
* @return symbol decision.
*/
private static Dibit toSymbol(float sample)
{
if(sample > 0)
{
return sample > SYMBOL_QUADRANT_BOUNDARY ? Dibit.D01_PLUS_3 : Dibit.D00_PLUS_1;
}
else
{
return sample < -SYMBOL_QUADRANT_BOUNDARY ? Dibit.D11_MINUS_3 : Dibit.D10_MINUS_1;
}
}
}

View File

@@ -0,0 +1,208 @@
/*
* *****************************************************************************
* 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.dmr;
import io.github.dsheirer.dsp.c4fm.C4FMSymbolProcessor;
import io.github.dsheirer.dsp.c4fm.ISoftC4FMSyncDetector;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.dmr.sync.DMRSoftSyncDetector;
import io.github.dsheirer.module.decode.dmr.sync.DMRSoftSyncDetectorFactory;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncDetectMode;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncModeMonitor;
import io.github.dsheirer.module.decode.dmr.sync.DMRSyncPattern;
/**
* DMR implementation of the C4FM symbol processor.
*
* DMR employs multiple sync patterns (BASE STATION, MOBILE STATION and DIRECT MODE, each with DATA and VOICE variants)
* and the sync detectors correlate against all active sync patterns, reporting the best scoring pattern. The sync mode
* monitor processes the sync detections to identify the dominant sync mode for the channel and then disables
* processing of the two unused sync modes for the remainder of the channel.
*
* On each sync detection, the detected sync pattern is passed to the message framer which maintains a delay line to
* retain the burst dibits from the CACH through the sync pattern so that the full burst can be framed.
*
* DMR sync patterns are made up of equal quantities of +3 and -3 symbols which supports the default (mean) balance
* measurement approach of the parent class.
*/
public class DMRSymbolProcessor extends C4FMSymbolProcessor
{
private static final int BURST_DIBIT_LENGTH = 144;
private static final int SYNC_DIBIT_LENGTH = 24;
//Maximum sync interval for data bursts is two bursts for single timeslot (ie mobile/direct mode) data.
private static final int MAX_DATA_SYNC_INTERVAL = BURST_DIBIT_LENGTH * 2 + SYNC_DIBIT_LENGTH;
//Maximum sync interval for a voice super frame is 6x bursts per timeslot for single timeslot (ie direct mode) voice.
private static final int MAX_VOICE_SYNC_INTERVAL = BURST_DIBIT_LENGTH * 12 + SYNC_DIBIT_LENGTH;
//Training period is limited to the remainder of the burst that follows the sync since the next burst can be
//empty (ie noise) for mobile station and direct mode channels. Testing: longer periods (120-144) gained less
//than 0.2% on base station recordings, which doesn't justify the risk of training on empty timeslots.
private static final int EQUALIZER_TRAINING_PERIOD_SYMBOL_COUNT = 54;
private static final int EQUALIZER_SKIP_TRAINING_FRAME_COUNT = 10;
private static final int EQUALIZER_SYMBOL_LENGTH = 7;
private static final int SYMBOL_RATE = 4800;
private static final double OUTER_SYMBOL_DEVIATION = 1944.0; //Hertz
//Sync thresholds. Testing against DMR recordings and injected noise: lowering the optimized and equalized
//thresholds from 100 to 70 (60 - 70 perform the same) increased valid bursts 5% on multipath recordings and
//7 - 27% with injected noise. Lowering the detection threshold below 80 floods the optimizer with false syncs.
private static final float SYNC_THRESHOLD_DETECTED = 80;
private static final float SYNC_THRESHOLD_OPTIMIZED = 70;
private static final float SYNC_THRESHOLD_EQUALIZED = 70;
//Once synchronized, sync detections are only allowed within this many symbols of a burst boundary.
private static final int SYNC_WINDOW_SYMBOLS = 2;
private final DMRSyncModeMonitor mSyncModeMonitor = new DMRSyncModeMonitor();
private final DMRMessageFramer mMessageFramer;
/**
* Constructs an instance
*
* @param feedbackDecoder parent decoder
* @param messageFramer to receive symbol decisions (dibits) and sync notifications.
*/
public DMRSymbolProcessor(FeedbackDecoder feedbackDecoder, DMRMessageFramer messageFramer)
{
this(feedbackDecoder, messageFramer, DMRSoftSyncDetectorFactory.getDetector(),
DMRSoftSyncDetectorFactory.getDetector());
}
/**
* Constructs an instance
*
* @param feedbackDecoder parent decoder
* @param messageFramer to receive symbol decisions (dibits) and sync notifications.
* @param primary sync detector
* @param lagging sync detector
*/
private DMRSymbolProcessor(FeedbackDecoder feedbackDecoder, DMRMessageFramer messageFramer,
DMRSoftSyncDetector primary, DMRSoftSyncDetector lagging)
{
super(feedbackDecoder, messageFramer, primary, lagging);
mMessageFramer = messageFramer;
mSyncModeMonitor.add(primary);
mSyncModeMonitor.add(lagging);
}
/**
* Sets base station sync detection mode. This disables detection for MOBILE and DIRECT sync patterns. This can
* be used for traffic channels that are known to be part of a base station.
* @param enabled true for base mode only, false otherwise.
*/
public void setBaseStationMode(boolean enabled)
{
mSyncModeMonitor.setMode(enabled ? DMRSyncDetectMode.BASE_ONLY : DMRSyncDetectMode.AUTOMATIC);
}
/**
* Captures the detected sync pattern to update the sync mode monitor and the message framer.
* @param syncDetector that triggered the sync detection.
*/
@Override
protected void syncDetected(ISoftC4FMSyncDetector syncDetector)
{
if(syncDetector instanceof DMRSoftSyncDetector dmrSyncDetector)
{
DMRSyncPattern pattern = dmrSyncDetector.getDetectedPattern();
mSyncModeMonitor.detected(pattern);
mMessageFramer.setSyncPattern(pattern);
}
}
/**
* Once synchronized with at least two consecutive sync detections, only allows sync detections within a small
* window around each expected burst boundary (multiples of the burst length) to prevent false sync detections
* from burst payload (ie voice) symbols that would otherwise cause the framer to abandon the current burst.
* @param symbolsSinceLastSync count of symbols since the final symbol of the previous sync detection.
* @return true if a sync detection is allowed.
*/
@Override
protected boolean isSyncDetectionAllowed(int symbolsSinceLastSync)
{
if(!mSynchronized || getConsecutiveSyncPeriods() < 2)
{
return true;
}
int offset = symbolsSinceLastSync % BURST_DIBIT_LENGTH;
return Math.min(offset, BURST_DIBIT_LENGTH - offset) <= SYNC_WINDOW_SYMBOLS;
}
@Override
public boolean isDynamicBalanceTrackingRequired()
{
return true;
}
/**
* Maximum dibit interval between sync detections, which is longer while the framer is assembling a voice super
* frame where only the first burst of each super frame contains a sync pattern.
*/
@Override
public int getMaxMessageDibitLength()
{
return mMessageFramer.isVoiceSuperFrame() ? MAX_VOICE_SYNC_INTERVAL : MAX_DATA_SYNC_INTERVAL;
}
@Override
public int getEqualizerTrainingPeriodDurationSymbolCount()
{
return EQUALIZER_TRAINING_PERIOD_SYMBOL_COUNT;
}
@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;
}
}

View File

@@ -22,6 +22,8 @@ package io.github.dsheirer.module.decode.dmr.bptc;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.Hamming16;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import io.github.dsheirer.edac.soft.SoftProductCodeDecoder;
/**
* Block product turbo code (BPTC) for decoding Full Link Control messages reassembled from the 32-bit payload fragments
@@ -29,6 +31,41 @@ import io.github.dsheirer.edac.Hamming16;
*/
public class BPTC_128_77 extends BPTCBase
{
private static final int LENGTH = 128;
private static final int ROW_COUNT = 8;
private static final int COLUMN_COUNT = 16;
//Maximum correction cost (sum of the normalized channel soft bit magnitudes for the corrected bits) for a soft
//decision result. The embedded link control is only protected by a 5-bit checksum, so soft decisions that
//require correcting high reliability bits are rejected to avoid false decodes. Noise injection testing: all
//soft decisions with a cost up to 3.0 were correct, whereas most of those with a higher cost were false decodes.
private static final float SOFT_MAX_CORRECTION_COST = 3.0f;
//Maximum corrected bit count for a hard decision result. Hard decisions that correct more bits are treated as
//uncorrectable to avoid false decodes, given the weak 5-bit checksum. Noise injection testing: all hard
//decisions with more than 6 corrected bits were false decodes.
private static final int HARD_MAX_CORRECTED_BITS = 6;
//Soft decision decoder for the Hamming(16,11,4) rows and even parity columns
private static final SoftProductCodeDecoder SOFT_DECODER = new SoftProductCodeDecoder(
SoftBlockCode.fromCodewordTest(COLUMN_COUNT, word -> Hamming16.getSyndrome(toMessage(word), 0) == 0),
ROW_COUNT, SOFT_MAX_CORRECTION_COST);
/**
* Creates a binary message from the word where bit i of the word is message bit i.
*/
private static BinaryMessage toMessage(int word)
{
BinaryMessage message = new BinaryMessage(COLUMN_COUNT);
for(int i = 0; i < COLUMN_COUNT; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return message;
}
public BPTC_128_77()
{
super(new Hamming16(), 16, 8);
@@ -49,10 +86,38 @@ public class BPTC_128_77 extends BPTCBase
* @return decoded message.
*/
public CorrectedBinaryMessage extract(BinaryMessage message)
{
return extract(message, null);
}
/**
* Performs error detection and correction and extracts the payload from the BPTC encoded message. Hard decision
* results that exceed the maximum corrected bit count are treated as uncorrectable. When the hard decision
* decoder can't correct the message and soft bits are provided, applies iterative soft decision decoding.
* @param message with BPTC encoding.
* @param softBits for the 128 (interleaved) bits of the message where a positive value favors a 1 bit, or null.
* @return decoded message.
*/
public CorrectedBinaryMessage extract(BinaryMessage message, float[] softBits)
{
CorrectedBinaryMessage deinterleaved = deinterleave(message);
correct(deinterleaved);
if(deinterleaved.getCorrectedBitCount() > HARD_MAX_CORRECTED_BITS)
{
deinterleaved.setCorrectedBitCount(ERRORS_NOT_CORRECTED);
}
if(softBits != null && softBits.length == LENGTH && deinterleaved.getCorrectedBitCount() < 0)
{
CorrectedBinaryMessage softDecoded = decodeSoft(deinterleave(softBits));
if(softDecoded != null)
{
deinterleaved = softDecoded;
}
}
CorrectedBinaryMessage extractedMessage = new CorrectedBinaryMessage(77);
int pointer = 0;
@@ -80,6 +145,38 @@ public class BPTC_128_77 extends BPTCBase
return extractedMessage;
}
/**
* Deinterleaves the transmitted soft bits.
* @param interleaved soft bits
* @return deinterleaved soft bits.
*/
public static float[] deinterleave(float[] interleaved)
{
float[] deinterleaved = new float[LENGTH];
deinterleaved[127] = interleaved[127];
for(int i = 0; i < 127; i++)
{
deinterleaved[i] = interleaved[(i * 8) % 127];
}
return deinterleaved;
}
/**
* Iterative soft decision decoding of the de-interleaved BPTC(128,77) soft bits. Alternately decodes the
* Hamming(16,11,4) rows using Chase-Pyndiah decoding and the even parity columns using min-sum decoding,
* exchanging extrinsic soft information, until the decisions form a valid product codeword. Rejects decisions
* where the correction cost exceeds the maximum.
* @param channel de-interleaved soft bits where a positive value favors a 1 bit.
* @return decoded message with the corrected bit count set, or null if decoding doesn't converge or the correction
* cost exceeds the maximum.
*/
public CorrectedBinaryMessage decodeSoft(float[] channel)
{
return SOFT_DECODER.decode(channel);
}
/**
* Deinterleaves the transmitted message.
* @param interleaved message

View File

@@ -25,6 +25,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.Hamming13;
import io.github.dsheirer.edac.Hamming15;
import io.github.dsheirer.edac.IHamming;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -42,6 +43,21 @@ public class BPTC_196_96
public static final int CHECKSUM_COLUMN_COUNT = 4;
public static final int MESSAGE_START_INDEX = 4;
public static final int MAXIMUM_RECURSION_TURBO_DEPTH = 20;
private static final int ROW_COUNT = 13;
private static final int ROW_LENGTH = 15;
private static final int COLUMN_LENGTH = 13;
//Soft decision (Chase-Pyndiah) iterative decoding settings. Each half iteration decodes either all rows or all
//columns, starting with the rows. Alpha scales the extrinsic information fed to the next half iteration and beta
//is the reliability assigned to bits without a competing candidate codeword.
private static final int CHASE_LEAST_RELIABLE_BITS = 4;
private static final float[] SOFT_ALPHA = new float[]{0.0f, 0.2f, 0.3f, 0.5f, 0.7f, 0.9f, 1.0f, 1.0f};
private static final float[] SOFT_BETA = new float[]{0.2f, 0.4f, 0.6f, 0.8f, 1.0f, 1.0f, 1.0f, 1.0f};
private static final int[] COLUMN_WORD_INDEXES = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
private static final SoftBlockCode ROW_CODE = SoftBlockCode.fromCodewordTest(ROW_LENGTH,
word -> Hamming15.getSyndrome(toMessage(word, ROW_LENGTH), 0) == 0);
private static final SoftBlockCode COLUMN_CODE = SoftBlockCode.fromCodewordTest(COLUMN_LENGTH,
word -> Hamming13.getSyndrome(toMessage(word, COLUMN_LENGTH), COLUMN_WORD_INDEXES) == 0);
/**
* DMR De-interleaving indices
@@ -76,6 +92,40 @@ public class BPTC_196_96
{14, 29, 44, 59, 74, 89, 104, 119, 134, 149, 164, 179, 194},
{15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195}};
/**
* DMR Row Indices
*/
public static final int[][] ROW_INDEXES = new int[ROW_COUNT][ROW_LENGTH];
static
{
for(int row = 0; row < ROW_COUNT; row++)
{
for(int x = 0; x < ROW_LENGTH; x++)
{
ROW_INDEXES[row][x] = getIndex(x, row);
}
}
}
/**
* Creates a binary message from the word where bit i of the word is message bit i.
*/
private static BinaryMessage toMessage(int word, int length)
{
BinaryMessage message = new BinaryMessage(length);
for(int i = 0; i < length; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return message;
}
/**
* Performs de-interleave, error detect and correct and extracts a 96-bit message from the DMR BPTC(196,96)
* protected message
@@ -83,6 +133,19 @@ public class BPTC_196_96
* @return error corrected and extracted 96-bit message
*/
public static CorrectedBinaryMessage extract(CorrectedBinaryMessage original)
{
return extract(original, null);
}
/**
* Performs de-interleave, error detect and correct and extracts a 96-bit message from the DMR BPTC(196,96)
* protected message. When the hard decision decoder can't correct the message and soft bits are provided,
* applies iterative soft decision decoding.
* @param original uncorrected BPTC(196,96) message
* @param softBits for the 196 (interleaved) bits of the original message, or null.
* @return error corrected and extracted 96-bit message
*/
public static CorrectedBinaryMessage extract(CorrectedBinaryMessage original, float[] softBits)
{
//De-Interleave
CorrectedBinaryMessage message = deinterleave(original);
@@ -91,6 +154,43 @@ public class BPTC_196_96
boolean pursueShadows = true;
correct(message, pursueShadows);
if(softBits != null && softBits.length == BPTC_LENGTH && message.getCorrectedBitCount() < 0)
{
CorrectedBinaryMessage softDecoded = decodeSoft(deinterleave(softBits));
if(softDecoded != null)
{
message = softDecoded;
}
}
return extractPayload(message);
}
/**
* Applies soft decision decoding to the soft bits and extracts the 96-bit message. Can be used when a hard
* decision result fails a subsequent message integrity (ie CRC) check.
* @param softBits for the 196 (interleaved) bits of the message where a positive value favors a 1 bit.
* @return error corrected and extracted 96-bit message, or null if soft decision decoding doesn't converge.
*/
public static CorrectedBinaryMessage extractSoft(float[] softBits)
{
if(softBits == null || softBits.length != BPTC_LENGTH)
{
return null;
}
CorrectedBinaryMessage decoded = decodeSoft(deinterleave(softBits));
return decoded != null ? extractPayload(decoded) : null;
}
/**
* Extracts the 96-bit message and the 3 reserved bits from the corrected (de-interleaved) 196-bit message.
* @param message corrected and de-interleaved.
* @return extracted message with the corrected bit count transferred.
*/
private static CorrectedBinaryMessage extractPayload(CorrectedBinaryMessage message)
{
//Extract the 96-bit message from the corrected 196 bits
CorrectedBinaryMessage extracted = new CorrectedBinaryMessage(EXTRACTED_LENGTH);
@@ -130,6 +230,174 @@ public class BPTC_196_96
return extracted;
}
/**
* De-interleaves the transmitted soft bits of a DMR BPTC(196,96) protected message
* @param softBits that contains interleaved (transmitted) soft bits
* @return de-interleaved soft bits
*/
private static float[] deinterleave(float[] softBits)
{
float[] deinterleaved = new float[BPTC_LENGTH];
for(int x = 0; x < BPTC_DEINTERLEAVE.length; x++)
{
deinterleaved[x] = softBits[BPTC_DEINTERLEAVE[x]];
}
return deinterleaved;
}
/**
* Iterative soft decision (Chase-Pyndiah turbo product) decoding of the de-interleaved BPTC(196,96) soft bits.
* Alternately decodes the Hamming(15,11) rows and Hamming(13,9) columns, exchanging extrinsic soft information,
* until the decisions form a valid product codeword.
* @param channel de-interleaved soft bits where a positive value favors a 1 bit.
* @return decoded message with the corrected bit count set, or null if decoding doesn't converge.
*/
public static CorrectedBinaryMessage decodeSoft(float[] channel)
{
//Normalize the channel soft bits to a mean magnitude of 1 for the alpha and beta settings
float sum = 0.0f;
for(int i = 1; i < BPTC_LENGTH; i++)
{
sum += Math.abs(channel[i]);
}
if(sum <= 0.0f)
{
return null;
}
float scale = (BPTC_LENGTH - 1) / sum;
float[] received = new float[BPTC_LENGTH];
for(int i = 0; i < BPTC_LENGTH; i++)
{
received[i] = channel[i] * scale;
}
float[] extrinsic = new float[BPTC_LENGTH];
float[] input = new float[BPTC_LENGTH];
float[] output = new float[BPTC_LENGTH];
int[] rowWords = new int[ROW_COUNT];
int[] columnWords = new int[COLUMN_COUNT];
for(int halfIteration = 0; halfIteration < SOFT_ALPHA.length; halfIteration++)
{
float alpha = SOFT_ALPHA[halfIteration];
float beta = SOFT_BETA[halfIteration];
for(int i = 0; i < BPTC_LENGTH; i++)
{
input[i] = received[i] + alpha * extrinsic[i];
}
boolean decodeRows = halfIteration % 2 == 0;
boolean allDecoded = true;
if(decodeRows)
{
for(int row = 0; row < ROW_COUNT; row++)
{
rowWords[row] = ROW_CODE.decodeChase(input, ROW_INDEXES[row], output, CHASE_LEAST_RELIABLE_BITS, beta);
allDecoded &= rowWords[row] >= 0;
}
}
else
{
for(int column = 0; column < COLUMN_COUNT; column++)
{
columnWords[column] = COLUMN_CODE.decodeChase(input, COLUMN_INDEXES[column], output,
CHASE_LEAST_RELIABLE_BITS, beta);
allDecoded &= columnWords[column] >= 0;
}
}
//Check for a valid product codeword from the decisions of this half iteration
if(allDecoded)
{
CorrectedBinaryMessage decoded = new CorrectedBinaryMessage(BPTC_LENGTH);
if(received[0] > 0)
{
decoded.set(0);
}
int[][] decodedIndexes = decodeRows ? ROW_INDEXES : COLUMN_INDEXES;
int[] decodedWords = decodeRows ? rowWords : columnWords;
for(int w = 0; w < decodedWords.length; w++)
{
for(int i = 0; i < decodedIndexes[w].length; i++)
{
if((decodedWords[w] & (1 << i)) != 0)
{
decoded.set(decodedIndexes[w][i]);
}
}
}
if(isValidProductCodeword(decoded))
{
int corrected = 0;
for(int i = 0; i < BPTC_LENGTH; i++)
{
if(decoded.get(i) != (received[i] > 0))
{
corrected++;
}
}
decoded.setCorrectedBitCount(corrected);
return decoded;
}
}
//Normalize the extrinsic information to a mean magnitude of 1 for the next half iteration
float extrinsicSum = 0.0f;
for(int i = 1; i < BPTC_LENGTH; i++)
{
extrinsicSum += Math.abs(output[i]);
}
float extrinsicScale = extrinsicSum > 0.0f ? (BPTC_LENGTH - 1) / extrinsicSum : 0.0f;
for(int i = 0; i < BPTC_LENGTH; i++)
{
extrinsic[i] = output[i] * extrinsicScale;
}
}
return null;
}
/**
* Indicates if all rows and columns of the de-interleaved message pass the Hamming checks.
*/
private static boolean isValidProductCodeword(CorrectedBinaryMessage message)
{
for(int row = 0; row < ROW_COUNT; row++)
{
if(!isRowCorrect(row, message))
{
return false;
}
}
for(int column = 0; column < COLUMN_COUNT; column++)
{
if(!isColumnCorrect(column, message))
{
return false;
}
}
return true;
}
/**
* De-interleaves the transmitted bits of a DMR BPTC(196,96) protected message
* @param message that contains interleaved (transmitted) message
@@ -311,11 +579,36 @@ public class BPTC_196_96
* Performs turbo decode of the message by testing multiple paths across rows and columns to identify paths
* that resolve errors validated by the Hamming checks. Can also detect and often correct shadowing where the bit
* errors occur in a 2x2, 2x3, 3x2 or 3x3 column/row grid and can't normally be corrected by the Hamming code.
* When corrected, the message corrected bit count is set to the number of bits that differ from the received
* message.
*
* @param message to correct.
* @param pursueShadows to test intersection flipping for double/triple shadows.
* @return true if the message was corrected, false otherwise.
*/
public static boolean correct(CorrectedBinaryMessage message, boolean pursueShadows)
{
CorrectedBinaryMessage received = new CorrectedBinaryMessage(message);
boolean corrected = correctTurbo(message, pursueShadows);
if(corrected)
{
//The turbo decoder's incremental accounting can count a bit more than once, so measure the corrections
received.xor(message);
message.setCorrectedBitCount(received.cardinality());
}
return corrected;
}
/**
* Turbo decode implementation for the correct() method. Note: the corrected bit count set by this method is
* not accurate and is replaced by the correct() method.
* @param message to correct.
* @param pursueShadows to test intersection flipping for double/triple shadows.
* @return true if the message was corrected, false otherwise.
*/
private static boolean correctTurbo(CorrectedBinaryMessage message, boolean pursueShadows)
{
List<Integer> columnErrors = getColumnErrors(message);
List<Integer> rowErrors = getRowErrors(message);
@@ -413,7 +706,7 @@ public class BPTC_196_96
//Set pursue shadows to false so we don't recursively chase shadows
pursueShadows = false;
if(correct(message, pursueShadows))
if(correctTurbo(message, pursueShadows))
{
columns.clear();
rows.clear();

View File

@@ -22,12 +22,45 @@ package io.github.dsheirer.module.decode.dmr.bptc;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.Hamming17;
import io.github.dsheirer.edac.soft.SoftBlockCode;
import io.github.dsheirer.edac.soft.SoftProductCodeDecoder;
/**
* Block product turbo code (BPTC) for decoding Short Link Control reassembled from CACH fragments.
*/
public class BPTC_68_36 extends BPTCBase
{
private static final int LENGTH = 68;
private static final int ROW_COUNT = 4;
private static final int COLUMN_COUNT = 17;
//Maximum correction cost (sum of the normalized channel soft bit magnitudes for the corrected bits) for a soft
//decision result. No limit is applied since the CRC-8 check rejects most false decodes. Noise injection testing:
//754 correct and 2 false soft decisions, where the false decisions had costs within the range of the correct
//decisions, so a cost limit would reject far more correct decisions than false decisions.
private static final float SOFT_MAX_CORRECTION_COST = Float.MAX_VALUE;
//Soft decision decoder for the Hamming(17,12) rows and even parity columns
private static final SoftProductCodeDecoder SOFT_DECODER = new SoftProductCodeDecoder(
SoftBlockCode.fromCodewordTest(COLUMN_COUNT, word -> Hamming17.getSyndrome(toMessage(word), 0) == 0),
ROW_COUNT, SOFT_MAX_CORRECTION_COST);
/**
* Creates a binary message from the word where bit i of the word is message bit i.
*/
private static BinaryMessage toMessage(int word)
{
BinaryMessage message = new BinaryMessage(COLUMN_COUNT);
for(int i = 0; i < COLUMN_COUNT; i++)
{
if((word & (1 << i)) != 0)
{
message.set(i);
}
}
return message;
}
public BPTC_68_36()
{
super(new Hamming17(), 17, 4);
@@ -42,7 +75,33 @@ public class BPTC_68_36 extends BPTCBase
{
CorrectedBinaryMessage deinterleaved = deinterleave(message);
correct(deinterleaved);
return extractPayload(deinterleaved);
}
/**
* Applies soft decision decoding to the soft bits and extracts the 36-bit payload. Can be used when the hard
* decision result fails BPTC error correction or the CRC-8 check.
* @param softBits for the 68 (interleaved) bits of the message where a positive value favors a 1 bit.
* @return decoded message, or null if soft decision decoding doesn't converge.
*/
public CorrectedBinaryMessage extractSoft(float[] softBits)
{
if(softBits == null || softBits.length != LENGTH)
{
return null;
}
CorrectedBinaryMessage decoded = SOFT_DECODER.decode(deinterleave(softBits));
return decoded != null ? extractPayload(decoded) : null;
}
/**
* Extracts the 36-bit payload from the corrected (de-interleaved) message.
* @param deinterleaved message
* @return extracted payload with the corrected bit count transferred.
*/
private static CorrectedBinaryMessage extractPayload(CorrectedBinaryMessage deinterleaved)
{
//Extract the message
CorrectedBinaryMessage extractedMessage = new CorrectedBinaryMessage(36);
@@ -59,6 +118,24 @@ public class BPTC_68_36 extends BPTCBase
}
/**
* Deinterleaves the transmitted soft bits.
* @param interleaved soft bits
* @return deinterleaved soft bits.
*/
public static float[] deinterleave(float[] interleaved)
{
float[] deinterleaved = new float[LENGTH];
for(int index = 0; index < 67; index++)
{
deinterleaved[(index * 17) % 67] = interleaved[index];
}
deinterleaved[67] = interleaved[67];
return deinterleaved;
}
/**
* Deinterleaves the transmitted message.
* @param interleaved message

View File

@@ -23,6 +23,7 @@ import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.module.decode.dmr.message.type.LCSS;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@@ -59,6 +60,7 @@ public class CACH
private CorrectedBinaryMessage mMessage;
private boolean mValid;
private float[] mPayloadSoftBits;
/**
* Constructs an instance. Note: constructor is private. Use the getCACH() method to extract a CACH from a
@@ -172,6 +174,42 @@ public class CACH
return mMessage.getSubMessage(PAYLOAD_START, PAYLOAD_END);
}
/**
* Soft bits for the 17-bit payload message fragment.
* @return soft bits where a positive value favors a 1 bit, or null if soft bits are not available.
*/
public float[] getPayloadSoftBits()
{
return mPayloadSoftBits;
}
/**
* Utility method to create a CACH from a raw transmitted DMR burst frame. Performs deinterleaving and error
* correction and retains the deinterleaved payload soft bits when the burst soft bits are provided.
*
* @param message containing the raw transmitted DMR burst
* @param softBits for the DMR burst (one per message bit) where a positive value favors a 1 bit, or null.
* @return constructed CACH message.
*/
public static CACH getCACH(CorrectedBinaryMessage message, float[] softBits)
{
CACH cach = getCACH(message);
if(softBits != null && softBits.length >= CACH_MESSAGE_LENGTH)
{
float[] decodedSoftBits = new float[CACH_MESSAGE_LENGTH];
for(int x = 0; x < CACH_MESSAGE_LENGTH; x++)
{
decodedSoftBits[x] = softBits[INTERLEAVE_MATRIX[x]];
}
cach.mPayloadSoftBits = Arrays.copyOfRange(decodedSoftBits, PAYLOAD_START, PAYLOAD_END);
}
return cach;
}
/**
* Utility method to create a CACH from a raw transmitted DMR burst frame. Performs deinterleaving and error
* correction.

View File

@@ -59,6 +59,22 @@ public class DMRMessageFactory
*/
public IMessage create(DMRSyncPattern syncPattern, CorrectedBinaryMessage binaryMessage, CACH cach, long timestamp,
int timeslot)
{
return create(syncPattern, binaryMessage, null, cach, timestamp, timeslot);
}
/**
* Creates a DMR message wrapper for the binary message. DMR message type is specified by the sync pattern.
* Data messages use soft decision decoding when the soft bits are provided.
* @param syncPattern for the message
* @param binaryMessage containing the raw DMR message burst
* @param softBits for the DMR burst (one per message bit) where a positive value favors a 1 bit, or null.
* @param timestamp for the burst
* @param timeslot for the burst
* @return DMRMessage instance or null for UNKNOWN sync pattern
*/
public IMessage create(DMRSyncPattern syncPattern, CorrectedBinaryMessage binaryMessage, float[] softBits, CACH cach,
long timestamp, int timeslot)
{
switch(syncPattern)
{
@@ -67,18 +83,18 @@ public class DMRMessageFactory
case MS_VOICE_FRAME_B, MS_VOICE_FRAME_C, MS_VOICE_FRAME_D, MS_VOICE_FRAME_E, MS_VOICE_FRAME_F:
case DIRECT_VOICE_FRAME_B, DIRECT_VOICE_FRAME_C, DIRECT_VOICE_FRAME_D, DIRECT_VOICE_FRAME_E,
DIRECT_VOICE_FRAME_F:
return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, timeslot);
return createVoiceMessage(syncPattern, binaryMessage, softBits, cach, timestamp, timeslot);
case DIRECT_VOICE_TIMESLOT_1:
return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, 1);
return createVoiceMessage(syncPattern, binaryMessage, softBits, cach, timestamp, 1);
case DIRECT_VOICE_TIMESLOT_2:
return createVoiceMessage(syncPattern, binaryMessage, cach, timestamp, 2);
return createVoiceMessage(syncPattern, binaryMessage, softBits, cach, timestamp, 2);
case BASE_STATION_DATA:
case MOBILE_STATION_DATA:
return mDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, timeslot);
return mDataMessageFactory.create(syncPattern, binaryMessage, softBits, cach, timestamp, timeslot);
case DIRECT_DATA_TIMESLOT_1:
return mDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 1);
return mDataMessageFactory.create(syncPattern, binaryMessage, softBits, cach, timestamp, 1);
case DIRECT_DATA_TIMESLOT_2:
return mDataMessageFactory.create(syncPattern, binaryMessage, cach, timestamp, 2);
return mDataMessageFactory.create(syncPattern, binaryMessage, softBits, cach, timestamp, 2);
case DIRECT_EMPTY_TIMESLOT:
return new EmptyTimeslotPlaceholderMessage(timestamp, 288, Protocol.DMR, timeslot);
case UNKNOWN:
@@ -94,13 +110,14 @@ public class DMRMessageFactory
* Creates a voice message from the raw transmitted DMR burst frame
* @param syncPattern for the frame
* @param message containing the DMR burst
* @param softBits for the DMR burst, or null
* @param cach extracted from the first 24 bits of the message
* @param timestamp the message was received
* @param timeslot for the DMR burst
* @return constructed voice message
*/
private static VoiceMessage createVoiceMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach,
long timestamp, int timeslot)
private static VoiceMessage createVoiceMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message,
float[] softBits, CACH cach, long timestamp, int timeslot)
{
switch(syncPattern)
{
@@ -114,7 +131,7 @@ public class DMRMessageFactory
case DIRECT_VOICE_FRAME_B, DIRECT_VOICE_FRAME_C, DIRECT_VOICE_FRAME_D, DIRECT_VOICE_FRAME_E,
DIRECT_VOICE_FRAME_F:
default:
return new VoiceEMBMessage(syncPattern, message, cach, timestamp, timeslot);
return new VoiceEMBMessage(syncPattern, message, softBits, cach, timestamp, timeslot);
}
}
}

View File

@@ -87,6 +87,23 @@ public class DMRDataMessageFactory
*/
public DataMessage create(DMRSyncPattern pattern, CorrectedBinaryMessage message, CACH cach, long timestamp,
int timeslot)
{
return create(pattern, message, null, cach, timestamp, timeslot);
}
/**
* Creates a data message class, using soft decision decoding for the BPTC(196,96) payload when the soft bits are
* provided.
* @param pattern for the DMR burst
* @param message DMR burst as transmitted
* @param softBits for the DMR burst (one per message bit) where a positive value favors a 1 bit, or null.
* @param cach from the DMR burst
* @param timestamp for the message
* @param timeslot for the message
* @return data message instance
*/
public DataMessage create(DMRSyncPattern pattern, CorrectedBinaryMessage message, float[] softBits, CACH cach,
long timestamp, int timeslot)
{
SlotType slotType = SlotType.getSlotType(message);
@@ -101,181 +118,225 @@ public class DMRDataMessageFactory
}
//Get the BPTC(196,96) protected payload. If the corrected bit count is -1, the payload/message is invalid
CorrectedBinaryMessage payload = getPayload(message);
CorrectedBinaryMessage payload = getPayload(message, softBits);
DataMessage dataMessage = createBPTCMessage(pattern, payload, cach, slotType, timestamp, timeslot);
switch(slotType.getDataType())
//The hard decision decoder can converge on an incorrect codeword (miscorrection) when the error count
//exceeds its correction capability, which then fails the message integrity (ie CRC) check. Soft decision
//decoding is otherwise only applied when hard decision decoding fails, so retry with soft decision
//decoding and use the soft decision message if it passes the integrity check.
if(!dataMessage.isValid() && softBits != null && payload.getCorrectedBitCount() >= 0)
{
case SLOT_IDLE:
DataMessage idle = new IDLEMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
idle.setValid(false);
}
return idle;
case CSBK:
DataMessage csbk = CSBKMessageFactory.create(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
csbk.setValid(false);
}
return csbk;
case USB_DATA:
DataMessage usb = new USBData(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
usb.setValid(false);
}
return usb;
case MBC_ENC_HEADER:
case MBC_HEADER:
DataMessage mbc = new MBCHeader(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
mbc.setValid(false);
}
return mbc;
case CHANNEL_CONTROL_ENC_HEADER:
case PI_HEADER:
LCMessage piLinkControl = mLinkControlMessageFactory.createFullEncryption(payload, timestamp,
CorrectedBinaryMessage softPayload = BPTC_196_96.extractSoft(extractSoftBits(softBits));
if(softPayload != null)
{
DataMessage softDataMessage = createBPTCMessage(pattern, softPayload, cach, slotType, timestamp,
timeslot);
DataMessage pi = new PiHeader(pattern, payload, cach, slotType, timestamp, timeslot, piLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
pi.setValid(false);
}
return pi;
case VOICE_HEADER:
LCMessage vhLinkControl = mLinkControlMessageFactory.createFull(payload, timestamp, timeslot, false);
DataMessage voice = new VoiceHeader(pattern, payload, cach, slotType, timestamp, timeslot,
vhLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
voice.setValid(false);
}
return voice;
case DATA_ENC_HEADER:
case DATA_HEADER:
int crcBitCount = CRCDMR.correctCCITT80(payload, 0, 80, 0xCCCC);
boolean valid = crcBitCount < 2 && payload.getCorrectedBitCount() != -1;
DataPacketFormat dpf = DataHeader.getDataPacketFormat(payload);
switch(dpf)
if(softDataMessage.isValid())
{
case CONFIRMED_DATA_PACKET:
ConfirmedDataHeader cdh = new ConfirmedDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
cdh.setValid(valid);
return cdh;
case PROPRIETARY_DATA_PACKET:
Vendor vendor = ProprietaryDataHeader.getVendor(payload);
switch(vendor)
{
case MOTOROLA_CAPACITY_PLUS:
case MOTOROLA_CONNECT_PLUS:
ServiceAccessPoint sap = ProprietaryDataHeader.getServiceAccessPoint(payload);
if(sap == ServiceAccessPoint.SAP_1)
{
MNISProprietaryDataHeader mpdh = new MNISProprietaryDataHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
mpdh.setValid(valid);
return mpdh;
}
else
{
MotorolaDataEncryptionHeader mprdh = new MotorolaDataEncryptionHeader(pattern,
payload, cach, slotType, timestamp, timeslot);
mprdh.setValid(valid);
return mprdh;
}
case HYTERA_68:
HyteraDataEncryptionHeader hsdh = new HyteraDataEncryptionHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
hsdh.setValid(valid);
return hsdh;
default:
ProprietaryDataHeader pdh = new ProprietaryDataHeader(pattern, payload, cach,
slotType, timestamp, timeslot);
pdh.setValid(valid);
return pdh;
}
case RAW_OR_STATUS_SHORT_DATA:
int appendedBlockCount = ShortDataHeader.getAppendedBlocks(payload);
if(appendedBlockCount == 0)
{
StatusDataHeader sdh = new StatusDataHeader(pattern, payload, cach, slotType, timestamp,
timeslot);
sdh.setValid(valid);
return sdh;
}
else
{
RawShortDataHeader rsdh = new RawShortDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
rsdh.setValid(valid);
return rsdh;
}
case RESPONSE_PACKET:
ResponseDataHeader rdh = new ResponseDataHeader(pattern, payload, cach, slotType, timestamp,
timeslot);
rdh.setValid(valid);
return rdh;
case DEFINED_SHORT_DATA:
DefinedShortDataHeader dsdh = new DefinedShortDataHeader(pattern, payload, cach,slotType,
timestamp, timeslot);
dsdh.setValid(valid);
return dsdh;
case UNCONFIRMED_DATA_PACKET:
UnconfirmedDataHeader udh = new UnconfirmedDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
udh.setValid(valid);
return udh;
case UNIFIED_DATA_TRANSPORT:
//TODO: this will eventually need its own factory
UDTHeader uh = new UDTHeader(pattern, payload, cach, slotType, timestamp, timeslot);
uh.setValid(valid);
return uh;
default:
DataHeader dh = new DataHeader(pattern, payload, cach, slotType, timestamp, timeslot);
dh.setValid(valid);
return dh;
return softDataMessage;
}
case TLC:
LCMessage tlcLinkControl = mLinkControlMessageFactory.createFull(payload, timestamp, timeslot, true);
DataMessage tlc = new Terminator(pattern, payload, cach, slotType, timestamp, timeslot, tlcLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
tlc.setValid(false);
}
return tlc;
case RATE_1_OF_2_DATA:
DataMessage data = new DataBlock1_2Rate(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
data.setValid(false);
}
return data;
case MBC_BLOCK:
DataMessage mbcBlock = new MBCContinuationBlock(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
mbcBlock.setValid(false);
}
return mbcBlock;
case RESERVED_15:
case UNKNOWN:
DataMessage unk = new UnknownDataMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
unk.setValid(false);
}
return unk;
}
}
return dataMessage;
}
CorrectedBinaryMessage payload = getPayload(message, softBits);
DataMessage unk = new UnknownDataMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
unk.setValid(false);
}
return unk;
}
/**
* Creates a data message from the BPTC(196,96) protected payload according to the slot type data type.
* @param pattern for the DMR burst
* @param payload extracted from the BPTC(196,96) protected message
* @param cach from the DMR burst
* @param slotType for the DMR burst
* @param timestamp for the message
* @param timeslot for the message
* @return data message instance
*/
private DataMessage createBPTCMessage(DMRSyncPattern pattern, CorrectedBinaryMessage payload, CACH cach,
SlotType slotType, long timestamp, int timeslot)
{
switch(slotType.getDataType())
{
case SLOT_IDLE:
DataMessage idle = new IDLEMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
idle.setValid(false);
}
return idle;
case CSBK:
DataMessage csbk = CSBKMessageFactory.create(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
csbk.setValid(false);
}
return csbk;
case USB_DATA:
DataMessage usb = new USBData(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
usb.setValid(false);
}
return usb;
case MBC_ENC_HEADER:
case MBC_HEADER:
DataMessage mbc = new MBCHeader(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
mbc.setValid(false);
}
return mbc;
case CHANNEL_CONTROL_ENC_HEADER:
case PI_HEADER:
LCMessage piLinkControl = mLinkControlMessageFactory.createFullEncryption(payload, timestamp,
timeslot);
DataMessage pi = new PiHeader(pattern, payload, cach, slotType, timestamp, timeslot, piLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
pi.setValid(false);
}
return pi;
case VOICE_HEADER:
LCMessage vhLinkControl = mLinkControlMessageFactory.createFull(payload, timestamp, timeslot, false);
DataMessage voice = new VoiceHeader(pattern, payload, cach, slotType, timestamp, timeslot,
vhLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
voice.setValid(false);
}
return voice;
case DATA_ENC_HEADER:
case DATA_HEADER:
int crcBitCount = CRCDMR.correctCCITT80(payload, 0, 80, 0xCCCC);
boolean valid = crcBitCount < 2 && payload.getCorrectedBitCount() != -1;
DataPacketFormat dpf = DataHeader.getDataPacketFormat(payload);
switch(dpf)
{
case CONFIRMED_DATA_PACKET:
ConfirmedDataHeader cdh = new ConfirmedDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
cdh.setValid(valid);
return cdh;
case PROPRIETARY_DATA_PACKET:
Vendor vendor = ProprietaryDataHeader.getVendor(payload);
switch(vendor)
{
case MOTOROLA_CAPACITY_PLUS:
case MOTOROLA_CONNECT_PLUS:
ServiceAccessPoint sap = ProprietaryDataHeader.getServiceAccessPoint(payload);
if(sap == ServiceAccessPoint.SAP_1)
{
MNISProprietaryDataHeader mpdh = new MNISProprietaryDataHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
mpdh.setValid(valid);
return mpdh;
}
else
{
MotorolaDataEncryptionHeader mprdh = new MotorolaDataEncryptionHeader(pattern,
payload, cach, slotType, timestamp, timeslot);
mprdh.setValid(valid);
return mprdh;
}
case HYTERA_68:
HyteraDataEncryptionHeader hsdh = new HyteraDataEncryptionHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
hsdh.setValid(valid);
return hsdh;
default:
ProprietaryDataHeader pdh = new ProprietaryDataHeader(pattern, payload, cach,
slotType, timestamp, timeslot);
pdh.setValid(valid);
return pdh;
}
case RAW_OR_STATUS_SHORT_DATA:
int appendedBlockCount = ShortDataHeader.getAppendedBlocks(payload);
if(appendedBlockCount == 0)
{
StatusDataHeader sdh = new StatusDataHeader(pattern, payload, cach, slotType, timestamp,
timeslot);
sdh.setValid(valid);
return sdh;
}
else
{
RawShortDataHeader rsdh = new RawShortDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
rsdh.setValid(valid);
return rsdh;
}
case RESPONSE_PACKET:
ResponseDataHeader rdh = new ResponseDataHeader(pattern, payload, cach, slotType, timestamp,
timeslot);
rdh.setValid(valid);
return rdh;
case DEFINED_SHORT_DATA:
DefinedShortDataHeader dsdh = new DefinedShortDataHeader(pattern, payload, cach,slotType,
timestamp, timeslot);
dsdh.setValid(valid);
return dsdh;
case UNCONFIRMED_DATA_PACKET:
UnconfirmedDataHeader udh = new UnconfirmedDataHeader(pattern, payload, cach, slotType,
timestamp, timeslot);
udh.setValid(valid);
return udh;
case UNIFIED_DATA_TRANSPORT:
//TODO: this will eventually need its own factory
UDTHeader uh = new UDTHeader(pattern, payload, cach, slotType, timestamp, timeslot);
uh.setValid(valid);
return uh;
default:
DataHeader dh = new DataHeader(pattern, payload, cach, slotType, timestamp, timeslot);
dh.setValid(valid);
return dh;
}
case TLC:
LCMessage tlcLinkControl = mLinkControlMessageFactory.createFull(payload, timestamp, timeslot, true);
DataMessage tlc = new Terminator(pattern, payload, cach, slotType, timestamp, timeslot, tlcLinkControl);
if(payload.getCorrectedBitCount() < 0)
{
tlc.setValid(false);
}
return tlc;
case RATE_1_OF_2_DATA:
DataMessage data = new DataBlock1_2Rate(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
data.setValid(false);
}
return data;
case MBC_BLOCK:
DataMessage mbcBlock = new MBCContinuationBlock(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
mbcBlock.setValid(false);
}
return mbcBlock;
case RESERVED_15:
case UNKNOWN:
DataMessage unk = new UnknownDataMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
unk.setValid(false);
}
return unk;
}
CorrectedBinaryMessage payload = getPayload(message);
DataMessage unk = new UnknownDataMessage(pattern, payload, cach, slotType, timestamp, timeslot);
if(payload.getCorrectedBitCount() < 0)
{
@@ -299,11 +360,30 @@ public class DMRDataMessageFactory
* De-scramble, decode and error check a BPTC protected raw message and return a 96-bit error corrected
* payload or null.
* @param message containing a payload
* @param softBits for the message, or null
*/
private static CorrectedBinaryMessage getPayload(CorrectedBinaryMessage message)
private static CorrectedBinaryMessage getPayload(CorrectedBinaryMessage message, float[] softBits)
{
CorrectedBinaryMessage extracted = extract(message);
return BPTC_196_96.extract(extracted);
return BPTC_196_96.extract(extracted, extractSoftBits(softBits));
}
/**
* Extracts the 196 soft bits of the message payload from the full DMR burst soft bits
* @param softBits for the DMR burst, or null
* @return extracted soft bits or null
*/
private static float[] extractSoftBits(float[] softBits)
{
if(softBits == null)
{
return null;
}
float[] extracted = new float[196];
System.arraycopy(softBits, 24, extracted, 0, 98);
System.arraycopy(softBits, 190, extracted, 98, 98);
return extracted;
}
/**

View File

@@ -33,6 +33,9 @@ import io.github.dsheirer.module.decode.dmr.message.type.LCSS;
public class FLCAssembler
{
private CorrectedBinaryMessage mAssemblingMessage;
//Soft bits parallel to the assembling message, or null when any fragment doesn't provide soft bits.
private float[] mAssemblingSoftBits;
private int mSoftBitsOffset;
private int mFragmentCount = 0;
private long mTimestamp;
private final int mTimeslot;
@@ -54,6 +57,7 @@ public class FLCAssembler
public void reset()
{
mAssemblingMessage = null;
mAssemblingSoftBits = null;
mFragmentCount = 0;
}
@@ -62,6 +66,19 @@ public class FLCAssembler
* @return a full link control message or null if the message is not yet fully assembled
*/
public FullLCMessage process(LCSS lcss, BinaryMessage fragment, long timestamp)
{
return process(lcss, fragment, null, timestamp);
}
/**
* Processes the full link control fragment
* @param lcss link control start/stop for the fragment
* @param fragment of 32 bits
* @param softFragment soft bits for the fragment where a positive value favors a 1 bit, or null.
* @param timestamp of the fragment
* @return a full link control message or null if the message is not yet fully assembled
*/
public FullLCMessage process(LCSS lcss, BinaryMessage fragment, float[] softFragment, long timestamp)
{
mTimestamp = timestamp;
@@ -71,24 +88,22 @@ public class FLCAssembler
{
case FIRST_FRAGMENT:
message = dispatch();
mAssemblingMessage = new CorrectedBinaryMessage(128);
add(fragment);
start(0);
add(fragment, softFragment);
break;
case CONTINUATION_FRAGMENT:
if(mAssemblingMessage == null)
{
mAssemblingMessage = new CorrectedBinaryMessage(128);
mAssemblingMessage.setPointer(32);
start(32);
}
add(fragment);
add(fragment, softFragment);
break;
case LAST_FRAGMENT:
if(mAssemblingMessage == null)
{
mAssemblingMessage = new CorrectedBinaryMessage(128);
mAssemblingMessage.setPointer(96);
start(96);
}
add(fragment);
add(fragment, softFragment);
message = dispatch();
break;
case SINGLE_FRAGMENT:
@@ -102,6 +117,18 @@ public class FLCAssembler
return message;
}
/**
* Starts assembling a new message
* @param offset for the first fragment
*/
private void start(int offset)
{
mAssemblingMessage = new CorrectedBinaryMessage(128);
mAssemblingMessage.setPointer(offset);
mAssemblingSoftBits = new float[128];
mSoftBitsOffset = offset;
}
/**
* Dispatches a fully-received message
*/
@@ -111,10 +138,11 @@ public class FLCAssembler
if(mFragmentCount == 4)
{
message = decode(mAssemblingMessage, mTimestamp, mTimeslot);
message = decode(mAssemblingMessage, mAssemblingSoftBits, mTimestamp, mTimeslot);
}
mAssemblingMessage = null;
mAssemblingSoftBits = null;
mFragmentCount = 0;
return message;
}
@@ -122,11 +150,11 @@ public class FLCAssembler
/**
* Decodes an assembled FLC message, performs error detection and correction and returns a constructed FLC message
*/
private FullLCMessage decode(BinaryMessage message, long timestamp, int timeslot)
private FullLCMessage decode(BinaryMessage message, float[] softBits, long timestamp, int timeslot)
{
if(message != null)
{
CorrectedBinaryMessage extractedMessage = mBPTC.extract(message);
CorrectedBinaryMessage extractedMessage = mBPTC.extract(message, softBits);
FullLCMessage flco = mMessageFactory.createFull(extractedMessage, timestamp, timeslot, false);
if(extractedMessage.getCorrectedBitCount() < 0)
@@ -141,9 +169,11 @@ public class FLCAssembler
}
/**
* Adds the cach payload to the currently assembling message
* Adds the embedded signalling fragment to the currently assembling message
* @param payload fragment bits
* @param softPayload fragment soft bits, or null
*/
private void add(BinaryMessage payload)
private void add(BinaryMessage payload, float[] softPayload)
{
if(payload == null || payload.size() != 32)
{
@@ -152,6 +182,17 @@ public class FLCAssembler
mFragmentCount++;
if(mAssemblingSoftBits != null && softPayload != null && softPayload.length == 32 &&
mSoftBitsOffset + 32 <= mAssemblingSoftBits.length)
{
System.arraycopy(softPayload, 0, mAssemblingSoftBits, mSoftBitsOffset, 32);
mSoftBitsOffset += 32;
}
else
{
mAssemblingSoftBits = null;
}
try
{
if(mAssemblingMessage == null)

View File

@@ -35,6 +35,9 @@ public class SLCAssembler
{
private final static Logger mLog = LoggerFactory.getLogger(SLCAssembler.class);
private CorrectedBinaryMessage mAssemblingMessage;
//Soft bits parallel to the assembling message, or null when any fragment doesn't provide soft bits.
private float[] mAssemblingSoftBits;
private int mSoftBitsOffset;
private int mFragmentCount = 0;
private long mTimestamp;
private BPTC_68_36 mBPTC = new BPTC_68_36();
@@ -52,6 +55,7 @@ public class SLCAssembler
public void reset()
{
mAssemblingMessage = null;
mAssemblingSoftBits = null;
mFragmentCount = 0;
mTimestamp = System.currentTimeMillis();
}
@@ -61,6 +65,19 @@ public class SLCAssembler
* @return a short link control message or null if the message is not yet fully assembled
*/
public ShortLCMessage process(LCSS lcss, BinaryMessage fragment, long timestamp)
{
return process(lcss, fragment, null, timestamp);
}
/**
* Processes the short link control fragment
* @param lcss link control start/stop for the fragment
* @param fragment of 17 bits
* @param softFragment soft bits for the fragment where a positive value favors a 1 bit, or null.
* @param timestamp of the fragment
* @return a short link control message or null if the message is not yet fully assembled
*/
public ShortLCMessage process(LCSS lcss, BinaryMessage fragment, float[] softFragment, long timestamp)
{
mTimestamp = timestamp;
@@ -70,24 +87,22 @@ public class SLCAssembler
{
case FIRST_FRAGMENT:
message = dispatch();
mAssemblingMessage = new CorrectedBinaryMessage(68);
add(fragment);
start(0);
add(fragment, softFragment);
break;
case CONTINUATION_FRAGMENT:
if(mAssemblingMessage == null)
{
mAssemblingMessage = new CorrectedBinaryMessage(68);
mAssemblingMessage.setPointer(17);
start(17);
}
add(fragment);
add(fragment, softFragment);
break;
case LAST_FRAGMENT:
if(mAssemblingMessage == null)
{
mAssemblingMessage = new CorrectedBinaryMessage(68);
mAssemblingMessage.setPointer(51);
start(51);
}
add(fragment);
add(fragment, softFragment);
message = dispatch();
break;
case SINGLE_FRAGMENT:
@@ -118,34 +133,63 @@ public class SLCAssembler
return LCMessageFactory.createShort(corrected, mTimestamp, 0);
}
/**
* Starts assembling a new message
* @param offset for the first fragment
*/
private void start(int offset)
{
mAssemblingMessage = new CorrectedBinaryMessage(68);
mAssemblingMessage.setPointer(offset);
mAssemblingSoftBits = new float[68];
mSoftBitsOffset = offset;
}
private ShortLCMessage dispatch()
{
ShortLCMessage message = null;
if(mFragmentCount == 4)
{
message = decode(mAssemblingMessage, mTimestamp);
message = decode(mAssemblingMessage, mAssemblingSoftBits, mTimestamp);
}
mAssemblingMessage = null;
mAssemblingSoftBits = null;
mFragmentCount = 0;
return message;
}
/**
* Performs error detection and correction and extracts the message from the BPTC protected interleaved message.
* When the hard decision result fails BPTC error correction or the CRC-8 check and soft bits are provided,
* retries with soft decision decoding and uses the soft decision result if it passes the CRC-8 check.
* @param interleaved message
* @param softBits for the interleaved message, or null.
* @param timestamp for the decoded message.
* @return decoded message.
*/
private ShortLCMessage decode(BinaryMessage interleaved, long timestamp)
private ShortLCMessage decode(BinaryMessage interleaved, float[] softBits, long timestamp)
{
if(interleaved != null)
{
CorrectedBinaryMessage corrected = mBPTC.extract(interleaved);
//Note: slco is timeslot-agnostic, so we use timeslot 0 every time
ShortLCMessage slco = LCMessageFactory.createShort(corrected, timestamp, 0);
slco.setValid(corrected.getCorrectedBitCount() >= 0);
ShortLCMessage slco = create(mBPTC.extract(interleaved), timestamp);
if(!slco.isValid() && softBits != null)
{
CorrectedBinaryMessage softCorrected = mBPTC.extractSoft(softBits);
if(softCorrected != null)
{
ShortLCMessage softSlco = create(softCorrected, timestamp);
if(softSlco.isValid())
{
return softSlco;
}
}
}
return slco;
}
@@ -153,9 +197,26 @@ public class SLCAssembler
}
/**
* Adds the cach payload to the currently assembling message
* Creates a short link control message from the corrected message.
* @param corrected message with the BPTC corrected bit count
* @param timestamp for the message
* @return message
*/
private void add(BinaryMessage payload)
private static ShortLCMessage create(CorrectedBinaryMessage corrected, long timestamp)
{
//Note: slco is timeslot-agnostic, so we use timeslot 0 every time
ShortLCMessage slco = LCMessageFactory.createShort(corrected, timestamp, 0);
//Valid requires both the CRC-8 check (set by the factory) and successful BPTC error correction
slco.setValid(slco.isValid() && corrected.getCorrectedBitCount() >= 0);
return slco;
}
/**
* Adds the cach payload to the currently assembling message
* @param payload fragment bits
* @param softPayload fragment soft bits, or null
*/
private void add(BinaryMessage payload, float[] softPayload)
{
if(payload == null || payload.size() != 17)
{
@@ -164,6 +225,17 @@ public class SLCAssembler
mFragmentCount++;
if(mAssemblingSoftBits != null && softPayload != null && softPayload.length == 17 &&
mSoftBitsOffset + 17 <= mAssemblingSoftBits.length)
{
System.arraycopy(softPayload, 0, mAssemblingSoftBits, mSoftBitsOffset, 17);
mSoftBitsOffset += 17;
}
else
{
mAssemblingSoftBits = null;
}
try
{
if(mAssemblingMessage == null)

View File

@@ -56,6 +56,8 @@ public class EMB
private static final int[] COLOR_CODE = new int[]{0, 1, 2, 3};
private static final int ENCRYPTION_PI = 4;
private static final int[] LINK_CONTROL_START_STOP = new int[]{5, 6};
//Maximum bit errors between the hard decision and the soft decision (maximum likelihood) word for validity.
private static final int SOFT_DECISION_MAX_BIT_ERRORS = 2;
private CorrectedBinaryMessage mMessage;
private boolean mValid = true;
@@ -71,6 +73,68 @@ public class EMB
checkCRC();
}
/**
* Constructs an instance using maximum likelihood soft decision decoding when the soft bits are provided.
*
* @param message with transmitted bits
* @param softBits for the 16 transmitted bits where a positive value favors a 1 bit, or null.
*/
public EMB(CorrectedBinaryMessage message, float[] softBits)
{
mMessage = message;
if(softBits != null && softBits.length == WORD.length)
{
decodeSoft(softBits);
}
else
{
checkCRC();
}
}
/**
* Maximum likelihood soft decision decoding against all valid words. Selects the valid word with the lowest sum
* of soft bit magnitudes for the bit positions where the valid word differs from the hard decision.
* @param softBits for the 16 transmitted bits.
*/
private void decodeSoft(float[] softBits)
{
int word = getMessage().getInt(WORD);
int best = word;
float bestMetric = Float.MAX_VALUE;
for(int validWord: VALID_WORDS)
{
int difference = word ^ validWord;
float metric = 0.0f;
while(difference != 0)
{
int bit = Integer.numberOfTrailingZeros(difference);
//Word bit 15 is message bit 0
metric += Math.abs(softBits[15 - bit]);
difference &= difference - 1;
}
if(metric < bestMetric)
{
bestMetric = metric;
best = validWord;
}
}
int bitErrors = Integer.bitCount(word ^ best);
if(bitErrors > 0)
{
getMessage().setInt(best, WORD);
getMessage().setCorrectedBitCount(bitErrors);
}
mValid = bitErrors <= SOFT_DECISION_MAX_BIT_ERRORS;
}
/**
* Checks the EMB word using the Quadratic Residue(16,7,6) code as detailed in TS 102 361-1 B.3.2.
*

View File

@@ -36,6 +36,7 @@ public class VoiceEMBMessage extends VoiceMessage
private static final int PAYLOAD_END = 172;
private EMB mEMB;
private final float[] mSoftBits;
private EmbeddedParameters mEmbeddedParameters;
/**
@@ -47,8 +48,23 @@ public class VoiceEMBMessage extends VoiceMessage
* @param message containing 288-bit DMR message with preliminary bit corrections indicated.
*/
public VoiceEMBMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, long timestamp, int timeslot)
{
this(syncPattern, message, null, cach, timestamp, timeslot);
}
/**
* Constructs an instance with soft bits to use for soft decision decoding of the EMB and the embedded signalling
* full link control fragment.
*
* @param syncPattern
* @param message containing 288-bit DMR message with preliminary bit corrections indicated.
* @param softBits for the 288-bit DMR message where a positive value favors a 1 bit, or null.
*/
public VoiceEMBMessage(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, float[] softBits, CACH cach,
long timestamp, int timeslot)
{
super(syncPattern, message, cach, timestamp, timeslot);
mSoftBits = softBits;
}
@Override
@@ -90,7 +106,19 @@ public class VoiceEMBMessage extends VoiceMessage
segment.set(x, getMessage().get(EMB[x]));
}
mEMB = new EMB(segment);
float[] softBits = null;
if(mSoftBits != null)
{
softBits = new float[EMB.length];
for(int x = 0; x < EMB.length; x++)
{
softBits[x] = mSoftBits[EMB[x]];
}
}
mEMB = new EMB(segment, softBits);
}
return mEMB;
@@ -104,6 +132,22 @@ public class VoiceEMBMessage extends VoiceMessage
return getMessage().getSubMessage(PAYLOAD_START, PAYLOAD_END);
}
/**
* Soft bits for the embedded signalling full link control fragment
* @return soft bits or null if soft bits are not available.
*/
public float[] getFLCFragmentSoftBits()
{
if(mSoftBits == null)
{
return null;
}
float[] softBits = new float[PAYLOAD_END - PAYLOAD_START];
System.arraycopy(mSoftBits, PAYLOAD_START, softBits, 0, softBits.length);
return softBits;
}
/**
* Optional embedded parameters for this voice super-frame.
* @return encryption parameters or null.

View File

@@ -19,14 +19,23 @@
package io.github.dsheirer.module.decode.dmr.sync;
import io.github.dsheirer.dsp.c4fm.DibitSyncDetector;
import io.github.dsheirer.dsp.c4fm.ISoftC4FMSyncDetector;
import io.github.dsheirer.dsp.c4fm.ReferenceSyncWaveform;
import io.github.dsheirer.dsp.symbol.Dibit;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.Map;
/**
* DMR Sync Detector. Uses soft demodulated symbols to detect sync patterns in the demodulated symbol stream.
*
* Implements the C4FM sync detector interface where the sync pattern accessor methods return the values for the sync
* pattern that produced the best correlation score from the most recent call to process().
*/
public abstract class DMRSoftSyncDetector extends DMRSyncDetector
public abstract class DMRSoftSyncDetector extends DMRSyncDetector implements ISoftC4FMSyncDetector
{
private static final int SYNC_DIBIT_LENGTH = 24;
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[] BASE_DATA = DMRSyncPattern.BASE_STATION_DATA.toSymbols();
@@ -40,9 +49,47 @@ public abstract class DMRSoftSyncDetector extends DMRSyncDetector
protected float[] mSymbols = new float[48]; //2x longer than 48-bit/24-dibit DMR sync patterns.
protected int mSymbolPointer = 0;
//Per-instance sync pattern detectors provide pattern dibits, symbols and (cached) reference waveforms
private final Map<DMRSyncPattern,DibitSyncDetector> mPatternDetectors = new EnumMap<>(DMRSyncPattern.class);
public DMRSoftSyncDetector()
{
for(DMRSyncPattern pattern: DMRSyncPattern.SYNC_PATTERNS)
{
mPatternDetectors.put(pattern, new DibitSyncDetector(pattern.getPattern(), SYNC_DIBIT_LENGTH));
}
}
/**
* Sync pattern detector for the currently detected sync pattern.
*/
private DibitSyncDetector getDetectedPatternDetector()
{
return mPatternDetectors.get(mDetectedPattern);
}
@Override
public float[] getPatternSymbols()
{
return getDetectedPatternDetector().getPatternSymbols();
}
@Override
public Dibit[] getPatternDibits()
{
return getDetectedPatternDetector().getPatternDibits();
}
@Override
public int getDibitLength()
{
return SYNC_DIBIT_LENGTH;
}
@Override
public ReferenceSyncWaveform getReferenceWaveform(double samplesPerSymbol)
{
return getDetectedPatternDetector().getReferenceWaveform(samplesPerSymbol);
}
/**
@@ -74,23 +121,15 @@ public abstract class DMRSoftSyncDetector extends DMRSyncDetector
* symbols that include this recent soft symbol.
*
* @param dibitSymbol to process that represents the soft demodulated symbol value in phase angle/radians.
* @return best correlation score where the best detected pattern is available at getDetectedPattern().
*/
public void process(float dibitSymbol)
@Override
public float process(float dibitSymbol)
{
mSymbols[mSymbolPointer] = dibitSymbol;
mSymbols[mSymbolPointer + 24] = dibitSymbol;
mSymbolPointer++;
mSymbolPointer %= 24;
}
/**
* Process the symbol and calculate the sync detection score.
* @param symbol to process
* @return sync detection score.
*/
public float processAndCalculate(float symbol)
{
process(symbol);
return calculate();
}
}

View File

@@ -50,7 +50,6 @@ public class NXDNMessageFramer extends C4FMMessageFramer
//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();
@@ -105,24 +104,6 @@ public class NXDNMessageFramer extends C4FMMessageFramer
}
}
/**
* 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.