mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
fix(dmr): use flat discriminator and reject noise sync
This commit is contained in:
+76
-2
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user