mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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_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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user