feat(dsp): режимы WFM, DIGI-U и DIGI-L

WFM использует новые wfmd/wfmmod из нашего форка WDSP. Демодулятор —
квадратурный дискриминатор на dsp_rate, поэтому при штатных 48 кГц
девиация 75 кГц не помещается в полосу. В WFM цепь RXA поднимается до
WFMDSPRate (наибольший FAudioRate*2^k <= 192 кГц, делящий SampleRate
нацело), rsmpin децимирует вход сам. Выше 192 кГц смысла нет: энергия
демодулированного тона выходит на полку (33.5k при 192 кГц против 31.8k
при 576 кГц), а FFT дорожает.

Две ловушки WDSP, найденные по дороге:

  * firmin.c: nfor = nc / size, idxmask = nfor - 1. Все fircore создаются
    с nc = max(2048, dsp_size), поэтому смена dsp_size без RXASetNC даёт
    nfor = 0 — пустой массив планов и падение в fftw_execute.

  * fir.c/bandpass.c планируют с FFTW_PATIENT, а wisdom.c пишет в
    wdspWisdom00 только степени двойки. При SampleRate 576 кГц наивный
    dsp_size = in_size = 12288 требовал FFT на 24576 точек — FFTW мерил
    план 25 секунд. Отсюда потолок 192 кГц и dsp_size = степень двойки.

RXA и TXA гейтятся раздельно: рейты TXA (48 кГц вход, 192 кГц выход) от
SampleRate не зависят, поэтому wfmmod уходит на широкий dsp_rate даже
когда приёмник остался на 48 кГц.

Заодно исправлен WDSP_SAM: было 12, а RXA_SAM = 10. Раньше 12 было вне
диапазона и WDSP молча игнорировал (SAM работал как DSB); после появления
RXA_WFM = 12 это включало бы WFM-демодулятор вместо SAM. txaMode
расходится с rxaMode после DRM (WFM: RXA 12, TXA 14) — отсюда ModeToWDSPTX.

DIGI-U/DIGI-L получили свою таблицу фильтров (3.0k…300) и полосу от нуля,
а не от среза на 100 Гц, как у SSB. В CAT они замаплены на коды Kenwood 6
(FSK) и 9 (FSK-R) — раньше эти коды сваливались в USB/LSB.

