mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(dmr): assemble aligned dibit bursts
This commit is contained in:
+154
-13
@@ -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
|
||||
|
||||
@@ -34,6 +34,64 @@ begin
|
||||
D.FeedAudio(L, R, Length(L));
|
||||
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
|
||||
D: TDMRDecoder;
|
||||
S: TDMRStatus;
|
||||
@@ -61,6 +119,11 @@ begin
|
||||
WriteLn('inverted sync failed');
|
||||
Halt(2);
|
||||
end;
|
||||
|
||||
D.SetEnabled(False);
|
||||
D.SetInverted(False);
|
||||
D.SetEnabled(True);
|
||||
TestBurst(D);
|
||||
WriteLn('DMR front-end sync tests passed');
|
||||
finally
|
||||
D.Free;
|
||||
|
||||
Reference in New Issue
Block a user