mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
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:
+16
-8
@@ -31,10 +31,12 @@ const
|
||||
CAT_EXEC_ERROR = 'E;';
|
||||
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_CWL = 3; MODE_CWU = 4; MODE_FM = 5;
|
||||
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_* (значения должны
|
||||
// совпадать: контроллер хранит FFMRptDir в этих же кодах).
|
||||
@@ -388,9 +390,12 @@ begin
|
||||
MODE_DSB: Result := 2; // treated as USB for CAT compatibility
|
||||
MODE_CWU: Result := 3;
|
||||
MODE_FM: Result := 4;
|
||||
MODE_WFM: Result := 4; // treated as FM
|
||||
MODE_AM: Result := 5;
|
||||
MODE_SAM: Result := 5; // treated as AM
|
||||
MODE_DIGU: Result := 6; // Kenwood FSK
|
||||
MODE_CWL: Result := 7;
|
||||
MODE_DIGL: Result := 9; // Kenwood FSK-R
|
||||
else Result := 2;
|
||||
end;
|
||||
end;
|
||||
@@ -403,9 +408,9 @@ begin
|
||||
3: Result := MODE_CWU;
|
||||
4: Result := MODE_FM;
|
||||
5: Result := MODE_AM;
|
||||
6: Result := MODE_USB; // FSK/RTTY → USB
|
||||
6: Result := MODE_DIGU; // FSK/RTTY
|
||||
7: Result := MODE_CWL;
|
||||
9: Result := MODE_LSB; // FSK-R → LSB
|
||||
9: Result := MODE_DIGL; // FSK-R
|
||||
else Result := MODE_USB;
|
||||
end;
|
||||
end;
|
||||
@@ -1787,17 +1792,20 @@ end;
|
||||
|
||||
function TCATEngine.ZZML: string;
|
||||
// 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;
|
||||
// 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;
|
||||
names: array[0..7] of string;
|
||||
names: array[0..CAT_MODE_COUNT-1] of string;
|
||||
begin
|
||||
names[0]:='LSB'; names[1]:='USB'; names[2]:='DSB'; names[3]:='CWL';
|
||||
names[4]:='CWU'; names[5]:='FM'; names[6]:='AM'; names[7]:='SAM';
|
||||
if Length(s) = 1 then begin
|
||||
k := EnsureRange(StrToIntDef(s,0),0,7);
|
||||
names[8]:='DIGU'; names[9]:='DIGL'; names[10]:='WFM';
|
||||
if (Length(s) = 1) or (Length(s) = 2) then begin
|
||||
k := EnsureRange(StrToIntDef(s,0),0,CAT_MODE_COUNT-1);
|
||||
Result := names[k];
|
||||
end else Result := CAT_ERROR;
|
||||
end;
|
||||
|
||||
+2
-2
@@ -18,9 +18,9 @@ uses
|
||||
|
||||
const
|
||||
// Копии из MainForm: нужны для выпадающих списков
|
||||
CH_MODE_COUNT = 8;
|
||||
CH_MODE_COUNT = 11;
|
||||
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_NAMES: array[0..CH_CTCSS_COUNT-1] of string = (
|
||||
|
||||
+69
-14
@@ -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;
|
||||
|
||||
@@ -5192,6 +5233,8 @@ begin
|
||||
case FController.FMode of
|
||||
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,7 +5318,8 @@ begin
|
||||
end;
|
||||
FController.FMode := Mode;
|
||||
FController.FFilterBW := FilterBW;
|
||||
if Mode = MODE_FM then
|
||||
case Mode of
|
||||
MODE_FM:
|
||||
begin
|
||||
FController.FFilter := FILT_FM_DEF;
|
||||
for Idx := 0 to FILT_FM_COUNT - 1 do
|
||||
@@ -5286,6 +5330,17 @@ begin
|
||||
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
|
||||
begin
|
||||
FController.FDSPEngine.SetMode(FController.FMode);
|
||||
@@ -5576,8 +5631,8 @@ 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;
|
||||
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));
|
||||
|
||||
+47
-18
@@ -1692,9 +1692,13 @@ var Half: Integer;
|
||||
begin
|
||||
Half := BW div 2;
|
||||
case Mode of
|
||||
0: begin Lo := -BW; Hi := -100; end; // LSB
|
||||
1: begin Lo := 100; Hi := BW; end; // USB
|
||||
else Lo := -Half; Hi := Half; // DSB/CW/FM/AM/SAM
|
||||
MODE_LSB: begin Lo := -BW; Hi := -100; end;
|
||||
MODE_USB: begin Lo := 100; Hi := BW; end;
|
||||
// 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;
|
||||
|
||||
@@ -1739,9 +1743,19 @@ begin
|
||||
FFilterBW := FILT_FM_BW[FFilter];
|
||||
FFMDeviation := FILT_FM_DEV[FFilter];
|
||||
end;
|
||||
0, 1: begin FFilter := FILT_SSB_DEF; FFilterBW := FILT_SSB_BW[FFilter]; end;
|
||||
2: begin FFilter := FILT_DSB_DEF; FFilterBW := FILT_DSB_BW[FFilter]; end;
|
||||
3, 4: begin FFilter := FILT_CW_DEF; FFilterBW := FILT_CW_BW[FFilter]; end;
|
||||
MODE_WFM:
|
||||
begin
|
||||
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;
|
||||
end;
|
||||
end;
|
||||
@@ -1751,10 +1765,15 @@ function TRadioController.FilterBWFor(M, Idx: Integer): Integer;
|
||||
// но НЕ меняет индекс (в отличие от ApplyModeDefaults) — нужно для XVTR-restore.
|
||||
begin
|
||||
case M of
|
||||
MODE_FM: Result := FILT_FM_BW[EnsureRange(Idx, 0, 1)];
|
||||
0, 1: Result := FILT_SSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
||||
2: Result := FILT_DSB_BW[EnsureRange(Idx, 0, FILT_COUNT - 1)];
|
||||
3, 4: Result := FILT_CW_BW[EnsureRange(Idx, 0, FILT_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_LSB, MODE_USB:
|
||||
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)];
|
||||
end;
|
||||
end;
|
||||
@@ -2382,14 +2401,19 @@ end;
|
||||
procedure TRadioController.SetFilterIdx(Idx: Integer);
|
||||
begin
|
||||
// Клампим индекс под текущий режим (как FilterBWFor) — публичная команда,
|
||||
// Idx может прийти из CAT/web без проверки; FM-таблицы всего на 2 элемента.
|
||||
if FMode = MODE_FM then Idx := EnsureRange(Idx, 0, FILT_FM_COUNT - 1)
|
||||
// Idx может прийти из CAT/web без проверки; FM/WFM-таблицы короче остальных.
|
||||
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);
|
||||
end;
|
||||
FFilter := Idx;
|
||||
case FMode of
|
||||
0, 1: FFilterBW := FILT_SSB_BW[Idx];
|
||||
2: FFilterBW := FILT_DSB_BW[Idx];
|
||||
3, 4: FFilterBW := FILT_CW_BW[Idx];
|
||||
MODE_LSB, MODE_USB: FFilterBW := FILT_SSB_BW[Idx];
|
||||
MODE_DIGU, MODE_DIGL: FFilterBW := FILT_DIGI_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:
|
||||
begin
|
||||
FFilterBW := FILT_FM_BW[Idx];
|
||||
@@ -2412,10 +2436,15 @@ begin
|
||||
FFilterBW := BWHz;
|
||||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := MaxInt;
|
||||
case FMode of
|
||||
0, 1: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_BW[i];
|
||||
2: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_DSB_BW[i];
|
||||
3, 4: for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_CW_BW[i];
|
||||
MODE_LSB, MODE_USB:
|
||||
for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_SSB_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_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];
|
||||
end;
|
||||
best := 0; dist := MaxInt;
|
||||
|
||||
@@ -42,10 +42,20 @@ const
|
||||
FILT_AM_BW: array[0..FILT_COUNT-1] of Integer =
|
||||
(20000, 18000, 16000, 12000, 10000, 9000, 8000, 7000, 6000, 5000);
|
||||
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_BW: array[0..1] of Integer = (11000, 16000);
|
||||
FILT_FM_DEV: array[0..1] of Double = (2500.0, 5000.0);
|
||||
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
|
||||
TBandSettings = record
|
||||
|
||||
+4
-2
@@ -1088,8 +1088,10 @@ begin
|
||||
Half := BW / 2;
|
||||
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
|
||||
case Mode of
|
||||
0: begin Lo_Hz := -BW; Hi_Hz := -100; end;
|
||||
1: begin Lo_Hz := 100; Hi_Hz := BW; end;
|
||||
0: begin Lo_Hz := -BW; Hi_Hz := -100; end; // LSB
|
||||
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;
|
||||
end;
|
||||
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
|
||||
|
||||
+44
-23
@@ -23,6 +23,13 @@ const
|
||||
VFO_OVERLAY_ALPHA = 210;
|
||||
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): одна на букву — общий цвет для
|
||||
// бейджа-карточки во флаге и для буквы на полосе фильтра в спектре.
|
||||
SLICE_COLORS: array[0..7] of TColor = (
|
||||
@@ -54,7 +61,7 @@ type
|
||||
FFilterBW: Integer;
|
||||
FVfoHz: Double;
|
||||
FSMeterDB: Double;
|
||||
FModeFilter: array[0..7] of Integer;
|
||||
FModeFilter: array[0..OVL_MODE_COUNT-1] of Integer;
|
||||
|
||||
FNRMode: Integer; // 0=off, 1..4
|
||||
FNBMode: Integer; // 0=off, 1=NB, 2=NB2
|
||||
@@ -220,8 +227,9 @@ const
|
||||
KEY_COLOR = TColor($00FF00FF);
|
||||
|
||||
// Значения хит-тестов
|
||||
// режимы: -(I+1) = -1..-8
|
||||
// фильтры: BW в Гц (>0)
|
||||
// режимы: HIT_MODE_BASE-I
|
||||
// фильтры: HIT_FILT_BASE+I (индекс, не BW: полосы WFM доходят до 200000
|
||||
// и налезали бы на HIT_AGC_PICK_BASE/HIT_DEV_BASE)
|
||||
// AGC: HIT_AGC_PICK_BASE+I
|
||||
// устройства: HIT_DEV_BASE+I
|
||||
HIT_NR = -9;
|
||||
@@ -243,6 +251,9 @@ const
|
||||
HIT_SQL_SLIDER = -32; // трек порога FM-squelch
|
||||
HIT_AGC_PICK_BASE = 100000;
|
||||
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');
|
||||
|
||||
@@ -268,10 +279,6 @@ const
|
||||
CLR_SPLIT_ON = TColor($0020C0FF); // оранжевый SPLIT
|
||||
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_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);
|
||||
@@ -282,6 +289,10 @@ const
|
||||
FM_LBL: array[0..1] of string = ('NFM', 'FM');
|
||||
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');
|
||||
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);
|
||||
|
||||
@@ -379,6 +390,8 @@ begin
|
||||
2: Result := DSB_BW[FilterIdx];
|
||||
3,4: Result := CW_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];
|
||||
end;
|
||||
end;
|
||||
@@ -390,13 +403,19 @@ begin
|
||||
2: Result := DSB_LBL[FilterIdx];
|
||||
3,4: Result := CW_LBL[FilterIdx];
|
||||
5: Result := FM_LBL[FilterIdx];
|
||||
8,9: Result := DIGI_LBL[FilterIdx];
|
||||
10: Result := WFM_LBL[FilterIdx];
|
||||
else Result := AM_LBL[FilterIdx];
|
||||
end;
|
||||
end;
|
||||
|
||||
function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer;
|
||||
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;
|
||||
|
||||
{ ---- Lifecycle ---- }
|
||||
@@ -413,7 +432,8 @@ begin
|
||||
FHotIdx := -1;
|
||||
FModeFilter[0] := 2700; FModeFilter[1] := 2700; FModeFilter[2] := 8000;
|
||||
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;
|
||||
FSQLOn := False; FSQLLevel := 40; FSqlDrag := False;
|
||||
FVolume := 70; FMuted := False; FSplit := False; FTx := False;
|
||||
@@ -554,7 +574,7 @@ end;
|
||||
function TVfoOverlay.PanelContentH: Integer;
|
||||
begin
|
||||
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
|
||||
Result := ROW_H + IGAP + ROW_H; // toggles + AGC
|
||||
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;
|
||||
HR: TRect;
|
||||
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
|
||||
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);
|
||||
BY := Y + Row * (ROW_H + IGAP);
|
||||
HR := Rect(BX, BY, BX + BW, BY + ROW_H);
|
||||
DrawButton(C, HR, OVL_MODE_NAMES[I], I = FMode,
|
||||
(FHotIdx >= 0) and (FHitActions[FHotIdx] = -(I+1)), 7);
|
||||
RegisterHit(HR, -(I+1));
|
||||
(FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_MODE_BASE - I), 7);
|
||||
RegisterHit(HR, HIT_MODE_BASE - I);
|
||||
end;
|
||||
|
||||
// Ряд фильтров текущего режима
|
||||
FiltN := GetFilterCountForMode(FMode);
|
||||
BY := Y + 2*(ROW_H + IGAP);
|
||||
BY := Y + OVL_MODE_ROWS*(ROW_H + IGAP);
|
||||
FBX := X;
|
||||
for I := 0 to FiltN - 1 do
|
||||
begin
|
||||
@@ -770,8 +790,8 @@ begin
|
||||
BV := GetFilterBWForMode(FMode, I);
|
||||
HR := Rect(FBX, BY, FBRight - 2, BY + ROW_H);
|
||||
DrawButton(C, HR, GetFilterLblForMode(FMode, I), BV = FFilterBW,
|
||||
(FHotIdx >= 0) and (FHitActions[FHotIdx] = BV), 7);
|
||||
RegisterHit(HR, BV);
|
||||
(FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_FILT_BASE + I), 7);
|
||||
RegisterHit(HR, HIT_FILT_BASE + I);
|
||||
FBX := FBRight;
|
||||
end;
|
||||
end;
|
||||
@@ -1184,25 +1204,26 @@ begin
|
||||
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
|
||||
NewMode := (-HV) - 1;
|
||||
NewMode := HIT_MODE_BASE - HV;
|
||||
if NewMode <> FMode then
|
||||
begin
|
||||
FModeFilter[FMode] := FFilterBW;
|
||||
FMode := NewMode;
|
||||
FFilterBW := FModeFilter[FMode];
|
||||
Height := ComputeHeight; // число фильтров могло смениться (FM=2)
|
||||
Height := ComputeHeight; // число фильтров могло смениться (FM=2, WFM=3)
|
||||
RequestInvalidate;
|
||||
if Assigned(FOnSelect) then FOnSelect(FMode, FFilterBW);
|
||||
end;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Фильтр (BW>0)
|
||||
if HV > 0 then
|
||||
// Фильтр текущего режима (по индексу кнопки, не по BW)
|
||||
if (HV >= HIT_FILT_BASE) and
|
||||
(HV < HIT_FILT_BASE + GetFilterCountForMode(FMode)) then
|
||||
begin
|
||||
NewBW := HV;
|
||||
NewBW := GetFilterBWForMode(FMode, HV - HIT_FILT_BASE);
|
||||
if NewBW <> FFilterBW then
|
||||
begin
|
||||
FFilterBW := NewBW;
|
||||
|
||||
@@ -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 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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+236
-16
@@ -29,7 +29,10 @@ uses
|
||||
WDSP;
|
||||
|
||||
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_USB = 1;
|
||||
WDSP_DSB = 2;
|
||||
@@ -37,9 +40,15 @@ const
|
||||
WDSP_CWU = 4;
|
||||
WDSP_FM = 5;
|
||||
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_USB = 1;
|
||||
MODE_DSB = 2;
|
||||
@@ -48,6 +57,27 @@ const
|
||||
MODE_FM = 5;
|
||||
MODE_AM = 6;
|
||||
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_OFF = 0;
|
||||
@@ -182,6 +212,10 @@ type
|
||||
FAnalyzerOpen: Boolean;
|
||||
FSampleRate: Integer;
|
||||
FAudioRate: Integer;
|
||||
// Текущий dsp_rate канала RXA. Обычно = FAudioRate; в WFM поднимается
|
||||
// до FSampleRate, иначе девиация 75 кГц не помещается в полосу.
|
||||
FRXDSPRate: Integer;
|
||||
FTXDSPSize: Integer; // текущий dsp_size канала TXA (нижняя граница nc)
|
||||
FBufSize: Integer; // входной буфер @ FSampleRate
|
||||
FAudioBufSize: Integer; // выходной буфер @ FAudioRate (= FBufSize * FAudioRate / FSampleRate)
|
||||
FTXSampleRate: Integer; // фиксированный rate TX/DUC (TX_SAMPLE_RATE), не меняется при ChangeSampleRate
|
||||
@@ -356,7 +390,17 @@ type
|
||||
FBeaconDec: TBeaconDecoder; // не владеет; тап маяка в PushIQItemToDSP
|
||||
|
||||
function ModeToWDSP(Mode: Integer): Integer;
|
||||
function ModeToWDSPTX(Mode: Integer): Integer;
|
||||
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 если нет
|
||||
procedure OpenSliceChannel(Idx: Integer); // OpenChannel + настройки
|
||||
@@ -778,10 +822,134 @@ begin
|
||||
MODE_FM: Result := WDSP_FM;
|
||||
MODE_AM: Result := WDSP_AM;
|
||||
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;
|
||||
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;
|
||||
begin
|
||||
case FMode of
|
||||
@@ -793,6 +961,9 @@ begin
|
||||
MODE_FM: SetFilter(-5000, 5000);
|
||||
MODE_AM: 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;
|
||||
|
||||
@@ -813,6 +984,8 @@ begin
|
||||
// TX rate фиксирован — не зависит от RX. FTXOutBufSize = out_size TXA @ FTXSampleRate.
|
||||
FTXSampleRate := TX_SAMPLE_RATE;
|
||||
FTXOutBufSize := BufSize * FTXSampleRate div AudioRate; // = 4096 @ 48k audio, 192k tx
|
||||
FRXDSPRate := AudioRate; // «узкий» режим; поднимается только в WFM
|
||||
FTXDSPSize := BufSize; // dsp_size TXA в «узком» режиме
|
||||
FInitialized := False;
|
||||
FAnalyzerOpen := False;
|
||||
FMode := MODE_USB;
|
||||
@@ -1157,6 +1330,11 @@ begin
|
||||
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_nobEXT(RXA_CHAN, 1, 0, FBufSize, FSampleRate, 0.0001, 0.0001, 0.0001, 0.05, 20);
|
||||
|
||||
@@ -1189,12 +1367,21 @@ begin
|
||||
// изредка фазовый разрыв PostGen-тона (всплеск на водопаде + щелчок).
|
||||
);
|
||||
|
||||
SetTXAMode(TXA_CHAN, ModeToWDSP(FMode));
|
||||
SetTXAMode(TXA_CHAN, ModeToWDSPTX(FMode));
|
||||
SetTXAPanelRun(TXA_CHAN, 1);
|
||||
// PatchPanel: 1=Q, 2=I. Mic подаётся в I (FTXIn[i*2]), Q=0 — выбираем I.
|
||||
SetTXAPanelSelect(TXA_CHAN, 2);
|
||||
|
||||
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,
|
||||
// Leveler, ALC, Compressor, Phase Rotator, AM Carrier, FM, CTCSS.
|
||||
ApplyTXChainSettings;
|
||||
@@ -1547,21 +1734,27 @@ end;
|
||||
// Открывает WDSP-канал слайса с той же геометрией, что RXA_CHAN. Вызывать
|
||||
// под FSliceLock и только при FInitialized.
|
||||
procedure TWDSPEngine.OpenSliceChannel(Idx: Integer);
|
||||
var Chan: Integer;
|
||||
var Chan, DSPRate, DSPSize: Integer;
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
if FSlices[Idx].Opened then Exit;
|
||||
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);
|
||||
// NB (ANB/NOB) требует создания EXT-блоков на канале, как для RXA_CHAN в Open.
|
||||
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);
|
||||
SetRXABandpassWindow(Chan, 1);
|
||||
SetRXAMode(Chan, ModeToWDSP(FSlices[Idx].Mode));
|
||||
if FSlices[Idx].Mode = MODE_WFM then ApplyRXWFMToChan(Chan);
|
||||
SetRXAShiftRun(Chan, 1);
|
||||
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);
|
||||
// NR/NB/SNB/ANF из сохранённого состояния слайса.
|
||||
ApplyNRToChan(Chan, FSlices[Idx].NRMode);
|
||||
@@ -1686,27 +1879,45 @@ begin
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceMode(Id, Mode: Integer);
|
||||
var idx: Integer;
|
||||
var idx: Integer; Rerate: Boolean;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
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;
|
||||
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
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetSliceFilter(Id, Low, High: Integer);
|
||||
var idx: Integer;
|
||||
var idx, Rate: Integer;
|
||||
begin
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
idx := FindSliceIdx(Id); if idx < 0 then Exit;
|
||||
FSlices[idx].FilterLo := Low;
|
||||
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
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
@@ -1865,8 +2076,12 @@ procedure TWDSPEngine.SetMode(Mode: Integer);
|
||||
begin
|
||||
FMode := Mode;
|
||||
if not FInitialized then Exit;
|
||||
// Rate — до режима: setDSPSamplerate_rxa перестраивает всю цепь (в т.ч. wfmd),
|
||||
// а SetRXAMode затем пересчитает bpsnba под уже актуальный dsp_rate.
|
||||
ApplyDSPRate(Mode);
|
||||
SetRXAMode(RXA_CHAN, ModeToWDSP(Mode));
|
||||
SetTXAMode(TXA_CHAN, ModeToWDSP(Mode));
|
||||
SetTXAMode(TXA_CHAN, ModeToWDSPTX(Mode));
|
||||
if Mode = MODE_WFM then ApplyWFMSettings;
|
||||
// НЕ вызываем ApplyDefaultFilter — фильтр устанавливается явно из UI
|
||||
end;
|
||||
|
||||
@@ -1875,8 +2090,13 @@ begin
|
||||
FFilterLow := Low;
|
||||
FFilterHigh := High;
|
||||
if not FInitialized then Exit;
|
||||
// Кромки не могут выходить за Найквист текущего dsp_rate: в WFM они доходят
|
||||
// до ±100 кГц, а dsp_rate зависит от SampleRate. Клампим только то, что
|
||||
// уходит в WDSP — запрошенные значения храним как есть, иначе понижение
|
||||
// SampleRate необратимо «съело» бы полосу.
|
||||
// RXASetPassband — правильный unified API, пересчитывает фильтр целиком
|
||||
RXASetPassband(RXA_CHAN, Low, High);
|
||||
RXASetPassband(RXA_CHAN, ClampToNyquist(Low, FRXDSPRate),
|
||||
ClampToNyquist(High, FRXDSPRate));
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetAGC(Mode: TWDSPAGCMode; FixedGain: Double);
|
||||
@@ -2135,7 +2355,7 @@ end;
|
||||
procedure TWDSPEngine.SetTXMode(Mode: Integer);
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
SetTXAMode(TXA_CHAN, ModeToWDSP(Mode));
|
||||
SetTXAMode(TXA_CHAN, ModeToWDSPTX(Mode));
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.SetTXFilter(Low, High: Integer);
|
||||
@@ -2401,7 +2621,7 @@ begin
|
||||
if FTXFilterWindow >= 0 then
|
||||
SetTXABandpassWindow(TXA_CHAN, FTXFilterWindow);
|
||||
if FTXFilterNC > 0 then
|
||||
TXASetNC(TXA_CHAN, FTXFilterNC);
|
||||
TXASetNC(TXA_CHAN, EffectiveTXNC);
|
||||
if FTXFilterMP >= 0 then
|
||||
TXASetMP(TXA_CHAN, FTXFilterMP);
|
||||
|
||||
@@ -2463,7 +2683,7 @@ begin
|
||||
if not FInitialized then Exit;
|
||||
SetTXABandpassFreqs(TXA_CHAN, Low, High);
|
||||
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);
|
||||
end;
|
||||
|
||||
|
||||
+11
-6
@@ -193,6 +193,7 @@ begin
|
||||
'<select id="modGrpSel">' +
|
||||
'<option value="ssb" selected>SSB</option><option value="cw">CW</option>' +
|
||||
'<option value="fm">FM</option><option value="am">AM</option>' +
|
||||
'<option value="digi">DIGI</option>' +
|
||||
'</select>' +
|
||||
'<select id="stepSel">' +
|
||||
'<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();}' +
|
||||
|
||||
|
||||
'const MODE_N=["LSB","USB","DSB","CWL","CWU","FM","AM","SAM"];' +
|
||||
'const GRP_MAP={ssb:[0,1],cw:[3,4],fm:[5,5],am:[6,7]};' +
|
||||
'const MODE_TO_GRP=["ssb","ssb","ssb","cw","cw","fm","am","am"];' +
|
||||
'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,10],am:[6,7],digi:[8,9]};' +
|
||||
'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 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];}' +
|
||||
@@ -316,14 +317,18 @@ begin
|
||||
'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"],' +
|
||||
'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],' +
|
||||
'dsb:[16000,12000,10000,8000,6600,5200,4000,3100,2900,2400],' +
|
||||
'cw:[1000,800,600,500,400,250,150,100,50,25],' +
|
||||
'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;' +
|
||||
'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 fnpOutside(e){if(fnpOv&&!fnpOv.contains(e.target)&&e.target!==q("fnpBtn"))closeFnp();}' +
|
||||
'function showFnp(){' +
|
||||
|
||||
+3
-3
@@ -1717,8 +1717,8 @@ end;
|
||||
|
||||
function TWebServer.BuildStateJson: string;
|
||||
const
|
||||
MODE_N: array[0..7] of string =
|
||||
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
|
||||
MODE_N: array[0..10] of string =
|
||||
('LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
|
||||
var
|
||||
FS: TFormatSettings;
|
||||
XvtrJson, DevJson, RateJson, BandJson: string;
|
||||
@@ -1790,7 +1790,7 @@ begin
|
||||
'"rf_agc_visible":%s,"rx_gain_mode":%d,"rx_gain_db":%d,' +
|
||||
'"rx_mute_visible":%s,"rx_mute_on":%s}',
|
||||
[FFreq, FVfoB, FActiveVfo,
|
||||
FMode, MODE_N[FMode mod 8],
|
||||
FMode, MODE_N[FMode mod Length(MODE_N)],
|
||||
FFilterIdx, FFilterBW,
|
||||
FAGCMode, FAGCTop,
|
||||
FSpanHz, FCenterHz, FVolume,
|
||||
|
||||
Reference in New Issue
Block a user