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
+57 -3
View File
@@ -49,7 +49,7 @@ interface
uses
Classes, SysUtils, Math,
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC,
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore;
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
@@ -249,6 +249,9 @@ type
// (узкий фильтр, ставится кликом); 0 = ещё не наведён
FBeaconDecInvert: Boolean; // зеркальный baseband (QO-100/LNB) — деф. True
// ---- DMR 4FSK receive decoder ----
FDMRDec: TDMRDecoder; // 48 kHz FM discriminator -> DMR worker
// ---- Аудио / TX ----
FVolume: Integer; // громкость RX
FTxMonVolume: Integer; // громкость self-monitor RX-аудио на TX (RX MUTE off, DUP)
@@ -401,6 +404,7 @@ type
// Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine
// вызывающим (GUI или демон) после CreateEngines.
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnDemodAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer);
procedure OnMicPacket(const Data: TMicDataPacket);
@@ -670,6 +674,7 @@ type
function BeaconDecodeInvert: Boolean;
function GetBeaconScope(out S: TBeaconScope): Boolean;
function GetBeaconFrame(out Frame: TBeaconFrame; out RSErrors: Integer): Boolean;
function GetDMRStatus(out S: TDMRStatus): Boolean;
// Отображение / приём
procedure SetCTun(On_: Boolean);
@@ -827,6 +832,7 @@ begin
// Роутинг аудио слайсов на их звуковухи — внутренний, вешаем сразу (не
// зависит от фронтенда, в отличие от OnAudio/OnSpectrum).
FDSPEngine.OnSliceAudio := OnSliceAudioReady;
FDSPEngine.OnDemodAudio := OnDemodAudioReady;
FDSPEngine.SetDisplayResLimit(FDisplayPixels);
// QO-100 beacon lock: эталон частоты = декодер (см. ServiceBeaconLock).
@@ -843,6 +849,9 @@ begin
FBeaconDec.SetInvert(FBeaconDecInvert);
FDSPEngine.SetBeaconDecoder(FBeaconDec);
// DMR front-end owns a worker thread. It stays idle until MODE_DMR is active.
FDMRDec := TDMRDecoder.Create;
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
// формы / при подключении устройства).
FAudioOut := TAudioOutput.Create(48000);
@@ -874,6 +883,7 @@ begin
FDSPEngine.Close;
FreeAndNil(FDSPEngine);
end;
if Assigned(FDMRDec) then FreeAndNil(FDMRDec);
if Assigned(FBeaconDec) then FreeAndNil(FBeaconDec);
end;
@@ -1148,6 +1158,10 @@ procedure TRadioController.OnAudioReady(const Left, Right: array of Single;
// port 1028) — независимый канал. Затем web-адаптер может перехватить аудио
// (активный клиент); если перехвачено — в локальный AudioOut не пишем.
begin
// Decoded DMR PCM will use this route later. Until then suppress raw 4FSK;
// the independent pre-volume tap is handled by OnDemodAudioReady.
if FMode = MODE_DMR then Exit;
if FSendAudioToRadio then
FNetwork.SendSpeakerAudio(Left, Right, Count);
if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then
@@ -1158,6 +1172,20 @@ begin
FAudioOut.Write(Left, Right, Count);
end;
procedure TRadioController.OnDemodAudioReady(const Left, Right: array of Single;
Count: Integer);
// DSP thread, pre-volume/pre-mute. FeedAudio only copies into a bounded ring.
begin
if not Assigned(FDMRDec) then Exit;
if FMode = MODE_DMR then
begin
if not FDMRDec.Enabled then FDMRDec.SetEnabled(True);
FDMRDec.FeedAudio(Left, Right, Count);
end
else if FDMRDec.Enabled then
FDMRDec.SetEnabled(False);
end;
// ---------------------------------------------------------------------------
// Мультислайсы (софт-fan-out)
// ---------------------------------------------------------------------------
@@ -2157,6 +2185,13 @@ begin
Result := Assigned(FBeaconDec) and FBeaconDec.GetLastFrame(Frame, RSErrors);
end;
function TRadioController.GetDMRStatus(out S: TDMRStatus): Boolean;
begin
Result := Assigned(FDMRDec);
if Result then FDMRDec.GetStatus(S)
else FillChar(S, SizeOf(S), 0);
end;
// Кромки полосы под режим/полосу (единый расчёт для главного канала и слайсов).
class procedure TRadioController.FilterEdgesFor(Mode, BW: Integer; out Lo, Hi: Integer);
var Half: Integer;
@@ -2190,12 +2225,13 @@ begin
begin
FDSPEngine.SetFilter(Lo, Hi);
FDSPEngine.SetTXFilter(Lo, Hi);
if FMode = MODE_FM then
if (FMode = MODE_FM) or (FMode = MODE_DMR) then
begin
FDSPEngine.SetRXFMDeviation(FFMDeviation);
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
FDSPEngine.SetFMSquelch(FFMSQOn, FFMSQLevel);
// A squelch would remove sync preambles; DMR always receives open.
FDSPEngine.SetFMSquelch((FMode = MODE_FM) and FFMSQOn, FFMSQLevel);
end
else
FDSPEngine.SetFMSquelch(False, 0);
@@ -2219,6 +2255,12 @@ begin
if FFilter > FILT_WFM_COUNT - 1 then FFilter := FILT_WFM_DEF;
FFilterBW := FILT_WFM_BW[FFilter];
end;
MODE_DMR:
begin
FFilter := 0;
FFilterBW := FILT_DMR_BW;
FFMDeviation := DMR_FM_DEV;
end;
MODE_LSB, MODE_USB:
begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
MODE_DIGU, MODE_DIGL:
@@ -2238,6 +2280,7 @@ begin
case M of
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, FILT_FM_COUNT - 1)];
MODE_WFM: Result := FILT_WFM_BW[EnsureRange(Idx, 0, FILT_WFM_COUNT - 1)];
MODE_DMR: Result := FILT_DMR_BW;
MODE_LSB, MODE_USB:
Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
MODE_DIGU, MODE_DIGL:
@@ -2310,6 +2353,12 @@ begin
FFilterBW := FILT_FM_BW[FFilter];
FFMDeviation := FILT_FM_DEV[FFilter];
end;
if FMode = MODE_DMR then
begin
FFilter := 0;
FFilterBW := FILT_DMR_BW;
FFMDeviation := DMR_FM_DEV;
end;
end;
function TRadioController.MakeBandSettings: TBandSettings;
@@ -2480,6 +2529,7 @@ procedure TRadioController.ApplyBandDSP(const B: TBandSettings);
begin
// --- Режим ---
FMode := B.Mode;
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
// --- Фильтр (санитизация FM до применения к DSP — как в оригинале) ---
@@ -2870,6 +2920,7 @@ end;
procedure TRadioController.SetMode(M: Integer);
begin
FMode := M;
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
ApplyModeDefaults; // дефолтный фильтр/девиация под режим
FBandCache[FCurrentBand].Mode := FMode;
if FWDSPReady and Assigned(FDSPEngine) then
@@ -2889,6 +2940,7 @@ begin
case FMode of
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
MODE_DMR: Idx := 0;
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
end;
FFilter := Idx;
@@ -2898,6 +2950,7 @@ begin
MODE_DSB: FFilterBW := FILT_DSB_BW[Idx];
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
MODE_DMR: FFilterBW := FILT_DMR_BW;
MODE_FM:
begin
FFilterBW := FILT_FM_BW[Idx];
@@ -2929,6 +2982,7 @@ begin
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
MODE_FM: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
MODE_WFM: for i := 0 to FILT_WFM_COUNT-1 do BW_arr[i] := FILT_WFM_BW[i];
MODE_DMR: BW_arr[0] := FILT_DMR_BW;
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
end;
best := 0; dist := MaxInt;