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
+10 -1
View File
@@ -71,7 +71,7 @@ const
{$ENDIF}
var
i: Integer;
LocalName: string;
LocalName, BuildName: string;
begin
if GLib <> dynlibs.NilHandle then Exit(True);
GLastError := '';
@@ -79,6 +79,15 @@ begin
begin
LocalName := ExtractFilePath(ParamStr(0)) + Names[i];
GLib := LoadLibrary(LocalName);
// Lazarus debug binaries live in bin/<target>/ while the documented
// optional adapter build lives in build/dmr. Make an in-tree build
// immediately usable without a system-wide cmake --install.
if GLib = dynlibs.NilHandle then
begin
BuildName := ExpandFileName(ExtractFilePath(ParamStr(0)) +
'../../build/dmr/' + Names[i]);
GLib := LoadLibrary(BuildName);
end;
if GLib = dynlibs.NilHandle then GLib := LoadLibrary(Names[i]);
if GLib <> dynlibs.NilHandle then Break;
end;
+38 -2
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,13 +474,31 @@ begin
FBestSyncPhase := Phase;
FBestCandidateKind := SYNC_KIND[i];
end;
// 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 FSyncShift[Phase] = Pat then
if (Distance <= DMR_SYNC_MAX_ERRORS) and CandidateAllowed then
begin
NoteSync(Phase, SYNC_KIND[i], FInputInverted);
Exit;
end;
end;
end;
end;
function TDMRDecoder.Digitize(Phase: Integer; Symbol: Double): Byte;
@@ -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;
+4
View File
@@ -859,6 +859,10 @@ begin
// DMR front-end owns a worker thread. It stays idle until MODE_DMR is active.
FDMRDiag := TDMRDiagnosticCapture.Create;
FDMRDec := TDMRDecoder.Create;
// WDSP's FM discriminator polarity on the EWSDR RX path is opposite to the
// DMR dibit convention: captured handheld MS voice sync appears as MS data
// without this inversion (the two sync words are complements).
FDMRDec.SetInverted(True);
FDMRDec.OnAudio := OnDMRAudioReady;
FDMRDec.OnBurst := OnDMRBurstReady;
+4
View File
@@ -72,6 +72,10 @@ begin
WriteLn('last_sync=', TDMRDecoder.SyncKindName(Status.SyncKind));
WriteLn('bursts=', Counter.Count);
WriteLn('dropped_samples=', Status.DroppedSamples);
WriteLn('selected_slot=', Status.SelectedSlot + 1);
WriteLn('voice_frames=', Status.VoiceFrameCount);
WriteLn('voice_errors=', Status.VoiceErrorCount);
WriteLn('vocoder_available=', Status.VocoderAvailable);
if Status.CACHValid then WriteLn('timeslot=', Status.Slot + 1);
if Status.SlotTypeValid then
WriteLn('color_code=', Status.ColorCode, ' data_type=', Status.DataType);