#2524 P25P2 Decoder enhancements (#2525)

* Adopts same architecture and symbol processor as P1 C4FM/LSM, DMR and NXDN.
* Soft symbol sync detection, processing and erro correction.
* Scalar and vector soft sync detection and calibration
* Improved time slot detection/prediction and ISCH processing.
* Fixes bug in parsing sequential mac structures in SACCH
* Vector calibration(s) cleanup across multiple calibrations
* Error correction bug in ISCH
* Renames differential demod shared with P1 LSM decoder.

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-10-29 07:32:17 -04:00
committed by GitHub
parent 6b9cd2d317
commit 9a6dc2b0a0
52 changed files with 4026 additions and 1314 deletions

View File

@@ -25,6 +25,7 @@ import io.github.dsheirer.edac.bch.BCH_63_16_23_P25;
import io.github.dsheirer.edac.trellis.P25_1_2_Node;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage;
import io.github.dsheirer.module.decode.p25.phase1.sync.P25P1SyncDetector;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.ArrayList;
import java.util.BitSet;
@@ -252,7 +253,7 @@ public class P25P1DecoderLSMTest
private static List<Dibit> getFrame(BitSet tsbk)
{
List<Dibit> data = new ArrayList<>();
addBits(data, 0x5575F5FF77FFL, 48);
addBits(data, P25P1SyncDetector.SYNC_PATTERN, 48);
addBits(data, BCH_63_16_23_P25.encode(NAC, TSBK_DUID) << 1, 64); //64th (parity) bit is zero
data.addAll(encodeTSBK(tsbk));

View File

@@ -1,353 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.symbol.Dibit;
import java.util.Random;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for the P25 Phase 1 LSM differential demodulator and differential integrator filter design.
*/
public class P25P1DifferentialDemodulatorLSMTest
{
private static final int SAMPLE_RATE = 24000;
private static final int SAMPLES_PER_SYMBOL = 5;
private static final int BUFFER_LENGTH = 1024;
private static final Dibit[] DIBITS = {Dibit.D00_PLUS_1, Dibit.D01_PLUS_3, Dibit.D10_MINUS_1, Dibit.D11_MINUS_3};
/**
* Synthetic differential QPSK signal generator. Ramps the phase across each symbol period by the symbol's ideal
* phase so that the phase change across each symbol period is the ideal symbol phase, plus a carrier offset.
*/
private static class SignalGenerator
{
private final Random mRandom = new Random(1);
private final double mOffset; //Radians per sample
private final double mNoise;
private double mPhase = 0.0;
private Dibit mSymbol = Dibit.D00_PLUS_1;
private int mSampleCounter = 0;
private Dibit mPreviousSymbol;
SignalGenerator(double offsetHertz, double noise)
{
mOffset = 2.0 * Math.PI * offsetHertz / SAMPLE_RATE;
mNoise = noise;
}
/**
* Generates the next buffer of samples, recording the symbol completed at each sample (or null).
*/
float[][] next(Dibit[] completedSymbols)
{
float[] i = new float[BUFFER_LENGTH];
float[] q = new float[BUFFER_LENGTH];
for(int x = 0; x < BUFFER_LENGTH; x++)
{
if(mSampleCounter % SAMPLES_PER_SYMBOL == 0)
{
mPreviousSymbol = mSymbol;
mSymbol = DIBITS[mRandom.nextInt(4)];
completedSymbols[x] = mSampleCounter > 0 ? mPreviousSymbol : null;
}
else
{
completedSymbols[x] = null;
}
i[x] = (float)(Math.cos(mPhase) + mRandom.nextGaussian() * mNoise);
q[x] = (float)(Math.sin(mPhase) + mRandom.nextGaussian() * mNoise);
mPhase += mSymbol.getIdealPhase() / SAMPLES_PER_SYMBOL + mOffset;
mSampleCounter++;
}
return new float[][]{i, q};
}
}
private static double toHertz(float radiansPerSample)
{
return radiansPerSample * SAMPLE_RATE / (2.0 * Math.PI);
}
/**
* Test: the integrator taps span one symbol period with the fractional remainder in the final tap and unity DC gain.
*/
@Test
void testDifferentialIntegratorDesign()
{
float[] taps = FilterFactory.getDifferentialIntegrator(5.2);
assertEquals(6, taps.length, "Tap count");
float sum = 0;
for(float tap : taps)
{
sum += tap;
}
assertEquals(1.0, sum, 1e-5, "DC gain");
assertEquals(1.0 / 5.2, taps[0], 1e-6, "Whole tap");
assertEquals(0.2 / 5.2, taps[5], 1e-6, "Fractional tap");
assertEquals(5, FilterFactory.getDifferentialIntegrator(5.0).length, "Integral samples per symbol");
}
/**
* Test: the carrier offset estimate converges and each symbol period's demodulated sample, scaled to radians per
* symbol, decides the transmitted symbol with the offset removed.
*/
@Test
void testOffsetRemovalAndSymbolRecovery()
{
SignalGenerator generator = new SignalGenerator(300, 0.0);
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
Dibit[] completed = new Dibit[BUFFER_LENGTH];
int symbolCount = 0;
int errorCount = 0;
for(int buffer = 0; buffer < 400; buffer++)
{
float[][] samples = generator.next(completed);
float[] demodulated = demodulator.demodulate(samples[0], samples[1], true);
//Check symbol decisions after the estimator converges
if(buffer >= 300)
{
for(int x = 0; x < BUFFER_LENGTH; x++)
{
if(completed[x] != null)
{
symbolCount++;
if(Dibit.fromSample(demodulated[x] * SAMPLES_PER_SYMBOL) != completed[x])
{
errorCount++;
}
}
}
}
}
assertEquals(300.0, toHertz(demodulator.getFrequencyOffset()), 5.0, "Offset estimate");
assertTrue(symbolCount > 10000, "Symbol count");
assertEquals(0, errorCount, "Symbol errors");
}
/**
* Test: an offset beyond the +/- 600 Hz fourth power estimator range is found by the ambiguity search while
* unsynchronized, and is held once synchronized.
*/
@Test
void testAmbiguitySearch()
{
SignalGenerator generator = new SignalGenerator(-750, 0.1);
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
Dibit[] completed = new Dibit[BUFFER_LENGTH];
boolean resolved = false;
for(int buffer = 0; buffer < 1000 && !resolved; buffer++)
{
float[][] samples = generator.next(completed);
demodulator.demodulate(samples[0], samples[1], false);
//Emulate the symbol processor synchronizing once the offset is resolved
resolved = Math.abs(toHertz(demodulator.getFrequencyOffset()) + 750) < 20;
}
assertTrue(resolved, "Search resolves the offset");
for(int buffer = 0; buffer < 200; buffer++)
{
float[][] samples = generator.next(completed);
demodulator.demodulate(samples[0], samples[1], true);
}
assertEquals(-750.0, toHertz(demodulator.getFrequencyOffset()), 10.0, "Offset held while synchronized");
}
/**
* Test: the ambiguity search holds the resolved offset range across a gap with no signal while unsynchronized (ie
* between transmissions on a traffic channel) so that the next transmission starts in the correct offset range.
*/
@Test
void testSearchHoldsAcrossSignalGap()
{
SignalGenerator generator = new SignalGenerator(-750, 0.1);
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
Dibit[] completed = new Dibit[BUFFER_LENGTH];
boolean resolved = false;
for(int buffer = 0; buffer < 1000 && !resolved; buffer++)
{
float[][] samples = generator.next(completed);
demodulator.demodulate(samples[0], samples[1], false);
resolved = Math.abs(toHertz(demodulator.getFrequencyOffset()) + 750) < 20;
}
assertTrue(resolved, "Search resolves the offset");
//Emulate the symbol processor synchronizing to the signal and the framer decoding a valid NID once the offset is
//resolved
for(int buffer = 0; buffer < 20; buffer++)
{
float[][] samples = generator.next(completed);
demodulator.demodulate(samples[0], samples[1], true);
demodulator.offsetVerified();
}
//Gap of ~20,000 symbols (10x the search dwell period) with no signal and no synchronization
Random random = new Random(3);
for(int buffer = 0; buffer < 100; buffer++)
{
float[] i = new float[BUFFER_LENGTH];
float[] q = new float[BUFFER_LENGTH];
for(int x = 0; x < BUFFER_LENGTH; x++)
{
i[x] = (float)random.nextGaussian();
q[x] = (float)random.nextGaussian();
}
demodulator.demodulate(i, q, false);
}
assertEquals(-750.0, toHertz(demodulator.getFrequencyOffset()), 20.0, "Offset held across the gap");
}
/**
* Test: symbol processor synchronization without a valid NID decode (ie a false sync detection in an incorrect
* offset range) doesn't hold the ambiguity search in the incorrect offset range for the longer dwell period, while
* a verified offset range is held.
*/
@Test
void testUnverifiedSynchronizationDoesNotHoldSearch()
{
SignalGenerator generatorUnverified = new SignalGenerator(-750, 0.1);
SignalGenerator generatorVerified = new SignalGenerator(-750, 0.1);
P25P1DifferentialDemodulatorLSM unverified = new P25P1DifferentialDemodulatorLSM();
P25P1DifferentialDemodulatorLSM verified = new P25P1DifferentialDemodulatorLSM();
unverified.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
verified.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
Dibit[] completed = new Dibit[BUFFER_LENGTH];
//Emulate a sync detection in the initial (incorrect) offset range, with and without a valid NID decode
for(int buffer = 0; buffer < 5; buffer++)
{
float[][] samples = generatorUnverified.next(completed);
unverified.demodulate(samples[0], samples[1], true);
samples = generatorVerified.next(completed);
verified.demodulate(samples[0], samples[1], true);
verified.offsetVerified();
}
//Unsynchronized for ~10,000 symbols: longer than the search dwell and shorter than the verified dwell
for(int buffer = 0; buffer < 50; buffer++)
{
float[][] samples = generatorUnverified.next(completed);
unverified.demodulate(samples[0], samples[1], false);
samples = generatorVerified.next(completed);
verified.demodulate(samples[0], samples[1], false);
}
//The unverified search steps to an adjacent (1200 Hz) offset range while the verified search holds its range
double difference = Math.abs(toHertz(unverified.getFrequencyOffset()) - toHertz(verified.getFrequencyOffset()));
assertTrue(difference > 600, "Unverified offset range search stepped away [" + difference + " Hz]");
}
/**
* Test: each sample's reliability is (|x[n]|^2 * |x[n - sps]|^2) / (|x[n]|^2 + |x[n - sps]|^2), including across the
* sample buffer boundary.
*/
@Test
void testReliabilities()
{
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
float[] amplitudes = new float[2 * BUFFER_LENGTH];
for(int x = 0; x < amplitudes.length; x++)
{
amplitudes[x] = 0.1f + 0.9f * (float)Math.abs(Math.sin(x * 0.37));
}
float[] reliabilities = new float[amplitudes.length];
for(int buffer = 0; buffer < 2; buffer++)
{
float[] i = new float[BUFFER_LENGTH];
float[] q = new float[BUFFER_LENGTH];
for(int x = 0; x < BUFFER_LENGTH; x++)
{
//Rotate the phase to show that the reliability depends only on the sample power
double phase = x * 0.5;
float amplitude = amplitudes[buffer * BUFFER_LENGTH + x];
i[x] = (float)(amplitude * Math.cos(phase));
q[x] = (float)(amplitude * Math.sin(phase));
}
demodulator.demodulate(i, q, true);
System.arraycopy(demodulator.getReliabilities(), 0, reliabilities, buffer * BUFFER_LENGTH, BUFFER_LENGTH);
}
for(int x = SAMPLES_PER_SYMBOL; x < amplitudes.length; x++)
{
double current = amplitudes[x] * amplitudes[x];
double delayed = amplitudes[x - SAMPLES_PER_SYMBOL] * amplitudes[x - SAMPLES_PER_SYMBOL];
assertEquals(current * delayed / (current + delayed), reliabilities[x], 1e-5, "Reliability at " + x);
}
}
/**
* Test: the offset estimate doesn't drift when the input is noise.
*/
@Test
void testNoiseDoesNotMoveEstimate()
{
Random random = new Random(2);
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
for(int buffer = 0; buffer < 2000; buffer++)
{
float[] i = new float[BUFFER_LENGTH];
float[] q = new float[BUFFER_LENGTH];
for(int x = 0; x < BUFFER_LENGTH; x++)
{
i[x] = (float)random.nextGaussian();
q[x] = (float)random.nextGaussian();
}
demodulator.demodulate(i, q, true);
}
assertEquals(0.0, toHertz(demodulator.getFrequencyOffset()), 1e-3, "Offset estimate");
}
}

View File

@@ -0,0 +1,80 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1.sync;
import java.util.Random;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* JUnit tests for the P25 Phase 1 soft sync detector scalar and SIMD vector implementations.
*/
public class P25P1SoftSyncDetectorTest
{
private static final float IDEAL_SCORE = (float)(24 * Math.pow(3.0 * Math.PI / 4.0, 2));
private static P25P1SoftSyncDetector[] getDetectors()
{
return new P25P1SoftSyncDetector[]{new P25P1SoftSyncDetectorScalar(), new P25P1SoftSyncDetectorVector64(),
new P25P1SoftSyncDetectorVector128(), new P25P1SoftSyncDetectorVector256(),
new P25P1SoftSyncDetectorVector512()};
}
@Test
public void detectsSyncPattern()
{
for(P25P1SoftSyncDetector detector : getDetectors())
{
float[] pattern = detector.getPatternSymbols();
float score = 0;
//Repeat the pattern so that the final symbol occurs at each of the circular buffer positions
for(int repeat = 0; repeat < 3; repeat++)
{
for(float symbol : pattern)
{
score = detector.process(symbol);
}
assertEquals(IDEAL_SCORE, score, 0.001, detector.getClass().getSimpleName());
}
}
}
@Test
public void vectorImplementationsMatchScalar()
{
P25P1SoftSyncDetector[] detectors = getDetectors();
Random random = new Random(1);
for(int x = 0; x < 1000; x++)
{
float symbol = (float)((random.nextDouble() * 2.0 - 1.0) * Math.PI);
float expected = detectors[0].process(symbol);
for(int d = 1; d < detectors.length; d++)
{
assertEquals(expected, detectors[d].process(symbol), 0.001,
detectors[d].getClass().getSimpleName() + " at symbol " + x);
}
}
}
}

View File

@@ -0,0 +1,548 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase2;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.FragmentedIntField;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.edac.CRCP25;
import io.github.dsheirer.edac.ReedSolomonTestEncoder;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
import io.github.dsheirer.module.decode.p25.phase2.message.isch.ISCHDecoder;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
import io.github.dsheirer.module.decode.p25.phase2.sync.P25P2SoftSyncDetector;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.FacchTimeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.ScramblingSequence;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* End-to-end JUnit tests for the P25 Phase 2 HDQPSK decoder: a synthetic outbound (FDMA) channel carrying super frame
* fragments with I-ISCH, S-ISCH (sync) and four scrambled FACCH timeslots is generated and decoded, and each valid
* decoded MAC message is checked against the transmitted MAC messages.
*/
public class P25P2DecoderHDQPSKTest
{
private static final double SAMPLE_RATE = 48000.0;
private static final double SYMBOL_RATE = 6000.0;
private static final int FRAGMENT_COUNT = 60;
private static final int TRAILING_FRAGMENT_COUNT = 2;
private static final ScrambleParameters SCRAMBLE_PARAMETERS = new ScrambleParameters(0xBEE00, 0x3BB, 0x3B2);
private static final long ISCH_CODE_WORD_OFFSET = 0x184229d461L;
private static final int[] DUID_BITS = {0, 1, 74, 75, 244, 245, 318, 319};
//FACCH MAC PDU: 156 bits, with the CRC-12 checksum in the final 12 bits
private static final int MAC_LENGTH = 156;
private static final int MAC_CRC_START = 144;
private static final int MAC_PDU_TYPE_IDLE = 3;
//Raised cosine transmit pulse
private static final double ROLLOFF = 0.2;
private static final int PULSE_SPAN_SYMBOLS = 8;
private static final int BUFFER_SIZE = 2048;
private static final Dibit[] DIBITS = {Dibit.D00_PLUS_1, Dibit.D01_PLUS_3, Dibit.D10_MINUS_1, Dibit.D11_MINUS_3};
private static final int MESSAGES_PER_FRAGMENT = 4;
/**
* Test: carrier offsets beyond the fourth power offset estimator range are resolved by the offset ambiguity search,
* after which every FACCH MAC message decodes. A fragment framed while the search steps to the correct offset range
* can produce an invalid (CRC failure) MAC message.
*/
@Test
void testOffsetAmbiguitySearch()
{
for(double carrierOffset : new double[]{1000.0, -1000.0, 1200.0, -1200.0})
{
Result result = decode(carrierOffset, -100.0, Double.NaN, 3, false);
assertTrue(result.invalid() <= 1, "Invalid messages at " + carrierOffset + " Hz: " + result.invalid());
assertTrue(result.first() <= 12, "Acquisition at " + carrierOffset + " Hz: " + result.first() + " fragments");
assertEquals(0, result.missed(), "Missed messages after acquisition at " + carrierOffset + " Hz");
}
}
/**
* Test: with additive white gaussian noise at 12 dB in-band (+/- 3.6 kHz) carrier to noise, nearly all FACCH MAC
* messages decode and every valid MAC message matches a transmitted MAC message.
*/
@Test
void testNoise()
{
Result result = decode(200.0, 50.0, 12.0, 5, false);
assertTrue(result.valid() >= FRAGMENT_COUNT * MESSAGES_PER_FRAGMENT * 0.9, "Valid messages: " + result.valid());
}
/**
* Generates and decodes the synthetic signal.
* @param carrierOffset in Hertz
* @param symbolRateError in parts per million
* @param carrierToNoise in dB, in-band (+/- 3.6 kHz), or NaN for no noise
* @param seed for the random MAC payloads and noise
* @param corruptIsch to corrupt both I-ISCH in the second and third fragment of each superframe
* @return decoded message counts (valid messages for the checked fragments), the index of the first fragment with a decoded MAC message (the fragment count
* when none decode), the count of MAC messages that didn't decode after that fragment, and the count of valid MAC
* messages decoded from the replayed dibit stream.
*/
private static Result decode(double carrierOffset, double symbolRateError, double carrierToNoise, long seed,
boolean corruptIsch)
{
Random random = new Random(seed);
ScramblingSequence scramblingSequence = new ScramblingSequence();
scramblingSequence.update(SCRAMBLE_PARAMETERS);
List<BinaryMessage> macs = new ArrayList<>();
Set<BinaryMessage> transmitted = new HashSet<>();
List<Dibit> dibits = new ArrayList<>();
for(int fragment = 0; fragment < FRAGMENT_COUNT + TRAILING_FRAGMENT_COUNT; fragment++)
{
List<BinaryMessage> fragmentMacs = new ArrayList<>();
for(int x = 0; x < MESSAGES_PER_FRAGMENT; x++)
{
fragmentMacs.add(getMacMessage(random));
}
//Trailing fragments aren't checked since the final fragment isn't dispatched until its trailing symbols are
//processed, but they are included in the transmitted messages for the CRC false positive check.
if(fragment < FRAGMENT_COUNT)
{
macs.addAll(fragmentMacs);
}
transmitted.addAll(fragmentMacs);
boolean corrupt = corruptIsch && (fragment % 3) != 0;
dibits.addAll(getFragment(fragment, fragmentMacs, scramblingSequence, corrupt, random));
}
float[][] samples = modulate(dibits, carrierOffset, symbolRateError, carrierToNoise, random);
List<MacMessage> received = new ArrayList<>();
List<ByteBuffer> buffers = new ArrayList<>();
DecodeConfigP25Phase2 config = new DecodeConfigP25Phase2();
config.setScrambleParameters(SCRAMBLE_PARAMETERS);
P25P2DecoderHDQPSK decoder = new P25P2DecoderHDQPSK(config);
decoder.setMessageListener((IMessage message) -> {
if(message instanceof MacMessage mac && mac.getDataUnitID().isFACCH())
{
received.add(mac);
}
});
decoder.setBufferListener(buffer -> {
ByteBuffer copy = ByteBuffer.allocate(buffer.capacity());
copy.put(buffer.array());
buffers.add(copy);
});
decoder.start();
decoder.setSampleRate(SAMPLE_RATE);
for(int offset = 0; offset < samples[0].length; offset += BUFFER_SIZE)
{
int length = Math.min(BUFFER_SIZE, samples[0].length - offset);
float[] i = new float[length];
float[] q = new float[length];
System.arraycopy(samples[0], offset, i, 0, length);
System.arraycopy(samples[1], offset, q, 0, length);
decoder.receive(new ComplexSamples(i, q, 0L));
}
decoder.stop();
int invalid = 0;
Set<BinaryMessage> decoded = new HashSet<>();
for(MacMessage mac : received)
{
if(mac.isValid())
{
BinaryMessage bits = getBits(mac.getMessage());
//A valid MAC message that doesn't match a transmitted MAC message is a CRC false positive
assertTrue(transmitted.contains(bits), "Decoded MAC message was not transmitted");
assertTrue(decoded.add(bits), "Decoded MAC message was duplicated");
}
else
{
invalid++;
}
}
int valid = 0;
int first = FRAGMENT_COUNT;
int missed = 0;
for(int x = 0; x < macs.size(); x++)
{
int fragment = x / MESSAGES_PER_FRAGMENT;
if(decoded.contains(macs.get(x)))
{
valid++;
first = Math.min(first, fragment);
}
else if(first < fragment)
{
missed++;
}
}
return new Result(valid, invalid, first, missed, replay(buffers, new HashSet<>(macs)));
}
/**
* Replays the demodulated dibit stream through a message framer with hard sync detection.
* @return count of valid MAC messages that match a checked (transmitted) MAC message.
*/
private static int replay(List<ByteBuffer> buffers, Set<BinaryMessage> transmitted)
{
Set<BinaryMessage> decoded = new HashSet<>();
P25P2MessageFramer framer = new P25P2MessageFramer();
framer.setScrambleParameters(SCRAMBLE_PARAMETERS);
P25P2MessageProcessor processor = new P25P2MessageProcessor();
processor.setMessageListener((IMessage message) -> {
if(message instanceof MacMessage mac && mac.getDataUnitID().isFACCH() && mac.isValid())
{
BinaryMessage bits = getBits(mac.getMessage());
if(transmitted.contains(bits))
{
decoded.add(bits);
}
}
});
framer.setListener(processor);
framer.start();
for(ByteBuffer buffer : buffers)
{
framer.receive(buffer);
}
return decoded.size();
}
/**
* Copies the MAC message bits for comparison against the transmitted MAC messages.
*/
private static BinaryMessage getBits(CorrectedBinaryMessage message)
{
BinaryMessage bits = new BinaryMessage(MAC_LENGTH);
for(int x = 0; x < MAC_LENGTH; x++)
{
if(message.get(x))
{
bits.set(x);
}
}
return bits;
}
/**
* Creates a random MAC IDLE PDU with a NULL INFORMATION MAC structure, random payload bits and the CRC-12 checksum.
*/
private static BinaryMessage getMacMessage(Random random)
{
CorrectedBinaryMessage message = new CorrectedBinaryMessage(MAC_LENGTH);
message.load(0, 3, MAC_PDU_TYPE_IDLE);
//Opcode (bits 8-15) is zero for the NULL INFORMATION MAC structure
for(int x = 16; x < MAC_CRC_START; x++)
{
if(random.nextBoolean())
{
message.set(x);
}
}
//The checksum isn't included in the CRC calculation, so exactly one checksum value passes the CRC check
for(int checksum = 0; checksum < 4096; checksum++)
{
message.load(MAC_CRC_START, 12, checksum);
if(CRCP25.crc12_FACCH(message))
{
return getBits(message);
}
}
throw new IllegalStateException("Unable to calculate the CRC-12 checksum");
}
/**
* Creates the 720 dibit super frame fragment: I-ISCH 1, timeslot A, I-ISCH 2, timeslot B, S-ISCH (sync),
* timeslot C, S-ISCH (sync), timeslot D.
* @param fragment index, which determines the ISCH sequence (fragment 1-3 of the superframe)
* @param macs for timeslots A-D
* @param scramblingSequence to scramble the timeslots
* @param corruptIsch to corrupt both I-ISCH beyond the error correction capability
* @param random for selecting the corrupted bits
*/
private static List<Dibit> getFragment(int fragment, List<BinaryMessage> macs, ScramblingSequence scramblingSequence,
boolean corruptIsch, Random random)
{
int ischSequence = fragment % 3;
int superframeSequence = (fragment / 3) % 4;
List<Dibit> dibits = new ArrayList<>();
for(int position = 0; position < 4; position++)
{
switch(position)
{
case 0, 1 -> addBits(dibits, getIISCH(position, ischSequence, superframeSequence, corruptIsch, random), 40);
default -> addBits(dibits, P25P2SoftSyncDetector.SYNC_PATTERN, 40);
}
BinaryMessage sequence = scramblingSequence.getTimeslotSequence(ischSequence * 4 + position);
BinaryMessage timeslot = getTimeslot(macs.get(position), sequence);
for(int x = 0; x < 320; x += 2)
{
dibits.add(DIBITS[(timeslot.get(x) ? 2 : 0) + (timeslot.get(x + 1) ? 1 : 0)]);
}
}
return dibits;
}
/**
* Creates the 40-bit I-ISCH codeword.
* @param channel 0 (timeslot 1) or 1 (timeslot 2)
* @param ischSequence 0-2 (fragment 1-3 of the superframe)
* @param superframeSequence 0-3
* @param corrupt to flip 12 codeword bits, which exceeds the 7 bit error correction capability
* @param random for selecting the corrupted bits
*/
private static long getIISCH(int channel, int ischSequence, int superframeSequence, boolean corrupt, Random random)
{
BinaryMessage message = new BinaryMessage(9);
message.load(2, 2, channel);
message.load(4, 2, ischSequence);
message.load(7, 2, superframeSequence);
long codeword = ISCHDecoder.getCodeWord(message) ^ ISCH_CODE_WORD_OFFSET;
if(corrupt)
{
Set<Integer> bits = new HashSet<>();
while(bits.size() < 12)
{
bits.add(random.nextInt(40));
}
for(int bit : bits)
{
codeword ^= 1L << bit;
}
}
return codeword;
}
/**
* Creates the scrambled 320-bit FACCH timeslot: RS(63,35,29) encodes the 26 MAC message hexbits, loads the message
* and transmitted (19) parity hexbits into the FACCH timeslot fields, scrambles the timeslot, and sets the
* (unscrambled) data unit ID.
*/
private static BinaryMessage getTimeslot(BinaryMessage mac, BinaryMessage scramblingSequence)
{
//Message hexbit h (INFO_h+1) is codeword index 53 - h, and parity hexbit p (PARITY_p) is codeword index 28 - p
int[] message = new int[35];
for(int h = 0; h < 26; h++)
{
message[25 - h] = mac.getInt(h * 6, h * 6 + 5);
}
int[] codeword = ReedSolomonTestEncoder.encode(message);
BinaryMessage timeslot = new BinaryMessage(320);
for(int h = 1; h <= 26; h++)
{
load(timeslot, getFacchField("INFO_" + h), codeword[54 - h]);
}
for(int p = 1; p <= 19; p++)
{
load(timeslot, getFacchField("PARITY_" + p), codeword[28 - p]);
}
timeslot.xor(scramblingSequence);
int duid = DataUnitID.SCRAMBLED_FACCH.getValueWithParity();
for(int x = 0; x < DUID_BITS.length; x++)
{
if(((duid >> (7 - x)) & 1) == 1)
{
timeslot.set(DUID_BITS[x]);
}
else
{
timeslot.clear(DUID_BITS[x]);
}
}
return timeslot;
}
/**
* Accesses the FACCH timeslot hexbit field (IntField or FragmentedIntField) by name.
*/
private static Object getFacchField(String name)
{
try
{
Field field = FacchTimeslot.class.getDeclaredField(name);
field.setAccessible(true);
return field.get(null);
}
catch(ReflectiveOperationException e)
{
throw new IllegalStateException("FACCH timeslot field " + name, e);
}
}
/**
* Loads the 6-bit value into the field bits, most significant bit first.
*/
private static void load(BinaryMessage message, Object field, int value)
{
int[] indices;
if(field instanceof IntField intField)
{
indices = new int[intField.width()];
for(int x = 0; x < indices.length; x++)
{
indices[x] = intField.start() + x;
}
}
else
{
indices = ((FragmentedIntField)field).indices();
}
for(int x = 0; x < indices.length; x++)
{
if(((value >> (indices.length - 1 - x)) & 1) == 1)
{
message.set(indices[x]);
}
}
}
/**
* Modulates the dibits as HDQPSK: differential QPSK with raised cosine pulse shaping.
* @return i and q sample arrays
*/
private static float[][] modulate(List<Dibit> dibits, double carrierOffset, double symbolRateError,
double carrierToNoise, Random random)
{
double[] symbolI = new double[dibits.size()];
double[] symbolQ = new double[dibits.size()];
double phase = 0.0;
for(int x = 0; x < dibits.size(); x++)
{
phase += dibits.get(x).getIdealPhase();
symbolI[x] = Math.cos(phase);
symbolQ[x] = Math.sin(phase);
}
double samplesPerSymbol = SAMPLE_RATE / (SYMBOL_RATE * (1.0 + symbolRateError * 1e-6));
//The decimation filter requires an even sample count
int length = (int)((dibits.size() + PULSE_SPAN_SYMBOLS) * samplesPerSymbol) / 2 * 2;
float[] i = new float[length];
float[] q = new float[length];
//Unit power symbols: the in-band noise variance is the in-band fraction of the total noise variance
double noise = Double.isNaN(carrierToNoise) ? 0.0 :
Math.sqrt(Math.pow(10.0, -carrierToNoise / 10.0) * SAMPLE_RATE / 7200.0 / 2.0);
for(int n = 0; n < length; n++)
{
double t = n / samplesPerSymbol - PULSE_SPAN_SYMBOLS / 2.0; //Symbol periods
double sumI = 0.0;
double sumQ = 0.0;
for(int k = (int)Math.ceil(t - PULSE_SPAN_SYMBOLS / 2.0); k <= t + PULSE_SPAN_SYMBOLS / 2.0; k++)
{
if(k >= 0 && k < dibits.size())
{
double pulse = getRaisedCosine(t - k);
sumI += symbolI[k] * pulse;
sumQ += symbolQ[k] * pulse;
}
}
double carrier = 2.0 * Math.PI * carrierOffset * n / SAMPLE_RATE;
double cos = Math.cos(carrier);
double sin = Math.sin(carrier);
i[n] = (float)(sumI * cos - sumQ * sin + noise * random.nextGaussian());
q[n] = (float)(sumI * sin + sumQ * cos + noise * random.nextGaussian());
}
return new float[][]{i, q};
}
/**
* Raised cosine pulse value
* @param t time in symbol periods
*/
private static double getRaisedCosine(double t)
{
double sinc = Math.abs(t) < 1e-9 ? 1.0 : Math.sin(Math.PI * t) / (Math.PI * t);
double denominator = 1.0 - Math.pow(2.0 * ROLLOFF * t, 2.0);
if(Math.abs(denominator) < 1e-9)
{
return Math.PI / 4.0 * sinc;
}
return sinc * Math.cos(Math.PI * ROLLOFF * t) / denominator;
}
/**
* Adds the most significant bits first, as dibits.
*/
private static void addBits(List<Dibit> dibits, long value, int bitCount)
{
for(int bit = bitCount - 2; bit >= 0; bit -= 2)
{
dibits.add(DIBITS[(int)((value >> bit) & 3)]);
}
}
private record Result(int valid, int invalid, int first, int missed, int replayValid) {}
}

View File

@@ -0,0 +1,47 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase2.enumeration;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* JUnit tests for the ISCH (super frame fragment) sequence.
*/
public class ISCHSequenceTest
{
@Test
public void advanceCyclesThroughFragments()
{
assertEquals(ISCHSequence.ISCH_2, ISCHSequence.ISCH_1.advance(1));
assertEquals(ISCHSequence.ISCH_3, ISCHSequence.ISCH_1.advance(2));
assertEquals(ISCHSequence.ISCH_1, ISCHSequence.ISCH_1.advance(3));
assertEquals(ISCHSequence.ISCH_1, ISCHSequence.ISCH_3.advance(1));
assertEquals(ISCHSequence.ISCH_2, ISCHSequence.ISCH_3.advance(11));
}
@Test
public void advanceOfNonFragmentSequenceIsUnknown()
{
assertEquals(ISCHSequence.UNKNOWN, ISCHSequence.RESERVED_4.advance(1));
assertEquals(ISCHSequence.UNKNOWN, ISCHSequence.UNKNOWN.advance(1));
}
}

View File

@@ -0,0 +1,211 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase2.message;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.edac.ReedSolomonTestEncoder;
import java.util.Random;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* JUnit tests for the P25 Phase 2 Encryption Synchronization Sequence (ESS) RS(44,16,29) decoding.
*/
public class EncryptionSynchronizationSequenceProcessorTest
{
/**
* Creates the RS codeword for the 16 ESS information symbols, where codeword index 43 is the first information
* symbol, indices 28-43 are information symbols and indices 0-27 are parity symbols.
*/
private static int[] encode(int[] information)
{
int[] message = new int[35];
for(int x = 0; x < 16; x++)
{
message[15 - x] = information[x];
}
return ReedSolomonTestEncoder.encode(message);
}
/**
* ESS-A fragment (168 bits) containing the 28 parity symbols, where hexbit x is codeword index 27 - x.
*/
private static BinaryMessage getEssA(int[] codeword)
{
BinaryMessage essA = new BinaryMessage(168);
for(int x = 0; x < 28; x++)
{
essA.load(x * 6, 6, codeword[27 - x]);
}
return essA;
}
/**
* ESS-B fragment (24 bits) containing 4 information symbols, where fragment 1 is codeword indices 40-43 and
* fragment 4 is codeword indices 28-31, and hexbit x is codeword index (base + 3 - x).
*/
private static BinaryMessage getEssB(int[] codeword, int fragment)
{
int base = 28 + (4 - fragment) * 4;
BinaryMessage essB = new BinaryMessage(24);
for(int x = 0; x < 4; x++)
{
essB.load(x * 6, 6, codeword[base + 3 - x]);
}
return essB;
}
/**
* Expected decoded 96-bit ESS message.
*/
private static String getExpected(int[] information)
{
BinaryMessage expected = new BinaryMessage(96);
for(int x = 0; x < 16; x++)
{
expected.load(x * 6, 6, information[x]);
}
return expected.toHexString();
}
/**
* Corrupts the specified number of distinct ESS-A (parity) symbols.
*/
private static void corruptParity(int[] codeword, int errors, Random random)
{
boolean[] corrupted = new boolean[28];
int count = 0;
while(count < errors)
{
int index = random.nextInt(28);
if(!corrupted[index])
{
corrupted[index] = true;
codeword[index] ^= 1 + random.nextInt(63);
count++;
}
}
}
private static int[] getEncryptedInformation(Random random)
{
int[] information = new int[16];
for(int x = 0; x < 16; x++)
{
information[x] = random.nextInt(64);
}
return information;
}
@Test
public void decodesCompleteSequenceWithErrors()
{
Random random = new Random(1);
for(int trial = 0; trial < 100; trial++)
{
int[] information = getEncryptedInformation(random);
int[] codeword = encode(information);
int[] received = codeword.clone();
corruptParity(received, 14, random);
EncryptionSynchronizationSequenceProcessor processor = new EncryptionSynchronizationSequenceProcessor(1);
processor.setESSA(getEssA(received));
processor.setESSB1(getEssB(received, 1));
processor.setESSB2(getEssB(received, 2));
processor.setESSB3(getEssB(received, 3));
processor.setESSB4(getEssB(received, 4));
EncryptionSynchronizationSequence sequence = processor.getSequence();
assertNotNull(sequence);
assertEquals(getExpected(information), sequence.getMessage().toHexString());
}
}
/**
* Encrypted sequence (random message indicator) with 3 missing ESS-B fragments and 4 parity symbol errors. The
* missing fragments can't be decoded as zero valued symbols (12 + 4 errors exceeds the 14 symbol error capacity),
* but can be decoded as erasures (12 erasures + 4 errors).
*/
@Test
public void decodesEncryptedSequenceWithMissingFragmentsAsErasures()
{
Random random = new Random(2);
for(int trial = 0; trial < 100; trial++)
{
int[] information = getEncryptedInformation(random);
int[] codeword = encode(information);
int[] received = codeword.clone();
corruptParity(received, 4, random);
EncryptionSynchronizationSequenceProcessor processor = new EncryptionSynchronizationSequenceProcessor(1);
processor.setESSA(getEssA(received));
processor.setESSB1(getEssB(received, 1));
EncryptionSynchronizationSequence sequence = processor.getSequence();
assertNotNull(sequence, "Trial " + trial);
assertEquals(getExpected(information), sequence.getMessage().toHexString());
}
}
/**
* Unencrypted sequence (algorithm 0x80 with zero key ID and message indicator) with 3 missing ESS-B fragments and
* 9 parity symbol errors. The missing fragments decode as zero valued symbols (9 errors), but can't be decoded
* as erasures (12 erasures + 9 errors exceeds the capacity).
*/
@Test
public void decodesUnencryptedSequenceWithMissingFragmentsAsZeros()
{
Random random = new Random(3);
int[] information = new int[16];
information[0] = 0x20; //Algorithm 0x80 (unencrypted) in the first 8 bits
for(int trial = 0; trial < 100; trial++)
{
int[] codeword = encode(information);
int[] received = codeword.clone();
corruptParity(received, 9, random);
EncryptionSynchronizationSequenceProcessor processor = new EncryptionSynchronizationSequenceProcessor(1);
processor.setESSA(getEssA(received));
processor.setESSB1(getEssB(received, 1));
EncryptionSynchronizationSequence sequence = processor.getSequence();
assertNotNull(sequence, "Trial " + trial);
assertEquals(getExpected(information), sequence.getMessage().toHexString());
assertEquals(0x80, sequence.getAlgorithmId());
}
}
}

View File

@@ -0,0 +1,119 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase2.message.isch;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import java.util.Random;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* JUnit tests for the I-ISCH (40,9,16) codeword decoder.
*/
public class ISCHDecoderTest
{
private static final long CODE_WORD_OFFSET = 0x184229d461L;
private final ISCHDecoder mDecoder = new ISCHDecoder();
/**
* Creates the transmitted 40-bit codeword for the 9-bit message value.
*/
private static BinaryMessage encode(int value)
{
BinaryMessage message = new BinaryMessage(9);
message.load(0, 9, value);
BinaryMessage codeword = new BinaryMessage(40);
codeword.load(0, 40, ISCHDecoder.getCodeWord(message) ^ CODE_WORD_OFFSET);
return codeword;
}
/**
* Channel number (1 or 2) for the message value, or 0 for an ISCH location (3) that isn't used for I-ISCH.
*/
private static int getChannel(int value)
{
int ischLocation = (value >> 3) & 0x3;
return ischLocation == 3 ? 0 : ((value >> 5) & 0x3) + 1;
}
@Test
public void correctsUpToSevenBitErrors()
{
Random random = new Random(1);
for(int value = 0; value < 512; value++)
{
int channel = getChannel(value);
if(channel != 1 && channel != 2)
{
continue;
}
for(int errors = 0; errors <= 7; errors++)
{
for(int trial = 0; trial < 10; trial++)
{
BinaryMessage codeword = encode(value);
int flipped = 0;
while(flipped < errors)
{
int bit = random.nextInt(40);
if(codeword.get(bit))
{
codeword.clear(bit);
}
else
{
codeword.set(bit);
}
flipped++;
}
CorrectedBinaryMessage decoded = mDecoder.decode(codeword, channel);
assertEquals(value, decoded.getInt(0, 8), "Value [" + value + "] errors [" + errors + "]");
assertTrue(decoded.getCorrectedBitCount() <= errors);
}
}
}
}
@Test
public void rejectsWrongTimeslotCodewords()
{
for(int value = 0; value < 512; value++)
{
int channel = getChannel(value);
if(channel == 1 || channel == 2)
{
//A codeword for the other timeslot is at least 16 bits from every codeword for the expected timeslot
CorrectedBinaryMessage decoded = mDecoder.decode(encode(value), channel == 1 ? 2 : 1);
assertTrue(decoded.getCorrectedBitCount() >= 8, "Value [" + value + "]");
}
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase2.sync;
import java.util.Random;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* JUnit tests for the P25 Phase 2 soft sync detector scalar and SIMD vector implementations.
*/
public class P25P2SoftSyncDetectorTest
{
private static final float IDEAL_SCORE = (float)(20 * Math.pow(3.0 * Math.PI / 4.0, 2));
private static P25P2SoftSyncDetector[] getDetectors()
{
return new P25P2SoftSyncDetector[]{new P25P2SoftSyncDetectorScalar(), new P25P2SoftSyncDetectorVector64(),
new P25P2SoftSyncDetectorVector128(), new P25P2SoftSyncDetectorVector256(),
new P25P2SoftSyncDetectorVector512()};
}
@Test
public void detectsSyncPattern()
{
for(P25P2SoftSyncDetector detector : getDetectors())
{
float[] pattern = detector.getPatternSymbols();
float score = 0;
//Repeat the pattern so that the final symbol occurs at each of the circular buffer positions
for(int repeat = 0; repeat < 3; repeat++)
{
for(float symbol : pattern)
{
score = detector.process(symbol);
}
assertEquals(IDEAL_SCORE, score, 0.001, detector.getClass().getSimpleName());
}
}
}
@Test
public void vectorImplementationsMatchScalar()
{
P25P2SoftSyncDetector[] detectors = getDetectors();
Random random = new Random(1);
for(int x = 0; x < 1000; x++)
{
float symbol = (float)((random.nextDouble() * 2.0 - 1.0) * Math.PI);
float expected = detectors[0].process(symbol);
for(int d = 1; d < detectors.length; d++)
{
assertEquals(expected, detectors[d].process(symbol), 0.001,
detectors[d].getClass().getSimpleName() + " at symbol " + x);
}
}
}
}