fix(dmr): use flat discriminator and reject noise sync

This commit is contained in:
2026-07-14 19:30:43 +03:00
parent 300bc91a46
commit a19c82a214
3 changed files with 118 additions and 10 deletions
+76 -2
View File
@@ -140,12 +140,15 @@ type
FSyncCount: QWord;
FLastSyncTick: QWord;
FLastSyncSample: QWord;
FPendingSyncSample: array[TDMRSyncKind] of QWord;
FPendingSyncCount: array[TDMRSyncKind] of Byte;
FMobileActiveBurst: Boolean;
function PopSamples(var Buf: array of Single): Integer;
procedure ProcessSample(S: Single);
procedure ProcessSymbol(Phase: Integer; Symbol: Double);
procedure NoteSync(Phase: Integer; Kind: TDMRSyncKind; Inverted: Boolean);
procedure LoseSync;
function Digitize(Phase: Integer; Symbol: Double): Byte;
procedure StartBurstTracking(Phase: Integer);
procedure CompleteBurst;
@@ -174,6 +177,7 @@ const
DMR_SYNC_HOLD_MS = 500;
DMR_SYNC_MAX_ERRORS = 2;
DMR_SUPERFRAME_SAMPLES = DMR_INPUT_RATE * 360 div 1000;
DMR_SYNC_CADENCE_TOLERANCE = DMR_SAMPLES_PER_SYM * 3;
// One bit per DSD-FME sync character: '1'=0, '3'=1. These are the eight
// ETSI DMR 48-bit sync patterns after collapsing each dibit to its sign.
@@ -337,6 +341,8 @@ begin
FSyncCount := 0;
FLastSyncTick := 0;
FLastSyncSample := 0;
FillChar(FPendingSyncSample, SizeOf(FPendingSyncSample), 0);
FillChar(FPendingSyncCount, SizeOf(FPendingSyncCount), 0);
FMobileActiveBurst := True;
finally
FStatusLock.Leave;
@@ -432,11 +438,19 @@ var
Pat: LongWord;
Dibit: Byte;
V: Double;
DeltaSamples, CycleOffset: QWord;
DeltaSamples, CycleOffset, PendingSample: QWord;
CandidateAllowed: Boolean;
begin
V := Symbol;
if FInputInverted then V := -V;
// Stop assembling/decoding an endless stream of noise after the carrier
// vanishes. Two superframes allow one damaged sync word without losing a
// valid call, while still closing the decoder in about 720 ms.
if (FLastSyncSample <> 0) and
(FSampleNo - FLastSyncSample > 2 * DMR_SUPERFRAME_SAMPLES +
DMR_SYNC_CADENCE_TOLERANCE) then
LoseSync;
Dibit := Digitize(Phase, V);
// Keep the 90 dibits ending at sync. Once the timing phase is selected this
@@ -479,7 +493,6 @@ begin
// match lost almost every superframe in recorded handheld-radio IQ.
DeltaSamples := FSampleNo - FLastSyncSample;
CandidateAllowed := (FLastSyncSample = 0) or
(DeltaSamples > DMR_INPUT_RATE * 2) or
(SYNC_KIND[i] = FSyncKind);
// MS voice/data sync repeats every 360 ms. Cadence gating rejects
// chance <=2-bit matches inside AMBE payload while still accepting a
@@ -494,6 +507,48 @@ begin
end;
if (Distance <= DMR_SYNC_MAX_ERRORS) and CandidateAllowed then
begin
// Fuzzy 24-symbol matches occur often enough in open-channel noise to
// make mbelib emit occasional garbage. Real voice sync repeats every
// 360 ms, so require three words of the same kind on that cadence
// before opening burst and vocoder processing.
if FLastSyncSample = 0 then
begin
PendingSample := FPendingSyncSample[SYNC_KIND[i]];
if PendingSample <> 0 then
begin
DeltaSamples := FSampleNo - PendingSample;
CycleOffset := DeltaSamples mod DMR_SUPERFRAME_SAMPLES;
if (DeltaSamples >= DMR_SUPERFRAME_SAMPLES -
DMR_SYNC_CADENCE_TOLERANCE) and
((CycleOffset <= DMR_SYNC_CADENCE_TOLERANCE) or
(CycleOffset >= DMR_SUPERFRAME_SAMPLES -
DMR_SYNC_CADENCE_TOLERANCE)) then
begin
Inc(FPendingSyncCount[SYNC_KIND[i]]);
FPendingSyncSample[SYNC_KIND[i]] := FSampleNo;
// Three consecutive superframe syncs make accidental capture
// in random discriminator noise vanishingly unlikely.
if FPendingSyncCount[SYNC_KIND[i]] >= 3 then
NoteSync(Phase, SYNC_KIND[i], FInputInverted);
end
else if DeltaSamples > DMR_SUPERFRAME_SAMPLES +
DMR_SYNC_CADENCE_TOLERANCE then
begin
FPendingSyncSample[SYNC_KIND[i]] := FSampleNo;
FPendingSyncCount[SYNC_KIND[i]] := 1;
end;
end;
if FLastSyncSample = 0 then
begin
if PendingSample = 0 then
begin
FPendingSyncSample[SYNC_KIND[i]] := FSampleNo;
FPendingSyncCount[SYNC_KIND[i]] := 1;
end;
Exit;
end;
end
else
NoteSync(Phase, SYNC_KIND[i], FInputInverted);
Exit;
end;
@@ -501,6 +556,25 @@ begin
end;
end;
procedure TDMRDecoder.LoseSync;
begin
FStatusLock.Enter;
try
FSyncKind := dskNone;
FInverted := False;
FLastSyncTick := 0;
FLastSyncSample := 0;
FillChar(FPendingSyncSample, SizeOf(FPendingSyncSample), 0);
FillChar(FPendingSyncCount, SizeOf(FPendingSyncCount), 0);
finally
FStatusLock.Leave;
end;
FTrackPhase := -1;
FCurrentBurstPos := 0;
FMobileActiveBurst := True;
DMRMbeReset(FMbe);
end;
function TDMRDecoder.Digitize(Phase: Integer; Symbol: Double): Byte;
var
A, Threshold: Double;
+17 -3
View File
@@ -60,7 +60,7 @@ const
MODE_DIGU = 8;
MODE_DIGL = 9;
MODE_WFM = 10;
MODE_DMR = 11; // DMR receive: WDSP NFM discriminator -> DMRDecoder
MODE_DMR = 11; // DMR receive: flat WDSP discriminator -> DMRDecoder
// Минимальная длина FIR у WDSP: create_* поднимают fircore с
// nc = max(2048, dsp_size). В firmin.c nfor = nc / size, idxmask = nfor - 1,
@@ -73,6 +73,7 @@ const
WFM_AF_LOW = 20.0;
WFM_AF_HIGH = 15000.0;
WFM_TAU_DE_US = 50.0e-6; // Европа/Россия (Америка — 75 мкс)
DMR_DEVIATION = 2500.0;
// Потолок dsp_rate для WFM. Вещательному каналу (200 кГц) хватает 192 кГц,
// а гнать цепь на полном IQ-rate нельзя: при 576 кГц dsp_size стал бы 12288,
// и fircore спланировал бы FFT на 2*12288 = 24576 точек. Это не степень
@@ -933,7 +934,10 @@ begin
MODE_DIGU: Result := WDSP_DIGU;
MODE_DIGL: Result := WDSP_DIGL;
MODE_WFM: Result := WDSP_RX_WFM;
MODE_DMR: Result := WDSP_FM;
// The narrow-FM path always applies its 300..3000 Hz speech filters and
// de-emphasis. Those filters collapse the four DMR discriminator levels.
// WFMD is a plain quadrature discriminator and permits flat data audio.
MODE_DMR: Result := WDSP_RX_WFM;
else Result := WDSP_USB;
end;
end;
@@ -942,6 +946,7 @@ function TWDSPEngine.ModeToWDSPTX(Mode: Integer): Integer;
// txaMode расходится с rxaMode после DRM — отдельный маппинг только для WFM.
begin
if Mode = MODE_WFM then Result := WDSP_TX_WFM
else if Mode = MODE_DMR then Result := WDSP_FM
else Result := ModeToWDSP(Mode);
end;
@@ -2693,7 +2698,13 @@ begin
ApplyDSPRate(Mode);
SetRXAMode(RXA_CHAN, ModeToWDSP(Mode));
SetTXAMode(TXA_CHAN, ModeToWDSPTX(Mode));
if Mode = MODE_WFM then ApplyWFMSettings;
if Mode = MODE_WFM then ApplyWFMSettings
else if Mode = MODE_DMR then
begin
SetRXAWFMDeviation (RXA_CHAN, DMR_DEVIATION);
SetRXAWFMAFFilter (RXA_CHAN, 20.0, 7000.0);
SetRXAWFMDeemphRun (RXA_CHAN, 0);
end;
// НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
end;
@@ -2929,6 +2940,9 @@ end;
procedure TWDSPEngine.SetRXFMDeviation(Deviation: Double);
begin
if not FInitialized then Exit;
if FMode = MODE_DMR then
SetRXAWFMDeviation(RXA_CHAN, Deviation)
else
SetRXAFMDeviation(RXA_CHAN, Deviation);
end;
+22 -2
View File
@@ -8,6 +8,7 @@ uses
const
BS_VOICE_SYNC = '131111333113313313113313';
DMR_TEST_SUPERFRAME_SAMPLES = DMR_INPUT_RATE * 360 div 1000;
type
TAudioSink = class
@@ -48,6 +49,23 @@ begin
D.FeedAudio(L, R, Length(L));
end;
procedure FeedConfirmedSync(D: TDMRDecoder; const Pattern: string;
Invert: Boolean);
var
L, R: array of Single;
GapSamples, RepeatIndex: Integer;
begin
GapSamples := DMR_TEST_SUPERFRAME_SAMPLES -
(3 + Length(Pattern) * DMR_SAMPLES_PER_SYM + 20);
SetLength(L, GapSamples);
SetLength(R, GapSamples);
for RepeatIndex := 1 to 3 do
begin
FeedSync(D, Pattern, Invert);
if RepeatIndex < 3 then D.FeedAudio(L, R, GapSamples);
end;
end;
procedure TestBurst(D: TDMRDecoder);
var
Want, Got: array[0..DMR_BURST_DIBITS - 1] of Byte;
@@ -116,7 +134,7 @@ begin
try
D.OnAudio := Sink.AudioReady;
D.SetEnabled(True);
FeedSync(D, BS_VOICE_SYNC, False);
FeedConfirmedSync(D, BS_VOICE_SYNC, False);
Sleep(50);
D.GetStatus(S);
if (not S.Synced) or (S.SyncKind <> dskBSVoice) or S.Inverted then
@@ -128,7 +146,7 @@ begin
D.SetEnabled(False);
D.SetInverted(True);
D.SetEnabled(True);
FeedSync(D, BS_VOICE_SYNC, True);
FeedConfirmedSync(D, BS_VOICE_SYNC, True);
Sleep(50);
D.GetStatus(S);
if (not S.Synced) or (S.SyncKind <> dskBSVoice) or (not S.Inverted) then
@@ -140,6 +158,8 @@ begin
D.SetEnabled(False);
D.SetInverted(False);
D.SetEnabled(True);
FeedConfirmedSync(D, BS_VOICE_SYNC, False);
Sleep(50);
TestBurst(D);
D.GetStatus(S);
if S.VocoderAvailable and