mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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
|
lazbuild -B --ws=win32 ewsdr.lpr
|
||||||
# RUN Build macOS
|
# RUN Build macOS
|
||||||
lazbuild -B --ws=cocoa ewsdr.lpr
|
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;
|
procedure TCATAdapter.SyncMode;
|
||||||
begin
|
begin
|
||||||
if (FSyncInt < 0) or (FSyncInt > MODE_WFM) then Exit;
|
if (FSyncInt < 0) or (FSyncInt > MODE_DMR) then Exit;
|
||||||
FController.SetMode(FSyncInt);
|
FController.SetMode(FSyncInt);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -35,8 +35,8 @@ const
|
|||||||
MODE_LSB = 0; MODE_USB = 1; MODE_DSB = 2;
|
MODE_LSB = 0; MODE_USB = 1; MODE_DSB = 2;
|
||||||
MODE_CWL = 3; MODE_CWU = 4; MODE_FM = 5;
|
MODE_CWL = 3; MODE_CWU = 4; MODE_FM = 5;
|
||||||
MODE_AM = 6; MODE_SAM = 7;
|
MODE_AM = 6; MODE_SAM = 7;
|
||||||
MODE_DIGU = 8; MODE_DIGL = 9; MODE_WFM = 10;
|
MODE_DIGU = 8; MODE_DIGL = 9; MODE_WFM = 10; MODE_DMR = 11;
|
||||||
CAT_MODE_COUNT = 11;
|
CAT_MODE_COUNT = 12;
|
||||||
|
|
||||||
// FM repeater offset directions — зеркало FMRepeater.RPT_* (значения должны
|
// FM repeater offset directions — зеркало FMRepeater.RPT_* (значения должны
|
||||||
// совпадать: контроллер хранит FFMRptDir в этих же кодах).
|
// совпадать: контроллер хранит FFMRptDir в этих же кодах).
|
||||||
@@ -391,6 +391,7 @@ begin
|
|||||||
MODE_CWU: Result := 3;
|
MODE_CWU: Result := 3;
|
||||||
MODE_FM: Result := 4;
|
MODE_FM: Result := 4;
|
||||||
MODE_WFM: Result := 4; // treated as FM
|
MODE_WFM: Result := 4; // treated as FM
|
||||||
|
MODE_DMR: Result := 4; // DMR RF transport is narrow FM
|
||||||
MODE_AM: Result := 5;
|
MODE_AM: Result := 5;
|
||||||
MODE_SAM: Result := 5; // treated as AM
|
MODE_SAM: Result := 5; // treated as AM
|
||||||
MODE_DIGU: Result := 6; // Kenwood FSK
|
MODE_DIGU: Result := 6; // Kenwood FSK
|
||||||
@@ -1792,7 +1793,7 @@ end;
|
|||||||
|
|
||||||
function TCATEngine.ZZML: string;
|
function TCATEngine.ZZML: string;
|
||||||
// ZZML: mode list (returns all mode names)
|
// 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;
|
function TCATEngine.ZZMN(const s: string): string;
|
||||||
// ZZMN: mode name — returns mode name for given mode number.
|
// ZZMN: mode name — returns mode name for given mode number.
|
||||||
@@ -1803,7 +1804,7 @@ var k: Integer;
|
|||||||
begin
|
begin
|
||||||
names[0]:='LSB'; names[1]:='USB'; names[2]:='DSB'; names[3]:='CWL';
|
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[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
|
if (Length(s) = 1) or (Length(s) = 2) then begin
|
||||||
k := EnsureRange(StrToIntDef(s,0),0,CAT_MODE_COUNT-1);
|
k := EnsureRange(StrToIntDef(s,0),0,CAT_MODE_COUNT-1);
|
||||||
Result := names[k];
|
Result := names[k];
|
||||||
|
|||||||
+2
-2
@@ -18,9 +18,9 @@ uses
|
|||||||
|
|
||||||
const
|
const
|
||||||
// Копии из MainForm: нужны для выпадающих списков
|
// Копии из MainForm: нужны для выпадающих списков
|
||||||
CH_MODE_COUNT = 11;
|
CH_MODE_COUNT = 12;
|
||||||
CH_MODE_NAMES: array[0..CH_MODE_COUNT-1] of string =
|
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_COUNT = 38;
|
||||||
CH_CTCSS_NAMES: array[0..CH_CTCSS_COUNT-1] of string = (
|
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,
|
WinFirewall,
|
||||||
BoardUtils, WisdomBuilder, UISync,
|
BoardUtils, WisdomBuilder, UISync,
|
||||||
FlatEdit, FMRepeater,
|
FlatEdit, FMRepeater,
|
||||||
ChannelStore, ChannelsForm, BeaconScopeForm,
|
ChannelStore, ChannelsForm, BeaconScopeForm, DMRDecoder,
|
||||||
PowerInhibit,
|
PowerInhibit,
|
||||||
DeviceStore,
|
DeviceStore,
|
||||||
RadioBackend, PlutoBackend,
|
RadioBackend, PlutoBackend,
|
||||||
@@ -67,7 +67,7 @@ const
|
|||||||
WDSP_WISDOM_FILE = 'wdspWisdom00';
|
WDSP_WISDOM_FILE = 'wdspWisdom00';
|
||||||
|
|
||||||
BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11)
|
BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11)
|
||||||
MODE_COUNT = 11;
|
MODE_COUNT = 12;
|
||||||
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
|
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
|
||||||
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
|
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
|
||||||
// UI-подписи кнопок (NAMES).
|
// UI-подписи кнопок (NAMES).
|
||||||
@@ -105,7 +105,7 @@ const
|
|||||||
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','');
|
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','');
|
||||||
|
|
||||||
MODE_NAMES: array[0..MODE_COUNT-1] of string = (
|
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 колонки; последний ряд неполный
|
MODE_ROWS = 3; // 4 колонки; последний ряд неполный
|
||||||
|
|
||||||
SMETER_WIDTH_RATIO = 0.32;
|
SMETER_WIDTH_RATIO = 0.32;
|
||||||
@@ -419,6 +419,7 @@ type
|
|||||||
procedure UpdateStatusField7; // поле 7: PLL (HPSDR) или beacon (Pluto)
|
procedure UpdateStatusField7; // поле 7: PLL (HPSDR) или beacon (Pluto)
|
||||||
procedure UpdateStatusField3; // поле 3: Supply (HPSDR) или Temp/RSSI (Pluto)
|
procedure UpdateStatusField3; // поле 3: Supply (HPSDR) или Temp/RSSI (Pluto)
|
||||||
function BeaconStatusText: string; // компактный статус QO-100 beacon lock
|
function BeaconStatusText: string; // компактный статус QO-100 beacon lock
|
||||||
|
function DMRStatusText: string; // sync/type/polarity DMR front-end
|
||||||
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
|
procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord);
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
@@ -3583,10 +3584,28 @@ begin
|
|||||||
end;
|
end;
|
||||||
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;
|
procedure TMainForm.UpdateStatusField7;
|
||||||
// Поле 7 статус-бара. openHPSDR — PLL-lock (как прежде). Pluto в трансвертере —
|
// Поле 7 статус-бара. openHPSDR — PLL-lock (как прежде). Pluto в трансвертере —
|
||||||
// статус QO-100 beacon lock (у Pluto нет PLL-телеметрии); Pluto без XVTR — пусто.
|
// статус QO-100 beacon lock (у Pluto нет PLL-телеметрии); Pluto без XVTR — пусто.
|
||||||
begin
|
begin
|
||||||
|
if FController.FMode = MODE_DMR then
|
||||||
|
begin
|
||||||
|
SetStatusText(7, DMRStatusText);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
if FController.IsPluto then
|
if FController.IsPluto then
|
||||||
begin
|
begin
|
||||||
if FController.FCurrentXvtr >= 0 then SetStatusText(7, BeaconStatusText)
|
if FController.FCurrentXvtr >= 0 then SetStatusText(7, BeaconStatusText)
|
||||||
@@ -4203,6 +4222,24 @@ begin
|
|||||||
Exit;
|
Exit;
|
||||||
end;
|
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
|
for i := 0 to FILT_COUNT - 1 do
|
||||||
begin
|
begin
|
||||||
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, 5, i mod 5);
|
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, 5, i mod 5);
|
||||||
@@ -5323,6 +5360,12 @@ begin
|
|||||||
Break;
|
Break;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
MODE_DMR:
|
||||||
|
begin
|
||||||
|
FController.FFilter := 0;
|
||||||
|
FController.FFilterBW := FILT_DMR_BW;
|
||||||
|
FController.FFMDeviation := DMR_FM_DEV;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
if FController.FWDSPReady then
|
if FController.FWDSPReady then
|
||||||
begin
|
begin
|
||||||
|
|||||||
+57
-3
@@ -49,7 +49,7 @@ interface
|
|||||||
uses
|
uses
|
||||||
Classes, SysUtils, Math,
|
Classes, SysUtils, Math,
|
||||||
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
|
||||||
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC,
|
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
|
||||||
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
|
||||||
|
|
||||||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||||||
@@ -249,6 +249,9 @@ type
|
|||||||
// (узкий фильтр, ставится кликом); 0 = ещё не наведён
|
// (узкий фильтр, ставится кликом); 0 = ещё не наведён
|
||||||
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
|
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
|
||||||
|
|
||||||
|
// ---- DMR 4FSK receive decoder ----
|
||||||
|
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
|
||||||
|
|
||||||
// ---- Аудио / TX ----
|
// ---- Аудио / TX ----
|
||||||
FVolume: Integer; // громкость RX
|
FVolume: Integer; // громкость RX
|
||||||
FTxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off, DUP)
|
FTxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off, DUP)
|
||||||
@@ -401,6 +404,7 @@ type
|
|||||||
// Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine
|
// Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine
|
||||||
// вызывающим (GUI или демон) после CreateEngines.
|
// вызывающим (GUI или демон) после CreateEngines.
|
||||||
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
|
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 OnSpectrumReady(const Pixels: array of Single; Count: Integer);
|
||||||
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
|
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
|
||||||
procedure OnMicPacket(const Data: TMicDataPacket);
|
procedure OnMicPacket(const Data: TMicDataPacket);
|
||||||
@@ -670,6 +674,7 @@ type
|
|||||||
function BeaconDecodeInvert: Boolean;
|
function BeaconDecodeInvert: Boolean;
|
||||||
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
function GetBeaconScope(out S: TBeaconScope): Boolean;
|
||||||
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
|
||||||
|
function GetDMRStatus(out S: TDMRStatus): Boolean;
|
||||||
|
|
||||||
// Отображение / приём
|
// Отображение / приём
|
||||||
procedure SetCTun(On_: Boolean);
|
procedure SetCTun(On_: Boolean);
|
||||||
@@ -827,6 +832,7 @@ begin
|
|||||||
// Роутинг аудио слайсов на их звуковухи — внутренний, вешаем сразу (не
|
// Роутинг аудио слайсов на их звуковухи — внутренний, вешаем сразу (не
|
||||||
// зависит от фронтенда, в отличие от OnAudio/OnSpectrum).
|
// зависит от фронтенда, в отличие от OnAudio/OnSpectrum).
|
||||||
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
|
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
|
||||||
|
FDSPEngine.OnDemodAudio := OnDemodAudioReady;
|
||||||
FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
FDSPEngine.SetDisplayResLimit(FDisplayPixels);
|
||||||
|
|
||||||
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
|
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
|
||||||
@@ -843,6 +849,9 @@ begin
|
|||||||
FBeaconDec.SetInvert(FBeaconDecInvert);
|
FBeaconDec.SetInvert(FBeaconDecInvert);
|
||||||
FDSPEngine.SetBeaconDecoder(FBeaconDec);
|
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 вызывается позже (после показа
|
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
|
||||||
// формы / при подключении устройства).
|
// формы / при подключении устройства).
|
||||||
FAudioOut := TAudioOutput.Create(48000);
|
FAudioOut := TAudioOutput.Create(48000);
|
||||||
@@ -874,6 +883,7 @@ begin
|
|||||||
FDSPEngine.Close;
|
FDSPEngine.Close;
|
||||||
FreeAndNil(FDSPEngine);
|
FreeAndNil(FDSPEngine);
|
||||||
end;
|
end;
|
||||||
|
if Assigned(FDMRDec) then FreeAndNil(FDMRDec);
|
||||||
if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec);
|
if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -1148,6 +1158,10 @@ procedure TRadioController.OnAudioReady(const Left, Right: array of Single;
|
|||||||
// port 1028) — независимый канал. Затем web-адаптер может перехватить аудио
|
// port 1028) — независимый канал. Затем web-адаптер может перехватить аудио
|
||||||
// (активный клиент); если перехвачено — в локальный AudioOut не пишем.
|
// (активный клиент); если перехвачено — в локальный AudioOut не пишем.
|
||||||
begin
|
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
|
if FSendAudioToRadio then
|
||||||
FNetwork.SendSpeakerAudio(Left, Right, Count);
|
FNetwork.SendSpeakerAudio(Left, Right, Count);
|
||||||
if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then
|
if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then
|
||||||
@@ -1158,6 +1172,20 @@ begin
|
|||||||
FAudioOut.Write(Left, Right, Count);
|
FAudioOut.Write(Left, Right, Count);
|
||||||
end;
|
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)
|
// Мультислайсы (софт-fan-out)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -2157,6 +2185,13 @@ begin
|
|||||||
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
|
||||||
end;
|
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);
|
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
|
||||||
var Half: Integer;
|
var Half: Integer;
|
||||||
@@ -2190,12 +2225,13 @@ begin
|
|||||||
begin
|
begin
|
||||||
FDSPEngine.SetFilter(Lo, Hi);
|
FDSPEngine.SetFilter(Lo, Hi);
|
||||||
FDSPEngine.SetTXFilter(Lo, Hi);
|
FDSPEngine.SetTXFilter(Lo, Hi);
|
||||||
if FMode = MODE_FM then
|
if (FMode = MODE_FM) or (FMode = MODE_DMR) then
|
||||||
begin
|
begin
|
||||||
FDSPEngine.SetRXFMDeviation(FFMDeviation);
|
FDSPEngine.SetRXFMDeviation(FFMDeviation);
|
||||||
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
|
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
|
||||||
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
|
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
|
end
|
||||||
else
|
else
|
||||||
FDSPEngine.SetFMSquelch(False, 0);
|
FDSPEngine.SetFMSquelch(False, 0);
|
||||||
@@ -2219,6 +2255,12 @@ begin
|
|||||||
if FFilter > FILT_WFM_COUNT - 1 then FFilter := FILT_WFM_DEF;
|
if FFilter > FILT_WFM_COUNT - 1 then FFilter := FILT_WFM_DEF;
|
||||||
FFilterBW := FILT_WFM_BW[FFilter];
|
FFilterBW := FILT_WFM_BW[FFilter];
|
||||||
end;
|
end;
|
||||||
|
MODE_DMR:
|
||||||
|
begin
|
||||||
|
FFilter := 0;
|
||||||
|
FFilterBW := FILT_DMR_BW;
|
||||||
|
FFMDeviation := DMR_FM_DEV;
|
||||||
|
end;
|
||||||
MODE_LSB, MODE_USB:
|
MODE_LSB, MODE_USB:
|
||||||
begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
|
begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
|
||||||
MODE_DIGU, MODE_DIGL:
|
MODE_DIGU, MODE_DIGL:
|
||||||
@@ -2238,6 +2280,7 @@ begin
|
|||||||
case M of
|
case M of
|
||||||
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, FILT_FM_COUNT - 1)];
|
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_WFM: Result := FILT_WFM_BW[EnsureRange(Idx, 0, FILT_WFM_COUNT - 1)];
|
||||||
|
MODE_DMR: Result := FILT_DMR_BW;
|
||||||
MODE_LSB, MODE_USB:
|
MODE_LSB, MODE_USB:
|
||||||
Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
||||||
MODE_DIGU, MODE_DIGL:
|
MODE_DIGU, MODE_DIGL:
|
||||||
@@ -2310,6 +2353,12 @@ begin
|
|||||||
FFilterBW := FILT_FM_BW[FFilter];
|
FFilterBW := FILT_FM_BW[FFilter];
|
||||||
FFMDeviation := FILT_FM_DEV[FFilter];
|
FFMDeviation := FILT_FM_DEV[FFilter];
|
||||||
end;
|
end;
|
||||||
|
if FMode = MODE_DMR then
|
||||||
|
begin
|
||||||
|
FFilter := 0;
|
||||||
|
FFilterBW := FILT_DMR_BW;
|
||||||
|
FFMDeviation := DMR_FM_DEV;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TRadioController.MakeBandSettings: TBandSettings;
|
function TRadioController.MakeBandSettings: TBandSettings;
|
||||||
@@ -2480,6 +2529,7 @@ procedure TRadioController.ApplyBandDSP(const B: TBandSettings);
|
|||||||
begin
|
begin
|
||||||
// --- Режим ---
|
// --- Режим ---
|
||||||
FMode := B.Mode;
|
FMode := B.Mode;
|
||||||
|
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
|
||||||
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
|
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
|
||||||
|
|
||||||
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
|
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
|
||||||
@@ -2870,6 +2920,7 @@ end;
|
|||||||
procedure TRadioController.SetMode(M: Integer);
|
procedure TRadioController.SetMode(M: Integer);
|
||||||
begin
|
begin
|
||||||
FMode := M;
|
FMode := M;
|
||||||
|
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
|
||||||
ApplyModeDefaults; // дефолтный фильтр/девиация под режим
|
ApplyModeDefaults; // дефолтный фильтр/девиация под режим
|
||||||
FBandCache[FCurrentBand].Mode := FMode;
|
FBandCache[FCurrentBand].Mode := FMode;
|
||||||
if FWDSPReady and Assigned(FDSPEngine) then
|
if FWDSPReady and Assigned(FDSPEngine) then
|
||||||
@@ -2889,6 +2940,7 @@ begin
|
|||||||
case FMode of
|
case FMode of
|
||||||
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
|
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
|
||||||
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
|
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
|
||||||
|
MODE_DMR: Idx := 0;
|
||||||
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
|
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
|
||||||
end;
|
end;
|
||||||
FFilter := Idx;
|
FFilter := Idx;
|
||||||
@@ -2898,6 +2950,7 @@ begin
|
|||||||
MODE_DSB: FFilterBW := FILT_DSB_BW[Idx];
|
MODE_DSB: FFilterBW := FILT_DSB_BW[Idx];
|
||||||
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
|
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
|
||||||
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
|
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
|
||||||
|
MODE_DMR: FFilterBW := FILT_DMR_BW;
|
||||||
MODE_FM:
|
MODE_FM:
|
||||||
begin
|
begin
|
||||||
FFilterBW := FILT_FM_BW[Idx];
|
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];
|
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_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_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];
|
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
|
||||||
end;
|
end;
|
||||||
best := 0; dist := MaxInt;
|
best := 0; dist := MaxInt;
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ const
|
|||||||
FILT_WFM_BW: array[0..2] of Integer = (200000, 160000, 120000);
|
FILT_WFM_BW: array[0..2] of Integer = (200000, 160000, 120000);
|
||||||
FILT_WFM_DEF = 1; // 160k — влезает и в 192k
|
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
|
type
|
||||||
TBandSettings = record
|
TBandSettings = record
|
||||||
VfoA: Double;
|
VfoA: Double;
|
||||||
|
|||||||
+5
-2
@@ -24,11 +24,11 @@ const
|
|||||||
OVL_W = 280; // ширина карточки
|
OVL_W = 280; // ширина карточки
|
||||||
|
|
||||||
// Индексы совпадают с MODE_* контроллера (WDSPEngine.pas).
|
// Индексы совпадают с MODE_* контроллера (WDSPEngine.pas).
|
||||||
OVL_MODE_COUNT = 11;
|
OVL_MODE_COUNT = 12;
|
||||||
OVL_MODE_COLS = 4;
|
OVL_MODE_COLS = 4;
|
||||||
OVL_MODE_ROWS = (OVL_MODE_COUNT + OVL_MODE_COLS - 1) div OVL_MODE_COLS;
|
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 = (
|
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): одна на букву — общий цвет для
|
// Палитра слайсов (TColor = $00BBGGRR): одна на букву — общий цвет для
|
||||||
// бейджа-карточки во флаге и для буквы на полосе фильтра в спектре.
|
// бейджа-карточки во флаге и для буквы на полосе фильтра в спектре.
|
||||||
@@ -394,6 +394,7 @@ begin
|
|||||||
5: Result := FM_BW[FilterIdx];
|
5: Result := FM_BW[FilterIdx];
|
||||||
8,9: Result := DIGI_BW[FilterIdx]; // DIGU/DIGL — полоса от нуля
|
8,9: Result := DIGI_BW[FilterIdx]; // DIGU/DIGL — полоса от нуля
|
||||||
10: Result := WFM_BW[FilterIdx];
|
10: Result := WFM_BW[FilterIdx];
|
||||||
|
11: Result := 12500;
|
||||||
else Result := AM_BW[FilterIdx];
|
else Result := AM_BW[FilterIdx];
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -407,6 +408,7 @@ begin
|
|||||||
5: Result := FM_LBL[FilterIdx];
|
5: Result := FM_LBL[FilterIdx];
|
||||||
8,9: Result := DIGI_LBL[FilterIdx];
|
8,9: Result := DIGI_LBL[FilterIdx];
|
||||||
10: Result := WFM_LBL[FilterIdx];
|
10: Result := WFM_LBL[FilterIdx];
|
||||||
|
11: Result := '12.5k';
|
||||||
else Result := AM_LBL[FilterIdx];
|
else Result := AM_LBL[FilterIdx];
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -416,6 +418,7 @@ begin
|
|||||||
case ModeIdx of
|
case ModeIdx of
|
||||||
5: Result := 2; // FM: NFM/FM
|
5: Result := 2; // FM: NFM/FM
|
||||||
10: Result := 3; // WFM: 200K/160K/120K
|
10: Result := 3; // WFM: 200K/160K/120K
|
||||||
|
11: Result := 1; // DMR: fixed 12.5 kHz channel
|
||||||
else Result := 6;
|
else Result := 6;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
+20
-2
@@ -60,6 +60,7 @@ const
|
|||||||
MODE_DIGU = 8;
|
MODE_DIGU = 8;
|
||||||
MODE_DIGL = 9;
|
MODE_DIGL = 9;
|
||||||
MODE_WFM = 10;
|
MODE_WFM = 10;
|
||||||
|
MODE_DMR = 11; // DMR receive: WDSP NFM discriminator -> DMRDecoder
|
||||||
|
|
||||||
// Минимальная длина FIR у WDSP: create_* поднимают fircore с
|
// Минимальная длина FIR у WDSP: create_* поднимают fircore с
|
||||||
// nc = max(2048, dsp_size). В firmin.c nfor = nc / size, idxmask = nfor - 1,
|
// nc = max(2048, dsp_size). В firmin.c nfor = nc / size, idxmask = nfor - 1,
|
||||||
@@ -322,6 +323,7 @@ type
|
|||||||
FViewHighHz: Double;
|
FViewHighHz: Double;
|
||||||
|
|
||||||
FOnAudio: TOnAudioReady;
|
FOnAudio: TOnAudioReady;
|
||||||
|
FOnDemodAudio:TOnAudioReady; // pre-volume/mute tap for digital decoders
|
||||||
FOnSpectrum: TOnSpectrumReady;
|
FOnSpectrum: TOnSpectrumReady;
|
||||||
FOnWaterfall: TOnWaterfallReady;
|
FOnWaterfall: TOnWaterfallReady;
|
||||||
FOnTXIQ: TOnTXIQReady;
|
FOnTXIQ: TOnTXIQReady;
|
||||||
@@ -682,6 +684,7 @@ type
|
|||||||
property WfAvgMode: Integer read FWfAvgMode;
|
property WfAvgMode: Integer read FWfAvgMode;
|
||||||
property WfAvgTimeMS: Double read FWfAvgTimeMS;
|
property WfAvgTimeMS: Double read FWfAvgTimeMS;
|
||||||
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
|
property OnAudio: TOnAudioReady read FOnAudio write FOnAudio;
|
||||||
|
property OnDemodAudio: TOnAudioReady read FOnDemodAudio write FOnDemodAudio;
|
||||||
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
|
property OnSliceAudio: TOnSliceAudio read FOnSliceAudio write FOnSliceAudio;
|
||||||
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
property OnSpectrum: TOnSpectrumReady read FOnSpectrum write FOnSpectrum;
|
||||||
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
property OnWaterfall: TOnWaterfallReady read FOnWaterfall write FOnWaterfall;
|
||||||
@@ -930,6 +933,7 @@ begin
|
|||||||
MODE_DIGU: Result := WDSP_DIGU;
|
MODE_DIGU: Result := WDSP_DIGU;
|
||||||
MODE_DIGL: Result := WDSP_DIGL;
|
MODE_DIGL: Result := WDSP_DIGL;
|
||||||
MODE_WFM: Result := WDSP_RX_WFM;
|
MODE_WFM: Result := WDSP_RX_WFM;
|
||||||
|
MODE_DMR: Result := WDSP_FM;
|
||||||
else Result := WDSP_USB;
|
else Result := WDSP_USB;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -1082,6 +1086,7 @@ begin
|
|||||||
MODE_DIGU: SetFilter( 0, 3000);
|
MODE_DIGU: SetFilter( 0, 3000);
|
||||||
MODE_DIGL: SetFilter(-3000, 0);
|
MODE_DIGL: SetFilter(-3000, 0);
|
||||||
MODE_WFM: SetFilter(-80000, 80000);
|
MODE_WFM: SetFilter(-80000, 80000);
|
||||||
|
MODE_DMR: SetFilter(-6250, 6250);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -2087,9 +2092,21 @@ begin
|
|||||||
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
|
// Full-duplex self-monitor (QO-100): при FMonitorRXDuringTX не глушим RX-аудио
|
||||||
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
|
// на TX — слышим свой downlink со спутника. Иначе (по умолчанию) глушим, как
|
||||||
// Thetis/piHPSDR при не-duplex MOX. FPostTXMuteUntil-хвост уважаем в любом случае.
|
// Thetis/piHPSDR при не-duplex MOX. FPostTXMuteUntil-хвост уважаем в любом случае.
|
||||||
if Assigned(FOnAudio) and not FMuted
|
if (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
|
||||||
and (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
|
|
||||||
and (FPostTXMuteUntil = 0) then
|
and (FPostTXMuteUntil = 0) then
|
||||||
|
begin
|
||||||
|
// 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
|
||||||
|
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
|
begin
|
||||||
for i := 0 to FAudioBufSize - 1 do
|
for i := 0 to FAudioBufSize - 1 do
|
||||||
begin
|
begin
|
||||||
@@ -2098,6 +2115,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
FOnAudio(FOutL, FOutR, FAudioBufSize);
|
FOnAudio(FOutL, FOutR, FAudioBufSize);
|
||||||
end;
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
// Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы.
|
// Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы.
|
||||||
ProcessSlices;
|
ProcessSlices;
|
||||||
|
|||||||
+1
-1
@@ -145,7 +145,7 @@ begin FSyncInt := Mode; FController.Invoke(@SyncMode); end;
|
|||||||
|
|
||||||
procedure TWebAdapter.SyncMode;
|
procedure TWebAdapter.SyncMode;
|
||||||
begin
|
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
|
FController.SetMode(FSyncInt); // дефолт фильтра + DSP + cache; рендер через rfMode
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -193,7 +193,7 @@ begin
|
|||||||
'<select id="modGrpSel">' +
|
'<select id="modGrpSel">' +
|
||||||
'<option value="ssb" selected>SSB</option><option value="cw">CW</option>' +
|
'<option value="ssb" selected>SSB</option><option value="cw">CW</option>' +
|
||||||
'<option value="fm">FM</option><option value="am">AM</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>' +
|
||||||
'<select id="stepSel">' +
|
'<select id="stepSel">' +
|
||||||
'<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' +
|
'<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();}' +
|
'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 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]};' +
|
'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"];' +
|
'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 modGrpFromMode(m){return MODE_TO_GRP[m]||"ssb";}' +
|
||||||
'function modGrp(){return q("modGrpSel").value||"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];}' +
|
'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"],' +
|
'am:["20k","18k","16k","12k","10k","9.0k","8.0k","7.0k","6.0k","5.0k"],' +
|
||||||
'fm:["NFM","FM"],' +
|
'fm:["NFM","FM"],' +
|
||||||
'wfm:["200k","160k","120k"],' +
|
'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"]};' +
|
'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],' +
|
'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],' +
|
'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],' +
|
'am:[20000,18000,16000,12000,10000,9000,8000,7000,6000,5000],' +
|
||||||
'fm:[11000,16000],' +
|
'fm:[11000,16000],' +
|
||||||
'wfm:[200000,160000,120000],' +
|
'wfm:[200000,160000,120000],' +
|
||||||
|
'dmr:[12500],' +
|
||||||
'digi:[3000,2700,2400,2100,1800,1500,1200,900,600,300]};' +
|
'digi:[3000,2700,2400,2100,1800,1500,1200,900,600,300]};' +
|
||||||
'var fnpOv=null;' +
|
'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 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 fnpOutside(e){if(fnpOv&&!fnpOv.contains(e.target)&&e.target!==q("fnpBtn"))closeFnp();}' +
|
||||||
'function showFnp(){' +
|
'function showFnp(){' +
|
||||||
|
|||||||
+2
-2
@@ -1717,8 +1717,8 @@ end;
|
|||||||
|
|
||||||
function TWebServer.BuildStateJson: string;
|
function TWebServer.BuildStateJson: string;
|
||||||
const
|
const
|
||||||
MODE_N: array[0..10] of string =
|
MODE_N: array[0..11] 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');
|
||||||
var
|
var
|
||||||
FS: TFormatSettings;
|
FS: TFormatSettings;
|
||||||
XvtrJson, DevJson, RateJson, BandJson: string;
|
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