#2226 DMR Decoder adds an equalizer to adjust symbol constellation balance/skew and gain to optimize symbol quadrant alignment/mapping. Reduces sync detectors from 3 to 2 and sets minimum objective channel sample rate to 19.2 kHz. (#2245)

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2025-06-24 04:38:10 -04:00
committed by GitHub
parent 189173e289
commit 15b7923a4f
27 changed files with 948 additions and 325 deletions

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -42,6 +42,7 @@ public class DecimationFilterFactory
switch(decimationRate)
{
case 0:
case 1:
return new RealDecimateX0Filter();
case 2:
return new RealDecimateX2Filter();

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -20,13 +20,10 @@
package io.github.dsheirer.dsp.filter.interpolator;
/**
* Linear interpolator for values in the range: -PI > 0 > PI with awareness of phase wrapping across the
* (+/-) PI boundary.
* Linear interpolator.
*/
public class PhaseAwareLinearInterpolator
public class LinearInterpolator
{
public static final float TWO_PI = (float)(Math.PI * 2.0);
/**
* Calculates an interpolated value between x1 and x2 at a linear position mu between 0.0 and 1.0
* @param x1 first value
@@ -36,39 +33,16 @@ public class PhaseAwareLinearInterpolator
*/
public static float calculate(float x1, float x2, float mu)
{
//Detect phase wrap at the +PI/-PI boundary ... ignore opposite side of the unit circle at 0 axis
if(x1 * x2 < -Math.PI)
if(mu < 0 || mu > 1)
{
if(x1 > 0)
{
float value = x1 + ((TWO_PI + x2 - x1) * mu);
if(value > Math.PI)
{
return -TWO_PI + value;
}
else
{
return value;
}
}
else
{
float value = x1 + ((-TWO_PI + x2 - x1) * mu);
if(value < -Math.PI)
{
return TWO_PI + value;
}
else
{
return value;
}
}
}
else
{
return x1 + ((x2 - x1) * mu);
throw new IllegalArgumentException("mu [" + mu + "] must be between 0 and 1");
}
return x1 + ((x2 - x1) * mu);
}
public static float calculate(float x1, float x2, double mu)
{
return calculate(x1, x2, (float)mu);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -17,16 +17,16 @@
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.dqpsk;
package io.github.dsheirer.dsp.psk.demod;
import io.github.dsheirer.dsp.filter.interpolator.Interpolator;
import io.github.dsheirer.dsp.filter.interpolator.InterpolatorFactory;
import io.github.dsheirer.dsp.fm.IDemodulator;
/**
* DQPSK demodulator base class
* Differential demodulator base class
*/
public abstract class DQPSKDemodulator implements IDemodulator
public abstract class DifferentialDemodulator implements IDemodulator
{
private int mSymbolRate;
private float mSampleRate;
@@ -43,7 +43,7 @@ public abstract class DQPSKDemodulator implements IDemodulator
* @param sampleRate in Hertz
* @param symbolRate symbols per second
*/
public DQPSKDemodulator(double sampleRate, int symbolRate)
public DifferentialDemodulator(double sampleRate, int symbolRate)
{
mSampleRate = (float) sampleRate;
mSymbolRate = symbolRate;
@@ -51,6 +51,12 @@ public abstract class DQPSKDemodulator implements IDemodulator
mMu = mSamplesPerSymbol % 1; //Fractional part of the samples per symbol rate
mInterpolationOffset = (int) Math.floor(mSamplesPerSymbol) - 4; //Interpolate at the middle of 8x samples
mBufferOverlap = (int) Math.floor(mSamplesPerSymbol) + 4;
while(mInterpolationOffset < 0)
{
mInterpolationOffset++;
mBufferOverlap++;
}
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.dqpsk;
package io.github.dsheirer.dsp.psk.demod;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
@@ -26,11 +26,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Factory for creating DQPSK demodulators
* Factory for creating PSK differential demodulators
*/
public class DQPSKDemodulatorFactory
public class DifferentialDemodulatorFactory
{
private static final Logger mLog = LoggerFactory.getLogger(DQPSKDemodulatorFactory.class);
private static final Logger mLog = LoggerFactory.getLogger(DifferentialDemodulatorFactory.class);
/**
* Creates the optimal demodulator using calibration data to select the optimal implementation from scalar and
@@ -39,17 +39,17 @@ public class DQPSKDemodulatorFactory
* @param symbolRate in Hertz
* @return demodulator instance
*/
public static DQPSKDemodulator getDemodulator(double sampleRate, int symbolRate)
public static DifferentialDemodulator getDemodulator(double sampleRate, int symbolRate)
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.DQPSK_DEMODULATOR);
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.DIFFERENTIAL_DEMODULATOR);
return switch(implementation)
{
case VECTOR_SIMD_64 -> new DQPSKDemodulatorVector64(sampleRate, symbolRate);
case VECTOR_SIMD_128 -> new DQPSKDemodulatorVector128(sampleRate, symbolRate);
case VECTOR_SIMD_256 -> new DQPSKDemodulatorVector256(sampleRate, symbolRate);
case VECTOR_SIMD_512 -> new DQPSKDemodulatorVector512(sampleRate, symbolRate);
default -> new DQPSKDemodulatorScalar(sampleRate, symbolRate); //Includes SCALAR case
case VECTOR_SIMD_64 -> new DifferentialDemodulatorVector64(sampleRate, symbolRate);
case VECTOR_SIMD_128 -> new DifferentialDemodulatorVector128(sampleRate, symbolRate);
case VECTOR_SIMD_256 -> new DifferentialDemodulatorVector256(sampleRate, symbolRate);
case VECTOR_SIMD_512 -> new DifferentialDemodulatorVector512(sampleRate, symbolRate);
default -> new DifferentialDemodulatorScalar(sampleRate, symbolRate); //Includes SCALAR case
};
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -17,25 +17,25 @@
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.dqpsk;
package io.github.dsheirer.dsp.psk.demod;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DQPSK demodulator that uses scalar calculations for demodulating the sample stream.
* Differential demodulator that uses scalar calculations for demodulating the sample stream.
*/
public class DQPSKDemodulatorScalar extends DQPSKDemodulator
public class DifferentialDemodulatorScalar extends DifferentialDemodulator
{
private static final Logger LOGGER = LoggerFactory.getLogger(DQPSKDemodulatorScalar.class);
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorScalar.class);
/**
* Constructor
*
* @param symbolRate symbols per second
*/
public DQPSKDemodulatorScalar(double sampleRate, int symbolRate)
public DifferentialDemodulatorScalar(double sampleRate, int symbolRate)
{
super(sampleRate, symbolRate);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.dqpsk;
package io.github.dsheirer.dsp.psk.demod;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
@@ -27,11 +27,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DQPSK demodulator that uses Vector SIMD 128 calculations for demodulating the sample stream.
* Differential demodulator that uses Vector SIMD 128 calculations for demodulating the sample stream.
*/
public class DQPSKDemodulatorVector128 extends DQPSKDemodulator
public class DifferentialDemodulatorVector128 extends DifferentialDemodulator
{
private static final Logger LOGGER = LoggerFactory.getLogger(DQPSKDemodulatorVector128.class);
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorVector128.class);
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
/**
@@ -40,7 +40,7 @@ public class DQPSKDemodulatorVector128 extends DQPSKDemodulator
* @param sampleRate in Hertz
* @param symbolRate symbols per second
*/
public DQPSKDemodulatorVector128(double sampleRate, int symbolRate)
public DifferentialDemodulatorVector128(double sampleRate, int symbolRate)
{
super(sampleRate, symbolRate);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.dqpsk;
package io.github.dsheirer.dsp.psk.demod;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
@@ -25,9 +25,9 @@ import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* DQPSK demodulator that uses Vector SIMD calculations for demodulating the sample stream.
* Differential demodulator that uses Vector SIMD calculations for demodulating the sample stream.
*/
public class DQPSKDemodulatorVector256 extends DQPSKDemodulator
public class DifferentialDemodulatorVector256 extends DifferentialDemodulator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
@@ -37,7 +37,7 @@ public class DQPSKDemodulatorVector256 extends DQPSKDemodulator
* @param sampleRate in Hertz
* @param symbolRate symbols per second
*/
public DQPSKDemodulatorVector256(double sampleRate, int symbolRate)
public DifferentialDemodulatorVector256(double sampleRate, int symbolRate)
{
super(sampleRate, symbolRate);
}
@@ -68,7 +68,6 @@ public class DQPSKDemodulatorVector256 extends DQPSKDemodulator
//Append new samples to the residual samples from the previous buffer.
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
//mIDecoded, mQDecoded and mPhase will be filled below during the decoding process.
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.dqpsk;
package io.github.dsheirer.dsp.psk.demod;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
@@ -27,11 +27,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DQPSK demodulator that uses Vector SIMD 512 calculations for demodulating the sample stream.
* Differential demodulator that uses Vector SIMD 512 calculations for demodulating the sample stream.
*/
public class DQPSKDemodulatorVector512 extends DQPSKDemodulator
public class DifferentialDemodulatorVector512 extends DifferentialDemodulator
{
private static final Logger LOGGER = LoggerFactory.getLogger(DQPSKDemodulatorVector512.class);
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorVector512.class);
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
/**
@@ -39,7 +39,7 @@ public class DQPSKDemodulatorVector512 extends DQPSKDemodulator
*
* @param symbolRate symbols per second
*/
public DQPSKDemodulatorVector512(double sampleRate, int symbolRate)
public DifferentialDemodulatorVector512(double sampleRate, int symbolRate)
{
super(sampleRate, symbolRate);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.dqpsk;
package io.github.dsheirer.dsp.psk.demod;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
@@ -27,11 +27,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* DQPSK demodulator that uses Vector SIMD 64 calculations for demodulating the sample stream.
* Differential demodulator that uses Vector SIMD 64 calculations for demodulating the sample stream.
*/
public class DQPSKDemodulatorVector64 extends DQPSKDemodulator
public class DifferentialDemodulatorVector64 extends DifferentialDemodulator
{
private static final Logger LOGGER = LoggerFactory.getLogger(DQPSKDemodulatorVector64.class);
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorVector64.class);
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
/**
@@ -40,7 +40,7 @@ public class DQPSKDemodulatorVector64 extends DQPSKDemodulator
* @param sampleRate in Hertz
* @param symbolRate symbols per second
*/
public DQPSKDemodulatorVector64(double sampleRate, int symbolRate)
public DifferentialDemodulatorVector64(double sampleRate, int symbolRate)
{
super(sampleRate, symbolRate);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -21,10 +21,10 @@ package io.github.dsheirer.dsp.symbol;
public enum Dibit
{
D01_PLUS_3(false, true, 1, 4, 1, (float)(Math.PI / 4 * 3), (float)-Math.sin(Math.toRadians(45)), (float)Math.sin(Math.toRadians(45))),
D00_PLUS_1(false, false, 0, 0, 0, (float)(Math.PI / 4.0), (float)Math.sin(Math.toRadians(45)), (float)Math.sin(Math.toRadians(45))),
D10_MINUS_1(true, false, 2, 8, 2, (float)(Math.PI / -4.0), (float)Math.sin(Math.toRadians(45)), (float)-Math.sin(Math.toRadians(45))),
D11_MINUS_3(true, true, 3, 12, 3, (float)(Math.PI / 4 * -3), (float)-Math.sin(Math.toRadians(45)), (float)-Math.sin(Math.toRadians(45)));
D01_PLUS_3(false, true, 1, 4, 1, (float)((Math.PI / 4) * 3)),
D00_PLUS_1(false, false, 0, 0, 0, (float)(Math.PI / 4)),
D10_MINUS_1(true, false, 2, 8, 2, (float)(Math.PI / -4)),
D11_MINUS_3(true, true, 3, 12, 3, (float)((Math.PI / 4) * -3));
private boolean mBit1;
private boolean mBit2;
@@ -32,8 +32,6 @@ public enum Dibit
private int mHighValue;
private int mValue;
private float mPhase;
private float mIdealI;
private float mIdealQ;
/**
* Dibit constructor.
@@ -43,10 +41,8 @@ public enum Dibit
* @param highValue integer value of the high order bit
* @param value integer value combined of both bits.
* @param phase optimal phase value for this symbol when transmitted.
* @param idealI ideal inphase value for this symbol when transmitted.
* @param idealQ ideal quadrature value for this symbol when transmitted.
*/
Dibit(boolean bit1, boolean bit2, int lowValue, int highValue, int value, float phase, float idealI, float idealQ)
Dibit(boolean bit1, boolean bit2, int lowValue, int highValue, int value, float phase)
{
mBit1 = bit1;
mBit2 = bit2;
@@ -54,8 +50,6 @@ public enum Dibit
mHighValue = highValue;
mValue = value;
mPhase = phase;
mIdealI = idealI;
mIdealQ = idealQ;
}
/**
@@ -75,24 +69,6 @@ public enum Dibit
return mPhase;
}
/**
* Ideal inphase value when this symbol is transmitted.
* @return ideal I
*/
public float getIdealI()
{
return mIdealI;
}
/**
* Ideal quadrature value when this symbol is transmitted.
* @return ideal Q
*/
public float getIdealQ()
{
return mIdealQ;
}
/**
* Allowable transition state 1 for trellis coded modulation. Indicates
* the dibit that differs from this dibit in the LSB bit position.

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -609,14 +609,14 @@ public class SDRTrunk implements Listener<TunerEvent>
final Path captureFile = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().resolve(filename);
EventQueue.invokeLater(() -> {
ThreadPool.CACHED.submit(() -> {
try
{
ImageIO.write(image, "png", captureFile.toFile());
}
catch(IOException e)
{
mLog.error("Couldn't write screen capture to file [" + captureFile.toString() + "]", e);
mLog.error("Couldn't write screen capture to file [" + captureFile + "]", e);
}
});
}

View File

@@ -21,10 +21,13 @@ package io.github.dsheirer.module.decode.dmr;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
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.psk.dqpsk.DQPSKDemodulator;
import io.github.dsheirer.dsp.psk.dqpsk.DQPSKDemodulatorFactory;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulator;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFactory;
import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
@@ -87,12 +90,16 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex
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 DQPSKDemodulator mDemodulator;
private DifferentialDemodulator mDemodulator;
private final DMRMessageFramer mMessageFramer;
private final DMRSoftSymbolProcessor mSymbolProcessor;
private final DMRMessageProcessor mMessageProcessor;
private IRealFilter mIBasebandFilter;
private IRealFilter mQBasebandFilter;
private IRealDecimationFilter mDecimationFilterI;
private IRealDecimationFilter mDecimationFilterQ;
private RealFIRFilter mRRCFilterI;
private RealFIRFilter mRRCFilterQ;
private final PowerMonitor mPowerMonitor = new PowerMonitor();
private final CarrierOffsetProcessor mCarrierOffsetProcessor = new CarrierOffsetProcessor();
@@ -134,13 +141,40 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex
}
mPowerMonitor.setSampleRate((int)sampleRate);
mCarrierOffsetProcessor.setSampleRate(sampleRate);
mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
mDemodulator = DQPSKDemodulatorFactory.getDemodulator(sampleRate, SYMBOL_RATE);
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)
{
decimation *= 2;
}
mDecimationFilterI = DecimationFilterFactory.getRealDecimationFilter(decimation);
mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation);
float decimatedSampleRate = (float)sampleRate / decimation;
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)
{
symbolLength = 2;
}
float[] taps = FilterFactory.getRootRaisedCosine(decimatedSampleRate / SYMBOL_RATE, symbolLength, rrcAlpha);
mRRCFilterI = new RealFIRFilter(taps);
mRRCFilterQ = new RealFIRFilter(taps);
mDemodulator = DifferentialDemodulatorFactory.getDemodulator(decimatedSampleRate, SYMBOL_RATE);
mSymbolProcessor.setSamplesPerSymbol(mDemodulator.getSamplesPerSymbol());
mMessageFramer.setListener(mMessageProcessor);
mMessageProcessor.setMessageListener(getMessageListener());
mCarrierOffsetProcessor.setSampleRate(sampleRate);
}
/**
@@ -150,6 +184,7 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex
@Override
public void receive(ComplexSamples samples)
{
mMessageFramer.setTimestamp(samples.timestamp());
float[] i = mIBasebandFilter.filter(samples.i());
@@ -158,6 +193,12 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex
//Process buffer for power measurements
mPowerMonitor.process(i, q);
i = mDecimationFilterI.decimateReal(i);
q = mDecimationFilterQ.decimateReal(q);
i = mRRCFilterI.filter(i);
q = mRRCFilterQ.filter(q);
float[] demodulated = mDemodulator.demodulate(i, q);
mSymbolProcessor.receive(demodulated);
@@ -312,19 +353,21 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex
// String directory = "D:\\DQPSK Equalizer Research\\"; //Windows
String directory = "/media/denny/T9/DQPSK Equalizer Research/"; //Linux
// String file = directory + "DMR_1_CAPPLUS.wav";
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";
// String file = directory + "20230819_064344_454575000_JPJ_Communications_(DMR)_Madison_Control_28_baseband.wav";
// String file = directory + "DMR_DCDM_4_4_baseband_20220318_142716.wav";
// String file = directory + "DMR_4_20241213_Saianet.wav";
String file = directory + "DMR_5_20241217_031219_451425000_SaiaNet_Onondaga_SaiaNet_Control_1_baseband.wav";
// String file = directory + "DMR_5_20241217_031219_451425000_SaiaNet_Onondaga_SaiaNet_Control_1_baseband.wav";
// 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_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";
// String file = directory + "DMR_19_POLY_2CHAN_20250611_032038_451250000_SaiaNet_Onondaga_SaiaNet-Control_1_baseband.wav";
boolean autoReplay = false;
@@ -387,6 +430,11 @@ public class DMRDecoder extends Decoder implements IByteBufferProvider, IComplex
boolean logIdles = false;
boolean logEverything = true;
if(iMessage instanceof SyncLossMessage)
{
int a = 0;
}
if(!logEverything && logFLC)
{
if(iMessage instanceof FullLCMessage)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -54,7 +54,7 @@ public class DMRHardSymbolProcessor
Dibit ejected = mDibitDelayLine.insert(dibit);
mMessageFramer.receive(ejected);
if(mSyncDetector.process(dibit))
if(mSyncDetector.processAndDetect(dibit))
{
mMessageFramer.syncDetected(mSyncDetector.getDetectedPattern());
}

View File

@@ -112,6 +112,15 @@ public class DMRMessageFramer implements Listener<Dibit>
}
}
/**
* Indicates if the framer is assembling a burst and the active timeslot is assembling a voice superframe.
*/
public boolean isVoiceSuperFrame()
{
return mAssemblingBurst && ((mBufferAActive && mBufferAPattern.isVoicePattern()) ||
(!mBufferAActive && mBufferBPattern.isVoicePattern()));
}
private void dispatchBufferA()
{
mDibitCounter -= 144;

View File

@@ -19,7 +19,7 @@
package io.github.dsheirer.module.decode.dmr;
import io.github.dsheirer.dsp.filter.interpolator.PhaseAwareLinearInterpolator;
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.module.decode.dmr.sync.DMRSoftSyncDetector;
@@ -27,18 +27,22 @@ 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.module.decode.dmr.sync.visualizer.SyncResultsViewer;
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 three soft sync correlation detectors to achieve initial sync lock with a primary sync detector and two lagging
* sync detectors sampling at 1/3 and 2/3 of the symbol period to ensure the sync is detected while only calculating at
* each symbol period interval. Once a sync is detected, sync lock is declared and the lagging sync detectors are
* disabled. Sync lock disables whenever the quantity of processed symbols exceeds one DMR burst length and the
* lagging detectors re-enable.
* 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
@@ -50,39 +54,52 @@ import java.nio.ByteBuffer;
* 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 MAX_POSITIVE_SOFT_SYMBOL = Dibit.D01_PLUS_3.getIdealPhase();
private static final float MAX_NEGATIVE_SOFT_SYMBOL = Dibit.D11_MINUS_3.getIdealPhase();
private static final float EQUALIZER_LOOP_GAIN = 0.3f;
private static final float MAXIMUM_EQUALIZER_BALANCE = (float)(Math.PI / 4.0);
private static final float MAXIMUM_EQUALIZER_GAIN = 1.35f;
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 mSyncDetectorLag1 = DMRSoftSyncDetectorFactory.getDetector();
private DMRSoftSyncDetector mSyncDetectorLag2 = 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 mSyncLock = false;
private float mLaggingSyncOffset1;
private float mLaggingSyncOffset2;
private float mObservedSamplesPerSymbol;
private float mSamplesPerSymbol;
private float mMaxSamplesPerSymbol;
private float mMinSamplesPerSymbol;
private float mSamplePoint;
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 mBufferInterpolatorReservedRegion;
private int mSymbolsSinceLastSync = 0;
private SyncResultsViewer mSyncResultsViewer;
/**
* Constructs an instance
@@ -97,21 +114,17 @@ public class DMRSoftSymbolProcessor
mMessageFramer = messageFramer;
mSyncModeMonitor.add(mSyncDetector);
mSyncModeMonitor.add(mSyncDetectorLag1);
mSyncModeMonitor.add(mSyncDetectorLag2);
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 baseStationMode true for base mode only, false otherwise.
* @param enabled true for base mode only, false otherwise.
*/
public void setBaseStationMode(boolean baseStationMode)
public void setBaseStationMode(boolean enabled)
{
if(baseStationMode)
{
mSyncModeMonitor.setMode(DMRSyncDetectMode.BASE_ONLY);
}
mSyncModeMonitor.setMode(enabled ? DMRSyncDetectMode.BASE_ONLY : DMRSyncDetectMode.AUTOMATIC);
}
/**
@@ -162,6 +175,33 @@ public class DMRSoftSymbolProcessor
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++;
@@ -169,16 +209,13 @@ public class DMRSoftSymbolProcessor
if(mSamplePoint < 1)
{
mSymbolsSinceLastSync++;
if(mSymbolsSinceLastSync > 144)
{
mSyncLock = false;
mFineSync = false;
}
float softSymbol = PhaseAwareLinearInterpolator.calculate(mBuffer[mBufferPointer],
mBuffer[mBufferPointer + 1], mSamplePoint);
softSymbol = LinearInterpolator.calculate(mBuffer[mBufferPointer], mBuffer[mBufferPointer + 1], mSamplePoint);
mSyncDetector.process(softSymbol);
Dibit symbol = toSymbol(softSymbol);
//Store the symbol in the delay line and broadcast the delayed ejected symbol to the message framer
@@ -186,52 +223,64 @@ public class DMRSoftSymbolProcessor
Dibit ejected = mDibitDelayLine.insert(symbol);
mMessageFramer.receive(ejected);
mDibitAssembler.receive(ejected);
mSymbolsSinceLastSync++;
float scorePrimary = mSyncDetector.process(softSymbol);
if(mSyncLock && scorePrimary > SYNC_DETECTION_THRESHOLD &&
optimize(mSyncDetector.getDetectedPattern(), 0.0f))
if(mFineSync)
{
mSymbolsSinceLastSync = 0;
mSyncModeMonitor.detected(mSyncDetector.getDetectedPattern());
mMessageFramer.syncDetected(mSyncDetector.getDetectedPattern());
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
{
float lag1 = mBufferPointer + mSamplePoint - mLaggingSyncOffset1;
float lag2 = mBufferPointer + mSamplePoint - mLaggingSyncOffset2;
int lagIntegral1 = (int)Math.floor(lag1);
int lagIntegral2 = (int)Math.floor(lag2);
float softSymbolLag1 = PhaseAwareLinearInterpolator.calculate(mBuffer[lagIntegral1],
mBuffer[lagIntegral1 + 1], lag1 - lagIntegral1);
float softSymbolLag2 = PhaseAwareLinearInterpolator.calculate(mBuffer[lagIntegral2],
mBuffer[lagIntegral2 + 1], lag2 - lagIntegral2);
float scoreLag1 = mSyncDetectorLag1.process(softSymbolLag1);
float scoreLag2 = mSyncDetectorLag2.process(softSymbolLag2);
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(mSymbolsSinceLastSync > 1 && scoreLag1 > scorePrimary && scoreLag1 > scoreLag2 &&
scoreLag1 > SYNC_DETECTION_THRESHOLD &&
optimize(mSyncDetectorLag1.getDetectedPattern(), -mLaggingSyncOffset1))
if(primaryScore > SYNC_DETECTION_THRESHOLD && optimizeCoarse(mSyncDetector.getDetectedPattern(), 0))
{
mSymbolsSinceLastSync = 0;
mSyncModeMonitor.detected(mSyncDetectorLag1.getDetectedPattern());
mMessageFramer.syncDetected(mSyncDetectorLag1.getDetectedPattern());
}
else if(mSymbolsSinceLastSync > 1 && scoreLag2 > scorePrimary &&
scoreLag2 > SYNC_DETECTION_THRESHOLD &&
optimize(mSyncDetectorLag2.getDetectedPattern(), -mLaggingSyncOffset2))
{
mSymbolsSinceLastSync = 0;
mSyncModeMonitor.detected(mSyncDetectorLag2.getDetectedPattern());
mMessageFramer.syncDetected(mSyncDetectorLag2.getDetectedPattern());
}
else if(scorePrimary > SYNC_DETECTION_THRESHOLD &&
optimize(mSyncDetector.getDetectedPattern(), 0.0f))
{
mSymbolsSinceLastSync = 0;
mSyncLock = true;
mSyncModeMonitor.detected(mSyncDetector.getDetectedPattern());
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;
}
}
@@ -242,6 +291,28 @@ public class DMRSoftSymbolProcessor
}
}
/**
* 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.
@@ -250,32 +321,32 @@ public class DMRSoftSymbolProcessor
* sync detector or an offset for the lagging sync detectors.
* @return true if there is a positive sync detection.
*/
private boolean optimize(DMRSyncPattern pattern, float additionalOffset)
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.
float offset = (mBufferPointer + mSamplePoint) + additionalOffset - (mObservedSamplesPerSymbol * 23);
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
float stepSize = mSyncLock ? (mObservedSamplesPerSymbol / 40.0f) : (mObservedSamplesPerSymbol / 10.0f);
float stepSizeMin = 0.03f;
float adjustment = 0.0f;
float adjustmentMax = mObservedSamplesPerSymbol / 2.0f;
float candidate = offset;
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;
int candidateIntegral = (int)Math.floor(candidate);
float candidateFractional = candidate - candidateIntegral;
float scoreCenter = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
float scoreCenter = score(candidate, mObservedSamplesPerSymbol, pattern);
candidate = offset - stepSize;
candidateIntegral = (int)Math.floor(candidate);
candidateFractional = candidate - candidateIntegral;
float scoreLeft = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
float scoreLeft = score(candidate, mObservedSamplesPerSymbol, pattern);
candidate = offset + stepSize;
candidateIntegral = (int)Math.floor(candidate);
candidateFractional = candidate - candidateIntegral;
float scoreRight = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
float scoreRight = score(candidate, mObservedSamplesPerSymbol, pattern);
while(stepSize > stepSizeMin && Math.abs(adjustment) <= adjustmentMax)
{
@@ -286,9 +357,7 @@ public class DMRSoftSymbolProcessor
scoreCenter = scoreLeft;
candidate = offset + adjustment - stepSize;
candidateIntegral = (int)Math.floor(candidate);
candidateFractional = candidate - candidateIntegral;
scoreLeft = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
scoreLeft = score(candidate, mObservedSamplesPerSymbol, pattern);
}
else if(scoreRight > scoreLeft && scoreRight > scoreCenter)
{
@@ -297,9 +366,7 @@ public class DMRSoftSymbolProcessor
scoreCenter = scoreRight;
candidate = offset + adjustment + stepSize;
candidateIntegral = (int)Math.floor(candidate);
candidateFractional = candidate - candidateIntegral;
scoreRight = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
scoreRight = score(candidate, mObservedSamplesPerSymbol, pattern);
}
else
{
@@ -308,14 +375,10 @@ public class DMRSoftSymbolProcessor
if(stepSize > stepSizeMin)
{
candidate = offset + adjustment - stepSize;
candidateIntegral = (int)Math.floor(candidate);
candidateFractional = candidate - candidateIntegral;
scoreLeft = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
scoreLeft = score(candidate, mObservedSamplesPerSymbol, pattern);
candidate = offset + adjustment + stepSize;
candidateIntegral = (int)Math.floor(candidate);
candidateFractional = candidate - candidateIntegral;
scoreRight = score(candidateIntegral, candidateFractional, mObservedSamplesPerSymbol, pattern);
scoreRight = score(candidate, mObservedSamplesPerSymbol, pattern);
}
}
}
@@ -326,11 +389,7 @@ public class DMRSoftSymbolProcessor
return false;
}
if(additionalOffset != 0.0)
{
adjustment += additionalOffset;
}
adjustment += additionalOffset;
mSamplePoint += adjustment;
while(mSamplePoint < 0)
@@ -345,38 +404,27 @@ public class DMRSoftSymbolProcessor
mBufferPointer++;
}
//Adjust the observed samples per symbol using the timing error measured across one or two bursts when we're in
//fine sync mode and the timing error is not excessive.
if(mSyncLock && Math.abs(adjustment) < 0.5 && mSymbolsSinceLastSync > 143 && mSymbolsSinceLastSync < 289)
{
mObservedSamplesPerSymbol += (float)((double)adjustment / (double)mSymbolsSinceLastSync * 0.2);
boolean resample = !mEqualizerInitialized || (Math.abs(adjustment) > 0.25);
if(mObservedSamplesPerSymbol > mMaxSamplesPerSymbol)
{
mObservedSamplesPerSymbol = mMaxSamplesPerSymbol;
}
else if(mObservedSamplesPerSymbol < mMinSamplesPerSymbol)
{
mObservedSamplesPerSymbol = mMinSamplesPerSymbol;
}
}
updateEqualizer(pattern);
// visualizeSyncDetect(pattern, scoreCenter, true);
//If 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(Math.abs(adjustment) > 0.05)
//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)
{
float resampleStart = mBufferPointer + mSamplePoint;
resampleStart -= (89 * mObservedSamplesPerSymbol); //Start at 89 (+ 1 current = 90) symbols
int resampleStartIntegral;
double resamplePointer = mBufferPointer + mSamplePoint;
resamplePointer -= (89 * mObservedSamplesPerSymbol); //Start at 89 (+ 1 current = 90) symbols
int integral;
for(int x = 0; x < 66; x++)
{
resampleStartIntegral = (int)Math.floor(resampleStart);
integral = (int)Math.floor(resamplePointer);
if(resampleStartIntegral >= 0)
if(integral >= 0)
{
float resampledSoftSymbol = PhaseAwareLinearInterpolator.calculate(mBuffer[resampleStartIntegral],
mBuffer[resampleStartIntegral + 1], resampleStart - resampleStartIntegral);
float resampledSoftSymbol = LinearInterpolator.calculate(mBuffer[integral], mBuffer[integral + 1], resamplePointer - integral);
mDibitDelayLine.insert(toSymbol(resampledSoftSymbol));
}
else
@@ -385,7 +433,7 @@ public class DMRSoftSymbolProcessor
mDibitDelayLine.insert(Dibit.D01_PLUS_3);
}
resampleStart += mObservedSamplesPerSymbol;
resamplePointer += mObservedSamplesPerSymbol;
}
//We don't need to resample the sync region ... just use the actual sync dibit values.
@@ -404,27 +452,256 @@ public class DMRSoftSymbolProcessor
}
/**
* Calculates the sync correlation score for sync pattern at the specified offsets and symbol timing.
* @param bufferPointer to the start of the samples in the soft symbol buffer
* @param fractional position to interpolate within the 8 samples starting at the buffer pointer.
* 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);
//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, "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,
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(int bufferPointer, float fractional, float samplesPerSymbol, DMRSyncPattern pattern)
public float score(double offset, double samplesPerSymbol, DMRSyncPattern pattern)
{
float score = 0;
float[] symbols = pattern.toSymbols();
int integral, maxPointer = mBuffer.length - 1;
float softSymbol = 0.0f;
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 = PhaseAwareLinearInterpolator.calculate(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], fractional);
softSymbol = Math.min(softSymbol, MAX_POSITIVE_SOFT_SYMBOL);
softSymbol = Math.max(softSymbol, MAX_NEGATIVE_SOFT_SYMBOL);
softSymbol = LinearInterpolator.calculate(mBuffer[bufferPointer], mBuffer[bufferPointer + 1], fractional);
}
else
{
@@ -432,10 +709,9 @@ public class DMRSoftSymbolProcessor
}
score += softSymbol * symbols[x];
fractional += samplesPerSymbol;
integral = (int)Math.floor(fractional);
bufferPointer += integral;
fractional -= integral;
pointer += samplesPerSymbol;
bufferPointer = (int)Math.floor(pointer);
fractional = pointer - bufferPointer;
}
return score;
@@ -448,14 +724,14 @@ public class DMRSoftSymbolProcessor
public void setSamplesPerSymbol(float samplesPerSymbol)
{
mSamplesPerSymbol = samplesPerSymbol;
mMaxSamplesPerSymbol = samplesPerSymbol * (1.0f + SAMPLES_PER_SYMBOL_ALLOWABLE_DEVIATION);
mMinSamplesPerSymbol = samplesPerSymbol * (1.0f - SAMPLES_PER_SYMBOL_ALLOWABLE_DEVIATION);
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((mMaxSamplesPerSymbol / 2.0f) + (mMaxSamplesPerSymbol / 10.0f));
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;
mLaggingSyncOffset1 = mSamplesPerSymbol / 3;
mLaggingSyncOffset2 = mLaggingSyncOffset1 * 2;
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];

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -75,4 +75,23 @@ public class DibitDelayLine
mPointer %= mLength;
}
}
public void log()
{
StringBuilder sb = new StringBuilder();
int pointer = mPointer;
for(int i = 0; i < mDelayLine.length; i++)
{
Dibit dibit = mDelayLine[pointer];
sb.append(dibit.getBit1() ? "1" : "0");
sb.append(dibit.getBit2() ? "1" : "0");
sb.append(" ");
pointer++;
pointer = pointer % mLength;
}
System.out.println(sb);
}
}

View File

@@ -302,8 +302,8 @@ public class DMRDataMessageFactory
*/
private static CorrectedBinaryMessage getPayload(CorrectedBinaryMessage message)
{
CorrectedBinaryMessage descrambled = extract(message);
return BPTC_196_96.extract(descrambled);
CorrectedBinaryMessage extracted = extract(message);
return BPTC_196_96.extract(extracted);
}
/**
@@ -321,7 +321,7 @@ public class DMRDataMessageFactory
{
extracted.add(message.get(i));
}
for(int i = 190; i < 190 + 98; i++)
for(int i = 190; i < 288; i++)
{
extracted.add(message.get(i));
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -29,6 +29,7 @@ public class DMRHardSyncDetector extends DMRSyncDetector
private static final long SYNC_MASK = 0xFFFFFFFFFFFFL;
private static final int MAXIMUM_BIT_ERROR = 5;
private long mValue;
private int mDelta;
private DMRSyncPattern mDetectedPattern = DMRSyncPattern.UNKNOWN;
/**
@@ -44,18 +45,41 @@ public class DMRHardSyncDetector extends DMRSyncDetector
}
/**
* Processes the dibit.
* Count of bit errors for a detected sync pattern.
* @return delta bit error count.
*/
public int getDelta()
{
return mDelta;
}
/**
* Process the dibit into the queue and detect any DMR sync pattern.
* @param dibit to process
* @return true if a sync pattern is detected.
*/
public boolean process(Dibit dibit)
public boolean processAndDetect(Dibit dibit)
{
process(dibit);
return detect();
}
public void process(Dibit dibit)
{
mValue = (Long.rotateLeft(mValue, 2) & SYNC_MASK) + dibit.getValue();
}
/**
* Checks the received sequence of dibits for sync pattern match. The detected pattern is at getDetectedPattern().
* @return true if a sync pattern is detected.
*/
public boolean detect()
{
int bitCount = Long.bitCount(mValue ^ DMRSyncPattern.BASE_STATION_DATA.getPattern());
if(bitCount <= MAXIMUM_BIT_ERROR)
{
mDetectedPattern = DMRSyncPattern.BASE_STATION_DATA;
mDelta = bitCount;
return true;
}
@@ -63,6 +87,7 @@ public class DMRHardSyncDetector extends DMRSyncDetector
if(bitCount <= MAXIMUM_BIT_ERROR)
{
mDetectedPattern = DMRSyncPattern.BASE_STATION_VOICE;
mDelta = bitCount;
return true;
}
@@ -70,6 +95,7 @@ public class DMRHardSyncDetector extends DMRSyncDetector
if(bitCount <= MAXIMUM_BIT_ERROR)
{
mDetectedPattern = DMRSyncPattern.MOBILE_STATION_DATA;
mDelta = bitCount;
return true;
}
@@ -77,6 +103,7 @@ public class DMRHardSyncDetector extends DMRSyncDetector
if(bitCount <= MAXIMUM_BIT_ERROR)
{
mDetectedPattern = DMRSyncPattern.MOBILE_STATION_VOICE;
mDelta = bitCount;
return true;
}
@@ -84,6 +111,7 @@ public class DMRHardSyncDetector extends DMRSyncDetector
if(bitCount <= MAXIMUM_BIT_ERROR)
{
mDetectedPattern = DMRSyncPattern.DIRECT_DATA_TIMESLOT_1;
mDelta = bitCount;
return true;
}
@@ -91,6 +119,7 @@ public class DMRHardSyncDetector extends DMRSyncDetector
if(bitCount <= MAXIMUM_BIT_ERROR)
{
mDetectedPattern = DMRSyncPattern.DIRECT_DATA_TIMESLOT_2;
mDelta = bitCount;
return true;
}
@@ -98,6 +127,7 @@ public class DMRHardSyncDetector extends DMRSyncDetector
if(bitCount <= MAXIMUM_BIT_ERROR)
{
mDetectedPattern = DMRSyncPattern.DIRECT_VOICE_TIMESLOT_1;
mDelta = bitCount;
return true;
}
@@ -105,9 +135,11 @@ public class DMRHardSyncDetector extends DMRSyncDetector
if(bitCount <= MAXIMUM_BIT_ERROR)
{
mDetectedPattern = DMRSyncPattern.DIRECT_VOICE_TIMESLOT_2;
mDelta = bitCount;
return true;
}
mDelta = 0;
return false;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -65,29 +65,32 @@ public abstract class DMRSoftSyncDetector extends DMRSyncDetector
/**
* Worker method to be implemented by the subclass implementation.
* @return best sync correlation score where the best detected pattern is available at
* @see DmrSyncDetector#getDetectedPattern() method.
* @return best sync correlation score where the best detected pattern is available at getDetectedPattern().
*/
protected abstract float calculate();
public abstract float calculate();
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft
* symbols that include this recent soft symbol.
*
* @param dibitSymbol to process that represents the soft demodulated symbol value in phase angle/radians.
* @return correlation score.
*/
public float process(float dibitSymbol)
public void process(float dibitSymbol)
{
//Constrain symbol to maximum positive or negative value to limit noisy symbols.
dibitSymbol = Math.min(dibitSymbol, MAX_POSITIVE);
dibitSymbol = Math.max(dibitSymbol, MAX_NEGATIVE);
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

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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

View File

@@ -0,0 +1,39 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.sync.visualizer;
import java.util.concurrent.CountDownLatch;
/**
* Interface for receiving sync detection results from a demodulated I/Q stream
*/
public interface ISyncResultsListener
{
/**
* Receive results
* @param symbols for the detected sync
* @param sync pattern symbols (ideal)
* @param samples demodulated samples
* @param syncIntervals for timing of each symbol in the samples array
* @param label to display in the UI
* @param release that pauses execution until the user acknowledges the displayed sync results. Decrement the latch to release.
*/
void receive(float[] symbols, float[] sync, float[] samples, float[] syncIntervals, String label, CountDownLatch release);
}

View File

@@ -0,0 +1,97 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.sync.visualizer;
import io.github.dsheirer.util.SwingUtils;
import java.util.concurrent.CountDownLatch;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javax.swing.JFrame;
/**
* Utility for viewing sync detection results
*/
public class SyncResultsViewer implements ISyncResultsListener
{
private ISyncResultsListener mListener;
public SyncResultsViewer()
{
initUI();
}
/**
* View sync detection results.
* @param symbols that were detected
* @param samples for the symbols
* @param sync that was detected
* @param syncIntervals symbol timing interval pointers into the I/Q sample arrays
* @param label to display to the user
* @param release latch to release via the UI when done viewing these results
*/
@Override
public void receive(float[] symbols, float[] sync, float[] samples, float[] syncIntervals, String label, CountDownLatch release)
{
if(mListener != null)
{
mListener.receive(symbols, samples, sync, syncIntervals, label, release);
}
}
/**
* Initializes the viewer UI
*/
private void initUI()
{
CountDownLatch latch = new CountDownLatch(1);
final JFXPanel fxPanel = new JFXPanel();
Platform.runLater(() -> {
Platform.setImplicitExit(false);
SyncVisualizer syncVisualizer = new SyncVisualizer();
mListener = syncVisualizer;
Scene scene = new Scene(syncVisualizer, 1400, 1000);
fxPanel.setScene(scene);
fxPanel.setVisible(true);
JFrame frame = new JFrame();
frame.setTitle("Sync Results Viewer");
frame.setContentPane(fxPanel);
frame.setSize(1400, 1000);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
SwingUtils.run(() -> {
frame.setVisible(true);
latch.countDown();
});
});
try
{
latch.await();
}
catch(InterruptedException e)
{
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,144 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.sync.visualizer;
import java.util.concurrent.CountDownLatch;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
/**
* JavaFX viewer application for visualizing sample buffers and detected sync patterns
*/
public class SyncVisualizer extends VBox implements ISyncResultsListener
{
private static final float SYMBOL = (float)(Math.PI / 4.0);
private final NumberAxis mConstellationI = new NumberAxis();
private final NumberAxis mConstellationQ = new NumberAxis();
private final NumberAxis mSampleTiming = new NumberAxis();
private final NumberAxis mSamplePhase = new NumberAxis();
private final ScatterChart<Number,Number> mConstellationChart = new ScatterChart<>(mConstellationI, mConstellationQ);
private final LineChart<Number,Number> mSampleChart = new LineChart<>(mSampleTiming, mSamplePhase);
private final Button mReleaseButton = new Button("Next Sync");
private final Label mPatternLabel = new Label();
private CountDownLatch mRelease;
/**
* Constructs an instance
*/
public SyncVisualizer()
{
setPadding(new Insets(5));
mConstellationI.setLabel("Inphase");
mConstellationQ.setLabel("Quadrature");
mSampleTiming.setLabel("Sample Timing");
mSamplePhase.setLabel("Sample Phase (+/- PI)");
mReleaseButton.setOnAction(e ->
{
if(mRelease != null)
{
mRelease.countDown();
mReleaseButton.setDisable(true);
}
});
mConstellationChart.setPrefSize(400, 400);
HBox hbox = new HBox();
hbox.getChildren().add(mConstellationChart);
// VBox.setVgrow(hbox, Priority.ALWAYS);
VBox.setVgrow(mSampleChart, Priority.ALWAYS);
HBox buttons = new HBox();
buttons.setAlignment(Pos.BASELINE_CENTER);
buttons.setSpacing(10);
buttons.getChildren().addAll(mReleaseButton, mPatternLabel);
VBox.setVgrow(buttons, Priority.NEVER);
getChildren().addAll(hbox, mSampleChart, buttons);
}
@Override
public void receive(float[] symbols, float[] samples, float[] sync, float[] syncIntervals, String label, CountDownLatch latch)
{
mRelease = latch;
mReleaseButton.setDisable(latch == null);
Platform.runLater(() -> {
XYChart.Series<Number,Number> constellationIdeal = new XYChart.Series<>();
constellationIdeal.setName("Constellation Ideal");
float PI_4 = (float)(Math.PI / 4);
constellationIdeal.getData().add(new XYChart.Data<>(PI_4, PI_4));
constellationIdeal.getData().add(new XYChart.Data<>(PI_4, -PI_4));
constellationIdeal.getData().add(new XYChart.Data<>(-PI_4, PI_4));
constellationIdeal.getData().add(new XYChart.Data<>(-PI_4, -PI_4));
constellationIdeal.getData().add(new XYChart.Data<>(1, 0));
constellationIdeal.getData().add(new XYChart.Data<>(-1, 0));
constellationIdeal.getData().add(new XYChart.Data<>(0, 1));
constellationIdeal.getData().add(new XYChart.Data<>(0, -1));
mConstellationChart.getData().clear();
mConstellationChart.getData().add(constellationIdeal);
XYChart.Series<Number,Number> constellation = new XYChart.Series<>();
constellation.setName("Constellation");
for(int x = 0; x < symbols.length; x++)
{
constellation.getData().add(new XYChart.Data<>(Math.cos(symbols[x]), Math.sin(symbols[x])));
}
mConstellationChart.getData().add(constellation);
XYChart.Series<Number,Number> sampleSeries = new XYChart.Series<>();
sampleSeries.setName("Demodulated Samples");
for(int x = 0; x < samples.length; x++)
{
sampleSeries.getData().add(new XYChart.Data<>(x, samples[x]));
}
mSampleChart.getData().clear();
mSampleChart.getData().add(sampleSeries);
XYChart.Series<Number,Number> patternSeries = new XYChart.Series<>();
patternSeries.setName("Ideal Sync Pattern");
for(int x = 0; x < sync.length; x++)
{
patternSeries.getData().add(new XYChart.Data<>(syncIntervals[x], sync[x]));
}
mSampleChart.getData().add(patternSeries);
mPatternLabel.setText(label);
});
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -25,7 +25,7 @@ import io.github.dsheirer.preference.calibration.VectorCalibrationPreference;
import io.github.dsheirer.vector.calibrate.airspy.AirspySampleConverterCalibration;
import io.github.dsheirer.vector.calibrate.airspy.AirspyUnpackedCalibration;
import io.github.dsheirer.vector.calibrate.airspy.AirspyUnpackedInterleavedCalibration;
import io.github.dsheirer.vector.calibrate.demodulator.DqpskDemodulatorCalibration;
import io.github.dsheirer.vector.calibrate.demodulator.DifferentialDemodulatorCalibration;
import io.github.dsheirer.vector.calibrate.demodulator.FmDemodulatorCalibration;
import io.github.dsheirer.vector.calibrate.filter.FirFilterCalibration;
import io.github.dsheirer.vector.calibrate.filter.RealDcRemovalCalibration;
@@ -108,7 +108,7 @@ public class CalibrationManager
sInstance.add(new ComplexOscillatorCalibration());
sInstance.add(new ComplexMixerCalibration());
sInstance.add(new DMRSoftSyncCalibration());
sInstance.add(new DqpskDemodulatorCalibration());
sInstance.add(new DifferentialDemodulatorCalibration());
sInstance.add(new FirFilterCalibration());
sInstance.add(new FmDemodulatorCalibration());
sInstance.add(new InterpolatorCalibration());
@@ -281,7 +281,7 @@ public class CalibrationManager
{
CalibrationManager manager = getInstance();
// manager.reset();
manager.reset(CalibrationType.DQPSK_DEMODULATOR);
manager.reset(CalibrationType.DIFFERENTIAL_DEMODULATOR);
if(!manager.isCalibrated())
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -38,7 +38,7 @@ public enum CalibrationType
AM_DEMODULATOR("AM Demodulator", 1),
DC_REMOVAL_REAL("Real DC Removal Filter", 1),
DMR_SOFT_SYNC_DETECTOR("DMR Soft Sync Detector", 1),
DQPSK_DEMODULATOR("DQPSK Demodulator", 1),
DIFFERENTIAL_DEMODULATOR("DQPSK Demodulator", 1),
FILTER_FIR("FIR Filter", 1),
FILTER_HALF_BAND_REAL_11_TAP("Real Half-Band Decimation Filter - 11 Tap", 1),
FILTER_HALF_BAND_REAL_15_TAP("Real Half-Band Decimation Filter - 15 Tap", 1),

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -19,12 +19,12 @@
package io.github.dsheirer.vector.calibrate.demodulator;
import io.github.dsheirer.dsp.psk.dqpsk.DQPSKDemodulator;
import io.github.dsheirer.dsp.psk.dqpsk.DQPSKDemodulatorScalar;
import io.github.dsheirer.dsp.psk.dqpsk.DQPSKDemodulatorVector128;
import io.github.dsheirer.dsp.psk.dqpsk.DQPSKDemodulatorVector256;
import io.github.dsheirer.dsp.psk.dqpsk.DQPSKDemodulatorVector512;
import io.github.dsheirer.dsp.psk.dqpsk.DQPSKDemodulatorVector64;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulator;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorScalar;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorVector128;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorVector256;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorVector512;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorVector64;
import io.github.dsheirer.vector.calibrate.Calibration;
import io.github.dsheirer.vector.calibrate.CalibrationException;
import io.github.dsheirer.vector.calibrate.CalibrationType;
@@ -32,26 +32,26 @@ import io.github.dsheirer.vector.calibrate.Implementation;
import org.apache.commons.math3.stat.descriptive.moment.Mean;
/**
* Calibrates DQPSK demodulator options
* Calibrates differential demodulator options
*/
public class DqpskDemodulatorCalibration extends Calibration
public class DifferentialDemodulatorCalibration extends Calibration
{
private static final int BUFFER_SIZE = 2048;
private static final int ITERATION_DURATION_MS = 1000;
private static final int WARMUP_ITERATIONS = 5;
private static final int TEST_ITERATIONS = 5;
private DQPSKDemodulator mScalarDemodulator = new DQPSKDemodulatorScalar(50000.0, 4800);
private final DQPSKDemodulator mVectorDemodulator64 = new DQPSKDemodulatorVector64(50000.0, 4800);
private final DQPSKDemodulator mVectorDemodulator128 = new DQPSKDemodulatorVector128(50000.0, 4800);
private final DQPSKDemodulator mVectorDemodulator256 = new DQPSKDemodulatorVector256(50000.0, 4800);
private final DQPSKDemodulator mVectorDemodulator512 = new DQPSKDemodulatorVector512(50000.0, 4800);
private DifferentialDemodulator mScalarDemodulator = new DifferentialDemodulatorScalar(50000.0, 4800);
private final DifferentialDemodulator mVectorDemodulator64 = new DifferentialDemodulatorVector64(50000.0, 4800);
private final DifferentialDemodulator mVectorDemodulator128 = new DifferentialDemodulatorVector128(50000.0, 4800);
private final DifferentialDemodulator mVectorDemodulator256 = new DifferentialDemodulatorVector256(50000.0, 4800);
private final DifferentialDemodulator mVectorDemodulator512 = new DifferentialDemodulatorVector512(50000.0, 4800);
/**
* Constructs an instance
*/
public DqpskDemodulatorCalibration()
public DifferentialDemodulatorCalibration()
{
super(CalibrationType.DQPSK_DEMODULATOR);
super(CalibrationType.DIFFERENTIAL_DEMODULATOR);
}
@Override public void calibrate() throws CalibrationException
@@ -273,7 +273,7 @@ public class DqpskDemodulatorCalibration extends Calibration
public static void main(String[] args)
{
DqpskDemodulatorCalibration calibration = new DqpskDemodulatorCalibration();
DifferentialDemodulatorCalibration calibration = new DifferentialDemodulatorCalibration();
try
{