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_SAMPLES_PER_SYM = DMR_INPUT_RATE div DMR_SYMBOL_RATE;
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
TDMRSyncKind = (
@@ -41,6 +43,8 @@ type
SyncKind: TDMRSyncKind;
Inverted: Boolean;
SyncCount: QWord;
BurstCount: QWord;
BurstDibits: Integer;
DroppedSamples: QWord;
SignalRMS: Single;
LastSyncAgeMs: QWord;
@@ -76,6 +80,20 @@ type
FWindowSum: Double;
FSampleNo: QWord;
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;
FSyncKind: TDMRSyncKind;
@@ -87,7 +105,10 @@ type
function PopSamples(var Buf: array of Single): Integer;
procedure ProcessSample(S: Single);
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;
public
constructor Create;
@@ -97,6 +118,7 @@ type
procedure SetInverted(On_: Boolean);
procedure FeedAudio(const Left, Right: array of Single; Count: Integer);
procedure GetStatus(out S: TDMRStatus);
function GetLastBurst(var Dibits: array of Byte; out Sequence: QWord): Boolean;
class function SyncKindName(Kind: TDMRSyncKind): string; static;
property Enabled: Boolean read FEnabled;
@@ -153,19 +175,22 @@ var
begin
while not Terminated do
begin
N := FOwner.PopSamples(Buf);
if N = 0 then
begin
RTLEventWaitFor(FOwner.FWake, 100);
Continue;
end;
// Pop and process share the DSP lock with ResetDSP. Otherwise a reset can
// clear the ring after this thread popped an old block but before it was
// processed, allowing stale symbols to leak into the new mode/session.
FOwner.FDSPLock.Enter;
try
N := FOwner.PopSamples(Buf);
for i := 0 to N - 1 do
FOwner.ProcessSample(Buf[i]);
finally
FOwner.FDSPLock.Leave;
end;
if N = 0 then
begin
RTLEventWaitFor(FOwner.FWake, 100);
Continue;
end;
end;
end;
@@ -215,6 +240,19 @@ begin
end;
FillChar(FSampleWindow, SizeOf(FSampleWindow), 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;
FWindowCount := 0;
FWindowSum := 0;
@@ -317,35 +355,118 @@ procedure TDMRDecoder.ProcessSymbol(Phase: Integer; Symbol: Double);
var
i: Integer;
Pat: LongWord;
Dibit: Byte;
V: Double;
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);
if ((Symbol < 0) xor FInputInverted) then
FSyncShift[Phase] := FSyncShift[Phase] or 1;
if V < 0 then FSyncShift[Phase] := FSyncShift[Phase] or 1;
for i := 0 to High(SYNC_TEXT) do
begin
Pat := SyncBits(SYNC_TEXT[i]);
if FSyncShift[Phase] = Pat then
begin
NoteSync(SYNC_KIND[i], FInputInverted);
NoteSync(Phase, SYNC_KIND[i], FInputInverted);
Exit;
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
NowTick: QWord;
begin
NowTick := GetTickCount64;
FStatusLock.Enter;
try
// Adjacent timing hypotheses may recognize the same physical burst.
if (FLastSyncTick <> 0) and (NowTick - FLastSyncTick < 5) then Exit;
// 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
begin
if FMagSum[Phase] > FBestSyncQuality then
begin
FBestSyncQuality := FMagSum[Phase];
FSyncKind := Kind;
FInverted := Inverted;
StartBurstTracking(Phase);
end;
Exit;
end;
FSyncKind := Kind;
FInverted := Inverted;
Inc(FSyncCount);
FLastSyncTick := NowTick;
FBestSyncQuality := FMagSum[Phase];
StartBurstTracking(Phase);
finally
FStatusLock.Leave;
end;
@@ -365,6 +486,8 @@ begin
S.SyncKind := FSyncKind;
S.Inverted := FInverted;
S.SyncCount := FSyncCount;
S.BurstCount := FBurstCount;
S.BurstDibits := FCurrentBurstPos;
if FLastSyncTick <> 0 then S.LastSyncAgeMs := NowTick - FLastSyncTick
else S.LastSyncAgeMs := High(QWord);
S.Synced := FEnabled and (FLastSyncTick <> 0) and
@@ -385,6 +508,24 @@ begin
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;
begin
case Kind of