feat(dmr): assemble aligned dibit bursts

This commit is contained in:
2026-07-14 18:42:20 +03:00
parent 53a3681618
commit 3fcf737685
2 changed files with 217 additions and 13 deletions
+154 -13
View File
@@ -25,6 +25,8 @@ const
DMR_SYMBOL_RATE = 4800; DMR_SYMBOL_RATE = 4800;
DMR_SAMPLES_PER_SYM = DMR_INPUT_RATE div DMR_SYMBOL_RATE; DMR_SAMPLES_PER_SYM = DMR_INPUT_RATE div DMR_SYMBOL_RATE;
DMR_RING_SIZE = 65536; // >1.3 s at 48 kHz, power of two DMR_RING_SIZE = 65536; // >1.3 s at 48 kHz, power of two
DMR_BURST_DIBITS = 144;
DMR_SYNC_END_DIBIT = 89; // sync occupies burst dibits 66..89
type type
TDMRSyncKind = ( TDMRSyncKind = (
@@ -41,6 +43,8 @@ type
SyncKind: TDMRSyncKind; SyncKind: TDMRSyncKind;
Inverted: Boolean; Inverted: Boolean;
SyncCount: QWord; SyncCount: QWord;
BurstCount: QWord;
BurstDibits: Integer;
DroppedSamples: QWord; DroppedSamples: QWord;
SignalRMS: Single; SignalRMS: Single;
LastSyncAgeMs: QWord; LastSyncAgeMs: QWord;
@@ -76,6 +80,20 @@ type
FWindowSum: Double; FWindowSum: Double;
FSampleNo: QWord; FSampleNo: QWord;
FSyncShift: array[0..DMR_SAMPLES_PER_SYM - 1] of LongWord; FSyncShift: array[0..DMR_SAMPLES_PER_SYM - 1] of LongWord;
FOuterLevel: array[0..DMR_SAMPLES_PER_SYM - 1] of Double;
FDibitHistory: array[0..DMR_SAMPLES_PER_SYM - 1,
0..DMR_SYNC_END_DIBIT] of Byte;
FHistoryPos: array[0..DMR_SAMPLES_PER_SYM - 1] of Integer;
FHistoryCount: array[0..DMR_SAMPLES_PER_SYM - 1] of Integer;
FMagHistory: array[0..DMR_SAMPLES_PER_SYM - 1, 0..23] of Double;
FMagPos: array[0..DMR_SAMPLES_PER_SYM - 1] of Integer;
FMagSum: array[0..DMR_SAMPLES_PER_SYM - 1] of Double;
FBestSyncQuality: Double;
FTrackPhase: Integer;
FCurrentBurst: array[0..DMR_BURST_DIBITS - 1] of Byte;
FCurrentBurstPos: Integer;
FLastBurst: array[0..DMR_BURST_DIBITS - 1] of Byte;
FBurstCount: QWord;
FRMSSq: Double; FRMSSq: Double;
FSyncKind: TDMRSyncKind; FSyncKind: TDMRSyncKind;
@@ -87,7 +105,10 @@ type
function PopSamples(var Buf: array of Single): Integer; function PopSamples(var Buf: array of Single): Integer;
procedure ProcessSample(S: Single); procedure ProcessSample(S: Single);
procedure ProcessSymbol(Phase: Integer; Symbol: Double); procedure ProcessSymbol(Phase: Integer; Symbol: Double);
procedure NoteSync(Kind: TDMRSyncKind; Inverted: Boolean); procedure NoteSync(Phase: Integer; Kind: TDMRSyncKind; Inverted: Boolean);
function Digitize(Phase: Integer; Symbol: Double): Byte;
procedure StartBurstTracking(Phase: Integer);
procedure CompleteBurst;
procedure ResetDSP; procedure ResetDSP;
public public
constructor Create; constructor Create;
@@ -97,6 +118,7 @@ type
procedure SetInverted(On_: Boolean); procedure SetInverted(On_: Boolean);
procedure FeedAudio(const Left, Right: array of Single; Count: Integer); procedure FeedAudio(const Left, Right: array of Single; Count: Integer);
procedure GetStatus(out S: TDMRStatus); procedure GetStatus(out S: TDMRStatus);
function GetLastBurst(var Dibits: array of Byte; out Sequence: QWord): Boolean;
class function SyncKindName(Kind: TDMRSyncKind): string; static; class function SyncKindName(Kind: TDMRSyncKind): string; static;
property Enabled: Boolean read FEnabled; property Enabled: Boolean read FEnabled;
@@ -153,19 +175,22 @@ var
begin begin
while not Terminated do while not Terminated do
begin begin
N := FOwner.PopSamples(Buf); // Pop and process share the DSP lock with ResetDSP. Otherwise a reset can
if N = 0 then // clear the ring after this thread popped an old block but before it was
begin // processed, allowing stale symbols to leak into the new mode/session.
RTLEventWaitFor(FOwner.FWake, 100);
Continue;
end;
FOwner.FDSPLock.Enter; FOwner.FDSPLock.Enter;
try try
N := FOwner.PopSamples(Buf);
for i := 0 to N - 1 do for i := 0 to N - 1 do
FOwner.ProcessSample(Buf[i]); FOwner.ProcessSample(Buf[i]);
finally finally
FOwner.FDSPLock.Leave; FOwner.FDSPLock.Leave;
end; end;
if N = 0 then
begin
RTLEventWaitFor(FOwner.FWake, 100);
Continue;
end;
end; end;
end; end;
@@ -215,6 +240,19 @@ begin
end; end;
FillChar(FSampleWindow, SizeOf(FSampleWindow), 0); FillChar(FSampleWindow, SizeOf(FSampleWindow), 0);
FillChar(FSyncShift, SizeOf(FSyncShift), 0); FillChar(FSyncShift, SizeOf(FSyncShift), 0);
FillChar(FOuterLevel, SizeOf(FOuterLevel), 0);
FillChar(FDibitHistory, SizeOf(FDibitHistory), 0);
FillChar(FHistoryPos, SizeOf(FHistoryPos), 0);
FillChar(FHistoryCount, SizeOf(FHistoryCount), 0);
FillChar(FMagHistory, SizeOf(FMagHistory), 0);
FillChar(FMagPos, SizeOf(FMagPos), 0);
FillChar(FMagSum, SizeOf(FMagSum), 0);
FBestSyncQuality := 0;
FillChar(FCurrentBurst, SizeOf(FCurrentBurst), 0);
FillChar(FLastBurst, SizeOf(FLastBurst), 0);
FTrackPhase := -1;
FCurrentBurstPos := 0;
FBurstCount := 0;
FWindowPos := 0; FWindowPos := 0;
FWindowCount := 0; FWindowCount := 0;
FWindowSum := 0; FWindowSum := 0;
@@ -317,35 +355,118 @@ procedure TDMRDecoder.ProcessSymbol(Phase: Integer; Symbol: Double);
var var
i: Integer; i: Integer;
Pat: LongWord; Pat: LongWord;
Dibit: Byte;
V: Double;
begin begin
V := Symbol;
if FInputInverted then V := -V;
Dibit := Digitize(Phase, V);
// Keep the 90 dibits ending at sync. Once the timing phase is selected this
// supplies burst positions 0..89 without waiting for another frame.
FDibitHistory[Phase, FHistoryPos[Phase]] := Dibit;
FHistoryPos[Phase] := (FHistoryPos[Phase] + 1) mod (DMR_SYNC_END_DIBIT + 1);
if FHistoryCount[Phase] < DMR_SYNC_END_DIBIT + 1 then Inc(FHistoryCount[Phase]);
FMagSum[Phase] := FMagSum[Phase] - FMagHistory[Phase, FMagPos[Phase]] + Abs(V);
FMagHistory[Phase, FMagPos[Phase]] := Abs(V);
FMagPos[Phase] := (FMagPos[Phase] + 1) mod 24;
if FTrackPhase = Phase then
begin
if FCurrentBurstPos < DMR_BURST_DIBITS then
begin
FCurrentBurst[FCurrentBurstPos] := Dibit;
Inc(FCurrentBurstPos);
if FCurrentBurstPos = DMR_BURST_DIBITS then CompleteBurst;
end;
end;
FSyncShift[Phase] := ((FSyncShift[Phase] shl 1) and DMR_SYNC_MASK); FSyncShift[Phase] := ((FSyncShift[Phase] shl 1) and DMR_SYNC_MASK);
if ((Symbol < 0) xor FInputInverted) then if V < 0 then FSyncShift[Phase] := FSyncShift[Phase] or 1;
FSyncShift[Phase] := FSyncShift[Phase] or 1;
for i := 0 to High(SYNC_TEXT) do for i := 0 to High(SYNC_TEXT) do
begin begin
Pat := SyncBits(SYNC_TEXT[i]); Pat := SyncBits(SYNC_TEXT[i]);
if FSyncShift[Phase] = Pat then if FSyncShift[Phase] = Pat then
begin begin
NoteSync(SYNC_KIND[i], FInputInverted); NoteSync(Phase, SYNC_KIND[i], FInputInverted);
Exit; Exit;
end; end;
end; end;
end; end;
procedure TDMRDecoder.NoteSync(Kind: TDMRSyncKind; Inverted: Boolean); function TDMRDecoder.Digitize(Phase: Integer; Symbol: Double): Byte;
var
A, Threshold: Double;
begin
A := Abs(Symbol);
if FOuterLevel[Phase] <= 1.0e-9 then FOuterLevel[Phase] := A
else if A > FOuterLevel[Phase] then
FOuterLevel[Phase] := A
else
FOuterLevel[Phase] := FOuterLevel[Phase] + 0.001 * (A - FOuterLevel[Phase]);
Threshold := 0.625 * FOuterLevel[Phase];
if Symbol >= 0 then
begin
if A >= Threshold then Result := 1 else Result := 0; // +3 / +1
end
else
begin
if A >= Threshold then Result := 3 else Result := 2; // -3 / -1
end;
end;
procedure TDMRDecoder.StartBurstTracking(Phase: Integer);
var
i, P: Integer;
begin
if FHistoryCount[Phase] < DMR_SYNC_END_DIBIT + 1 then Exit;
FTrackPhase := Phase;
P := FHistoryPos[Phase]; // oldest item; ring is exactly 90 dibits full
for i := 0 to DMR_SYNC_END_DIBIT do
begin
FCurrentBurst[i] := FDibitHistory[Phase, P];
P := (P + 1) mod (DMR_SYNC_END_DIBIT + 1);
end;
FCurrentBurstPos := DMR_SYNC_END_DIBIT + 1;
end;
procedure TDMRDecoder.CompleteBurst;
begin
Move(FCurrentBurst[0], FLastBurst[0], SizeOf(FLastBurst));
Inc(FBurstCount);
FCurrentBurstPos := 0;
end;
procedure TDMRDecoder.NoteSync(Phase: Integer; Kind: TDMRSyncKind;
Inverted: Boolean);
var var
NowTick: QWord; NowTick: QWord;
begin begin
NowTick := GetTickCount64; NowTick := GetTickCount64;
FStatusLock.Enter; FStatusLock.Enter;
try try
// Adjacent timing hypotheses may recognize the same physical burst. // Adjacent timing hypotheses recognize the same physical burst within one
if (FLastSyncTick <> 0) and (NowTick - FLastSyncTick < 5) then Exit; // 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
begin
if FMagSum[Phase] > FBestSyncQuality then
begin
FBestSyncQuality := FMagSum[Phase];
FSyncKind := Kind;
FInverted := Inverted;
StartBurstTracking(Phase);
end;
Exit;
end;
FSyncKind := Kind; FSyncKind := Kind;
FInverted := Inverted; FInverted := Inverted;
Inc(FSyncCount); Inc(FSyncCount);
FLastSyncTick := NowTick; FLastSyncTick := NowTick;
FBestSyncQuality := FMagSum[Phase];
StartBurstTracking(Phase);
finally finally
FStatusLock.Leave; FStatusLock.Leave;
end; end;
@@ -365,6 +486,8 @@ begin
S.SyncKind := FSyncKind; S.SyncKind := FSyncKind;
S.Inverted := FInverted; S.Inverted := FInverted;
S.SyncCount := FSyncCount; S.SyncCount := FSyncCount;
S.BurstCount := FBurstCount;
S.BurstDibits := FCurrentBurstPos;
if FLastSyncTick <> 0 then S.LastSyncAgeMs := NowTick - FLastSyncTick if FLastSyncTick <> 0 then S.LastSyncAgeMs := NowTick - FLastSyncTick
else S.LastSyncAgeMs := High(QWord); else S.LastSyncAgeMs := High(QWord);
S.Synced := FEnabled and (FLastSyncTick <> 0) and S.Synced := FEnabled and (FLastSyncTick <> 0) and
@@ -385,6 +508,24 @@ begin
end; end;
end; end;
function TDMRDecoder.GetLastBurst(var Dibits: array of Byte;
out Sequence: QWord): Boolean;
var
N: Integer;
begin
Sequence := 0;
FDSPLock.Enter;
try
Result := FBurstCount <> 0;
if not Result then Exit;
N := Min(Length(Dibits), DMR_BURST_DIBITS);
if N > 0 then Move(FLastBurst[0], Dibits[0], N * SizeOf(Byte));
Sequence := FBurstCount;
finally
FDSPLock.Leave;
end;
end;
class function TDMRDecoder.SyncKindName(Kind: TDMRSyncKind): string; class function TDMRDecoder.SyncKindName(Kind: TDMRSyncKind): string;
begin begin
case Kind of case Kind of
+63
View File
@@ -34,6 +34,64 @@ begin
D.FeedAudio(L, R, Length(L)); D.FeedAudio(L, R, Length(L));
end; end;
procedure TestBurst(D: TDMRDecoder);
var
Want, Got: array[0..DMR_BURST_DIBITS - 1] of Byte;
L, R: array of Single;
i, j, p: Integer;
V: Single;
Seq: QWord;
Status: TDMRStatus;
begin
for i := 0 to High(Want) do Want[i] := Byte((i * 3 + 1) and 3);
for i := 1 to Length(BS_VOICE_SYNC) do
if BS_VOICE_SYNC[i] = '1' then Want[65 + i] := 1
else Want[65 + i] := 3;
SetLength(L, 3 + (DMR_BURST_DIBITS + 2) * DMR_SAMPLES_PER_SYM);
SetLength(R, Length(L));
// One complete preceding symbol mirrors a continuous stream and fills the
// 90-dibit history even for the latest timing hypothesis.
V := 0.70; // Want[0] = 1
p := 3 + DMR_SAMPLES_PER_SYM;
for j := 0 to p - 1 do begin L[j] := V; R[j] := V; end;
for i := 0 to High(Want) do
begin
case Want[i] of
0: V := 0.23;
1: V := 0.70;
2: V := -0.23;
else
V := -0.70;
end;
for j := 0 to DMR_SAMPLES_PER_SYM - 1 do
begin
L[p] := V; R[p] := V; Inc(p);
end;
end;
// Hold the last level for one symbol so every one of the ten timing
// hypotheses has enough tail samples to close dibit 143.
for j := 0 to DMR_SAMPLES_PER_SYM - 1 do
begin
L[p] := V; R[p] := V; Inc(p);
end;
D.FeedAudio(L, R, Length(L));
Sleep(80);
if not D.GetLastBurst(Got, Seq) then
begin
D.GetStatus(Status);
WriteLn('burst was not assembled; sync=', Status.SyncCount,
' bursts=', Status.BurstCount, ' pos=', Status.BurstDibits);
Halt(3);
end;
for i := 0 to High(Want) do
if Got[i] <> Want[i] then
begin
WriteLn('burst dibit mismatch at ', i, ': ', Got[i], ' <> ', Want[i]);
Halt(4);
end;
end;
var var
D: TDMRDecoder; D: TDMRDecoder;
S: TDMRStatus; S: TDMRStatus;
@@ -61,6 +119,11 @@ begin
WriteLn('inverted sync failed'); WriteLn('inverted sync failed');
Halt(2); Halt(2);
end; end;
D.SetEnabled(False);
D.SetInverted(False);
D.SetEnabled(True);
TestBurst(D);
WriteLn('DMR front-end sync tests passed'); WriteLn('DMR front-end sync tests passed');
finally finally
D.Free; D.Free;