diff --git a/DMRDecoder.pas b/DMRDecoder.pas index 39382c2..01ea46d 100644 --- a/DMRDecoder.pas +++ b/DMRDecoder.pas @@ -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 diff --git a/tests/dmr_frontend_test.pas b/tests/dmr_frontend_test.pas index e3e3704..ccea570 100644 --- a/tests/dmr_frontend_test.pas +++ b/tests/dmr_frontend_test.pas @@ -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;