#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

@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1;
package io.github.dsheirer.dsp.c4fm;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.symbol.Dibit;
@@ -28,9 +28,9 @@ 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.
* JUnit tests for the DQPSK differential demodulator and differential integrator filter design.
*/
public class P25P1DifferentialDemodulatorLSMTest
public class DQPSKDifferentialDemodulatorTest
{
private static final int SAMPLE_RATE = 24000;
private static final int SAMPLES_PER_SYMBOL = 5;
@@ -122,7 +122,7 @@ public class P25P1DifferentialDemodulatorLSMTest
void testOffsetRemovalAndSymbolRecovery()
{
SignalGenerator generator = new SignalGenerator(300, 0.0);
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
Dibit[] completed = new Dibit[BUFFER_LENGTH];
int symbolCount = 0;
@@ -164,7 +164,7 @@ public class P25P1DifferentialDemodulatorLSMTest
void testAmbiguitySearch()
{
SignalGenerator generator = new SignalGenerator(-750, 0.1);
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
Dibit[] completed = new Dibit[BUFFER_LENGTH];
boolean resolved = false;
@@ -197,7 +197,7 @@ public class P25P1DifferentialDemodulatorLSMTest
void testSearchHoldsAcrossSignalGap()
{
SignalGenerator generator = new SignalGenerator(-750, 0.1);
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
Dibit[] completed = new Dibit[BUFFER_LENGTH];
boolean resolved = false;
@@ -250,8 +250,8 @@ public class P25P1DifferentialDemodulatorLSMTest
{
SignalGenerator generatorUnverified = new SignalGenerator(-750, 0.1);
SignalGenerator generatorVerified = new SignalGenerator(-750, 0.1);
P25P1DifferentialDemodulatorLSM unverified = new P25P1DifferentialDemodulatorLSM();
P25P1DifferentialDemodulatorLSM verified = new P25P1DifferentialDemodulatorLSM();
DQPSKDifferentialDemodulator unverified = new DQPSKDifferentialDemodulator();
DQPSKDifferentialDemodulator verified = new DQPSKDifferentialDemodulator();
unverified.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
verified.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
Dibit[] completed = new Dibit[BUFFER_LENGTH];
@@ -287,7 +287,7 @@ public class P25P1DifferentialDemodulatorLSMTest
@Test
void testReliabilities()
{
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
float[] amplitudes = new float[2 * BUFFER_LENGTH];
@@ -331,7 +331,7 @@ public class P25P1DifferentialDemodulatorLSMTest
void testNoiseDoesNotMoveEstimate()
{
Random random = new Random(2);
P25P1DifferentialDemodulatorLSM demodulator = new P25P1DifferentialDemodulatorLSM();
DQPSKDifferentialDemodulator demodulator = new DQPSKDifferentialDemodulator();
demodulator.setSamplesPerSymbol(SAMPLES_PER_SYMBOL);
for(int buffer = 0; buffer < 2000; buffer++)

View File

@@ -0,0 +1,181 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.edac;
import java.util.Arrays;
import java.util.Random;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests the Reed-Solomon errors-and-erasures decoder against the P25 RS(63,35,29) code.
*/
public class ReedSolomonErasureDecoderTest
{
private static final int N = 63;
private static final int K = 35;
private static final int PARITY = N - K;
private int[] randomCodeword(Random random)
{
int[] message = new int[K];
for(int i = 0; i < K; i++)
{
message[i] = random.nextInt(64);
}
return ReedSolomonTestEncoder.encode(message);
}
/**
* Corrupts the codeword with the specified number of errors and erasures at distinct random positions.
* @return erasure positions
*/
private int[] corrupt(int[] codeword, int errors, int erasures, Random random)
{
int[] positions = new int[N];
for(int i = 0; i < N; i++)
{
positions[i] = i;
}
for(int i = N - 1; i > 0; i--)
{
int j = random.nextInt(i + 1);
int t = positions[i];
positions[i] = positions[j];
positions[j] = t;
}
for(int i = 0; i < errors; i++)
{
codeword[positions[i]] ^= 1 + random.nextInt(63);
}
int[] erased = Arrays.copyOfRange(positions, errors, errors + erasures);
for(int position : erased)
{
codeword[position] = random.nextInt(64);
}
return erased;
}
@Test
public void encoderProducesValidCodewords()
{
Random random = new Random(1);
BerlekempMassey reference = new ReedSolomon_63_35_29_P25();
for(int trial = 0; trial < 100; trial++)
{
int[] codeword = randomCodeword(random);
int[] output = new int[N];
assertFalse(reference.decode(codeword, output));
assertArrayEquals(codeword, output);
}
}
@Test
public void correctsErrorsAndErasuresWithinCapacity()
{
Random random = new Random(2);
ReedSolomonErasureDecoder decoder = ReedSolomonErasureDecoder.p25_63_35_29();
for(int erasures = 0; erasures <= PARITY; erasures++)
{
int maxErrors = (PARITY - erasures) / 2;
for(int errors = 0; errors <= maxErrors; errors++)
{
for(int trial = 0; trial < 20; trial++)
{
int[] codeword = randomCodeword(random);
int[] received = codeword.clone();
int[] erased = corrupt(received, errors, erasures, random);
int[] output = new int[N];
assertFalse(decoder.decode(received, output, erased, erased.length),
"Failed with errors [" + errors + "] erasures [" + erasures + "]");
assertArrayEquals(codeword, output, "Errors [" + errors + "] erasures [" + erasures + "]");
}
}
}
}
@Test
public void detectsMostDecodingFailuresBeyondCapacity()
{
Random random = new Random(3);
ReedSolomonErasureDecoder decoder = ReedSolomonErasureDecoder.p25_63_35_29();
int miscorrections = 0;
int trials = 1000;
for(int trial = 0; trial < trials; trial++)
{
int[] codeword = randomCodeword(random);
int[] received = codeword.clone();
int erasures = 6;
int errors = (PARITY - erasures) / 2 + 3;
int[] erased = corrupt(received, errors, erasures, random);
int[] output = new int[N];
if(!decoder.decode(received, output, erased, erased.length))
{
//A miscorrection must still produce a valid (but incorrect) codeword
assertFalse(Arrays.equals(codeword, output));
int[] check = new int[N];
assertFalse(new ReedSolomon_63_35_29_P25().decode(output, check));
miscorrections++;
}
}
assertTrue(miscorrections < trials / 100, "Miscorrections: " + miscorrections);
}
@Test
public void matchesErrorsOnlyDecoder()
{
Random random = new Random(4);
ReedSolomonErasureDecoder decoder = ReedSolomonErasureDecoder.p25_63_35_29();
BerlekempMassey reference = new ReedSolomon_63_35_29_P25();
for(int trial = 0; trial < 500; trial++)
{
int[] received = randomCodeword(random);
corrupt(received, random.nextInt(18), 0, random);
int[] output = new int[N];
int[] referenceOutput = new int[N];
boolean failed = decoder.decode(received, output, new int[0], 0);
boolean referenceFailed = reference.decode(received, referenceOutput);
assertEquals(referenceFailed, failed);
if(!failed)
{
assertArrayEquals(referenceOutput, output);
}
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.edac;
/**
* Systematic encoder for the P25 RS(63,35,29) code (and codes shortened from it) for unit tests. Parity symbols are
* at indices 0-27 and message symbols at indices 28-62, with generator polynomial roots alpha^1 through alpha^28.
*/
public class ReedSolomonTestEncoder
{
private static final int N = 63;
private static final int K = 35;
private static final int PARITY = N - K;
private static final int[] EXP = new int[63];
private static final int[] LOG = new int[64];
static
{
int value = 1;
for(int i = 0; i < 63; i++)
{
EXP[i] = value;
LOG[value] = i;
value <<= 1;
if(value > 63)
{
value = (value & 63) ^ 3; //x^6 = x + 1
}
}
}
private static int multiply(int a, int b)
{
return (a == 0 || b == 0) ? 0 : EXP[(LOG[a] + LOG[b]) % 63];
}
/**
* Encodes the message symbols.
* @param message symbols (35), where trailing zero symbols represent a shortened code
* @return codeword (63 symbols)
*/
public static int[] encode(int[] message)
{
int[] generator = new int[PARITY + 1];
generator[0] = 1;
for(int i = 1; i <= PARITY; i++)
{
int root = EXP[i % 63];
for(int j = i; j > 0; j--)
{
generator[j] = generator[j - 1] ^ multiply(generator[j], root);
}
generator[0] = multiply(generator[0], root);
}
//Remainder of x^28 * m(x) divided by g(x)
int[] remainder = new int[N];
System.arraycopy(message, 0, remainder, PARITY, K);
for(int i = N - 1; i >= PARITY; i--)
{
int coefficient = remainder[i];
if(coefficient != 0)
{
for(int j = 0; j <= PARITY; j++)
{
remainder[i - PARITY + j] ^= multiply(generator[j], coefficient);
}
}
}
int[] codeword = new int[N];
System.arraycopy(remainder, 0, codeword, 0, PARITY);
System.arraycopy(message, 0, codeword, PARITY, K);
return codeword;
}
}

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

@@ -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);
}
}
}
}