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
+16 -8
View File
@@ -31,10 +31,12 @@ const
CAT_EXEC_ERROR = 'E;'; CAT_EXEC_ERROR = 'E;';
CAT_ID_NUM = '019'; // TS-2000 compatible CAT_ID_NUM = '019'; // TS-2000 compatible
// Mode mapping ewsdr → CAT // Mode mapping ewsdr → CAT (зеркало MODE_* из WDSPEngine)
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;
CAT_MODE_COUNT = 11;
// FM repeater offset directions — зеркало FMRepeater.RPT_* (значения должны // FM repeater offset directions — зеркало FMRepeater.RPT_* (значения должны
// совпадать: контроллер хранит FFMRptDir в этих же кодах). // совпадать: контроллер хранит FFMRptDir в этих же кодах).
@@ -388,9 +390,12 @@ begin
MODE_DSB: Result := 2; // treated as USB for CAT compatibility MODE_DSB: Result := 2; // treated as USB for CAT compatibility
MODE_CWU: Result := 3; MODE_CWU: Result := 3;
MODE_FM: Result := 4; MODE_FM: Result := 4;
MODE_WFM: Result := 4; // treated as 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_CWL: Result := 7; MODE_CWL: Result := 7;
MODE_DIGL: Result := 9; // Kenwood FSK-R
else Result := 2; else Result := 2;
end; end;
end; end;
@@ -403,9 +408,9 @@ begin
3: Result := MODE_CWU; 3: Result := MODE_CWU;
4: Result := MODE_FM; 4: Result := MODE_FM;
5: Result := MODE_AM; 5: Result := MODE_AM;
6: Result := MODE_USB; // FSK/RTTY → USB 6: Result := MODE_DIGU; // FSK/RTTY
7: Result := MODE_CWL; 7: Result := MODE_CWL;
9: Result := MODE_LSB; // FSK-R → LSB 9: Result := MODE_DIGL; // FSK-R
else Result := MODE_USB; else Result := MODE_USB;
end; end;
end; end;
@@ -1787,17 +1792,20 @@ 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 '; end; begin Result := 'LSB USB DSB CWL CWU FM AM SAM DIGUDIGLWFM '; 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.
// Индекс режима теперь двузначный (0..10), но односимвольный запрос
// продолжаем принимать — старые клиенты шлют одну цифру.
var k: Integer; var k: Integer;
names: array[0..7] of string; names: array[0..CAT_MODE_COUNT-1] of string;
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';
if Length(s) = 1 then begin names[8]:='DIGU'; names[9]:='DIGL'; names[10]:='WFM';
k := EnsureRange(StrToIntDef(s,0),0,7); if (Length(s) = 1) or (Length(s) = 2) then begin
k := EnsureRange(StrToIntDef(s,0),0,CAT_MODE_COUNT-1);
Result := names[k]; Result := names[k];
end else Result := CAT_ERROR; end else Result := CAT_ERROR;
end; end;
+2 -2
View File
@@ -18,9 +18,9 @@ uses
const const
// Копии из MainForm: нужны для выпадающих списков // Копии из MainForm: нужны для выпадающих списков
CH_MODE_COUNT = 8; CH_MODE_COUNT = 11;
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'); ('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
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 = (
+69 -14
View File
@@ -64,7 +64,7 @@ const
WDSP_WISDOM_FILE = 'wdspWisdom00'; WDSP_WISDOM_FILE = 'wdspWisdom00';
BAND_COUNT = 11; BAND_COUNT = 11;
MODE_COUNT = 8; MODE_COUNT = 11;
// Полосы/дефолты фильтров (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).
@@ -76,7 +76,10 @@ const
('16k','12k','10k','8.0k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k'); ('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 = 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'); ('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_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 tones (38 standard tones, no None — toggle via CTCSS button)
// CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController). // CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController).
@@ -97,7 +100,8 @@ 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'); 'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
MODE_ROWS = 3; // 4 колонки; последний ряд неполный
SMETER_WIDTH_RATIO = 0.32; SMETER_WIDTH_RATIO = 0.32;
SMETER_MARGIN = 3; SMETER_MARGIN = 3;
@@ -1551,6 +1555,8 @@ const
BTN_H = 26; BTN_H = 26;
BTN_SM = 24; BTN_SM = 24;
LEFT_W = 232; 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) PLUTO_AGC_H = 64; // высота отдельного Pluto RF-AGC блока (под AGC)
TOP_VFO_W = 244; TOP_VFO_W = 244;
TOP_VFO_FREQ_H = 47; TOP_VFO_FREQ_H = 47;
@@ -2005,10 +2011,10 @@ begin
Inc(Y, 76); Inc(Y, 76);
// Mode selector // Mode selector — MODE_ROWS рядов по 4 кнопки (последний неполный).
PanelMode := TPanel.Create(Self); PanelMode := TPanel.Create(Self);
PanelMode.Parent := PanelLeft; PanelMode.Parent := PanelLeft;
PanelMode.SetBounds(0, Y, LEFT_W, 74); PanelMode.SetBounds(0, Y, LEFT_W, MODE_PANEL_H);
PanelMode.BevelOuter := bvNone; PanelMode.BevelOuter := bvNone;
MakeLbl(PanelMode, 'MODE', 4, 2); MakeLbl(PanelMode, 'MODE', 4, 2);
for i := 0 to MODE_COUNT - 1 do for i := 0 to MODE_COUNT - 1 do
@@ -2023,7 +2029,7 @@ begin
StyleButton(B, i = FController.FMode); StyleButton(B, i = FController.FMode);
end; end;
Inc(Y, 76); Inc(Y, MODE_PANEL_H + 2);
// Filter selector (10 кнопок в 2 ряда по 5) // Filter selector (10 кнопок в 2 ряда по 5)
PanelFilter := TPanel.Create(Self); PanelFilter := TPanel.Create(Self);
@@ -4057,6 +4063,28 @@ begin
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False; if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False;
if PanelFMStep <> nil then begin CloseFMStepPopup; PanelFMStep.Visible := False; end; if PanelFMStep <> nil then begin CloseFMStepPopup; PanelFMStep.Visible := False; end;
if PanelFMRpt <> nil then PanelFMRpt.Visible := False; 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 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);
@@ -4066,15 +4094,23 @@ begin
end; end;
case FController.FMode of 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]; for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i];
DefIdx := FILT_SSB_DEF; DefIdx := FILT_SSB_DEF;
end; 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]; for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DSB_NAMES[i];
DefIdx := FILT_DSB_DEF; DefIdx := FILT_DSB_DEF;
end; end;
3, 4: begin MODE_CWL, MODE_CWU:
begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i]; for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i];
DefIdx := FILT_CW_DEF; DefIdx := FILT_CW_DEF;
end; end;
@@ -4086,9 +4122,14 @@ begin
FController.FFilter := DefIdx; FController.FFilter := DefIdx;
case FController.FMode of case FController.FMode of
0, 1: FController.FFilterBW := FILT_SSB_BW[DefIdx]; MODE_LSB, MODE_USB:
2: FController.FFilterBW := FILT_DSB_BW[DefIdx]; FController.FFilterBW := FILT_SSB_BW[DefIdx];
3, 4: FController.FFilterBW := FILT_CW_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]; else FController.FFilterBW := FILT_AM_BW[DefIdx];
end; end;
@@ -5192,6 +5233,8 @@ begin
case FController.FMode of case FController.FMode of
MODE_LSB: begin LoHz := -FController.FFilterBW; HiHz := -100; end; MODE_LSB: begin LoHz := -FController.FFilterBW; HiHz := -100; end;
MODE_USB: begin LoHz := 100; HiHz := FController.FFilterBW; 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; else begin LoHz := -FController.FFilterBW / 2; HiHz := FController.FFilterBW / 2; end;
end; end;
Ovls[n] := FVfoOverlay; Ovls[n] := FVfoOverlay;
@@ -5275,7 +5318,8 @@ begin
end; end;
FController.FMode := Mode; FController.FMode := Mode;
FController.FFilterBW := FilterBW; FController.FFilterBW := FilterBW;
if Mode = MODE_FM then case Mode of
MODE_FM:
begin begin
FController.FFilter := FILT_FM_DEF; FController.FFilter := FILT_FM_DEF;
for Idx := 0 to FILT_FM_COUNT - 1 do for Idx := 0 to FILT_FM_COUNT - 1 do
@@ -5286,6 +5330,17 @@ begin
Break; Break;
end; end;
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 if FController.FWDSPReady then
begin begin
FController.FDSPEngine.SetMode(FController.FMode); FController.FDSPEngine.SetMode(FController.FMode);
@@ -5576,8 +5631,8 @@ begin
O := FindSliceFlag(SliceId); O := FindSliceFlag(SliceId);
if (O = nil) or not FController.GetSlice(SliceId, S) then Exit; if (O = nil) or not FController.GetSlice(SliceId, S) then Exit;
case S.Mode of case S.Mode of
MODE_LSB: BW := -S.FilterLo; MODE_LSB, MODE_DIGL: BW := -S.FilterLo;
MODE_USB: BW := S.FilterHi; MODE_USB, MODE_DIGU: BW := S.FilterHi;
else BW := S.FilterHi - S.FilterLo; else BW := S.FilterHi - S.FilterLo;
end; end;
O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId)); O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId));
+47 -18
View File
@@ -1692,9 +1692,13 @@ var Half: Integer;
begin begin
Half := BW div 2; Half := BW div 2;
case Mode of case Mode of
0: begin Lo := -BW; Hi := -100; end; // LSB MODE_LSB: begin Lo := -BW; Hi := -100; end;
1: begin Lo := 100; Hi := BW; end; // USB MODE_USB: begin Lo := 100; Hi := BW; end;
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM // DIGI: полоса от нуля — поднесущие цифровых видов начинаются низко,
// срез на 100 Гц как в SSB съел бы их.
MODE_DIGL: begin Lo := -BW; Hi := 0; end;
MODE_DIGU: begin Lo := 0; Hi := BW; end;
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM/WFM
end; end;
end; end;
@@ -1739,9 +1743,19 @@ begin
FFilterBW := FILT_FM_BW[FFilter]; FFilterBW := FILT_FM_BW[FFilter];
FFMDeviation := FILT_FM_DEV[FFilter]; FFMDeviation := FILT_FM_DEV[FFilter];
end; end;
0, 1: begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end; MODE_WFM:
2: begin FFilter := FILT_DSB_DEF; FFilterBW := FILT_DSB_BW[FFilter]; end; begin
3, 4: begin FFilter := FILT_CW_DEF; FFilterBW := FILT_CW_BW[FFilter]; end; if FFilter > FILT_WFM_COUNT - 1 then FFilter := FILT_WFM_DEF;
FFilterBW := FILT_WFM_BW[FFilter];
end;
MODE_LSB, MODE_USB:
begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
MODE_DIGU, MODE_DIGL:
begin FFilter := FILT_DIGI_DEF; FFilterBW := FILT_DIGI_BW[FFilter]; end;
MODE_DSB:
begin FFilter := FILT_DSB_DEF; FFilterBW := FILT_DSB_BW[FFilter]; end;
MODE_CWL, MODE_CWU:
begin FFilter := FILT_CW_DEF; FFilterBW := FILT_CW_BW[FFilter]; end;
else begin FFilter := FILT_AM_DEF; FFilterBW := FILT_AM_BW[FFilter]; end; else begin FFilter := FILT_AM_DEF; FFilterBW := FILT_AM_BW[FFilter]; end;
end; end;
end; end;
@@ -1751,10 +1765,15 @@ function TRadioController.FilterBWFor(M, Idx: Integer): Integer;
// но НЕ меняет индекс (в отличие от ApplyModeDefaults) — нужно для XVTR-restore. // но НЕ меняет индекс (в отличие от ApplyModeDefaults) — нужно для XVTR-restore.
begin begin
case M of case M of
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, 1)]; MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, FILT_FM_COUNT - 1)];
0, 1: Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)]; MODE_WFM: Result := FILT_WFM_BW[EnsureRange(Idx, 0, FILT_WFM_COUNT - 1)];
2: Result := FILT_DSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)]; MODE_LSB, MODE_USB:
3, 4: Result := FILT_CW_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)]; Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
MODE_DIGU, MODE_DIGL:
Result := FILT_DIGI_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
MODE_DSB: Result := FILT_DSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
MODE_CWL, MODE_CWU:
Result := FILT_CW_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
else Result := FILT_AM_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)]; else Result := FILT_AM_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
end; end;
end; end;
@@ -2382,14 +2401,19 @@ end;
procedure TRadioController.SetFilterIdx(Idx: Integer); procedure TRadioController.SetFilterIdx(Idx: Integer);
begin begin
// Клампим индекс под текущий режим (как FilterBWFor) — публичная команда, // Клампим индекс под текущий режим (как FilterBWFor) — публичная команда,
// Idx может прийти из CAT/web без проверки; FM-таблицы всего на 2 элемента. // Idx может прийти из CAT/web без проверки; FM/WFM-таблицы короче остальных.
if FMode = MODE_FM then Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1) case FMode of
MODE_FM: Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1);
MODE_WFM: Idx := EnsureRange(Idx, 0, FILT_WFM_COUNT - 1);
else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1); else Idx := EnsureRange(Idx, 0, FILT_COUNT - 1);
end;
FFilter := Idx; FFilter := Idx;
case FMode of case FMode of
0, 1: FFilterBW := FILT_SSB_BW[Idx]; MODE_LSB, MODE_USB: FFilterBW := FILT_SSB_BW[Idx];
2: FFilterBW := FILT_DSB_BW[Idx]; MODE_DIGU, MODE_DIGL: FFilterBW := FILT_DIGI_BW[Idx];
3, 4: FFilterBW := FILT_CW_BW[Idx]; MODE_DSB: FFilterBW := FILT_DSB_BW[Idx];
MODE_CWL, MODE_CWU: FFilterBW := FILT_CW_BW[Idx];
MODE_WFM: FFilterBW := FILT_WFM_BW[Idx];
MODE_FM: MODE_FM:
begin begin
FFilterBW := FILT_FM_BW[Idx]; FFilterBW := FILT_FM_BW[Idx];
@@ -2412,10 +2436,15 @@ begin
FFilterBW := BWHz; FFilterBW := BWHz;
for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt; for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt;
case FMode of case FMode of
0, 1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i]; MODE_LSB, MODE_USB:
2: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i]; for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
3, 4: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i]; MODE_DIGU, MODE_DIGL:
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DIGI_BW[i];
MODE_DSB: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i];
MODE_CWL, MODE_CWU:
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];
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;
+10
View File
@@ -42,10 +42,20 @@ const
FILT_AM_BW: array[0..FILT_COUNT-1] of Integer = FILT_AM_BW: array[0..FILT_COUNT-1] of Integer =
(20000, 18000, 16000, 12000, 10000, 9000, 8000, 7000, 6000, 5000); (20000, 18000, 16000, 12000, 10000, 9000, 8000, 7000, 6000, 5000);
FILT_AM_DEF = 4; // 10k FILT_AM_DEF = 4; // 10k
// DIGI: полоса считается от нуля (кромки 0..BW), поэтому таблица своя —
// SSB-полосы отмерены от среза на 100 Гц и здесь не подходят.
FILT_DIGI_BW: array[0..FILT_COUNT-1] of Integer =
(3000, 2700, 2400, 2100, 1800, 1500, 1200, 900, 600, 300);
FILT_DIGI_DEF = 0; // 3.0k
FILT_FM_COUNT = 2; FILT_FM_COUNT = 2;
FILT_FM_BW: array[0..1] of Integer = (11000, 16000); FILT_FM_BW: array[0..1] of Integer = (11000, 16000);
FILT_FM_DEV: array[0..1] of Double = (2500.0, 5000.0); FILT_FM_DEV: array[0..1] of Double = (2500.0, 5000.0);
FILT_FM_DEF = 0; // NFM FILT_FM_DEF = 0; // NFM
// WFM: вещательный канал. 200 кГц требует SampleRate 384k — при 192k
// WDSPEngine.SetFilter сам зажмёт кромки по Найквисту dsp_rate.
FILT_WFM_COUNT = 3;
FILT_WFM_BW: array[0..2] of Integer = (200000, 160000, 120000);
FILT_WFM_DEF = 1; // 160k — влезает и в 192k
type type
TBandSettings = record TBandSettings = record
+4 -2
View File
@@ -1088,8 +1088,10 @@ begin
Half := BW / 2; Half := BW / 2;
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W); VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
case Mode of case Mode of
0: begin Lo_Hz := -BW; Hi_Hz := -100; end; 0: begin Lo_Hz := -BW; Hi_Hz := -100; end; // LSB
1: begin Lo_Hz := 100; Hi_Hz := BW; end; 1: begin Lo_Hz := 100; Hi_Hz := BW; end; // USB
9: begin Lo_Hz := -BW; Hi_Hz := 0; end; // DIGL — полоса от нуля
8: begin Lo_Hz := 0; Hi_Hz := BW; end; // DIGU
else begin Lo_Hz := -Half; Hi_Hz := Half; end; else begin Lo_Hz := -Half; Hi_Hz := Half; end;
end; end;
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W))); X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
+44 -23
View File
@@ -23,6 +23,13 @@ const
VFO_OVERLAY_ALPHA = 210; VFO_OVERLAY_ALPHA = 210;
OVL_W = 280; // ширина карточки OVL_W = 280; // ширина карточки
// Индексы совпадают с MODE_* контроллера (WDSPEngine.pas).
OVL_MODE_COUNT = 11;
OVL_MODE_COLS = 4;
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 = (
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
// Палитра слайсов (TColor = $00BBGGRR): одна на букву — общий цвет для // Палитра слайсов (TColor = $00BBGGRR): одна на букву — общий цвет для
// бейджа-карточки во флаге и для буквы на полосе фильтра в спектре. // бейджа-карточки во флаге и для буквы на полосе фильтра в спектре.
SLICE_COLORS: array[0..7] of TColor = ( SLICE_COLORS: array[0..7] of TColor = (
@@ -54,7 +61,7 @@ type
FFilterBW: Integer; FFilterBW: Integer;
FVfoHz: Double; FVfoHz: Double;
FSMeterDB: Double; FSMeterDB: Double;
FModeFilter: array[0..7] of Integer; FModeFilter: array[0..OVL_MODE_COUNT-1] of Integer;
FNRMode: Integer; // 0=off, 1..4 FNRMode: Integer; // 0=off, 1..4
FNBMode: Integer; // 0=off, 1=NB, 2=NB2 FNBMode: Integer; // 0=off, 1=NB, 2=NB2
@@ -220,8 +227,9 @@ const
KEY_COLOR = TColor($00FF00FF); KEY_COLOR = TColor($00FF00FF);
// Значения хит-тестов // Значения хит-тестов
// режимы: -(I+1) = -1..-8 // режимы: HIT_MODE_BASE-I
// фильтры: BW в Гц (>0) // фильтры: HIT_FILT_BASE+I (индекс, не BW: полосы WFM доходят до 200000
// и налезали бы на HIT_AGC_PICK_BASE/HIT_DEV_BASE)
// AGC: HIT_AGC_PICK_BASE+I // AGC: HIT_AGC_PICK_BASE+I
// устройства: HIT_DEV_BASE+I // устройства: HIT_DEV_BASE+I
HIT_NR = -9; HIT_NR = -9;
@@ -243,6 +251,9 @@ const
HIT_SQL_SLIDER = -32; // трек порога FM-squelch HIT_SQL_SLIDER = -32; // трек порога FM-squelch
HIT_AGC_PICK_BASE = 100000; HIT_AGC_PICK_BASE = 100000;
HIT_DEV_BASE = 200000; HIT_DEV_BASE = 200000;
// Свои диапазоны, чтобы не пересекаться с тумблерами (-9..-32) и с базами выше.
HIT_MODE_BASE = -1000; // -1000 .. -1000-(OVL_MODE_COUNT-1)
HIT_FILT_BASE = 1000; // 1000 + индекс кнопки в ряду фильтров
AGC_NAMES: array[0..4] of string = ('FAST', 'MED', 'SLOW', 'LONG', 'OFF'); AGC_NAMES: array[0..4] of string = ('FAST', 'MED', 'SLOW', 'LONG', 'OFF');
@@ -268,10 +279,6 @@ const
CLR_SPLIT_ON = TColor($0020C0FF); // оранжевый SPLIT CLR_SPLIT_ON = TColor($0020C0FF); // оранжевый SPLIT
CLR_BADGE_TXT = TColor($00181818); // тёмный текст поверх ярких бейджей CLR_BADGE_TXT = TColor($00181818); // тёмный текст поверх ярких бейджей
OVL_MODE_COUNT = 8;
OVL_MODE_NAMES: array[0..7] of string = (
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
SSB_BW: array[0..5] of Integer = (1800,2100,2400,2700,3300,3800); SSB_BW: array[0..5] of Integer = (1800,2100,2400,2700,3300,3800);
SSB_LBL: array[0..5] of string = ('1.8K','2.1K','2.4K','2.7K','3.3K','3.8K'); SSB_LBL: array[0..5] of string = ('1.8K','2.1K','2.4K','2.7K','3.3K','3.8K');
DSB_BW: array[0..5] of Integer = (16000,12000,10000,8000,5200,4000); DSB_BW: array[0..5] of Integer = (16000,12000,10000,8000,5200,4000);
@@ -282,6 +289,10 @@ const
FM_LBL: array[0..1] of string = ('NFM', 'FM'); FM_LBL: array[0..1] of string = ('NFM', 'FM');
AM_BW: array[0..5] of Integer = (20000,16000,12000,10000,8000,6000); AM_BW: array[0..5] of Integer = (20000,16000,12000,10000,8000,6000);
AM_LBL: array[0..5] of string = ('20K','16K','12K','10K','8K','6K'); AM_LBL: array[0..5] of string = ('20K','16K','12K','10K','8K','6K');
WFM_BW: array[0..2] of Integer = (200000, 160000, 120000);
WFM_LBL: array[0..2] of string = ('200K', '160K', '120K');
DIGI_BW: array[0..5] of Integer = (300, 600, 1200, 1800, 2400, 3000);
DIGI_LBL: array[0..5] of string = ('300','600','1.2K','1.8K','2.4K','3.0K');
DB_S: array[1..9] of Double = (-121,-115,-109,-103,-97,-91,-85,-79,-73); DB_S: array[1..9] of Double = (-121,-115,-109,-103,-97,-91,-85,-79,-73);
@@ -379,6 +390,8 @@ begin
2: Result := DSB_BW[FilterIdx]; 2: Result := DSB_BW[FilterIdx];
3,4: Result := CW_BW[FilterIdx]; 3,4: Result := CW_BW[FilterIdx];
5: Result := FM_BW[FilterIdx]; 5: Result := FM_BW[FilterIdx];
8,9: Result := DIGI_BW[FilterIdx]; // DIGU/DIGL — полоса от нуля
10: Result := WFM_BW[FilterIdx];
else Result := AM_BW[FilterIdx]; else Result := AM_BW[FilterIdx];
end; end;
end; end;
@@ -390,13 +403,19 @@ begin
2: Result := DSB_LBL[FilterIdx]; 2: Result := DSB_LBL[FilterIdx];
3,4: Result := CW_LBL[FilterIdx]; 3,4: Result := CW_LBL[FilterIdx];
5: Result := FM_LBL[FilterIdx]; 5: Result := FM_LBL[FilterIdx];
8,9: Result := DIGI_LBL[FilterIdx];
10: Result := WFM_LBL[FilterIdx];
else Result := AM_LBL[FilterIdx]; else Result := AM_LBL[FilterIdx];
end; end;
end; end;
function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer; function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer;
begin begin
if ModeIdx = 5 then Result := 2 else Result := 6; case ModeIdx of
5: Result := 2; // FM: NFM/FM
10: Result := 3; // WFM: 200K/160K/120K
else Result := 6;
end;
end; end;
{ ---- Lifecycle ---- } { ---- Lifecycle ---- }
@@ -413,7 +432,8 @@ begin
FHotIdx := -1; FHotIdx := -1;
FModeFilter[0] := 2700; FModeFilter[1] := 2700; FModeFilter[2] := 8000; FModeFilter[0] := 2700; FModeFilter[1] := 2700; FModeFilter[2] := 8000;
FModeFilter[3] := 500; FModeFilter[4] := 500; FModeFilter[5] := 11000; FModeFilter[3] := 500; FModeFilter[4] := 500; FModeFilter[5] := 11000;
FModeFilter[6] := 8000; FModeFilter[7] := 8000; FModeFilter[6] := 8000; FModeFilter[7] := 8000; FModeFilter[8] := 3000;
FModeFilter[9] := 3000; FModeFilter[10] := 160000;
FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0; FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0;
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False; FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
FVolume := 70; FMuted := False; FSplit := False; FTx := False; FVolume := 70; FMuted := False; FSplit := False; FTx := False;
@@ -554,7 +574,7 @@ end;
function TVfoOverlay.PanelContentH: Integer; function TVfoOverlay.PanelContentH: Integer;
begin begin
case FPanel of case FPanel of
fpMode: Result := ROW_H*2 + IGAP + ROW_H + IGAP; // 2 ряда режимов + ряд фильтров fpMode: Result := OVL_MODE_ROWS*(ROW_H + IGAP) + ROW_H; // ряды режимов + ряд фильтров
fpDsp: begin fpDsp: begin
Result := ROW_H + IGAP + ROW_H; // toggles + AGC Result := ROW_H + IGAP + ROW_H; // toggles + AGC
if IsFM then Inc(Result, IGAP + ROW_H); // + ряд SQL (только FM) if IsFM then Inc(Result, IGAP + ROW_H); // + ряд SQL (только FM)
@@ -748,21 +768,21 @@ var
I, Col, Row, BX, BY, BW, FiltN, FBX, FBRight, BV: Integer; I, Col, Row, BX, BY, BW, FiltN, FBX, FBRight, BV: Integer;
HR: TRect; HR: TRect;
begin begin
BW := (RowW - 3*3) div 4; // 4 колонки, зазор 3 BW := (RowW - (OVL_MODE_COLS - 1)*3) div OVL_MODE_COLS; // зазор 3
for I := 0 to OVL_MODE_COUNT - 1 do for I := 0 to OVL_MODE_COUNT - 1 do
begin begin
Col := I mod 4; Row := I div 4; Col := I mod OVL_MODE_COLS; Row := I div OVL_MODE_COLS;
BX := X + Col * (BW + 3); BX := X + Col * (BW + 3);
BY := Y + Row * (ROW_H + IGAP); BY := Y + Row * (ROW_H + IGAP);
HR := Rect(BX, BY, BX + BW, BY + ROW_H); HR := Rect(BX, BY, BX + BW, BY + ROW_H);
DrawButton(C, HR, OVL_MODE_NAMES[I], I = FMode, DrawButton(C, HR, OVL_MODE_NAMES[I], I = FMode,
(FHotIdx >= 0) and (FHitActions[FHotIdx] = -(I+1)), 7); (FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_MODE_BASE - I), 7);
RegisterHit(HR, -(I+1)); RegisterHit(HR, HIT_MODE_BASE - I);
end; end;
// Ряд фильтров текущего режима // Ряд фильтров текущего режима
FiltN := GetFilterCountForMode(FMode); FiltN := GetFilterCountForMode(FMode);
BY := Y + 2*(ROW_H + IGAP); BY := Y + OVL_MODE_ROWS*(ROW_H + IGAP);
FBX := X; FBX := X;
for I := 0 to FiltN - 1 do for I := 0 to FiltN - 1 do
begin begin
@@ -770,8 +790,8 @@ begin
BV := GetFilterBWForMode(FMode, I); BV := GetFilterBWForMode(FMode, I);
HR := Rect(FBX, BY, FBRight - 2, BY + ROW_H); HR := Rect(FBX, BY, FBRight - 2, BY + ROW_H);
DrawButton(C, HR, GetFilterLblForMode(FMode, I), BV = FFilterBW, DrawButton(C, HR, GetFilterLblForMode(FMode, I), BV = FFilterBW,
(FHotIdx >= 0) and (FHitActions[FHotIdx] = BV), 7); (FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_FILT_BASE + I), 7);
RegisterHit(HR, BV); RegisterHit(HR, HIT_FILT_BASE + I);
FBX := FBRight; FBX := FBRight;
end; end;
end; end;
@@ -1184,25 +1204,26 @@ begin
end; end;
// Режим // Режим
if (HV < 0) and (HV >= -OVL_MODE_COUNT) then if (HV <= HIT_MODE_BASE) and (HV > HIT_MODE_BASE - OVL_MODE_COUNT) then
begin begin
NewMode := (-HV) - 1; NewMode := HIT_MODE_BASE - HV;
if NewMode <> FMode then if NewMode <> FMode then
begin begin
FModeFilter[FMode] := FFilterBW; FModeFilter[FMode] := FFilterBW;
FMode := NewMode; FMode := NewMode;
FFilterBW := FModeFilter[FMode]; FFilterBW := FModeFilter[FMode];
Height := ComputeHeight; // число фильтров могло смениться (FM=2) Height := ComputeHeight; // число фильтров могло смениться (FM=2, WFM=3)
RequestInvalidate; RequestInvalidate;
if Assigned(FOnSelect) then FOnSelect(FMode, FFilterBW); if Assigned(FOnSelect) then FOnSelect(FMode, FFilterBW);
end; end;
Exit; Exit;
end; end;
// Фильтр (BW>0) // Фильтр текущего режима (по индексу кнопки, не по BW)
if HV > 0 then if (HV >= HIT_FILT_BASE) and
(HV < HIT_FILT_BASE + GetFilterCountForMode(FMode)) then
begin begin
NewBW := HV; NewBW := GetFilterBWForMode(FMode, HV - HIT_FILT_BASE);
if NewBW <> FFilterBW then if NewBW <> FFilterBW then
begin begin
FFilterBW := NewBW; FFilterBW := NewBW;
+22
View File
@@ -505,6 +505,28 @@ procedure SetTXAFMNC(channel: Integer; nc: Integer); cdecl; external WDSP_LIB;
procedure SetTXAFMMP(channel: Integer; mp: Integer); cdecl; external WDSP_LIB; procedure SetTXAFMMP(channel: Integer; mp: Integer); cdecl; external WDSP_LIB;
procedure SetTXAFMAFFreqs(channel: Integer; low: Double; high: Double); cdecl; external WDSP_LIB; procedure SetTXAFMAFFreqs(channel: Integer; low: Double; high: Double); cdecl; external WDSP_LIB;
// ---------------------------------------------------------------------------
// wfmd.c (Wideband FM Demodulator)
// ---------------------------------------------------------------------------
procedure SetRXAWFMDeviation(channel: Integer; deviation: Double); cdecl; external WDSP_LIB;
procedure SetRXAWFMNCaud(channel: Integer; nc: Integer); cdecl; external WDSP_LIB;
procedure SetRXAWFMMPaud(channel: Integer; mp: Integer); cdecl; external WDSP_LIB;
procedure SetRXAWFMAFFilter(channel: Integer; low: Double; high: Double); cdecl; external WDSP_LIB;
procedure SetRXAWFMDeemphRun(channel: Integer; run: Integer); cdecl; external WDSP_LIB;
procedure SetRXAWFMDeemphTau(channel: Integer; tau: Double); cdecl; external WDSP_LIB;
procedure SetRXAWFMLimRun(channel: Integer; run: Integer); cdecl; external WDSP_LIB;
procedure SetRXAWFMLimGain(channel: Integer; gaindB: Double); cdecl; external WDSP_LIB;
// ---------------------------------------------------------------------------
// wfmmod.c (Wideband FM Modulator)
// ---------------------------------------------------------------------------
procedure SetTXAWFMDeviation(channel: Integer; deviation: Double); cdecl; external WDSP_LIB;
procedure SetTXAWFMNC(channel: Integer; nc: Integer); cdecl; external WDSP_LIB;
procedure SetTXAWFMMP(channel: Integer; mp: Integer); cdecl; external WDSP_LIB;
procedure SetTXAWFMAFFreqs(channel: Integer; low: Double; high: Double); cdecl; external WDSP_LIB;
procedure SetTXAWFMPreEmphRun(channel: Integer; run: Integer); cdecl; external WDSP_LIB;
procedure SetTXAWFMPreEmphTau(channel: Integer; tau: Double); cdecl; external WDSP_LIB;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// fmsq.c (FM Squelch) // fmsq.c (FM Squelch)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+236 -16
View File
@@ -29,7 +29,10 @@ uses
WDSP; WDSP;
const const
// WDSP mode integers (нет именованных констант в WDSP.pas) // WDSP mode integers (нет именованных констант в WDSP.pas).
// Значения — enum rxaMode (RXA.h) / txaMode (TXA.h). До DRM включительно
// нумерация совпадает, дальше расходится: TXA вставляет AM_LSB/AM_USB,
// поэтому WFM у RXA = 12, а у TXA = 14. Отсюда две отдельные константы.
WDSP_LSB = 0; WDSP_LSB = 0;
WDSP_USB = 1; WDSP_USB = 1;
WDSP_DSB = 2; WDSP_DSB = 2;
@@ -37,9 +40,15 @@ const
WDSP_CWU = 4; WDSP_CWU = 4;
WDSP_FM = 5; WDSP_FM = 5;
WDSP_AM = 6; WDSP_AM = 6;
WDSP_SAM = 12; WDSP_DIGU = 7;
WDSP_DIGL = 9;
WDSP_SAM = 10;
WDSP_RX_WFM = 12;
WDSP_TX_WFM = 14;
// Наши индексы режимов (совпадают с кнопками MainForm) // Наши индексы режимов (совпадают с кнопками MainForm).
// Новые режимы дописываются В КОНЕЦ: индекс уходит в settings.json,
// band-cache, CAT (ZZMD/ZZMV) и web-API.
MODE_LSB = 0; MODE_LSB = 0;
MODE_USB = 1; MODE_USB = 1;
MODE_DSB = 2; MODE_DSB = 2;
@@ -48,6 +57,27 @@ const
MODE_FM = 5; MODE_FM = 5;
MODE_AM = 6; MODE_AM = 6;
MODE_SAM = 7; MODE_SAM = 7;
MODE_DIGU = 8;
MODE_DIGL = 9;
MODE_WFM = 10;
// Минимальная длина FIR у WDSP: create_* поднимают fircore с
// nc = max(2048, dsp_size). В firmin.c nfor = nc / size, idxmask = nfor - 1,
// так что при nc < dsp_size получается nfor = 0 — пустой массив FFT-планов и
// падение в fftw_execute. Любое изменение dsp_size обязано перевыставить nc.
WDSP_MIN_NC = 2048;
// WFM: девиация и параметры де-эмфазиса вещательного FM.
WFM_DEVIATION = 75000.0;
WFM_AF_LOW = 20.0;
WFM_AF_HIGH = 15000.0;
WFM_TAU_DE_US = 50.0e-6; // Европа/Россия (Америка — 75 мкс)
// Потолок dsp_rate для WFM. Вещательному каналу (200 кГц) хватает 192 кГц,
// а гнать цепь на полном IQ-rate нельзя: при 576 кГц dsp_size стал бы 12288,
// и fircore спланировал бы FFT на 2*12288 = 24576 точек. Это не степень
// двойки — такого плана нет в wdspWisdom00, и FFTW_PATIENT меряет его ~25 с
// при первом входе в режим. rsmpin децимирует in_rate → dsp_rate сам.
WFM_DSP_RATE = 192000;
// AGC modes // AGC modes
AGC_OFF = 0; AGC_OFF = 0;
@@ -182,6 +212,10 @@ type
FAnalyzerOpen: Boolean; FAnalyzerOpen: Boolean;
FSampleRate: Integer; FSampleRate: Integer;
FAudioRate: Integer; FAudioRate: Integer;
// Текущий dsp_rate канала RXA. Обычно = FAudioRate; в WFM поднимается
// до FSampleRate, иначе девиация 75 кГц не помещается в полосу.
FRXDSPRate: Integer;
FTXDSPSize: Integer; // текущий dsp_size канала TXA (нижняя граница nc)
FBufSize: Integer; // входной буфер @ FSampleRate FBufSize: Integer; // входной буфер @ FSampleRate
FAudioBufSize: Integer; // выходной буфер @ FAudioRate (= FBufSize * FAudioRate / FSampleRate) FAudioBufSize: Integer; // выходной буфер @ FAudioRate (= FBufSize * FAudioRate / FSampleRate)
FTXSampleRate: Integer; // фиксированный rate TX/DUC (TX_SAMPLE_RATE), не меняется при ChangeSampleRate FTXSampleRate: Integer; // фиксированный rate TX/DUC (TX_SAMPLE_RATE), не меняется при ChangeSampleRate
@@ -356,7 +390,17 @@ type
FBeaconDec: TBeaconDecoder; // не владеет; тап маяка в PushIQItemToDSP FBeaconDec: TBeaconDecoder; // не владеет; тап маяка в PushIQItemToDSP
function ModeToWDSP(Mode: Integer): Integer; function ModeToWDSP(Mode: Integer): Integer;
function ModeToWDSPTX(Mode: Integer): Integer;
procedure ApplyDefaultFilter; procedure ApplyDefaultFilter;
// WFM требует dsp_rate >= 192k (девиация 75 кГц не влезает в 48 кГц).
function WantWideDSP(Mode: Integer): Boolean;
function WFMDSPRate: Integer;
function SliceDSPRate(Mode: Integer): Integer;
function ClampToNyquist(Hz, Rate: Integer): Integer;
function EffectiveTXNC: Integer;
procedure ApplyDSPRate(Mode: Integer);
procedure ApplyRXWFMToChan(Chan: Integer);
procedure ApplyWFMSettings;
// --- Мультислайсы --- // --- Мультислайсы ---
function FindSliceIdx(Id: Integer): Integer; // -1 если нет function FindSliceIdx(Id: Integer): Integer; // -1 если нет
procedure OpenSliceChannel(Idx: Integer); // OpenChannel + настройки procedure OpenSliceChannel(Idx: Integer); // OpenChannel + настройки
@@ -778,10 +822,134 @@ begin
MODE_FM: Result := WDSP_FM; MODE_FM: Result := WDSP_FM;
MODE_AM: Result := WDSP_AM; MODE_AM: Result := WDSP_AM;
MODE_SAM: Result := WDSP_SAM; MODE_SAM: Result := WDSP_SAM;
MODE_DIGU: Result := WDSP_DIGU;
MODE_DIGL: Result := WDSP_DIGL;
MODE_WFM: Result := WDSP_RX_WFM;
else Result := WDSP_USB; else Result := WDSP_USB;
end; end;
end; end;
function TWDSPEngine.ModeToWDSPTX(Mode: Integer): Integer;
// txaMode расходится с rxaMode после DRM — отдельный маппинг только для WFM.
begin
if Mode = MODE_WFM then Result := WDSP_TX_WFM
else Result := ModeToWDSP(Mode);
end;
function TWDSPEngine.WantWideDSP(Mode: Integer): Boolean;
// WFM демодулируется квадратурным дискриминатором на dsp_rate. При 48 кГц
// полоса ±24 кГц — вещательный канал (±100 кГц) туда не влезает, поэтому в
// WFM цепь RXA поднимается до WFMDSPRate. Если поднимать некуда — работаем
// как есть (узкий WFM), но dsp_rate не трогаем.
begin
Result := (Mode = MODE_WFM) and (WFMDSPRate > FAudioRate);
end;
function TWDSPEngine.WFMDSPRate: Integer;
// Наибольший rate вида FAudioRate * 2^k, который не выше WFM_DSP_RATE и делит
// FSampleRate нацело. Оба требования жёсткие:
// * pre_main_build считает dsp_insize через ЦЕЛОЧИСЛЕННОЕ in_rate/dsp_rate,
// поэтому отношение обязано быть точным;
// * dsp_size = FAudioBufSize * (rate/FAudioRate) должен остаться степенью
// двойки, иначе fircore планирует FFT нестандартного размера мимо wisdom.
var
R: Integer;
begin
R := WFM_DSP_RATE div FAudioRate; // 4 при 48 кГц аудио
while R > 1 do
begin
if (FAudioRate * R <= FSampleRate) and (FSampleRate mod (FAudioRate * R) = 0) then
Break;
R := R div 2;
end;
Result := FAudioRate * R;
end;
function TWDSPEngine.SliceDSPRate(Mode: Integer): Integer;
begin
if WantWideDSP(Mode) then Result := WFMDSPRate
else Result := FAudioRate;
end;
procedure TWDSPEngine.ApplyDSPRate(Mode: Integer);
// Переводит RXA/TXA между «узким» (dsp_rate = FAudioRate) и «широким» режимом.
// dsp_size всегда = FAudioBufSize * (dsp_rate / FAudioRate), поэтому dsp_outsize
// остаётся FAudioBufSize и буферы fexchange0 (FRXIn/FRXOut/FTXIn/FTXOut)
// переаллокации не требуют:
// RXA wide: dsp_size=4096 @192k -> out=FAudioBufSize
// RXA narw: dsp_size=FAudioBufSize @48k -> out=FAudioBufSize
// TXA wide: dsp_size=FTXOutBufSize @192k -> in=FAudioBufSize, out=FTXOutBufSize
// TXA narw: dsp_size=FAudioBufSize @48k -> in=FAudioBufSize, out=FTXOutBufSize
// Зовётся и из Open — до FInitialized, но уже после OpenChannel обоих каналов.
// RXA и TXA гейтятся раздельно: у TXA рейты свои (FAudioRate вход,
// FTXSampleRate выход) и от FSampleRate не зависят, поэтому wfmmod уходит
// на широкий dsp_rate даже когда приёмник остался на 48 кГц.
var
Wide: Boolean;
RXRate, RXSize, TXRate, TXSize, Old: Integer;
begin
Wide := (Mode = MODE_WFM);
if Wide then RXRate := WFMDSPRate else RXRate := FAudioRate;
RXSize := FAudioBufSize * (RXRate div FAudioRate);
if RXRate <> FRXDSPRate then
begin
// rate, size и nc меняем под одной остановкой канала: между вызовами
// dsp_insize не совпал бы с in_size, и fexchange0 прочитал бы мусор.
// RXASetNC/TXASetNC обязательны — см. комментарий к WDSP_MIN_NC.
Old := SetChannelState(RXA_CHAN, 0, 1);
SetDSPSamplerate(RXA_CHAN, RXRate);
SetDSPBuffsize (RXA_CHAN, RXSize);
FRXDSPRate := RXRate;
RXASetNC (RXA_CHAN, Max(WDSP_MIN_NC, RXSize));
SetChannelState (RXA_CHAN, Old, 0);
end;
if Wide then TXRate := FTXSampleRate else TXRate := FAudioRate;
TXSize := FAudioBufSize * (TXRate div FAudioRate);
if TXSize <> FTXDSPSize then
begin
Old := SetChannelState(TXA_CHAN, 0, 1);
SetDSPSamplerate(TXA_CHAN, TXRate);
SetDSPBuffsize (TXA_CHAN, TXSize);
FTXDSPSize := TXSize;
TXASetNC (TXA_CHAN, EffectiveTXNC);
SetChannelState (TXA_CHAN, Old, 0);
end;
end;
function TWDSPEngine.EffectiveTXNC: Integer;
// Пользовательский FilterNC (1024/2048/4096) не может быть короче dsp_size
// канала: иначе nfor = nc / dsp_size = 0. В WFM dsp_size = FTXOutBufSize.
begin
Result := Max(FTXFilterNC, FTXDSPSize);
end;
procedure TWDSPEngine.ApplyRXWFMToChan(Chan: Integer);
// create_wfmd поднимается с американской постоянной 75 мкс.
begin
SetRXAWFMDeviation (Chan, WFM_DEVIATION);
SetRXAWFMAFFilter (Chan, WFM_AF_LOW, WFM_AF_HIGH);
SetRXAWFMDeemphRun (Chan, 1);
SetRXAWFMDeemphTau (Chan, WFM_TAU_DE_US);
end;
procedure TWDSPEngine.ApplyWFMSettings;
begin
ApplyRXWFMToChan(RXA_CHAN);
SetTXAWFMDeviation (TXA_CHAN, WFM_DEVIATION);
SetTXAWFMAFFreqs (TXA_CHAN, WFM_AF_LOW, WFM_AF_HIGH);
SetTXAWFMPreEmphRun (TXA_CHAN, 1);
SetTXAWFMPreEmphTau (TXA_CHAN, WFM_TAU_DE_US);
end;
function TWDSPEngine.ClampToNyquist(Hz, Rate: Integer): Integer;
var Limit: Integer;
begin
Limit := Rate div 2 - 1;
Result := Max(-Limit, Min(Limit, Hz));
end;
procedure TWDSPEngine.ApplyDefaultFilter; procedure TWDSPEngine.ApplyDefaultFilter;
begin begin
case FMode of case FMode of
@@ -793,6 +961,9 @@ begin
MODE_FM: SetFilter(-5000, 5000); MODE_FM: SetFilter(-5000, 5000);
MODE_AM: SetFilter(-4000, 4000); MODE_AM: SetFilter(-4000, 4000);
MODE_SAM: SetFilter(-4000, 4000); MODE_SAM: SetFilter(-4000, 4000);
MODE_DIGU: SetFilter( 0, 3000);
MODE_DIGL: SetFilter(-3000, 0);
MODE_WFM: SetFilter(-80000, 80000);
end; end;
end; end;
@@ -813,6 +984,8 @@ begin
// TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate. // TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate.
FTXSampleRate := TX_SAMPLE_RATE; FTXSampleRate := TX_SAMPLE_RATE;
FTXOutBufSize := BufSize * FTXSampleRate div AudioRate; // = 4096 @ 48k audio, 192k tx FTXOutBufSize := BufSize * FTXSampleRate div AudioRate; // = 4096 @ 48k audio, 192k tx
FRXDSPRate := AudioRate; // «узкий» режим; поднимается только в WFM
FTXDSPSize := BufSize; // dsp_size TXA в «узком» режиме
FInitialized := False; FInitialized := False;
FAnalyzerOpen := False; FAnalyzerOpen := False;
FMode := MODE_USB; FMode := MODE_USB;
@@ -1157,6 +1330,11 @@ begin
0 0
); );
// Каналы только что созданы с dsp_rate = FAudioRate; забываем «широкую»
// геометрию прошлой сессии (Close/Open через ChangeSampleRate).
FRXDSPRate := FAudioRate;
FTXDSPSize := FAudioBufSize;
create_anbEXT(RXA_CHAN, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20); create_anbEXT(RXA_CHAN, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
create_nobEXT(RXA_CHAN, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20); create_nobEXT(RXA_CHAN, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
@@ -1189,12 +1367,21 @@ begin
// изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок). // изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок).
); );
SetTXAMode(TXA_CHAN, ModeToWDSP(FMode)); SetTXAMode(TXA_CHAN, ModeToWDSPTX(FMode));
SetTXAPanelRun(TXA_CHAN, 1); SetTXAPanelRun(TXA_CHAN, 1);
// PatchPanel: 1=Q, 2=I. Mic подаётся в I (FTXIn[i*2]), Q=0 — выбираем I. // PatchPanel: 1=Q, 2=I. Mic подаётся в I (FTXIn[i*2]), Q=0 — выбираем I.
SetTXAPanelSelect(TXA_CHAN, 2); SetTXAPanelSelect(TXA_CHAN, 2);
FInitialized := True; FInitialized := True;
// Оба канала созданы — только теперь можно поднять dsp_rate под WFM.
// SetMode переставит rate, режимы обоих каналов и параметры wfmd/wfmmod;
// SetFilter пересчитает полосу уже под новый dsp_rate.
if FMode = MODE_WFM then
begin
SetMode(FMode);
SetFilter(FFilterLow, FFilterHigh);
end;
// Прокидываем все TX-параметры из FTX*-полей: фильтр, mic gain, EQ, // Прокидываем все TX-параметры из FTX*-полей: фильтр, mic gain, EQ,
// Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS. // Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS.
ApplyTXChainSettings; ApplyTXChainSettings;
@@ -1547,21 +1734,27 @@ end;
// Открывает WDSP-канал слайса с той же геометрией, что RXA_CHAN. Вызывать // Открывает WDSP-канал слайса с той же геометрией, что RXA_CHAN. Вызывать
// под FSliceLock и только при FInitialized. // под FSliceLock и только при FInitialized.
procedure TWDSPEngine.OpenSliceChannel(Idx: Integer); procedure TWDSPEngine.OpenSliceChannel(Idx: Integer);
var Chan: Integer; var Chan, DSPRate, DSPSize: Integer;
begin begin
if not FInitialized then Exit; if not FInitialized then Exit;
if FSlices[Idx].Opened then Exit; if FSlices[Idx].Opened then Exit;
Chan := FSlices[Idx].Chan; Chan := FSlices[Idx].Chan;
OpenChannel(Chan, FBufSize, FAudioBufSize, FSampleRate, FAudioRate, FAudioRate, // WFM-слайс, как и главный канал, живёт на WFMDSPRate: rsmpin децимирует
// вход, rsmpout сводит к FAudioRate, так что out_size остаётся FAudioBufSize.
DSPRate := SliceDSPRate(FSlices[Idx].Mode);
DSPSize := FAudioBufSize * (DSPRate div FAudioRate);
OpenChannel(Chan, FBufSize, DSPSize, FSampleRate, DSPRate, FAudioRate,
0, 0, 0.010, 0.010, 0.010, 0.010, 0); 0, 0, 0.010, 0.010, 0.010, 0.010, 0);
// NB (ANB/NOB) требует создания EXT-блоков на канале, как для RXA_CHAN в Open. // NB (ANB/NOB) требует создания EXT-блоков на канале, как для RXA_CHAN в Open.
create_anbEXT(Chan, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20); create_anbEXT(Chan, 1, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
create_nobEXT(Chan, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20); create_nobEXT(Chan, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
SetRXABandpassWindow(Chan, 1); SetRXABandpassWindow(Chan, 1);
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode)); SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan);
SetRXAShiftRun(Chan, 1); SetRXAShiftRun(Chan, 1);
SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz); SetRXAShiftFreq(Chan, FSlices[Idx].ShiftHz);
RXASetPassband(Chan, FSlices[Idx].FilterLo, FSlices[Idx].FilterHi); RXASetPassband(Chan, ClampToNyquist(FSlices[Idx].FilterLo, DSPRate),
ClampToNyquist(FSlices[Idx].FilterHi, DSPRate));
ApplyAGCToChan(Chan, FSlices[Idx].AGCMode, 50.0); ApplyAGCToChan(Chan, FSlices[Idx].AGCMode, 50.0);
// NR/NB/SNB/ANF из сохранённого состояния слайса. // NR/NB/SNB/ANF из сохранённого состояния слайса.
ApplyNRToChan(Chan, FSlices[Idx].NRMode); ApplyNRToChan(Chan, FSlices[Idx].NRMode);
@@ -1686,27 +1879,45 @@ begin
end; end;
procedure TWDSPEngine.SetSliceMode(Id, Mode: Integer); procedure TWDSPEngine.SetSliceMode(Id, Mode: Integer);
var idx: Integer; var idx: Integer; Rerate: Boolean;
begin begin
FSliceLock.Enter; FSliceLock.Enter;
try try
idx := FindSliceIdx(Id); if idx < 0 then Exit; idx := FindSliceIdx(Id); if idx < 0 then Exit;
// Уход в/из WFM меняет dsp_rate канала — проще пересоздать его целиком,
// чем гонять SetDSPSamplerate: слайс всё равно перестраивает все фильтры.
Rerate := FSlices[idx].Opened and
(SliceDSPRate(Mode) <> SliceDSPRate(FSlices[idx].Mode));
FSlices[idx].Mode := Mode; FSlices[idx].Mode := Mode;
if FSlices[idx].Opened then SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode)); if Rerate then
begin
CloseSliceChannel(idx);
OpenSliceChannel(idx);
end
else if FSlices[idx].Opened then
begin
SetRXAMode(FSlices[idx].Chan, ModeToWDSP(Mode));
if Mode = MODE_WFM then ApplyRXWFMToChan(FSlices[idx].Chan);
end;
finally finally
FSliceLock.Leave; FSliceLock.Leave;
end; end;
end; end;
procedure TWDSPEngine.SetSliceFilter(Id, Low, High: Integer); procedure TWDSPEngine.SetSliceFilter(Id, Low, High: Integer);
var idx: Integer; var idx, Rate: Integer;
begin begin
FSliceLock.Enter; FSliceLock.Enter;
try try
idx := FindSliceIdx(Id); if idx < 0 then Exit; idx := FindSliceIdx(Id); if idx < 0 then Exit;
FSlices[idx].FilterLo := Low; FSlices[idx].FilterLo := Low;
FSlices[idx].FilterHi := High; FSlices[idx].FilterHi := High;
if FSlices[idx].Opened then RXASetPassband(FSlices[idx].Chan, Low, High); if FSlices[idx].Opened then
begin
Rate := SliceDSPRate(FSlices[idx].Mode);
RXASetPassband(FSlices[idx].Chan, ClampToNyquist(Low, Rate),
ClampToNyquist(High, Rate));
end;
finally finally
FSliceLock.Leave; FSliceLock.Leave;
end; end;
@@ -1865,8 +2076,12 @@ procedure TWDSPEngine.SetMode(Mode: Integer);
begin begin
FMode := Mode; FMode := Mode;
if not FInitialized then Exit; if not FInitialized then Exit;
// Rate — до режима: setDSPSamplerate_rxa перестраивает всю цепь (в т.ч. wfmd),
// а SetRXAMode затем пересчитает bpsnba под уже актуальный dsp_rate.
ApplyDSPRate(Mode);
SetRXAMode(RXA_CHAN, ModeToWDSP(Mode)); SetRXAMode(RXA_CHAN, ModeToWDSP(Mode));
SetTXAMode(TXA_CHAN, ModeToWDSP(Mode)); SetTXAMode(TXA_CHAN, ModeToWDSPTX(Mode));
if Mode = MODE_WFM then ApplyWFMSettings;
// НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI // НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
end; end;
@@ -1875,8 +2090,13 @@ begin
FFilterLow := Low; FFilterLow := Low;
FFilterHigh := High; FFilterHigh := High;
if not FInitialized then Exit; if not FInitialized then Exit;
// Кромки не могут выходить за Найквист текущего dsp_rate: в WFM они доходят
// до ±100 кГц, а dsp_rate зависит от SampleRate. Клампим только то, что
// уходит в WDSP — запрошенные значения храним как есть, иначе понижение
// SampleRate необратимо «съело» бы полосу.
// RXASetPassband — правильный unified API, пересчитывает фильтр целиком // RXASetPassband — правильный unified API, пересчитывает фильтр целиком
RXASetPassband(RXA_CHAN, Low, High); RXASetPassband(RXA_CHAN, ClampToNyquist(Low, FRXDSPRate),
ClampToNyquist(High, FRXDSPRate));
end; end;
procedure TWDSPEngine.SetAGC(Mode: TWDSPAGCMode; FixedGain: Double); procedure TWDSPEngine.SetAGC(Mode: TWDSPAGCMode; FixedGain: Double);
@@ -2135,7 +2355,7 @@ end;
procedure TWDSPEngine.SetTXMode(Mode: Integer); procedure TWDSPEngine.SetTXMode(Mode: Integer);
begin begin
if not FInitialized then Exit; if not FInitialized then Exit;
SetTXAMode(TXA_CHAN, ModeToWDSP(Mode)); SetTXAMode(TXA_CHAN, ModeToWDSPTX(Mode));
end; end;
procedure TWDSPEngine.SetTXFilter(Low, High: Integer); procedure TWDSPEngine.SetTXFilter(Low, High: Integer);
@@ -2401,7 +2621,7 @@ begin
if FTXFilterWindow >= 0 then if FTXFilterWindow >= 0 then
SetTXABandpassWindow(TXA_CHAN, FTXFilterWindow); SetTXABandpassWindow(TXA_CHAN, FTXFilterWindow);
if FTXFilterNC > 0 then if FTXFilterNC > 0 then
TXASetNC(TXA_CHAN, FTXFilterNC); TXASetNC(TXA_CHAN, EffectiveTXNC);
if FTXFilterMP >= 0 then if FTXFilterMP >= 0 then
TXASetMP(TXA_CHAN, FTXFilterMP); TXASetMP(TXA_CHAN, FTXFilterMP);
@@ -2463,7 +2683,7 @@ begin
if not FInitialized then Exit; if not FInitialized then Exit;
SetTXABandpassFreqs(TXA_CHAN, Low, High); SetTXABandpassFreqs(TXA_CHAN, Low, High);
SetTXABandpassWindow(TXA_CHAN, Window); SetTXABandpassWindow(TXA_CHAN, Window);
if NC > 0 then TXASetNC(TXA_CHAN, NC); if NC > 0 then TXASetNC(TXA_CHAN, EffectiveTXNC);
if MP >= 0 then TXASetMP(TXA_CHAN, MP); if MP >= 0 then TXASetMP(TXA_CHAN, MP);
end; end;
+11 -6
View File
@@ -193,6 +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>' +
'</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>' +
@@ -298,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"];' + 'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM","DIGU","DIGL","WFM"];' +
'const GRP_MAP={ssb:[0,1],cw:[3,4],fm:[5,5],am:[6,7]};' + 'const GRP_MAP={ssb:[0,1],cw:[3,4],fm:[5,10],am:[6,7],digi:[8,9]};' +
'const MODE_TO_GRP=["ssb","ssb","ssb","cw","cw","fm","am","am"];' + 'const MODE_TO_GRP=["ssb","ssb","ssb","cw","cw","fm","am","am","digi","digi","fm"];' +
'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];}' +
@@ -316,14 +317,18 @@ begin
'dsb:["16k","12k","10k","8.0k","6.6k","5.2k","4.0k","3.1k","2.9k","2.4k"],' + 'dsb:["16k","12k","10k","8.0k","6.6k","5.2k","4.0k","3.1k","2.9k","2.4k"],' +
'cw:["1.0k","800","600","500","400","250","150","100","50","25"],' + 'cw:["1.0k","800","600","500","400","250","150","100","50","25"],' +
'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"],' +
'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],' +
'cw:[1000,800,600,500,400,250,150,100,50,25],' + 'cw:[1000,800,600,500,400,250,150,100,50,25],' +
'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],' +
'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===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===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(){' +
+3 -3
View File
@@ -1717,8 +1717,8 @@ end;
function TWebServer.BuildStateJson: string; function TWebServer.BuildStateJson: string;
const const
MODE_N: array[0..7] of string = MODE_N: array[0..10] of string =
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM'); ('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
var var
FS: TFormatSettings; FS: TFormatSettings;
XvtrJson, DevJson, RateJson, BandJson: string; XvtrJson, DevJson, RateJson, BandJson: string;
@@ -1790,7 +1790,7 @@ begin
'"rf_agc_visible":%s,"rx_gain_mode":%d,"rx_gain_db":%d,' + '"rf_agc_visible":%s,"rx_gain_mode":%d,"rx_gain_db":%d,' +
'"rx_mute_visible":%s,"rx_mute_on":%s}', '"rx_mute_visible":%s,"rx_mute_on":%s}',
[FFreq, FVfoB, FActiveVfo, [FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[FMode mod 8], FMode, MODE_N[FMode mod Length(MODE_N)],
FFilterIdx, FFilterBW, FFilterIdx, FFilterBW,
FAGCMode, FAGCTop, FAGCMode, FAGCTop,
FSpanHz, FCenterHz, FVolume, FSpanHz, FCenterHz, FVolume,