From a19c82a214fb174c7bdb3e66360e852f9b870811 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 14 Jul 2026 19:30:43 +0300 Subject: [PATCH] fix(dmr): use flat discriminator and reject noise sync --- DMRDecoder.pas | 80 +++++++++++++++++++++++++++++++++++-- WDSPEngine.pas | 24 ++++++++--- tests/dmr_frontend_test.pas | 24 ++++++++++- 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/DMRDecoder.pas b/DMRDecoder.pas index 3adc847..fce392a 100644 --- a/DMRDecoder.pas +++ b/DMRDecoder.pas @@ -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,13 +507,74 @@ begin end; if (Distance <= DMR_SYNC_MAX_ERRORS) and CandidateAllowed then begin - NoteSync(Phase, SYNC_KIND[i], FInputInverted); + // 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; end; 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; diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 61c6313..c8144ee 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -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,7 +946,8 @@ function TWDSPEngine.ModeToWDSPTX(Mode: Integer): Integer; // txaMode расходится с rxaMode после DRM — отдельный маппинг только для WFM. begin if Mode = MODE_WFM then Result := WDSP_TX_WFM - else Result := ModeToWDSP(Mode); + else if Mode = MODE_DMR then Result := WDSP_FM + else Result := ModeToWDSP(Mode); end; function TWDSPEngine.WantWideDSP(Mode: Integer): Boolean; @@ -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,7 +2940,10 @@ end; procedure TWDSPEngine.SetRXFMDeviation(Deviation: Double); begin if not FInitialized then Exit; - SetRXAFMDeviation(RXA_CHAN, Deviation); + if FMode = MODE_DMR then + SetRXAWFMDeviation(RXA_CHAN, Deviation) + else + SetRXAFMDeviation(RXA_CHAN, Deviation); end; procedure TWDSPEngine.SetFMSquelch(Enable: Boolean; Level: Integer); diff --git a/tests/dmr_frontend_test.pas b/tests/dmr_frontend_test.pas index 11bec84..29eacf5 100644 --- a/tests/dmr_frontend_test.pas +++ b/tests/dmr_frontend_test.pas @@ -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