Новые индексы дописаны в конец (DIGU=8, DIGL=9, WFM=10): индекс режима
уходит в settings.json, band-cache, CAT и web-API. В VfoOverlay пришлось
развести хит-коды — режимы кодировались как -(I+1) и налезли на HIT_NR/
HIT_NB/HIT_SNB, а полосы кодировались значением BW, и WFM-овские
120000/160000/200000 налезли на HIT_AGC_PICK_BASE/HIT_DEV_BASE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 16:59:27 +03:00
co-authored by Claude Opus 4.8
parent 388687d684
commit ba2a75bb04
11 changed files with 532 additions and 160 deletions
+79 -24
View File
@@ -64,7 +64,7 @@ const
WDSP_WISDOM_FILE = 'wdspWisdom00';
BAND_COUNT = 11;
MODE_COUNT = 8;
MODE_COUNT = 11;
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
// UI-подписи кнопок (NAMES).
@@ -76,7 +76,10 @@ const
('16k','12k','10k','8.0k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k');
FILT_AM_NAMES: array[0..FILT_COUNT-1] of string =
('20k','18k','16k','12k','10k','9.0k','8.0k','7.0k','6.0k','5.0k');
FILT_DIGI_NAMES: array[0..FILT_COUNT-1] of string =
('3.0k','2.7k','2.4k','2.1k','1.8k','1.5k','1.2k','900','600','300');
FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM');
FILT_WFM_NAMES: array[0..FILT_WFM_COUNT-1] of string = ('200k', '160k', '120k');
// CTCSS tones (38 standard tones, no None — toggle via CTCSS button)
// CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController).
@@ -97,7 +100,8 @@ const
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
MODE_NAMES: array[0..MODE_COUNT-1] of string = (
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
MODE_ROWS = 3; // 4 колонки; последний ряд неполный
SMETER_WIDTH_RATIO = 0.32;
SMETER_MARGIN = 3;
@@ -1551,6 +1555,8 @@ const
BTN_H = 26;
BTN_SM = 24;
LEFT_W = 232;
// Заголовок (16) + MODE_ROWS рядов с шагом 27 + нижний отступ.
MODE_PANEL_H = 16 + (MODE_ROWS - 1) * 27 + BTN_SM + 7;
PLUTO_AGC_H = 64; // высота отдельного Pluto RF-AGC блока (под AGC)
TOP_VFO_W = 244;
TOP_VFO_FREQ_H = 47;
@@ -2005,10 +2011,10 @@ begin
Inc(Y, 76);
// Mode selector
// Mode selector — MODE_ROWS рядов по 4 кнопки (последний неполный).
PanelMode := TPanel.Create(Self);
PanelMode.Parent := PanelLeft;
PanelMode.SetBounds(0, Y, LEFT_W, 74);
PanelMode.SetBounds(0, Y, LEFT_W, MODE_PANEL_H);
PanelMode.BevelOuter := bvNone;
MakeLbl(PanelMode, 'MODE', 4, 2);
for i := 0 to MODE_COUNT - 1 do
@@ -2023,7 +2029,7 @@ begin
StyleButton(B, i = FController.FMode);
end;
Inc(Y, 76);
Inc(Y, MODE_PANEL_H + 2);
// Filter selector (10 кнопок в 2 ряда по 5)
PanelFilter := TPanel.Create(Self);
@@ -4057,6 +4063,28 @@ begin
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False;
if PanelFMStep <> nil then begin CloseFMStepPopup; PanelFMStep.Visible := False; end;
if PanelFMRpt <> nil then PanelFMRpt.Visible := False;
if FController.FMode = MODE_WFM then
begin
// WFM: полосы вещательного канала — три кнопки в один ряд.
CloseCTCSSPopup;
if FController.FFilter > FILT_WFM_COUNT - 1 then FController.FFilter := FILT_WFM_DEF;
FController.FFilterBW := FILT_WFM_BW[FController.FFilter];
for i := 0 to FILT_WFM_COUNT - 1 do
begin
BtnFilter[i].Caption := FILT_WFM_NAMES[i];
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, FILT_WFM_COUNT, i);
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, FILT_WFM_COUNT, i);
BtnFilter[i].Top := 16;
BtnFilter[i].Visible := True;
StyleButton(BtnFilter[i], i = FController.FFilter);
end;
for i := FILT_WFM_COUNT to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
RelayoutBelowBands;
Exit;
end;
for i := 0 to FILT_COUNT - 1 do
begin
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, 5, i mod 5);
@@ -4066,15 +4094,23 @@ begin
end;
case FController.FMode of
0, 1: begin
MODE_LSB, MODE_USB:
begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i];
DefIdx := FILT_SSB_DEF;
end;
2: begin
MODE_DIGU, MODE_DIGL:
begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DIGI_NAMES[i];
DefIdx := FILT_DIGI_DEF;
end;
MODE_DSB:
begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DSB_NAMES[i];
DefIdx := FILT_DSB_DEF;
end;
3, 4: begin
MODE_CWL, MODE_CWU:
begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i];
DefIdx := FILT_CW_DEF;
end;
@@ -4086,9 +4122,14 @@ begin
FController.FFilter := DefIdx;
case FController.FMode of
0, 1: FController.FFilterBW := FILT_SSB_BW[DefIdx];
2: FController.FFilterBW := FILT_DSB_BW[DefIdx];
3, 4: FController.FFilterBW := FILT_CW_BW[DefIdx];
MODE_LSB, MODE_USB:
FController.FFilterBW := FILT_SSB_BW[DefIdx];
MODE_DIGU, MODE_DIGL:
FController.FFilterBW := FILT_DIGI_BW[DefIdx];
MODE_DSB:
FController.FFilterBW := FILT_DSB_BW[DefIdx];
MODE_CWL, MODE_CWU:
FController.FFilterBW := FILT_CW_BW[DefIdx];
else FController.FFilterBW := FILT_AM_BW[DefIdx];
end;
@@ -5190,8 +5231,10 @@ begin
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
begin
case FController.FMode of
MODE_LSB: begin LoHz := -FController.FFilterBW; HiHz := -100; end;
MODE_USB: begin LoHz := 100; HiHz := FController.FFilterBW; end;
MODE_LSB: begin LoHz := -FController.FFilterBW; HiHz := -100; end;
MODE_USB: begin LoHz := 100; HiHz := FController.FFilterBW; end;
MODE_DIGL: begin LoHz := -FController.FFilterBW; HiHz := 0; end;
MODE_DIGU: begin LoHz := 0; HiHz := FController.FFilterBW; end;
else begin LoHz := -FController.FFilterBW / 2; HiHz := FController.FFilterBW / 2; end;
end;
Ovls[n] := FVfoOverlay;
@@ -5275,15 +5318,27 @@ begin
end;
FController.FMode := Mode;
FController.FFilterBW := FilterBW;
if Mode = MODE_FM then
begin
FController.FFilter := FILT_FM_DEF;
for Idx := 0 to FILT_FM_COUNT - 1 do
if FILT_FM_BW[Idx] = FilterBW then
case Mode of
MODE_FM:
begin
FController.FFilter := Idx;
FController.FFMDeviation := FILT_FM_DEV[Idx];
Break;
FController.FFilter := FILT_FM_DEF;
for Idx := 0 to FILT_FM_COUNT - 1 do
if FILT_FM_BW[Idx] = FilterBW then
begin
FController.FFilter := Idx;
FController.FFMDeviation := FILT_FM_DEV[Idx];
Break;
end;
end;
MODE_WFM:
begin
FController.FFilter := FILT_WFM_DEF;
for Idx := 0 to FILT_WFM_COUNT - 1 do
if FILT_WFM_BW[Idx] = FilterBW then
begin
FController.FFilter := Idx;
Break;
end;
end;
end;
if FController.FWDSPReady then
@@ -5576,9 +5631,9 @@ begin
O := FindSliceFlag(SliceId);
if (O = nil) or not FController.GetSlice(SliceId, S) then Exit;
case S.Mode of
MODE_LSB: BW := -S.FilterLo;
MODE_USB: BW := S.FilterHi;
else BW := S.FilterHi - S.FilterLo;
MODE_LSB, MODE_DIGL: BW := -S.FilterLo;
MODE_USB, MODE_DIGU: BW := S.FilterHi;
else BW := S.FilterHi - S.FilterLo;
end;
O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId));
O.SetDSPState(0, 0, False, False, FController.FAGCMode);