mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
feat(dmr): add receive frontend and AMBE adapter
This commit is contained in:
+404
@@ -0,0 +1,404 @@
|
||||
unit DMRDecoder;
|
||||
|
||||
{
|
||||
DMR 4FSK receive front-end.
|
||||
|
||||
Input is 48 kHz discriminator audio from the WDSP FM demodulator. The DSP
|
||||
callback only copies complete blocks into a bounded ring; symbol timing and
|
||||
sync detection run in TDMRDecoderThread and can never stall the WDSP thread.
|
||||
|
||||
This first layer deliberately stops at burst sync. The following layer will
|
||||
pass aligned dibits to the trimmed dsd-fme DMR/FEC/AMBE core.
|
||||
}
|
||||
|
||||
{$IFDEF FPC}
|
||||
{$MODE Delphi}
|
||||
{$ENDIF}
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, SyncObjs, Math, DMRBindings;
|
||||
|
||||
const
|
||||
DMR_INPUT_RATE = 48000;
|
||||
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
|
||||
|
||||
type
|
||||
TDMRSyncKind = (
|
||||
dskNone,
|
||||
dskBSData, dskBSVoice,
|
||||
dskMSData, dskMSVoice,
|
||||
dskDirectTS1Data, dskDirectTS1Voice,
|
||||
dskDirectTS2Data, dskDirectTS2Voice
|
||||
);
|
||||
|
||||
TDMRStatus = record
|
||||
Enabled: Boolean;
|
||||
Synced: Boolean;
|
||||
SyncKind: TDMRSyncKind;
|
||||
Inverted: Boolean;
|
||||
SyncCount: QWord;
|
||||
DroppedSamples: QWord;
|
||||
SignalRMS: Single;
|
||||
LastSyncAgeMs: QWord;
|
||||
VocoderAvailable: Boolean;
|
||||
end;
|
||||
|
||||
TDMRDecoder = class;
|
||||
|
||||
TDMRDecoderThread = class(TThread)
|
||||
private
|
||||
FOwner: TDMRDecoder;
|
||||
protected
|
||||
procedure Execute; override;
|
||||
public
|
||||
constructor Create(AOwner: TDMRDecoder);
|
||||
end;
|
||||
|
||||
TDMRDecoder = class
|
||||
private
|
||||
FEnabled: Boolean;
|
||||
FRingLock: TCriticalSection;
|
||||
FDSPLock: TCriticalSection;
|
||||
FStatusLock: TCriticalSection;
|
||||
FWake: PRTLEvent;
|
||||
FThread: TDMRDecoderThread;
|
||||
FMbe: Pointer;
|
||||
FRing: array[0..DMR_RING_SIZE - 1] of Single;
|
||||
FReadPos, FWritePos: Integer;
|
||||
FDropped: QWord;
|
||||
|
||||
FSampleWindow: array[0..DMR_SAMPLES_PER_SYM - 1] of Double;
|
||||
FWindowPos, FWindowCount: Integer;
|
||||
FWindowSum: Double;
|
||||
FSampleNo: QWord;
|
||||
FSyncShift: array[0..DMR_SAMPLES_PER_SYM - 1] of LongWord;
|
||||
FRMSSq: Double;
|
||||
|
||||
FSyncKind: TDMRSyncKind;
|
||||
FInputInverted: Boolean;
|
||||
FInverted: Boolean;
|
||||
FSyncCount: QWord;
|
||||
FLastSyncTick: QWord;
|
||||
|
||||
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 ResetDSP;
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
|
||||
procedure SetEnabled(On_: Boolean);
|
||||
procedure SetInverted(On_: Boolean);
|
||||
procedure FeedAudio(const Left, Right: array of Single; Count: Integer);
|
||||
procedure GetStatus(out S: TDMRStatus);
|
||||
|
||||
class function SyncKindName(Kind: TDMRSyncKind): string; static;
|
||||
property Enabled: Boolean read FEnabled;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
const
|
||||
DMR_SYNC_MASK = LongWord($00FFFFFF);
|
||||
DMR_SYNC_HOLD_MS = 500;
|
||||
|
||||
// One bit per DSD-FME sync character: '1'=0, '3'=1. These are the eight
|
||||
// ETSI DMR 48-bit sync patterns after collapsing each dibit to its sign.
|
||||
SYNC_TEXT: array[0..7] of string = (
|
||||
'313333111331131131331131', // BS data
|
||||
'131111333113313313113313', // BS voice
|
||||
'311131133313133331131113', // MS data
|
||||
'133313311131311113313331', // MS voice
|
||||
'331333313111313133311111', // direct TS1 data
|
||||
'113111131333131311133333', // direct TS1 voice
|
||||
'311311111333113333133311', // direct TS2 data
|
||||
'133133333111331111311133' // direct TS2 voice
|
||||
);
|
||||
|
||||
SYNC_KIND: array[0..7] of TDMRSyncKind = (
|
||||
dskBSData, dskBSVoice, dskMSData, dskMSVoice,
|
||||
dskDirectTS1Data, dskDirectTS1Voice,
|
||||
dskDirectTS2Data, dskDirectTS2Voice
|
||||
);
|
||||
|
||||
function SyncBits(const Text: string): LongWord;
|
||||
var
|
||||
i: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
for i := 1 to Length(Text) do
|
||||
begin
|
||||
Result := (Result shl 1) and DMR_SYNC_MASK;
|
||||
if Text[i] = '3' then Result := Result or 1;
|
||||
end;
|
||||
end;
|
||||
|
||||
constructor TDMRDecoderThread.Create(AOwner: TDMRDecoder);
|
||||
begin
|
||||
inherited Create(True);
|
||||
FreeOnTerminate := False;
|
||||
FOwner := AOwner;
|
||||
end;
|
||||
|
||||
procedure TDMRDecoderThread.Execute;
|
||||
var
|
||||
Buf: array[0..2047] of Single;
|
||||
N, i: Integer;
|
||||
begin
|
||||
while not Terminated do
|
||||
begin
|
||||
N := FOwner.PopSamples(Buf);
|
||||
if N = 0 then
|
||||
begin
|
||||
RTLEventWaitFor(FOwner.FWake, 100);
|
||||
Continue;
|
||||
end;
|
||||
FOwner.FDSPLock.Enter;
|
||||
try
|
||||
for i := 0 to N - 1 do
|
||||
FOwner.ProcessSample(Buf[i]);
|
||||
finally
|
||||
FOwner.FDSPLock.Leave;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
constructor TDMRDecoder.Create;
|
||||
begin
|
||||
inherited Create;
|
||||
FRingLock := TCriticalSection.Create;
|
||||
FDSPLock := TCriticalSection.Create;
|
||||
FStatusLock := TCriticalSection.Create;
|
||||
FWake := RTLEventCreate;
|
||||
FThread := TDMRDecoderThread.Create(Self);
|
||||
if DMRLoad then FMbe := DMRMbeCreate(3);
|
||||
ResetDSP;
|
||||
FThread.Start;
|
||||
end;
|
||||
|
||||
destructor TDMRDecoder.Destroy;
|
||||
begin
|
||||
if FThread <> nil then
|
||||
begin
|
||||
FThread.Terminate;
|
||||
RTLEventSetEvent(FWake);
|
||||
FThread.WaitFor;
|
||||
FreeAndNil(FThread);
|
||||
end;
|
||||
DMRMbeDestroy(FMbe);
|
||||
FMbe := nil;
|
||||
DMRUnload;
|
||||
RTLEventDestroy(FWake);
|
||||
FStatusLock.Free;
|
||||
FDSPLock.Free;
|
||||
FRingLock.Free;
|
||||
inherited Destroy;
|
||||
end;
|
||||
|
||||
procedure TDMRDecoder.ResetDSP;
|
||||
begin
|
||||
FDSPLock.Enter;
|
||||
try
|
||||
FRingLock.Enter;
|
||||
try
|
||||
FReadPos := 0;
|
||||
FWritePos := 0;
|
||||
FDropped := 0;
|
||||
finally
|
||||
FRingLock.Leave;
|
||||
end;
|
||||
FillChar(FSampleWindow, SizeOf(FSampleWindow), 0);
|
||||
FillChar(FSyncShift, SizeOf(FSyncShift), 0);
|
||||
FWindowPos := 0;
|
||||
FWindowCount := 0;
|
||||
FWindowSum := 0;
|
||||
FSampleNo := 0;
|
||||
FRMSSq := 0;
|
||||
FStatusLock.Enter;
|
||||
try
|
||||
FSyncKind := dskNone;
|
||||
FInverted := False;
|
||||
FSyncCount := 0;
|
||||
FLastSyncTick := 0;
|
||||
finally
|
||||
FStatusLock.Leave;
|
||||
end;
|
||||
finally
|
||||
FDSPLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TDMRDecoder.SetEnabled(On_: Boolean);
|
||||
begin
|
||||
if FEnabled = On_ then Exit;
|
||||
FEnabled := On_;
|
||||
ResetDSP;
|
||||
if On_ then RTLEventSetEvent(FWake);
|
||||
end;
|
||||
|
||||
procedure TDMRDecoder.SetInverted(On_: Boolean);
|
||||
begin
|
||||
if FInputInverted = On_ then Exit;
|
||||
FInputInverted := On_;
|
||||
if FEnabled then ResetDSP;
|
||||
end;
|
||||
|
||||
procedure TDMRDecoder.FeedAudio(const Left, Right: array of Single;
|
||||
Count: Integer);
|
||||
var
|
||||
i, Next: Integer;
|
||||
V: Single;
|
||||
begin
|
||||
if not FEnabled or (Count <= 0) then Exit;
|
||||
Count := Min(Count, Min(Length(Left), Length(Right)));
|
||||
FRingLock.Enter;
|
||||
try
|
||||
for i := 0 to Count - 1 do
|
||||
begin
|
||||
Next := (FWritePos + 1) and (DMR_RING_SIZE - 1);
|
||||
if Next = FReadPos then
|
||||
begin
|
||||
Inc(FDropped);
|
||||
Continue;
|
||||
end;
|
||||
V := 0.5 * (Left[i] + Right[i]);
|
||||
FRing[FWritePos] := V;
|
||||
FWritePos := Next;
|
||||
end;
|
||||
finally
|
||||
FRingLock.Leave;
|
||||
end;
|
||||
RTLEventSetEvent(FWake);
|
||||
end;
|
||||
|
||||
function TDMRDecoder.PopSamples(var Buf: array of Single): Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
FRingLock.Enter;
|
||||
try
|
||||
while (FReadPos <> FWritePos) and (Result < Length(Buf)) do
|
||||
begin
|
||||
Buf[Result] := FRing[FReadPos];
|
||||
FReadPos := (FReadPos + 1) and (DMR_RING_SIZE - 1);
|
||||
Inc(Result);
|
||||
end;
|
||||
finally
|
||||
FRingLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TDMRDecoder.ProcessSample(S: Single);
|
||||
var
|
||||
Phase: Integer;
|
||||
Sym: Double;
|
||||
begin
|
||||
FRMSSq := FRMSSq + 0.001 * (S * S - FRMSSq);
|
||||
FWindowSum := FWindowSum - FSampleWindow[FWindowPos] + S;
|
||||
FSampleWindow[FWindowPos] := S;
|
||||
FWindowPos := (FWindowPos + 1) mod DMR_SAMPLES_PER_SYM;
|
||||
if FWindowCount < DMR_SAMPLES_PER_SYM then Inc(FWindowCount);
|
||||
Inc(FSampleNo);
|
||||
if FWindowCount < DMR_SAMPLES_PER_SYM then Exit;
|
||||
|
||||
// Ten interleaved timing hypotheses. Each receives one boxcar-integrated
|
||||
// symbol every ten input samples; the correct phase produces exact sync.
|
||||
Phase := Integer(FSampleNo mod DMR_SAMPLES_PER_SYM);
|
||||
Sym := FWindowSum / DMR_SAMPLES_PER_SYM;
|
||||
ProcessSymbol(Phase, Sym);
|
||||
end;
|
||||
|
||||
procedure TDMRDecoder.ProcessSymbol(Phase: Integer; Symbol: Double);
|
||||
var
|
||||
i: Integer;
|
||||
Pat: LongWord;
|
||||
begin
|
||||
FSyncShift[Phase] := ((FSyncShift[Phase] shl 1) and DMR_SYNC_MASK);
|
||||
if ((Symbol < 0) xor FInputInverted) 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);
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TDMRDecoder.NoteSync(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;
|
||||
FSyncKind := Kind;
|
||||
FInverted := Inverted;
|
||||
Inc(FSyncCount);
|
||||
FLastSyncTick := NowTick;
|
||||
finally
|
||||
FStatusLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TDMRDecoder.GetStatus(out S: TDMRStatus);
|
||||
var
|
||||
NowTick: QWord;
|
||||
begin
|
||||
FillChar(S, SizeOf(S), 0);
|
||||
NowTick := GetTickCount64;
|
||||
FDSPLock.Enter;
|
||||
try
|
||||
FStatusLock.Enter;
|
||||
try
|
||||
S.Enabled := FEnabled;
|
||||
S.SyncKind := FSyncKind;
|
||||
S.Inverted := FInverted;
|
||||
S.SyncCount := FSyncCount;
|
||||
if FLastSyncTick <> 0 then S.LastSyncAgeMs := NowTick - FLastSyncTick
|
||||
else S.LastSyncAgeMs := High(QWord);
|
||||
S.Synced := FEnabled and (FLastSyncTick <> 0) and
|
||||
(S.LastSyncAgeMs <= DMR_SYNC_HOLD_MS);
|
||||
S.SignalRMS := Sqrt(FRMSSq);
|
||||
S.VocoderAvailable := FMbe <> nil;
|
||||
finally
|
||||
FStatusLock.Leave;
|
||||
end;
|
||||
finally
|
||||
FDSPLock.Leave;
|
||||
end;
|
||||
FRingLock.Enter;
|
||||
try
|
||||
S.DroppedSamples := FDropped;
|
||||
finally
|
||||
FRingLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
class function TDMRDecoder.SyncKindName(Kind: TDMRSyncKind): string;
|
||||
begin
|
||||
case Kind of
|
||||
dskBSData: Result := 'BS data';
|
||||
dskBSVoice: Result := 'BS voice';
|
||||
dskMSData: Result := 'MS data';
|
||||
dskMSVoice: Result := 'MS voice';
|
||||
dskDirectTS1Data: Result := 'direct TS1 data';
|
||||
dskDirectTS1Voice: Result := 'direct TS1 voice';
|
||||
dskDirectTS2Data: Result := 'direct TS2 data';
|
||||
dskDirectTS2Voice: Result := 'direct TS2 voice';
|
||||
else
|
||||
Result := 'none';
|
||||
end;
|
||||
end;
|
||||
|
||||
end.
|
||||
Reference in New Issue
Block a user