feat(fm): add universal FM RAW decoder output

This commit is contained in:
2026-07-16 18:58:28 +03:00
parent 3acd569da5
commit cd816c54d6
16 changed files with 656 additions and 148 deletions
+74 -24
View File
@@ -354,6 +354,7 @@ type
// ---- FM ----
FFMDeviation: Double;
FRawFMDeviation: Double;
FFMCTCSSOn: Boolean;
FFMCTCSSToneIdx: Integer;
FFMSQOn: Boolean;
@@ -563,6 +564,7 @@ type
procedure SetMode(M: Integer);
procedure SetFilterIdx(Idx: Integer);
procedure SetFilterBW(BWHz: Integer);
procedure SetRawFMDeviation(Hz: Integer);
procedure SetAGCMode(M: Integer);
procedure SetAGCTop(DB: Integer);
procedure AGCTopBy(Delta: Integer);
@@ -916,7 +918,7 @@ begin
FDeviceStore := TDeviceStore.Create; // общий список устройств (desktop+web)
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
FMode := 1; FFilter := 5; FFilterBW := 2700;
FMode := MODE_USB; FFilter := 5; FFilterBW := 2700;
FAGCMode := 1; FAGCTop := 90;
FRxGainMode := 2; FRxGainDb := 40; // Pluto: slow_attack по умолчанию
FRxMuteOnTx := True; // QO-100 self-monitor выкл по умолчанию (RX заглушён на TX)
@@ -934,6 +936,7 @@ begin
FWfPalette := 0; FWfGamma := 1.0; FDisplayPixels := 4096;
FWfSpeedDiv := 1; FSpecMSAA := 4;
FFMStepOn := True;
FRawFMDeviation := RAW_FM_DEV_DEFAULT;
FLocalAudio := True; // GUI: звук на локальную карту; демон выключит явно
FLastSMeter := -130; FLastSWR := 1; FLastSupplyV := -1; FLastSupplyA := -1;
FLastPlutoTempC := TELEMETRY_NONE; FLastPlutoSocTempC := TELEMETRY_NONE;
@@ -1316,7 +1319,7 @@ procedure TRadioController.OnAudioReady(const Left, Right: array of Single;
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 FMode in [MODE_DMR, MODE_FMRAW] then Exit;
if FSendAudioToRadio then
FNetwork.SendSpeakerAudio(Left, Right, Count);
@@ -1332,13 +1335,19 @@ 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 Assigned(FDMRDec) then Exit;
if not FDMRDec.Enabled then FDMRDec.SetEnabled(True);
FDMRDec.FeedAudio(Left, Right, Count);
end
else if FDMRDec.Enabled then
else if FMode = MODE_FMRAW then
begin
// FM RAW decoder audio deliberately bypasses speaker volume and mute.
if FLocalAudio and Assigned(FAudioOut) then
FAudioOut.Write(Left, Right, Count);
end
else if Assigned(FDMRDec) and FDMRDec.Enabled then
FDMRDec.SetEnabled(False);
end;
@@ -1396,7 +1405,7 @@ begin
if not FLocalAudio then Exit;
idx := FindSliceIndex(SliceId);
if idx < 0 then Exit;
if FSlices[idx].Mode = MODE_DMR then Exit;
if FSlices[idx].Mode in [MODE_DMR, MODE_FMRAW] then Exit;
if SliceMutedByTx(idx) then Exit;
if FSlices[idx].Audio <> nil then
FSlices[idx].Audio.Write(Left, Right, Count);
@@ -1417,9 +1426,12 @@ procedure TRadioController.OnSliceDemodAudioReady(SliceId: Integer;
var idx: Integer;
begin
idx := FindSliceIndex(SliceId);
if (idx < 0) or (FSlices[idx].Mode <> MODE_DMR) then Exit;
if Assigned(FSlices[idx].DMR) then
if idx < 0 then Exit;
if (FSlices[idx].Mode = MODE_DMR) and Assigned(FSlices[idx].DMR) then
FSlices[idx].DMR.FeedAudio(Left, Right, Count);
if (FSlices[idx].Mode = MODE_FMRAW) and FLocalAudio and
Assigned(FSlices[idx].Audio) then
FSlices[idx].Audio.Write(Left, Right, Count);
end;
// DMR worker thread: decoded 8 kHz mono -> this slice's 48 kHz local output.
@@ -1821,6 +1833,7 @@ var
AO: TAudioOutput;
begin
Result := -1;
Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX);
if not Assigned(FDSPEngine) then Exit;
if (APanId < 0) or (APanId >= MAX_PANS) then Exit;
if (APanId > 0) and (FPanDDCIdx[APanId] < 0) then Exit; // пан не создан
@@ -1901,6 +1914,7 @@ end;
procedure TRadioController.SetSliceMode(Id, Mode: Integer);
var idx, OldMode: Integer;
begin
Mode := EnsureRange(Mode, MODE_MIN, MODE_MAX);
idx := FindSliceIndex(Id);
if idx < 0 then Exit;
OldMode := FSlices[idx].Mode;
@@ -1910,7 +1924,7 @@ begin
if Assigned(FDSPEngine) then FDSPEngine.SetSliceMode(Id, Mode);
// DMR is receive-only. If this slice used to be the TX source, hand TX back
// to the main receiver as soon as it enters DMR.
if (Mode = MODE_DMR) and (FTxSliceId = Id) then SetTxSlice(0);
if (Mode in [MODE_DMR, MODE_FMRAW]) and (FTxSliceId = Id) then SetTxSlice(0);
if (OldMode = MODE_DMR) and (Mode <> MODE_DMR) then
FreeAndNil(FSlices[idx].DMR);
end;
@@ -2221,12 +2235,12 @@ begin
if not GetSlice(SliceId, S) then Exit;
// EWSDR has no DMR transmitter/AMBE encoder. Do not accidentally use the
// analog WFM transmitter for a receive-only DMR slice.
if S.Mode = MODE_DMR then Exit;
if S.Mode in [MODE_DMR, MODE_FMRAW] then Exit;
end;
if FTxSliceId = SliceId then Exit;
// Selecting main A while it is in DMR is allowed as an RX selection, but it
// cannot inherit an active carrier from a previously selected TX slice.
if (SliceId = 0) and (FMode = MODE_DMR) then
if (SliceId = 0) and (FMode in [MODE_DMR, MODE_FMRAW]) then
begin
if FTuning then SetTune(False)
else if FTransmitting then SetMOX(False);
@@ -2529,11 +2543,15 @@ begin
begin
FDSPEngine.SetFilter(Lo, Hi);
FDSPEngine.SetTXFilter(Lo, Hi);
if (FMode = MODE_FM) or (FMode = MODE_DMR) then
if FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then
begin
FDSPEngine.SetRXFMDeviation(FFMDeviation);
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
if FMode = MODE_FMRAW then
FDSPEngine.SetRawFMDeviation(FRawFMDeviation)
else
FDSPEngine.SetRXFMDeviation(FFMDeviation);
if FMode <> MODE_FMRAW then
FDSPEngine.SetTXFMParams(FFMDeviation, FTXSettings.FMLowCut,
FTXSettings.FMHighCut, FTXSettings.FMEmphPosition);
// A squelch would remove sync preambles; DMR always receives open.
FDSPEngine.SetFMSquelch((FMode = MODE_FM) and FFMSQOn, FFMSQLevel);
end
@@ -2565,6 +2583,11 @@ begin
FFilterBW := FILT_DMR_BW;
FFMDeviation := DMR_FM_DEV;
end;
MODE_FMRAW:
begin
if FFilter > FILT_RAW_COUNT - 1 then FFilter := FILT_RAW_DEF;
FFilterBW := FILT_RAW_BW[FFilter];
end;
MODE_LSB, MODE_USB:
begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
MODE_DIGU, MODE_DIGL:
@@ -2585,6 +2608,7 @@ begin
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_FMRAW: Result := FILT_RAW_BW[EnsureRange(Idx, 0, FILT_RAW_COUNT - 1)];
MODE_LSB, MODE_USB:
Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
MODE_DIGU, MODE_DIGL:
@@ -2663,6 +2687,11 @@ begin
FFilterBW := FILT_DMR_BW;
FFMDeviation := DMR_FM_DEV;
end;
if FMode = MODE_FMRAW then
begin
if FFilter > FILT_RAW_COUNT - 1 then FFilter := FILT_RAW_DEF;
FFilterBW := FILT_RAW_BW[FFilter];
end;
end;
function TRadioController.MakeBandSettings: TBandSettings;
@@ -2821,6 +2850,7 @@ begin
Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName;
Result.SendAudioToRadio := FSendAudioToRadio;
Result.RawFMDeviation := Round(FRawFMDeviation);
end;
procedure TRadioController.SaveGlobalSettings;
@@ -2832,7 +2862,7 @@ procedure TRadioController.ApplyBandDSP(const B: TBandSettings);
// DSP-настройки диапазона + события рендера. Частоты VFO/центр НЕ трогает.
begin
// --- Режим ---
FMode := B.Mode;
FMode := EnsureRange(B.Mode, MODE_MIN, MODE_MAX);
if Assigned(FDMRDec) then FDMRDec.SetEnabled(FMode = MODE_DMR);
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetMode(FMode);
@@ -2867,7 +2897,7 @@ begin
end;
FFMStepOn := B.FMStepOn;
FFMStepIdx := EnsureRange(B.FMStepIdx, 0, 3);
if FMode = MODE_DMR then
if FMode in [MODE_DMR, MODE_FMRAW] then
begin
FFMStepOn := True;
FFMStepIdx := 1; // DMR default raster: 12.5 kHz
@@ -2974,8 +3004,8 @@ begin
if FWDSPReady and Assigned(FDSPEngine) then FDSPEngine.SetShift(Offset);
HalfSpan := FSpanHz / 2;
case FMode of
0: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
1: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
MODE_LSB: begin FiltLo := Offset - FFilterBW; FiltHi := Offset - 100; end;
MODE_USB: begin FiltLo := Offset + 100; FiltHi := Offset + FFilterBW; end;
else begin FiltLo := Offset - FFilterBW/2; FiltHi := Offset + FFilterBW/2; end;
end;
if (FiltHi > HalfSpan) or (FiltLo < -HalfSpan) then
@@ -3228,19 +3258,20 @@ end;
procedure TRadioController.SetMode(M: Integer);
begin
M := EnsureRange(M, MODE_MIN, MODE_MAX);
FMode := M;
// DMR channels use a 12.5 kHz raster by default. Keep the ordinary STEP
// controls enabled so the operator can select another raster afterwards.
if FMode = MODE_DMR then
if FMode in [MODE_DMR, MODE_FMRAW] then
begin
FFMStepOn := True;
FFMStepIdx := 1; // 12.5 kHz
if FMode = MODE_DMR then FFMStepIdx := 1; // 12.5 kHz
Changed(rfFMStep);
Changed(rfFMStepIdx);
end;
// Entering receive-only DMR on the selected main TX source immediately
// releases an existing carrier. SetTune(False) also releases MOX.
if ActiveTXMode = MODE_DMR then
if ActiveTXMode in [MODE_DMR, MODE_FMRAW] then
begin
if FTuning then SetTune(False)
else if FTransmitting then SetMOX(False);
@@ -3266,6 +3297,7 @@ begin
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
MODE_DMR: Idx := 0;
MODE_FMRAW: Idx := EnsureRange(Idx, 0, FILT_RAW_COUNT - 1);
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
end;
FFilter := Idx;
@@ -3276,6 +3308,7 @@ begin
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
MODE_DMR: FFilterBW := FILT_DMR_BW;
MODE_FMRAW: FFilterBW := FILT_RAW_BW[Idx];
MODE_FM:
begin
FFilterBW := FILT_FM_BW[Idx];
@@ -3309,6 +3342,7 @@ begin
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;
MODE_FMRAW: for i := 0 to FILT_RAW_COUNT-1 do BW_arr[i] := FILT_RAW_BW[i];
else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_BW[i];
end;
best := 0; dist := MaxInt;
@@ -3325,6 +3359,15 @@ begin
Changed(rfFilter);
end;
procedure TRadioController.SetRawFMDeviation(Hz: Integer);
begin
FRawFMDeviation := EnsureRange(Hz, RAW_FM_DEV_MIN, RAW_FM_DEV_MAX);
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetRawFMDeviation(FRawFMDeviation);
SaveGlobalSettings;
Changed(rfFMDeviation);
end;
// UI-индекс 0..4 (FAST/MED/SLOW/LONG/OFF) -> WDSP enum. Единый маппинг для всех
// фронтендов и слайсов (раньше web/слайдер ошибочно кастовали индекс прямо в enum).
class function TRadioController.AGCModeFromUI(M: Integer): TWDSPAGCMode;
@@ -4132,7 +4175,7 @@ var
begin
// DMR support is receive-only. This guard also covers CAT, web and hardware
// PTT. A selected non-DMR TX slice remains valid while main A receives DMR.
if On_ and (ActiveTXMode = MODE_DMR) then Exit;
if On_ and (ActiveTXMode in [MODE_DMR, MODE_FMRAW]) then Exit;
// Safety: блокируем TX на бэндах с DoNotTx (Alex) и на RX-only трансвертере.
if On_ and FDevConnected and (FCurrentXvtr < 0)
and FAlexSettings.DoNotTx[EnsureRange(TxBandIdx, 0, 10)] then
@@ -4250,7 +4293,7 @@ end;
procedure TRadioController.SetTune(On_: Boolean);
begin
if On_ = FTuning then Exit;
if On_ and (ActiveTXMode = MODE_DMR) then Exit;
if On_ and (ActiveTXMode in [MODE_DMR, MODE_FMRAW]) then Exit;
// Safety: те же блокировки, что и в SetMOX — TUN это TX через WDSP PostGen-тон,
// он тоже должен молчать на бэндах с DoNotTx и на RX-only XVTR.
if On_ and FDevConnected and (FCurrentXvtr < 0)
@@ -4364,6 +4407,9 @@ begin
FTXSettings.PSPin, FTXSettings.PSMap,
FTXSettings.PSStabilize, Ints, Spi,
FTXSettings.PSHWPeak);
// Правка частот двухтональника при активном 2TON — применяем живьём.
if FTwoTone then
FDSPEngine.SetTXTwoTone(True, FTXSettings.PSTTFreq1, FTXSettings.PSTTFreq2);
end;
function TRadioController.PSFeedbackLevel: Integer;
@@ -4541,7 +4587,7 @@ procedure TRadioController.SetTwoTone(On_: Boolean);
// Drive; тона PSTTFreq1/2 (700/1900 по умолчанию).
begin
if On_ = FTwoTone then Exit;
if On_ and (ActiveTXMode = MODE_DMR) then Exit;
if On_ and (ActiveTXMode in [MODE_DMR, MODE_FMRAW]) then Exit;
if On_ and FTuning then Exit; // не поверх TUN-тона
// Safety: те же блокировки, что SetMOX/SetTune.
if On_ and FDevConnected and (FCurrentXvtr < 0)
@@ -4952,6 +4998,10 @@ begin
FDitherEnabled := FLoadedGlobal.DitherEnabled;
FRandomEnabled := FLoadedGlobal.RandomEnabled;
FSendAudioToRadio := FLoadedGlobal.SendAudioToRadio;
FRawFMDeviation := EnsureRange(FLoadedGlobal.RawFMDeviation,
RAW_FM_DEV_MIN, RAW_FM_DEV_MAX);
if FWDSPReady and Assigned(FDSPEngine) then
FDSPEngine.SetRawFMDeviation(FRawFMDeviation);
FWfManualHigh := FLoadedGlobal.WfManualHigh;
FWfManualLow := FLoadedGlobal.WfManualLow;
FWfAGCOffset := FLoadedGlobal.WfAGCOffset;