feat(dmr): add receive frontend and AMBE adapter

This commit is contained in:
2026-07-14 18:38:39 +03:00
parent f33a1fbd0a
commit 53a3681618
20 changed files with 998 additions and 30 deletions
+5 -1
View File
@@ -3,4 +3,8 @@ lazbuild -B --ws=qt6 ewsdr.lpr
# RUN Build Windows # RUN Build Windows
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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+4
View File
@@ -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
View File
@@ -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;
+24 -6
View File
@@ -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,16 +2092,29 @@ 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 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 begin
FOutL[i] := FRXOut[i * 2] * FVolume; for i := 0 to FAudioBufSize - 1 do
FOutR[i] := FRXOut[i * 2 + 1] * FVolume; 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; end;
FOnAudio(FOutL, FOutR, FAudioBufSize);
end; end;
// Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы. // Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы.
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+21
View File
@@ -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})
+17
View File
@@ -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.
+86
View File
@@ -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;
}
+53
View File
@@ -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
+31
View File
@@ -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.
+68
View File
@@ -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.