mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
#1565 updates LTR, LTRNet and Passport decoders with new slope decoder. Resolves issue with LTR Standard decoder state that was combining individual calls to the same talkgroup and causing excessively long calls. (#1567)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -16,61 +16,64 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.fsk;
|
||||
|
||||
import io.github.dsheirer.bits.MessageFramer;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.dc.IIRSinglePoleDCRemovalFilter;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.Arrays;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Logic Trunked Radio (LTR) 300-baud FSK decoder is designed to operate against an 8 kHz FM demodulated audio stream.
|
||||
* LTR decoder designed to work with 8 kHz input audio samples and decode 300-baud signalling.
|
||||
*
|
||||
* This decoder employs a zero-crossing timing error detector with adjustable symbol timing error gain. This decoder
|
||||
* also employs a synchronization monitor that automatically adjusts symbol timing error gain according to the quantity
|
||||
* of successful message sync pattern detections. This is optimized for LTR signals that periodically transmit idle
|
||||
* bursts and transmit continuous message streams during voice transmissions. The synchronization monitor adjusts to an
|
||||
* aggressive gain value when sync is lost and automatically reduces timing error gain as successive sync pattern
|
||||
* detections occur.
|
||||
* This decoder monitors the incoming sample stream looking for either a positive or negative sequence of samples where
|
||||
* the energy delta across the baud period exceeds a threshold value and causes the symbol state to flip. The symbol
|
||||
* state remains across successive baud periods until the next state change sequence is detected.
|
||||
*
|
||||
* FM demodulated sample buffers should not be filtered prior to this decoder. An internal DC-removal filter coupled
|
||||
* with a low-pass filter removes any tuning offset and/or audio components above 300 Hertz. The DC removal filter is
|
||||
* adjusted for a slower offset removal in order to prevent overly distorting the FSK signalling component. This mild
|
||||
* adjustment may not fully compensate for mis-tuned signals and may result in missed decodes of idle bursts. However,
|
||||
* the DC filter will track a mis-tuned signal to baseband quickly upon a continuous transmission.
|
||||
* Valid LTR messages start with a zero symbol and transition to a positive symbol for the sync bit, this decoder
|
||||
* monitors for excessively long sequences of a one symbol and forces the symbol state back to a zero.
|
||||
*/
|
||||
public class LTRDecoder implements Listener<float[]>, ISyncDetectListener, ISyncStateListener
|
||||
public class LTRDecoder implements Listener<float[]>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(LTRDecoder.class);
|
||||
|
||||
public static final float SAMPLES_PER_SYMBOL = 8000.0f / 300.0f;
|
||||
|
||||
protected static final float COARSE_TIMING_GAIN = 1.0f / 3.0f;
|
||||
protected static final float MEDIUM_TIMING_GAIN = 1.0f / 4.0f;
|
||||
protected static final float FINE_TIMING_GAIN = 1.0f / 5.0f;
|
||||
private static final Logger mLog = LoggerFactory.getLogger(LTRDecoder.class);
|
||||
//Slope calculation sumXX value is always the same for 27 evenly spaced samples
|
||||
private static final double SLOPE_CALCULATION_SUM_XX = 1637.999992057681;
|
||||
private static final float BAUD_LENGTH = 8000.0f / 300.0f;
|
||||
private static final float SLOPE_THRESHOLD = 0.0048f;
|
||||
private static final int MAX_ONES_SEQUENCE = 18;
|
||||
private static final int OVERLAP = 26;
|
||||
private static final int POST_TRANSITION_SAMPLES_TO_SKIP = 26;
|
||||
private static final int SLOPE_CALCULATION_LENGTH = 27;
|
||||
private static final int SYMBOL_TRANSITION_IDEAL_MIN = 11;
|
||||
private static final int SYMBOL_TRANSITION_IDEAL_MAX = 16;
|
||||
|
||||
private static float[] sLowPassFilterCoefficients;
|
||||
private boolean mSymbol = false;
|
||||
private float[] mResidual = new float[OVERLAP];
|
||||
private float mBaudCounter = 0f;
|
||||
private float mMaxSlope = 0;
|
||||
private int mExcessiveOneSequenceCounter = 0;
|
||||
private Listener<boolean[]> mSymbolListener;
|
||||
|
||||
static
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(8000)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff(300)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart(500)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.03) //Approximately 60 dB attenuation
|
||||
.build();
|
||||
.sampleRate(8000)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff(300)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart(500)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.03) //Approximately 60 dB attenuation
|
||||
.build();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -87,143 +90,193 @@ public class LTRDecoder implements Listener<float[]>, ISyncDetectListener, ISync
|
||||
}
|
||||
}
|
||||
|
||||
protected float mSymbolTimingGain = COARSE_TIMING_GAIN;
|
||||
protected SampleBuffer mSampleBuffer;
|
||||
protected ZeroCrossingErrorDetector mTimingErrorDetector = new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL);
|
||||
protected SynchronizationMonitor mSynchronizationMonitor;
|
||||
private IIRSinglePoleDCRemovalFilter mDCFilter = new IIRSinglePoleDCRemovalFilter(0.99999f);
|
||||
private IRealFilter mLowPassFilter = FilterFactory.getRealFilter(sLowPassFilterCoefficients);
|
||||
private MessageFramer mMessageFramer;
|
||||
|
||||
private boolean mSampleDecision;
|
||||
|
||||
/**
|
||||
* Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling decoder
|
||||
*
|
||||
* @param messageLength in symbols
|
||||
* Constructs an instance
|
||||
*/
|
||||
public LTRDecoder(int messageLength)
|
||||
public LTRDecoder()
|
||||
{
|
||||
this(messageLength, new SampleBuffer(SAMPLES_PER_SYMBOL, COARSE_TIMING_GAIN),
|
||||
new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling decoder.
|
||||
*
|
||||
* @param messageLength in symbols
|
||||
* @param sampleBuffer to use for storing sample decisions
|
||||
* @param detector for symbol timing errors
|
||||
* Registers the listener to receive decoded symbols.
|
||||
* @param listener to register
|
||||
*/
|
||||
public LTRDecoder(int messageLength, SampleBuffer sampleBuffer, ZeroCrossingErrorDetector detector)
|
||||
public void setListener(Listener<boolean[]> listener)
|
||||
{
|
||||
mSynchronizationMonitor = new SynchronizationMonitor(messageLength);
|
||||
mSynchronizationMonitor.setListener(this);
|
||||
mTimingErrorDetector = detector;
|
||||
mSampleBuffer = sampleBuffer;
|
||||
mSampleBuffer.setTimingGain(mSymbolTimingGain);
|
||||
mSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the ISyncDetectedListener interface to be notified of message sync detection events.
|
||||
*
|
||||
* This allows the internal timing error detector to adjust symbol timing error gain levels according to the
|
||||
* message synchronization state to quickly adjust to initial signal streams or to reduce gain levels once
|
||||
* synchronization has been achieved.
|
||||
* Processes the demodulated 8 kHz audio samples to extract the 300-baud LTR signalling and delivers the decoded
|
||||
* symbol array to the registered listener.
|
||||
* @param samples to demodulate.
|
||||
*/
|
||||
@Override
|
||||
public void syncDetected(int bitErrors)
|
||||
public void receive(float[] samples)
|
||||
{
|
||||
mSynchronizationMonitor.syncDetected(bitErrors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is
|
||||
* greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is
|
||||
* adjusted and then each of the symbols are decoded using a simple majority decision.
|
||||
*
|
||||
* @param buffer containing 8.0 kHz unfiltered FM demodulated audio samples with sub-audible LTR signalling.
|
||||
*/
|
||||
@Override
|
||||
public void receive(float[] buffer)
|
||||
{
|
||||
float[] dcFiltered = mDCFilter.filter(buffer);
|
||||
float[] lowPassFiltered = mLowPassFilter.filter(dcFiltered);
|
||||
|
||||
for(float sample : lowPassFiltered)
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSampleDecision = sample > 0.0;
|
||||
float[] filtered = mLowPassFilter.filter(samples);
|
||||
|
||||
mSampleBuffer.receive(mSampleDecision);
|
||||
mTimingErrorDetector.receive(mSampleDecision);
|
||||
int timingAdjust = 0;
|
||||
int samplesToSkip = 0;
|
||||
|
||||
if(mSampleBuffer.hasSymbol())
|
||||
float[] buffer = new float[filtered.length + OVERLAP];
|
||||
boolean[] symbols = new boolean[(int)((filtered.length + mBaudCounter) / BAUD_LENGTH)];
|
||||
int symbolPointer = 0;
|
||||
float slope = 0;
|
||||
|
||||
try
|
||||
{
|
||||
if(mMessageFramer != null)
|
||||
System.arraycopy(mResidual, 0, buffer, 0, mResidual.length);
|
||||
System.arraycopy(filtered, 0, buffer, mResidual.length, filtered.length);
|
||||
System.arraycopy(filtered, filtered.length - OVERLAP, mResidual, 0, OVERLAP);
|
||||
|
||||
for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer++)
|
||||
{
|
||||
mMessageFramer.process(mSampleBuffer.getSymbol());
|
||||
//Don't calculate slope if we're within the baud period following last symbol transition
|
||||
if(samplesToSkip > 0)
|
||||
{
|
||||
samplesToSkip--;
|
||||
}
|
||||
else
|
||||
{
|
||||
slope = calculateSlope(buffer, bufferPointer);
|
||||
|
||||
if(mSymbol)
|
||||
{
|
||||
//Loop for crest of slope at lowest point below the negative slope threshold for transition
|
||||
if(slope > mMaxSlope && mMaxSlope < -SLOPE_THRESHOLD)
|
||||
{
|
||||
mSymbol = false;
|
||||
|
||||
//Coarse symbol timing adjust - ideal toggle is middle of baud, between 11 & 16 samples
|
||||
if(mBaudCounter < SYMBOL_TRANSITION_IDEAL_MIN)
|
||||
{
|
||||
timingAdjust = 1;
|
||||
}
|
||||
else if(mBaudCounter > SYMBOL_TRANSITION_IDEAL_MAX)
|
||||
{
|
||||
timingAdjust = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
timingAdjust = 0;
|
||||
}
|
||||
|
||||
mBaudCounter += timingAdjust;
|
||||
samplesToSkip = POST_TRANSITION_SAMPLES_TO_SKIP + timingAdjust;
|
||||
}
|
||||
else if(slope < mMaxSlope)
|
||||
{
|
||||
mMaxSlope = slope;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Loop for crest of slope at the highest point above the positive threshold
|
||||
if(slope < mMaxSlope && mMaxSlope > SLOPE_THRESHOLD)
|
||||
{
|
||||
mSymbol = true;
|
||||
|
||||
//Coarse symbol timing adjust - ideal toggle is middle of baud, between 11 & 16 samples
|
||||
if(mBaudCounter < SYMBOL_TRANSITION_IDEAL_MIN)
|
||||
{
|
||||
timingAdjust = 1;
|
||||
}
|
||||
else if(mBaudCounter > SYMBOL_TRANSITION_IDEAL_MAX)
|
||||
{
|
||||
timingAdjust = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
timingAdjust = 0;
|
||||
}
|
||||
mBaudCounter += timingAdjust;
|
||||
samplesToSkip = 26 + timingAdjust;
|
||||
}
|
||||
else if(slope > mMaxSlope)
|
||||
{
|
||||
mMaxSlope = slope;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mBaudCounter++;
|
||||
|
||||
if(mBaudCounter > BAUD_LENGTH)
|
||||
{
|
||||
//Expand symbols array length if needed to account for symbol timing adjustments
|
||||
if(symbolPointer >= symbols.length)
|
||||
{
|
||||
symbols = Arrays.copyOf(symbols, symbols.length + 1);
|
||||
}
|
||||
|
||||
symbols[symbolPointer++] = mSymbol;
|
||||
|
||||
mBaudCounter -= BAUD_LENGTH;
|
||||
|
||||
//Check for a continuous string of ones and flip the symbol state to zero.
|
||||
if(mSymbol)
|
||||
{
|
||||
mExcessiveOneSequenceCounter++;
|
||||
|
||||
if(mExcessiveOneSequenceCounter > MAX_ONES_SEQUENCE) //Verify that this is the right threshold for a string of ones
|
||||
{
|
||||
mSymbol = false;
|
||||
mMaxSlope = -1f;
|
||||
mExcessiveOneSequenceCounter = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mExcessiveOneSequenceCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.warn("Unexpected error while processing LTR samples", e);
|
||||
}
|
||||
|
||||
mSampleBuffer.resetAndAdjust(-mTimingErrorDetector.getError());
|
||||
|
||||
mSynchronizationMonitor.increment();
|
||||
//Truncate the symbols array if we didn't fill it completely, due to symbol timing adjustments.
|
||||
if(symbolPointer != symbols.length)
|
||||
{
|
||||
mSymbolListener.receive(Arrays.copyOf(symbols, symbolPointer));
|
||||
}
|
||||
else
|
||||
{
|
||||
mSymbolListener.receive(symbols);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener to receive decoded LTR symbols.
|
||||
*
|
||||
* @param messageFramer to receive symbols.
|
||||
* Calculates the slope of the 27 samples in the values array starting at the specified offset.
|
||||
* @param samples to load from
|
||||
* @param offset to the sample start for the slope calculation
|
||||
*/
|
||||
public void setMessageFramer(MessageFramer messageFramer)
|
||||
public float calculateSlope(float[] samples, int offset)
|
||||
{
|
||||
mMessageFramer = messageFramer;
|
||||
}
|
||||
/** mean of accumulated x & y values, used in updating formulas */
|
||||
double xbar = 0;
|
||||
double ybar = samples[offset];
|
||||
|
||||
/**
|
||||
* Removes the symbol listener from receiving decoded LTR symbols.
|
||||
*/
|
||||
public void removeListener()
|
||||
{
|
||||
mMessageFramer = null;
|
||||
}
|
||||
double sumXY = 0;
|
||||
double fact1, dx, dy;
|
||||
|
||||
|
||||
/**
|
||||
* Implements the ISyncStateListener interface to receive synchronization state events and adjust the symbol timing
|
||||
* error gain levels on the internal timing error detector.
|
||||
*
|
||||
* Gain levels are defined relative to a unit gain of 1.0 over a number of symbol periods. LTR uses an initial
|
||||
* ramp-up symbol reversal pattern of 0101 which provides three zero crossing detection opportunities for the timing
|
||||
* error detector, therefore we react to a COARSE gain state to average timing error over 3 symbols.
|
||||
*
|
||||
* @param syncState from an external synchronization state monitor
|
||||
*/
|
||||
@Override
|
||||
public void setSyncState(SyncState syncState)
|
||||
{
|
||||
switch(syncState)
|
||||
for(int x = 1; x < SLOPE_CALCULATION_LENGTH; x++)
|
||||
{
|
||||
case FINE:
|
||||
mSymbolTimingGain = FINE_TIMING_GAIN;
|
||||
break;
|
||||
case MEDIUM:
|
||||
mSymbolTimingGain = MEDIUM_TIMING_GAIN;
|
||||
break;
|
||||
case COARSE:
|
||||
mSymbolTimingGain = COARSE_TIMING_GAIN;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized sync state level: " + syncState.name());
|
||||
fact1 = 1.0f + x;
|
||||
dx = x - xbar;
|
||||
dy = samples[offset + x] - ybar;
|
||||
sumXY += dx * dy * (x / (1.0f + x));
|
||||
xbar += dx / fact1;
|
||||
ybar += dy / fact1;
|
||||
}
|
||||
|
||||
mSampleBuffer.setTimingGain(mSymbolTimingGain);
|
||||
return (float)(sumXY / SLOPE_CALCULATION_SUM_XX);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user