mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
feat(dmr): add receive frontend and AMBE adapter
This commit is contained in:
@@ -4,3 +4,7 @@ lazbuild -B --ws=qt6 ewsdr.lpr
|
||||
lazbuild -B --ws=win32 ewsdr.lpr
|
||||
# RUN Build macOS
|
||||
lazbuild -B --ws=cocoa ewsdr.lpr
|
||||
|
||||
# Optional DMR AMBE+2 adapter (requires mbelib development files)
|
||||
cmake -S native/dmr -B build/dmr -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build/dmr
|
||||
|
||||
+1
-1
@@ -307,7 +307,7 @@ begin FSyncInt := V; FController.Invoke(@SyncMode); end;
|
||||
|
||||
procedure TCATAdapter.SyncMode;
|
||||
begin
|
||||
if (FSyncInt < 0) or (FSyncInt > MODE_WFM) then Exit;
|
||||
if (FSyncInt < 0) or (FSyncInt > MODE_DMR) then Exit;
|
||||
FController.SetMode(FSyncInt);
|
||||
end;
|
||||
|
||||
|
||||
+5
-4
@@ -35,8 +35,8 @@ const
|
||||
MODE_LSB = 0; MODE_USB = 1; MODE_DSB = 2;
|
||||
MODE_CWL = 3; MODE_CWU = 4; MODE_FM = 5;
|
||||
MODE_AM = 6; MODE_SAM = 7;
|
||||
MODE_DIGU = 8; MODE_DIGL = 9; MODE_WFM = 10;
|
||||
CAT_MODE_COUNT = 11;
|
||||
MODE_DIGU = 8; MODE_DIGL = 9; MODE_WFM = 10; MODE_DMR = 11;
|
||||
CAT_MODE_COUNT = 12;
|
||||
|
||||
// FM repeater offset directions — зеркало FMRepeater.RPT_* (значения должны
|
||||
// совпадать: контроллер хранит FFMRptDir в этих же кодах).
|
||||
@@ -391,6 +391,7 @@ begin
|
||||
MODE_CWU: Result := 3;
|
||||
MODE_FM: Result := 4;
|
||||
MODE_WFM: Result := 4; // treated as FM
|
||||
MODE_DMR: Result := 4; // DMR RF transport is narrow FM
|
||||
MODE_AM: Result := 5;
|
||||
MODE_SAM: Result := 5; // treated as AM
|
||||
MODE_DIGU: Result := 6; // Kenwood FSK
|
||||
@@ -1792,7 +1793,7 @@ end;
|
||||
|
||||
function TCATEngine.ZZML: string;
|
||||
// ZZML: mode list (returns all mode names)
|
||||
begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM '; end;
|
||||
begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM DMR '; end;
|
||||
|
||||
function TCATEngine.ZZMN(const s: string): string;
|
||||
// ZZMN: mode name — returns mode name for given mode number.
|
||||
@@ -1803,7 +1804,7 @@ var k: Integer;
|
||||
begin
|
||||
names[0]:='LSB'; names[1]:='USB'; names[2]:='DSB'; names[3]:='CWL';
|
||||
names[4]:='CWU'; names[5]:='FM'; names[6]:='AM'; names[7]:='SAM';
|
||||
names[8]:='DIGU'; names[9]:='DIGL'; names[10]:='WFM';
|
||||
names[8]:='DIGU'; names[9]:='DIGL'; names[10]:='WFM'; names[11]:='DMR';
|
||||
if (Length(s) = 1) or (Length(s) = 2) then begin
|
||||
k := EnsureRange(StrToIntDef(s,0),0,CAT_MODE_COUNT-1);
|
||||
Result := names[k];
|
||||
|
||||
+2
-2
@@ -18,9 +18,9 @@ uses
|
||||
|
||||
const
|
||||
// Копии из MainForm: нужны для выпадающих списков
|
||||
CH_MODE_COUNT = 11;
|
||||
CH_MODE_COUNT = 12;
|
||||
CH_MODE_NAMES: array[0..CH_MODE_COUNT-1] of string =
|
||||
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
|
||||
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR');
|
||||
|
||||
CH_CTCSS_COUNT = 38;
|
||||
CH_CTCSS_NAMES: array[0..CH_CTCSS_COUNT-1] of string = (
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
unit DMRBindings;
|
||||
|
||||
{$IFDEF FPC}
|
||||
{$MODE Delphi}
|
||||
{$ENDIF}
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, DynLibs;
|
||||
|
||||
const
|
||||
DMR_ABI_VERSION = 1;
|
||||
DMR_AMBE_BITS = 96;
|
||||
DMR_PCM_SAMPLES = 160;
|
||||
|
||||
type
|
||||
TDMRMbeResult = packed record
|
||||
ErrorsC0: LongInt;
|
||||
ErrorsTotal: LongInt;
|
||||
end;
|
||||
PDMRMbeResult = ^TDMRMbeResult;
|
||||
|
||||
function DMRLoad: Boolean;
|
||||
procedure DMRUnload;
|
||||
function DMRLoaded: Boolean;
|
||||
function DMRLastError: string;
|
||||
|
||||
function DMRMbeCreate(UVQuality: Integer): Pointer;
|
||||
procedure DMRMbeDestroy(Decoder: Pointer);
|
||||
procedure DMRMbeReset(Decoder: Pointer);
|
||||
function DMRMbeDecode(Decoder: Pointer; Bits: PByte; PCM: PSingle;
|
||||
ResultInfo: PDMRMbeResult): Integer;
|
||||
|
||||
implementation
|
||||
|
||||
type
|
||||
TAbiVersion = function: LongWord; cdecl;
|
||||
TMbeCreate = function(UVQuality: LongInt): Pointer; cdecl;
|
||||
TMbeDestroy = procedure(Decoder: Pointer); cdecl;
|
||||
TMbeReset = procedure(Decoder: Pointer); cdecl;
|
||||
TMbeDecode = function(Decoder: Pointer; Bits: PByte; PCM: PSingle;
|
||||
ResultInfo: Pointer): LongInt; cdecl;
|
||||
|
||||
var
|
||||
GLib: TLibHandle = dynlibs.NilHandle;
|
||||
GLastError: string = '';
|
||||
GAbiVersion: TAbiVersion = nil;
|
||||
GMbeCreate: TMbeCreate = nil;
|
||||
GMbeDestroy: TMbeDestroy = nil;
|
||||
GMbeReset: TMbeReset = nil;
|
||||
GMbeDecode: TMbeDecode = nil;
|
||||
|
||||
function Symbol(const Name: PChar): Pointer;
|
||||
begin
|
||||
Result := GetProcedureAddress(GLib, Name);
|
||||
if Result = nil then
|
||||
GLastError := 'Missing DMR symbol: ' + string(Name);
|
||||
end;
|
||||
|
||||
function DMRLoad: Boolean;
|
||||
const
|
||||
{$IFDEF WINDOWS}
|
||||
Names: array[0..0] of string = ('ewsdr_dmr.dll');
|
||||
{$ELSE}
|
||||
{$IFDEF DARWIN}
|
||||
Names: array[0..0] of string = ('libewsdr_dmr.dylib');
|
||||
{$ELSE}
|
||||
Names: array[0..1] of string = ('libewsdr_dmr.so', 'libewsdr_dmr.so.1');
|
||||
{$ENDIF}
|
||||
{$ENDIF}
|
||||
var
|
||||
i: Integer;
|
||||
LocalName: string;
|
||||
begin
|
||||
if GLib <> dynlibs.NilHandle then Exit(True);
|
||||
GLastError := '';
|
||||
for i := Low(Names) to High(Names) do
|
||||
begin
|
||||
LocalName := ExtractFilePath(ParamStr(0)) + Names[i];
|
||||
GLib := LoadLibrary(LocalName);
|
||||
if GLib = dynlibs.NilHandle then GLib := LoadLibrary(Names[i]);
|
||||
if GLib <> dynlibs.NilHandle then Break;
|
||||
end;
|
||||
if GLib = dynlibs.NilHandle then
|
||||
begin
|
||||
GLastError := 'Optional EWSDR DMR library not found';
|
||||
Exit(False);
|
||||
end;
|
||||
|
||||
GAbiVersion := TAbiVersion(Symbol('ewsdr_dmr_abi_version'));
|
||||
GMbeCreate := TMbeCreate(Symbol('ewsdr_dmr_mbe_create'));
|
||||
GMbeDestroy := TMbeDestroy(Symbol('ewsdr_dmr_mbe_destroy'));
|
||||
GMbeReset := TMbeReset(Symbol('ewsdr_dmr_mbe_reset'));
|
||||
GMbeDecode := TMbeDecode(Symbol('ewsdr_dmr_mbe_decode'));
|
||||
if not Assigned(GAbiVersion) or not Assigned(GMbeCreate) or
|
||||
not Assigned(GMbeDestroy) or not Assigned(GMbeReset) or
|
||||
not Assigned(GMbeDecode) then
|
||||
begin
|
||||
DMRUnload;
|
||||
Exit(False);
|
||||
end;
|
||||
if GAbiVersion() <> DMR_ABI_VERSION then
|
||||
begin
|
||||
GLastError := Format('Unsupported EWSDR DMR ABI: %d', [GAbiVersion()]);
|
||||
DMRUnload;
|
||||
Exit(False);
|
||||
end;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure DMRUnload;
|
||||
begin
|
||||
GAbiVersion := nil;
|
||||
GMbeCreate := nil;
|
||||
GMbeDestroy := nil;
|
||||
GMbeReset := nil;
|
||||
GMbeDecode := nil;
|
||||
if GLib <> dynlibs.NilHandle then
|
||||
begin
|
||||
UnloadLibrary(GLib);
|
||||
GLib := dynlibs.NilHandle;
|
||||
end;
|
||||
end;
|
||||
|
||||
function DMRLoaded: Boolean;
|
||||
begin
|
||||
Result := GLib <> dynlibs.NilHandle;
|
||||
end;
|
||||
|
||||
function DMRLastError: string;
|
||||
begin
|
||||
Result := GLastError;
|
||||
end;
|
||||
|
||||
function DMRMbeCreate(UVQuality: Integer): Pointer;
|
||||
begin
|
||||
if Assigned(GMbeCreate) then Result := GMbeCreate(UVQuality)
|
||||
else Result := nil;
|
||||
end;
|
||||
|
||||
procedure DMRMbeDestroy(Decoder: Pointer);
|
||||
begin
|
||||
if Assigned(GMbeDestroy) and (Decoder <> nil) then GMbeDestroy(Decoder);
|
||||
end;
|
||||
|
||||
procedure DMRMbeReset(Decoder: Pointer);
|
||||
begin
|
||||
if Assigned(GMbeReset) and (Decoder <> nil) then GMbeReset(Decoder);
|
||||
end;
|
||||
|
||||
function DMRMbeDecode(Decoder: Pointer; Bits: PByte; PCM: PSingle;
|
||||
ResultInfo: PDMRMbeResult): Integer;
|
||||
begin
|
||||
if not Assigned(GMbeDecode) then Exit(-1);
|
||||
Result := GMbeDecode(Decoder, Bits, PCM, ResultInfo);
|
||||
end;
|
||||
|
||||
end.
|
||||
+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.
|
||||
+46
-3
@@ -40,7 +40,7 @@ uses
|
||||
WinFirewall,
|
||||
BoardUtils, WisdomBuilder, UISync,
|
||||
FlatEdit, FMRepeater,
|
||||
ChannelStore, ChannelsForm, BeaconScopeForm,
|
||||
ChannelStore, ChannelsForm, BeaconScopeForm, DMRDecoder,
|
||||
PowerInhibit,
|
||||
DeviceStore,
|
||||
RadioBackend, PlutoBackend,
|
||||
@@ -67,7 +67,7 @@ const
|
||||
WDSP_WISDOM_FILE = 'wdspWisdom00';
|
||||
|
||||
BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11)
|
||||
MODE_COUNT = 11;
|
||||
MODE_COUNT = 12;
|
||||
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
|
||||
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
|
||||
// UI-подписи кнопок (NAMES).
|
||||
@@ -105,7 +105,7 @@ const
|
||||
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','');
|
||||
|
||||
MODE_NAMES: array[0..MODE_COUNT-1] of string = (
|
||||
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
|
||||
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR');
|
||||
MODE_ROWS = 3; // 4 колонки; последний ряд неполный
|
||||
|
||||
SMETER_WIDTH_RATIO = 0.32;
|
||||
@@ -419,6 +419,7 @@ type
|
||||
procedure UpdateStatusField7; // поле 7: PLL (HPSDR) или beacon (Pluto)
|
||||
procedure UpdateStatusField3; // поле 3: Supply (HPSDR) или Temp/RSSI (Pluto)
|
||||
function BeaconStatusText: string; // компактный статус QO-100 beacon lock
|
||||
function DMRStatusText: string; // sync/type/polarity DMR front-end
|
||||
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
|
||||
|
||||
// Event handlers
|
||||
@@ -3583,10 +3584,28 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
function TMainForm.DMRStatusText: string;
|
||||
var
|
||||
S: TDMRStatus;
|
||||
Inv: string;
|
||||
begin
|
||||
if not FController.GetDMRStatus(S) then Exit('DMR unavailable');
|
||||
if not S.Enabled then Exit('DMR off');
|
||||
if not S.Synced then Exit(Format('DMR sync... %.3f', [S.SignalRMS]));
|
||||
if S.Inverted then Inv := ' INV' else Inv := '';
|
||||
Result := Format('DMR %s%s #%d',
|
||||
[TDMRDecoder.SyncKindName(S.SyncKind), Inv, S.SyncCount]);
|
||||
end;
|
||||
|
||||
procedure TMainForm.UpdateStatusField7;
|
||||
// Поле 7 статус-бара. openHPSDR — PLL-lock (как прежде). Pluto в трансвертере —
|
||||
// статус QO-100 beacon lock (у Pluto нет PLL-телеметрии); Pluto без XVTR — пусто.
|
||||
begin
|
||||
if FController.FMode = MODE_DMR then
|
||||
begin
|
||||
SetStatusText(7, DMRStatusText);
|
||||
Exit;
|
||||
end;
|
||||
if FController.IsPluto then
|
||||
begin
|
||||
if FController.FCurrentXvtr >= 0 then SetStatusText(7, BeaconStatusText)
|
||||
@@ -4203,6 +4222,24 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
|
||||
if FController.FMode = MODE_DMR then
|
||||
begin
|
||||
CloseCTCSSPopup;
|
||||
FController.FFilter := 0;
|
||||
FController.FFilterBW := FILT_DMR_BW;
|
||||
FController.FFMDeviation := DMR_FM_DEV;
|
||||
BtnFilter[0].Caption := '12.5k';
|
||||
BtnFilter[0].Left := LeftPanelButtonLeft(PanelFilter.Width, 1, 0);
|
||||
BtnFilter[0].Width := LeftPanelButtonWidth(PanelFilter.Width, 1, 0);
|
||||
BtnFilter[0].Top := 16;
|
||||
BtnFilter[0].Visible := True;
|
||||
StyleButton(BtnFilter[0], True);
|
||||
for i := 1 to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
|
||||
PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
|
||||
RelayoutBelowBands;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
for i := 0 to FILT_COUNT - 1 do
|
||||
begin
|
||||
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, 5, i mod 5);
|
||||
@@ -5323,6 +5360,12 @@ begin
|
||||
Break;
|
||||
end;
|
||||
end;
|
||||
MODE_DMR:
|
||||
begin
|
||||
FController.FFilter := 0;
|
||||
FController.FFilterBW := FILT_DMR_BW;
|
||||
FController.FFMDeviation := DMR_FM_DEV;
|
||||
end;
|
||||
end;
|
||||
if FController.FWDSPReady then
|
||||
begin
|
||||
|
||||
+57
-3
@@ -49,7 +49,7 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, Math,
|
||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC,
|
||||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
|
||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||
|
||||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||||
@@ -249,6 +249,9 @@ type
|
||||
// (узкий фильтр, ставится кликом); 0 = ещё не наведён
|
||||
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
|
||||
|
||||
// ---- DMR 4FSK receive decoder ----
|
||||
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
|
||||
|
||||
// ---- Аудио / TX ----
|
||||
FVolume: Integer; // громкость RX
|
||||
FTxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off, DUP)
|
||||
@@ -401,6 +404,7 @@ type
|
||||
// Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine
|
||||
// вызывающим (GUI или демон) после CreateEngines.
|
||||
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
|
||||
procedure OnDemodAudioReady(const Left, Right: array of Single; Count: Integer);
|
||||
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
|
||||
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
|
||||
procedure OnMicPacket(const Data: TMicDataPacket);
|
||||
@@ -670,6 +674,7 @@ type
|
||||
function BeaconDecodeInvert: Boolean;
|
||||
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
||||
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||||
function GetDMRStatus(out S: TDMRStatus): Boolean;
|
||||
|
||||
// Отображение / приём
|
||||
procedure SetCTun(On_: Boolean);
|
||||
@@ -827,6 +832,7 @@ begin
|
||||
// Роутинг аудио слайсов на их звуковухи — внутренний, вешаем сразу (не
|
||||
// зависит от фронтенда, в отличие от OnAudio/OnSpectrum).
|
||||
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
|
||||
FDSPEngine.OnDemodAudio := OnDemodAudioReady;
|
||||
FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||||
|
||||
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
|
||||
@@ -843,6 +849,9 @@ begin
|
||||
FBeaconDec.SetInvert(FBeaconDecInvert);
|
||||
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
||||
|
||||
// DMR front-end owns a worker thread. It stays idle until MODE_DMR is active.
|
||||
FDMRDec := TDMRDecoder.Create;
|
||||
|
||||
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||||
// формы / при подключении устройства).
|
||||
FAudioOut := TAudioOutput.Create(48000);
|
||||
@@ -874,6 +883,7 @@ begin
|
||||
FDSPEngine.Close;
|
||||
FreeAndNil(FDSPEngine);
|
||||
end;
|
||||
if Assigned(FDMRDec) then FreeAndNil(FDMRDec);
|
||||
if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec);
|
||||
end;
|
||||
|
||||
@@ -1148,6 +1158,10 @@ procedure TRadioController.OnAudioReady(const Left, Right: array of Single;
|
||||
// port 1028) — независимый канал. Затем web-адаптер может перехватить аудио
|
||||
// (активный клиент); если перехвачено — в локальный AudioOut не пишем.
|
||||
begin
|
||||
// Decoded DMR PCM will use this route later. Until then suppress raw 4FSK;
|
||||
// the independent pre-volume tap is handled by OnDemodAudioReady.
|
||||
if FMode = MODE_DMR then Exit;
|
||||
|
||||
if FSendAudioToRadio then
|
||||
FNetwork.SendSpeakerAudio(Left, Right, Count);
|
||||
if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then
|
||||
@@ -1158,6 +1172,20 @@ begin
|
||||
FAudioOut.Write(Left, Right, Count);
|
||||
end;
|
||||
|
||||
procedure TRadioController.OnDemodAudioReady(const Left, Right: array of Single;
|
||||
Count: Integer);
|
||||
// DSP thread, pre-volume/pre-mute. FeedAudio only copies into a bounded ring.
|
||||
begin
|
||||
if not Assigned(FDMRDec) then Exit;
|
||||
if FMode = MODE_DMR then
|
||||
begin
|
||||
if not FDMRDec.Enabled then FDMRDec.SetEnabled(True);
|
||||
FDMRDec.FeedAudio(Left, Right, Count);
|
||||
end
|
||||
else if FDMRDec.Enabled then
|
||||
FDMRDec.SetEnabled(False);
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Мультислайсы (софт-fan-out)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2157,6 +2185,13 @@ begin
|
||||
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
||||
end;
|
||||
|
||||
function TRadioController.GetDMRStatus(out S: TDMRStatus): Boolean;
|
||||
begin
|
||||
Result := Assigned(FDMRDec);
|
||||
if Result then FDMRDec.GetStatus(S)
|
||||
else FillChar(S, SizeOf(S), 0);
|
||||
end;
|
||||
|
||||
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
|
||||
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||
var Half: Integer;
|
||||
@@ -2190,12 +2225,13 @@ begin
|
||||
begin
|
||||
FDSPEngine.SetFilter(Lo, Hi);
|
||||
FDSPEngine.SetTXFilter(Lo, Hi);
|
||||
if FMode = MODE_FM then
|
||||
if (FMode = MODE_FM) or (FMode = MODE_DMR) then
|
||||
begin
|
||||
FDSPEngine.SetRXFMDeviation(FFMDeviation);
|
||||
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
|
||||
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
|
||||
FDSPEngine.SetFMSquelch(FFMSQOn, FFMSQLevel);
|
||||
// A squelch would remove sync preambles; DMR always receives open.
|
||||
FDSPEngine.SetFMSquelch((FMode = MODE_FM) and FFMSQOn, FFMSQLevel);
|
||||
end
|
||||
else
|
||||
FDSPEngine.SetFMSquelch(False, 0);
|
||||
@@ -2219,6 +2255,12 @@ begin
|
||||
if FFilter > FILT_WFM_COUNT - 1 then FFilter := FILT_WFM_DEF;
|
||||
FFilterBW := FILT_WFM_BW[FFilter];
|
||||
end;
|
||||
MODE_DMR:
|
||||
begin
|
||||
FFilter := 0;
|
||||
FFilterBW := FILT_DMR_BW;
|
||||
FFMDeviation := DMR_FM_DEV;
|
||||
end;
|
||||
MODE_LSB, MODE_USB:
|
||||
begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
|
||||
MODE_DIGU, MODE_DIGL:
|
||||
@@ -2238,6 +2280,7 @@ begin
|
||||
case M of
|
||||
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, FILT_FM_COUNT - 1)];
|
||||
MODE_WFM: Result := FILT_WFM_BW[EnsureRange(Idx, 0, FILT_WFM_COUNT - 1)];
|
||||
MODE_DMR: Result := FILT_DMR_BW;
|
||||
MODE_LSB, MODE_USB:
|
||||
Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
||||
MODE_DIGU, MODE_DIGL:
|
||||
@@ -2310,6 +2353,12 @@ begin
|
||||
FFilterBW := FILT_FM_BW[FFilter];
|
||||
FFMDeviation := FILT_FM_DEV[FFilter];
|
||||
end;
|
||||
if FMode = MODE_DMR then
|
||||
begin
|
||||
FFilter := 0;
|
||||
FFilterBW := FILT_DMR_BW;
|
||||
FFMDeviation := DMR_FM_DEV;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRadioController.MakeBandSettings: TBandSettings;
|
||||
@@ -2480,6 +2529,7 @@ procedure TRadioController.ApplyBandDSP(const B: TBandSettings);
|
||||
begin
|
||||
// --- Режим ---
|
||||
FMode := B.Mode;
|
||||
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
|
||||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
|
||||
|
||||
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
|
||||
@@ -2870,6 +2920,7 @@ end;
|
||||
procedure TRadioController.SetMode(M: Integer);
|
||||
begin
|
||||
FMode := M;
|
||||
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
|
||||
ApplyModeDefaults; // дефолтный фильтр/девиация под режим
|
||||
FBandCache[FCurrentBand].Mode := FMode;
|
||||
if FWDSPReady and Assigned(FDSPEngine) then
|
||||
@@ -2889,6 +2940,7 @@ begin
|
||||
case FMode of
|
||||
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
|
||||
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
|
||||
MODE_DMR: Idx := 0;
|
||||
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
|
||||
end;
|
||||
FFilter := Idx;
|
||||
@@ -2898,6 +2950,7 @@ begin
|
||||
MODE_DSB: FFilterBW := FILT_DSB_BW[Idx];
|
||||
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
|
||||
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
|
||||
MODE_DMR: FFilterBW := FILT_DMR_BW;
|
||||
MODE_FM:
|
||||
begin
|
||||
FFilterBW := FILT_FM_BW[Idx];
|
||||
@@ -2929,6 +2982,7 @@ begin
|
||||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
|
||||
MODE_FM: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
|
||||
MODE_WFM: for i := 0 to FILT_WFM_COUNT-1 do BW_arr[i] := FILT_WFM_BW[i];
|
||||
MODE_DMR: BW_arr[0] := FILT_DMR_BW;
|
||||
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
|
||||
end;
|
||||
best := 0; dist := MaxInt;
|
||||
|
||||
@@ -60,6 +60,10 @@ const
|
||||
FILT_WFM_BW: array[0..2] of Integer = (200000, 160000, 120000);
|
||||
FILT_WFM_DEF = 1; // 160k — влезает и в 192k
|
||||
|
||||
// DMR Tier II: 12.5 kHz RF channel, 4FSK nominal deviation +/-1.944 kHz.
|
||||
FILT_DMR_BW = 12500;
|
||||
DMR_FM_DEV = 2500;
|
||||
|
||||
type
|
||||
TBandSettings = record
|
||||
VfoA: Double;
|
||||
|
||||
+5
-2
@@ -24,11 +24,11 @@ const
|
||||
OVL_W = 280; // ширина карточки
|
||||
|
||||
// Индексы совпадают с MODE_* контроллера (WDSPEngine.pas).
|
||||
OVL_MODE_COUNT = 11;
|
||||
OVL_MODE_COUNT = 12;
|
||||
OVL_MODE_COLS = 4;
|
||||
OVL_MODE_ROWS = (OVL_MODE_COUNT + OVL_MODE_COLS - 1) div OVL_MODE_COLS;
|
||||
OVL_MODE_NAMES: array[0..OVL_MODE_COUNT-1] of string = (
|
||||
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
|
||||
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR');
|
||||
|
||||
// Палитра слайсов (TColor = $00BBGGRR): одна на букву — общий цвет для
|
||||
// бейджа-карточки во флаге и для буквы на полосе фильтра в спектре.
|
||||
@@ -394,6 +394,7 @@ begin
|
||||
5: Result := FM_BW[FilterIdx];
|
||||
8,9: Result := DIGI_BW[FilterIdx]; // DIGU/DIGL — полоса от нуля
|
||||
10: Result := WFM_BW[FilterIdx];
|
||||
11: Result := 12500;
|
||||
else Result := AM_BW[FilterIdx];
|
||||
end;
|
||||
end;
|
||||
@@ -407,6 +408,7 @@ begin
|
||||
5: Result := FM_LBL[FilterIdx];
|
||||
8,9: Result := DIGI_LBL[FilterIdx];
|
||||
10: Result := WFM_LBL[FilterIdx];
|
||||
11: Result := '12.5k';
|
||||
else Result := AM_LBL[FilterIdx];
|
||||
end;
|
||||
end;
|
||||
@@ -416,6 +418,7 @@ begin
|
||||
case ModeIdx of
|
||||
5: Result := 2; // FM: NFM/FM
|
||||
10: Result := 3; // WFM: 200K/160K/120K
|
||||
11: Result := 1; // DMR: fixed 12.5 kHz channel
|
||||
else Result := 6;
|
||||
end;
|
||||
end;
|
||||
|
||||
+24
-6
@@ -60,6 +60,7 @@ const
|
||||
MODE_DIGU = 8;
|
||||
MODE_DIGL = 9;
|
||||
MODE_WFM = 10;
|
||||
MODE_DMR = 11; // DMR receive: WDSP NFM discriminator -> DMRDecoder
|
||||
|
||||
// Минимальная длина FIR у WDSP: create_* поднимают fircore с
|
||||
// nc = max(2048, dsp_size). В firmin.c nfor = nc / size, idxmask = nfor - 1,
|
||||
@@ -322,6 +323,7 @@ type
|
||||
FViewHighHz: Double;
|
||||
|
||||
FOnAudio: TOnAudioReady;
|
||||
FOnDemodAudio:TOnAudioReady; // pre-volume/mute tap for digital decoders
|
||||
FOnSpectrum: TOnSpectrumReady;
|
||||
FOnWaterfall: TOnWaterfallReady;
|
||||
FOnTXIQ: TOnTXIQReady;
|
||||
@@ -682,6 +684,7 @@ type
|
||||
property WfAvgMode: Integer read FWfAvgMode;
|
||||
property WfAvgTimeMS: Double read FWfAvgTimeMS;
|
||||
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
|
||||
property OnDemodAudio: TOnAudioReady read FOnDemodAudio write FOnDemodAudio;
|
||||
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
|
||||
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
||||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||||
@@ -930,6 +933,7 @@ begin
|
||||
MODE_DIGU: Result := WDSP_DIGU;
|
||||
MODE_DIGL: Result := WDSP_DIGL;
|
||||
MODE_WFM: Result := WDSP_RX_WFM;
|
||||
MODE_DMR: Result := WDSP_FM;
|
||||
else Result := WDSP_USB;
|
||||
end;
|
||||
end;
|
||||
@@ -1082,6 +1086,7 @@ begin
|
||||
MODE_DIGU: SetFilter( 0, 3000);
|
||||
MODE_DIGL: SetFilter(-3000, 0);
|
||||
MODE_WFM: SetFilter(-80000, 80000);
|
||||
MODE_DMR: SetFilter(-6250, 6250);
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -2087,16 +2092,29 @@ begin
|
||||
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
|
||||
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
|
||||
// Thetis/piHPSDR при не-duplex MOX. FPostTXMuteUntil-хвост уважаем в любом случае.
|
||||
if Assigned(FOnAudio) and not FMuted
|
||||
and (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
|
||||
if (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
|
||||
and (FPostTXMuteUntil = 0) then
|
||||
begin
|
||||
for i := 0 to FAudioBufSize - 1 do
|
||||
// Decoder tap is independent from speaker mute and volume. This is the
|
||||
// actual demodulator output; digital sync must survive MUTE/volume=0.
|
||||
if Assigned(FOnDemodAudio) then
|
||||
begin
|
||||
FOutL[i] := FRXOut[i * 2] * FVolume;
|
||||
FOutR[i] := FRXOut[i * 2 + 1] * FVolume;
|
||||
for i := 0 to FAudioBufSize - 1 do
|
||||
begin
|
||||
FOutL[i] := FRXOut[i * 2];
|
||||
FOutR[i] := FRXOut[i * 2 + 1];
|
||||
end;
|
||||
FOnDemodAudio(FOutL, FOutR, FAudioBufSize);
|
||||
end;
|
||||
if Assigned(FOnAudio) and not FMuted then
|
||||
begin
|
||||
for i := 0 to FAudioBufSize - 1 do
|
||||
begin
|
||||
FOutL[i] := FRXOut[i * 2] * FVolume;
|
||||
FOutR[i] := FRXOut[i * 2 + 1] * FVolume;
|
||||
end;
|
||||
FOnAudio(FOutL, FOutR, FAudioBufSize);
|
||||
end;
|
||||
FOnAudio(FOutL, FOutR, FAudioBufSize);
|
||||
end;
|
||||
|
||||
// Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы.
|
||||
|
||||
+1
-1
@@ -145,7 +145,7 @@ begin FSyncInt := Mode; FController.Invoke(@SyncMode); end;
|
||||
|
||||
procedure TWebAdapter.SyncMode;
|
||||
begin
|
||||
if (FSyncInt < 0) or (FSyncInt > MODE_WFM) then Exit;
|
||||
if (FSyncInt < 0) or (FSyncInt > MODE_DMR) then Exit;
|
||||
FController.SetMode(FSyncInt); // дефолт фильтра + DSP + cache; рендер через rfMode
|
||||
end;
|
||||
|
||||
|
||||
+7
-5
@@ -193,7 +193,7 @@ begin
|
||||
'<select id="modGrpSel">' +
|
||||
'<option value="ssb" selected>SSB</option><option value="cw">CW</option>' +
|
||||
'<option value="fm">FM</option><option value="am">AM</option>' +
|
||||
'<option value="digi">DIGI</option>' +
|
||||
'<option value="digi">DIGI</option><option value="dmr">DMR</option>' +
|
||||
'</select>' +
|
||||
'<select id="stepSel">' +
|
||||
'<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' +
|
||||
@@ -299,9 +299,9 @@ begin
|
||||
'function updRfAgc(){const b=q("rfagcBtn");if(!b)return;b.style.display=cur.rf_agc_visible?"":"none";b.textContent="RF AGC "+rfagcModeName(cur.rx_gain_mode);if(rfagcEls)rfagcRefresh();}' +
|
||||
|
||||
|
||||
'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM","DIGU","DIGL","WFM"];' +
|
||||
'const GRP_MAP={ssb:[0,1],cw:[3,4],fm:[5,10],am:[6,7],digi:[8,9]};' +
|
||||
'const MODE_TO_GRP=["ssb","ssb","ssb","cw","cw","fm","am","am","digi","digi","fm"];' +
|
||||
'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM","DIGU","DIGL","WFM","DMR"];' +
|
||||
'const GRP_MAP={ssb:[0,1],cw:[3,4],fm:[5,10],am:[6,7],digi:[8,9],dmr:[11,11]};' +
|
||||
'const MODE_TO_GRP=["ssb","ssb","ssb","cw","cw","fm","am","am","digi","digi","fm","dmr"];' +
|
||||
'function modGrpFromMode(m){return MODE_TO_GRP[m]||"ssb";}' +
|
||||
'function modGrp(){return q("modGrpSel").value||"ssb";}' +
|
||||
'function modTglTarget(){const grp=GRP_MAP[modGrp()];const m=cur.mode||0;return(m===grp[0])?grp[1]:grp[0];}' +
|
||||
@@ -319,6 +319,7 @@ begin
|
||||
'am:["20k","18k","16k","12k","10k","9.0k","8.0k","7.0k","6.0k","5.0k"],' +
|
||||
'fm:["NFM","FM"],' +
|
||||
'wfm:["200k","160k","120k"],' +
|
||||
'dmr:["12.5k"],' +
|
||||
'digi:["3.0k","2.7k","2.4k","2.1k","1.8k","1.5k","1.2k","900","600","300"]};' +
|
||||
'var FNP_BW={ssb:[5000,4400,3800,3300,2900,2700,2400,2100,1800,1000],' +
|
||||
'dsb:[16000,12000,10000,8000,6600,5200,4000,3100,2900,2400],' +
|
||||
@@ -326,9 +327,10 @@ begin
|
||||
'am:[20000,18000,16000,12000,10000,9000,8000,7000,6000,5000],' +
|
||||
'fm:[11000,16000],' +
|
||||
'wfm:[200000,160000,120000],' +
|
||||
'dmr:[12500],' +
|
||||
'digi:[3000,2700,2400,2100,1800,1500,1200,900,600,300]};' +
|
||||
'var fnpOv=null;' +
|
||||
'function fnpGrp(){var m=cur.mode|0;return(m===3||m===4)?"cw":(m===5)?"fm":(m===10)?"wfm":(m===8||m===9)?"digi":(m===2)?"dsb":(m===6||m===7)?"am":"ssb";}' +
|
||||
'function fnpGrp(){var m=cur.mode|0;return(m===3||m===4)?"cw":(m===5)?"fm":(m===10)?"wfm":(m===11)?"dmr":(m===8||m===9)?"digi":(m===2)?"dsb":(m===6||m===7)?"am":"ssb";}' +
|
||||
'function closeFnp(){if(fnpOv){fnpOv.remove();fnpOv=null;}document.removeEventListener("click",fnpOutside);}' +
|
||||
'function fnpOutside(e){if(fnpOv&&!fnpOv.contains(e.target)&&e.target!==q("fnpBtn"))closeFnp();}' +
|
||||
'function showFnp(){' +
|
||||
|
||||
+2
-2
@@ -1717,8 +1717,8 @@ end;
|
||||
|
||||
function TWebServer.BuildStateJson: string;
|
||||
const
|
||||
MODE_N: array[0..10] of string =
|
||||
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
|
||||
MODE_N: array[0..11] of string =
|
||||
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR');
|
||||
var
|
||||
FS: TFormatSettings;
|
||||
XvtrJson, DevJson, RateJson, BandJson: string;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(ewsdr_dmr C)
|
||||
|
||||
find_path(MBELIB_INCLUDE_DIR mbelib.h REQUIRED)
|
||||
find_library(MBELIB_LIBRARY NAMES mbe REQUIRED)
|
||||
|
||||
add_library(ewsdr_dmr SHARED ewsdr_dmr.c)
|
||||
target_compile_definitions(ewsdr_dmr PRIVATE EWSDR_DMR_BUILD)
|
||||
target_include_directories(ewsdr_dmr
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PRIVATE ${MBELIB_INCLUDE_DIR})
|
||||
target_link_libraries(ewsdr_dmr PRIVATE ${MBELIB_LIBRARY})
|
||||
set_target_properties(ewsdr_dmr PROPERTIES
|
||||
C_VISIBILITY_PRESET hidden
|
||||
VISIBILITY_INLINES_HIDDEN YES)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS ewsdr_dmr
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
install(FILES ewsdr_dmr.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
@@ -0,0 +1,17 @@
|
||||
# EWSDR DMR native adapter
|
||||
|
||||
This optional shared library isolates `mbelib` and the imported DMR core from
|
||||
the Lazarus application. It intentionally does not depend on PulseAudio,
|
||||
ncurses, libsndfile or the DSD-FME command-line frontend.
|
||||
|
||||
Build on Linux:
|
||||
|
||||
```sh
|
||||
cmake -S native/dmr -B build/dmr -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build/dmr
|
||||
```
|
||||
|
||||
The current ABI implements the stateful AMBE+2 vocoder boundary. The burst/FEC
|
||||
layer will feed its row-major `char[4][24]` output to this library. Keep the ABI
|
||||
versioned: the GUI and daemon must remain usable when the optional library is
|
||||
not installed.
|
||||
@@ -0,0 +1,86 @@
|
||||
#include "ewsdr_dmr.h"
|
||||
|
||||
#include <mbelib.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
struct ewsdr_dmr_mbe {
|
||||
mbe_parms current;
|
||||
mbe_parms previous;
|
||||
mbe_parms previous_enhanced;
|
||||
int uv_quality;
|
||||
};
|
||||
|
||||
uint32_t ewsdr_dmr_abi_version(void)
|
||||
{
|
||||
return EWSDR_DMR_ABI_VERSION;
|
||||
}
|
||||
|
||||
void ewsdr_dmr_mbe_reset(ewsdr_dmr_mbe *decoder)
|
||||
{
|
||||
if (decoder == NULL)
|
||||
return;
|
||||
mbe_initMbeParms(&decoder->current, &decoder->previous,
|
||||
&decoder->previous_enhanced);
|
||||
}
|
||||
|
||||
ewsdr_dmr_mbe *ewsdr_dmr_mbe_create(int uv_quality)
|
||||
{
|
||||
ewsdr_dmr_mbe *decoder = calloc(1, sizeof(*decoder));
|
||||
if (decoder == NULL)
|
||||
return NULL;
|
||||
if (uv_quality < 1)
|
||||
uv_quality = 1;
|
||||
if (uv_quality > 64)
|
||||
uv_quality = 64;
|
||||
decoder->uv_quality = uv_quality;
|
||||
ewsdr_dmr_mbe_reset(decoder);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
void ewsdr_dmr_mbe_destroy(ewsdr_dmr_mbe *decoder)
|
||||
{
|
||||
if (decoder == NULL)
|
||||
return;
|
||||
memset(decoder, 0, sizeof(*decoder));
|
||||
free(decoder);
|
||||
}
|
||||
|
||||
int ewsdr_dmr_mbe_decode(ewsdr_dmr_mbe *decoder,
|
||||
const uint8_t ambe_bits[EWSDR_DMR_AMBE_BITS],
|
||||
float pcm_out[EWSDR_DMR_PCM_SAMPLES],
|
||||
ewsdr_dmr_mbe_result *result)
|
||||
{
|
||||
char frame[4][24];
|
||||
char data[49];
|
||||
char error_text[64];
|
||||
int errors_c0 = 0;
|
||||
int errors_total = 0;
|
||||
size_t row;
|
||||
size_t col;
|
||||
|
||||
if (decoder == NULL || ambe_bits == NULL || pcm_out == NULL)
|
||||
return -1;
|
||||
|
||||
for (row = 0; row < 4; ++row) {
|
||||
for (col = 0; col < 24; ++col) {
|
||||
const uint8_t bit = ambe_bits[row * 24 + col];
|
||||
if (bit > 1)
|
||||
return -2;
|
||||
frame[row][col] = (char)bit;
|
||||
}
|
||||
}
|
||||
|
||||
memset(data, 0, sizeof(data));
|
||||
memset(error_text, 0, sizeof(error_text));
|
||||
mbe_processAmbe3600x2450Framef(
|
||||
pcm_out, &errors_c0, &errors_total, error_text, frame, data,
|
||||
&decoder->current, &decoder->previous, &decoder->previous_enhanced,
|
||||
decoder->uv_quality);
|
||||
|
||||
if (result != NULL) {
|
||||
result->errors_c0 = errors_c0;
|
||||
result->errors_total = errors_total;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#ifndef EWSDR_DMR_H
|
||||
#define EWSDR_DMR_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
# if defined(EWSDR_DMR_BUILD)
|
||||
# define EWSDR_DMR_API __declspec(dllexport)
|
||||
# else
|
||||
# define EWSDR_DMR_API __declspec(dllimport)
|
||||
# endif
|
||||
#else
|
||||
# define EWSDR_DMR_API __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define EWSDR_DMR_ABI_VERSION 1u
|
||||
#define EWSDR_DMR_AMBE_BITS 96u
|
||||
#define EWSDR_DMR_PCM_SAMPLES 160u
|
||||
|
||||
typedef struct ewsdr_dmr_mbe ewsdr_dmr_mbe;
|
||||
|
||||
typedef struct ewsdr_dmr_mbe_result {
|
||||
int errors_c0;
|
||||
int errors_total;
|
||||
} ewsdr_dmr_mbe_result;
|
||||
|
||||
EWSDR_DMR_API uint32_t ewsdr_dmr_abi_version(void);
|
||||
EWSDR_DMR_API ewsdr_dmr_mbe *ewsdr_dmr_mbe_create(int uv_quality);
|
||||
EWSDR_DMR_API void ewsdr_dmr_mbe_destroy(ewsdr_dmr_mbe *decoder);
|
||||
EWSDR_DMR_API void ewsdr_dmr_mbe_reset(ewsdr_dmr_mbe *decoder);
|
||||
|
||||
/*
|
||||
* Decode one already deinterleaved DMR AMBE+2 frame. ambe_bits is a row-major
|
||||
* char[4][24] matrix containing only 0/1 values. pcm_out receives 160 float
|
||||
* samples at 8 kHz. Frame extraction/deinterleaving remains in the DMR burst
|
||||
* layer, keeping mbelib and its state behind this stable ABI.
|
||||
*/
|
||||
EWSDR_DMR_API int ewsdr_dmr_mbe_decode(
|
||||
ewsdr_dmr_mbe *decoder,
|
||||
const uint8_t ambe_bits[EWSDR_DMR_AMBE_BITS],
|
||||
float pcm_out[EWSDR_DMR_PCM_SAMPLES],
|
||||
ewsdr_dmr_mbe_result *result);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
program dmr_bindings_test;
|
||||
|
||||
{$MODE Delphi}
|
||||
|
||||
uses
|
||||
SysUtils, DMRBindings;
|
||||
|
||||
var
|
||||
Decoder: Pointer;
|
||||
Bits: array[0..DMR_AMBE_BITS - 1] of Byte;
|
||||
PCM: array[0..DMR_PCM_SAMPLES - 1] of Single;
|
||||
Info: TDMRMbeResult;
|
||||
begin
|
||||
if not DMRLoad then
|
||||
begin
|
||||
WriteLn(DMRLastError);
|
||||
Halt(1);
|
||||
end;
|
||||
Decoder := DMRMbeCreate(3);
|
||||
if Decoder = nil then Halt(2);
|
||||
try
|
||||
FillChar(Bits, SizeOf(Bits), 0);
|
||||
Bits[0] := 2; // ABI must reject values other than 0/1 before mbelib.
|
||||
if DMRMbeDecode(Decoder, @Bits[0], @PCM[0], @Info) <> -2 then Halt(3);
|
||||
DMRMbeReset(Decoder);
|
||||
finally
|
||||
DMRMbeDestroy(Decoder);
|
||||
DMRUnload;
|
||||
end;
|
||||
WriteLn('DMR native bindings tests passed');
|
||||
end.
|
||||
@@ -0,0 +1,68 @@
|
||||
program dmr_frontend_test;
|
||||
|
||||
{$MODE Delphi}
|
||||
|
||||
uses
|
||||
{$IFDEF UNIX}cthreads,{$ENDIF}
|
||||
Classes, SysUtils, DMRDecoder;
|
||||
|
||||
const
|
||||
BS_VOICE_SYNC = '131111333113313313113313';
|
||||
|
||||
procedure FeedSync(D: TDMRDecoder; const Pattern: string; Invert: Boolean);
|
||||
var
|
||||
L, R: array of Single;
|
||||
i, j, p: Integer;
|
||||
V: Single;
|
||||
begin
|
||||
// Three leading samples ensure the detector really searches all ten timing
|
||||
// hypotheses instead of relying on an aligned first sample.
|
||||
SetLength(L, 3 + Length(Pattern) * DMR_SAMPLES_PER_SYM + 20);
|
||||
SetLength(R, Length(L));
|
||||
p := 3;
|
||||
for i := 1 to Length(Pattern) do
|
||||
begin
|
||||
if Pattern[i] = '1' then V := 0.7 else V := -0.7;
|
||||
if Invert then V := -V;
|
||||
for j := 0 to DMR_SAMPLES_PER_SYM - 1 do
|
||||
begin
|
||||
L[p] := V;
|
||||
R[p] := V;
|
||||
Inc(p);
|
||||
end;
|
||||
end;
|
||||
D.FeedAudio(L, R, Length(L));
|
||||
end;
|
||||
|
||||
var
|
||||
D: TDMRDecoder;
|
||||
S: TDMRStatus;
|
||||
begin
|
||||
D := TDMRDecoder.Create;
|
||||
try
|
||||
D.SetEnabled(True);
|
||||
FeedSync(D, BS_VOICE_SYNC, False);
|
||||
Sleep(50);
|
||||
D.GetStatus(S);
|
||||
if (not S.Synced) or (S.SyncKind <> dskBSVoice) or S.Inverted then
|
||||
begin
|
||||
WriteLn('normal sync failed');
|
||||
Halt(1);
|
||||
end;
|
||||
|
||||
D.SetEnabled(False);
|
||||
D.SetInverted(True);
|
||||
D.SetEnabled(True);
|
||||
FeedSync(D, BS_VOICE_SYNC, True);
|
||||
Sleep(50);
|
||||
D.GetStatus(S);
|
||||
if (not S.Synced) or (S.SyncKind <> dskBSVoice) or (not S.Inverted) then
|
||||
begin
|
||||
WriteLn('inverted sync failed');
|
||||
Halt(2);
|
||||
end;
|
||||
WriteLn('DMR front-end sync tests passed');
|
||||
finally
|
||||
D.Free;
|
||||
end;
|
||||
end.
|
||||
Reference in New Issue
Block a user