fix(dmr): recover handheld voice sync

This commit is contained in:
2026-07-14 19:17:55 +03:00
parent 39b4627c2b
commit 300bc91a46
4 changed files with 60 additions and 7 deletions
+42 -6
View File
@@ -139,6 +139,8 @@ type
FInverted: Boolean;
FSyncCount: QWord;
FLastSyncTick: QWord;
FLastSyncSample: QWord;
FMobileActiveBurst: Boolean;
function PopSamples(var Buf: array of Single): Integer;
procedure ProcessSample(S: Single);
@@ -170,6 +172,8 @@ implementation
const
DMR_SYNC_MASK = LongWord($00FFFFFF);
DMR_SYNC_HOLD_MS = 500;
DMR_SYNC_MAX_ERRORS = 2;
DMR_SUPERFRAME_SAMPLES = DMR_INPUT_RATE * 360 div 1000;
// 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.
@@ -332,6 +336,8 @@ begin
FInverted := False;
FSyncCount := 0;
FLastSyncTick := 0;
FLastSyncSample := 0;
FMobileActiveBurst := True;
finally
FStatusLock.Leave;
end;
@@ -426,6 +432,8 @@ var
Pat: LongWord;
Dibit: Byte;
V: Double;
DeltaSamples, CycleOffset: QWord;
CandidateAllowed: Boolean;
begin
V := Symbol;
if FInputInverted then V := -V;
@@ -466,11 +474,29 @@ begin
FBestSyncPhase := Phase;
FBestCandidateKind := SYNC_KIND[i];
end;
end;
if FSyncShift[Phase] = Pat then
begin
NoteSync(Phase, SYNC_KIND[i], FInputInverted);
Exit;
// Real on-air sync commonly carries one or two sign errors. DSD-FME
// likewise uses a bounded sync tolerance; requiring an exact 24-symbol
// 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
// superframe after one or more missed sync words.
if CandidateAllowed and (FLastSyncSample <> 0) and
(FSyncKind in [dskMSData, dskMSVoice]) and
(SYNC_KIND[i] = FSyncKind) and (DeltaSamples > 20) then
begin
CycleOffset := DeltaSamples mod DMR_SUPERFRAME_SAMPLES;
CandidateAllowed := (CycleOffset <= 20) or
(CycleOffset >= DMR_SUPERFRAME_SAMPLES - 20);
end;
if (Distance <= DMR_SYNC_MAX_ERRORS) and CandidateAllowed then
begin
NoteSync(Phase, SYNC_KIND[i], FInputInverted);
Exit;
end;
end;
end;
end;
@@ -542,6 +568,8 @@ begin
end;
end;
DecodeVoiceBurst;
if FSyncKind in [dskMSData, dskMSVoice] then
FMobileActiveBurst := not FMobileActiveBurst;
Inc(FBurstCount);
FCurrentBurstPos := 0;
end;
@@ -558,6 +586,10 @@ begin
(GetTickCount64 - FLastSyncTick > DMR_SYNC_HOLD_MS) then Exit;
if not (FSyncKind in [dskBSVoice, dskMSVoice, dskDirectTS1Voice,
dskDirectTS2Voice]) then Exit;
// Mobile simplex occupies one 30 ms half-slot and leaves the alternating
// half-slot idle. A 144-dibit continuous assembler sees both; only every
// other record contains the three AMBE frames.
if (FSyncKind = dskMSVoice) and not FMobileActiveBurst then Exit;
if FSyncKind in [dskBSVoice] then
begin
@@ -566,6 +598,7 @@ begin
end
else if FSyncKind = dskDirectTS2Voice then Slot := 1
else Slot := 0;
if FSyncKind = dskMSVoice then FSelectedSlot := 0;
NowTick := GetTickCount64;
if (FSelectedSlot < 0) or
((Slot <> FSelectedSlot) and (FLastSelectedVoiceTick <> 0) and
@@ -608,7 +641,8 @@ begin
// Adjacent timing hypotheses recognize the same physical burst within one
// sample period. Keep the phase with the largest 24-symbol eye opening,
// not merely the first phase that happened to match the sign pattern.
if (FLastSyncTick <> 0) and (NowTick - FLastSyncTick < 5) then
if (FLastSyncSample <> 0) and
(FSampleNo - FLastSyncSample <= DMR_SAMPLES_PER_SYM * 2) then
begin
if FMagSum[Phase] > FBestSyncQuality then
begin
@@ -623,7 +657,9 @@ begin
FInverted := Inverted;
Inc(FSyncCount);
FLastSyncTick := NowTick;
FLastSyncSample := FSampleNo;
FBestSyncQuality := FMagSum[Phase];
if Kind in [dskMSData, dskMSVoice] then FMobileActiveBurst := True;
StartBurstTracking(Phase);
finally
FStatusLock.Leave;