diff --git a/CATEngine.pas b/CATEngine.pas
index c9c98fb..7c8c697 100644
--- a/CATEngine.pas
+++ b/CATEngine.pas
@@ -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 в этих же кодах).
@@ -383,15 +385,18 @@ end;
function TCATEngine.ModeToK(M: Integer): Integer;
begin
case M of
- MODE_LSB: Result := 1;
- MODE_USB: Result := 2;
- MODE_DSB: Result := 2; // treated as USB for CAT compatibility
- MODE_CWU: Result := 3;
- MODE_FM: Result := 4;
- MODE_AM: Result := 5;
- MODE_SAM: Result := 5; // treated as AM
- MODE_CWL: Result := 7;
- else Result := 2;
+ MODE_LSB: Result := 1;
+ MODE_USB: Result := 2;
+ 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[0]:='LSB'; names[1]:='USB'; names[2]:='DSB'; names[3]:='CWL';
+ names[4]:='CWU'; names[5]:='FM'; names[6]:='AM'; names[7]:='SAM';
+ 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;
diff --git a/ChannelsForm.pas b/ChannelsForm.pas
index e6390c1..13e0831 100644
--- a/ChannelsForm.pas
+++ b/ChannelsForm.pas
@@ -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 = (
diff --git a/MainForm.pas b/MainForm.pas
index 502fdaf..2caab1b 100644
--- a/MainForm.pas
+++ b/MainForm.pas
@@ -64,7 +64,7 @@ const
WDSP_WISDOM_FILE = 'wdspWisdom00';
BAND_COUNT = 11;
- MODE_COUNT = 8;
+ MODE_COUNT = 11;
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
// UI-подписи кнопок (NAMES).
@@ -76,7 +76,10 @@ const
('16k','12k','10k','8.0k','6.6k','5.2k','4.0k','3.1k','2.9k','2.4k');
FILT_AM_NAMES: array[0..FILT_COUNT-1] of string =
('20k','18k','16k','12k','10k','9.0k','8.0k','7.0k','6.0k','5.0k');
+ FILT_DIGI_NAMES: array[0..FILT_COUNT-1] of string =
+ ('3.0k','2.7k','2.4k','2.1k','1.8k','1.5k','1.2k','900','600','300');
FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM');
+ FILT_WFM_NAMES: array[0..FILT_WFM_COUNT-1] of string = ('200k', '160k', '120k');
// CTCSS tones (38 standard tones, no None — toggle via CTCSS button)
// CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController).
@@ -97,7 +100,8 @@ const
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
MODE_NAMES: array[0..MODE_COUNT-1] of string = (
- 'LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
+ 'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM');
+ MODE_ROWS = 3; // 4 колонки; последний ряд неполный
SMETER_WIDTH_RATIO = 0.32;
SMETER_MARGIN = 3;
@@ -1551,6 +1555,8 @@ const
BTN_H = 26;
BTN_SM = 24;
LEFT_W = 232;
+ // Заголовок (16) + MODE_ROWS рядов с шагом 27 + нижний отступ.
+ MODE_PANEL_H = 16 + (MODE_ROWS - 1) * 27 + BTN_SM + 7;
PLUTO_AGC_H = 64; // высота отдельного Pluto RF-AGC блока (под AGC)
TOP_VFO_W = 244;
TOP_VFO_FREQ_H = 47;
@@ -2005,10 +2011,10 @@ begin
Inc(Y, 76);
- // Mode selector
+ // Mode selector — MODE_ROWS рядов по 4 кнопки (последний неполный).
PanelMode := TPanel.Create(Self);
PanelMode.Parent := PanelLeft;
- PanelMode.SetBounds(0, Y, LEFT_W, 74);
+ PanelMode.SetBounds(0, Y, LEFT_W, MODE_PANEL_H);
PanelMode.BevelOuter := bvNone;
MakeLbl(PanelMode, 'MODE', 4, 2);
for i := 0 to MODE_COUNT - 1 do
@@ -2023,7 +2029,7 @@ begin
StyleButton(B, i = FController.FMode);
end;
- Inc(Y, 76);
+ Inc(Y, MODE_PANEL_H + 2);
// Filter selector (10 кнопок в 2 ряда по 5)
PanelFilter := TPanel.Create(Self);
@@ -4057,6 +4063,28 @@ begin
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False;
if PanelFMStep <> nil then begin CloseFMStepPopup; PanelFMStep.Visible := False; end;
if PanelFMRpt <> nil then PanelFMRpt.Visible := False;
+
+ if FController.FMode = MODE_WFM then
+ begin
+ // WFM: полосы вещательного канала — три кнопки в один ряд.
+ CloseCTCSSPopup;
+ if FController.FFilter > FILT_WFM_COUNT - 1 then FController.FFilter := FILT_WFM_DEF;
+ FController.FFilterBW := FILT_WFM_BW[FController.FFilter];
+ for i := 0 to FILT_WFM_COUNT - 1 do
+ begin
+ BtnFilter[i].Caption := FILT_WFM_NAMES[i];
+ BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, FILT_WFM_COUNT, i);
+ BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, FILT_WFM_COUNT, i);
+ BtnFilter[i].Top := 16;
+ BtnFilter[i].Visible := True;
+ StyleButton(BtnFilter[i], i = FController.FFilter);
+ end;
+ for i := FILT_WFM_COUNT to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
+ PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
+ RelayoutBelowBands;
+ Exit;
+ end;
+
for i := 0 to FILT_COUNT - 1 do
begin
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, 5, i mod 5);
@@ -4066,15 +4094,23 @@ begin
end;
case FController.FMode of
- 0, 1: begin
+ MODE_LSB, MODE_USB:
+ begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_SSB_NAMES[i];
DefIdx := FILT_SSB_DEF;
end;
- 2: begin
+ MODE_DIGU, MODE_DIGL:
+ begin
+ for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DIGI_NAMES[i];
+ DefIdx := FILT_DIGI_DEF;
+ end;
+ MODE_DSB:
+ begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_DSB_NAMES[i];
DefIdx := FILT_DSB_DEF;
end;
- 3, 4: begin
+ MODE_CWL, MODE_CWU:
+ begin
for i := 0 to FILT_COUNT-1 do Names[i] := FILT_CW_NAMES[i];
DefIdx := FILT_CW_DEF;
end;
@@ -4086,9 +4122,14 @@ begin
FController.FFilter := DefIdx;
case FController.FMode of
- 0, 1: FController.FFilterBW := FILT_SSB_BW[DefIdx];
- 2: FController.FFilterBW := FILT_DSB_BW[DefIdx];
- 3, 4: FController.FFilterBW := FILT_CW_BW[DefIdx];
+ MODE_LSB, MODE_USB:
+ FController.FFilterBW := FILT_SSB_BW[DefIdx];
+ MODE_DIGU, MODE_DIGL:
+ FController.FFilterBW := FILT_DIGI_BW[DefIdx];
+ MODE_DSB:
+ FController.FFilterBW := FILT_DSB_BW[DefIdx];
+ MODE_CWL, MODE_CWU:
+ FController.FFilterBW := FILT_CW_BW[DefIdx];
else FController.FFilterBW := FILT_AM_BW[DefIdx];
end;
@@ -5190,8 +5231,10 @@ begin
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
begin
case FController.FMode of
- MODE_LSB: begin LoHz := -FController.FFilterBW; HiHz := -100; end;
- MODE_USB: begin LoHz := 100; HiHz := FController.FFilterBW; end;
+ MODE_LSB: begin LoHz := -FController.FFilterBW; HiHz := -100; end;
+ MODE_USB: begin LoHz := 100; HiHz := FController.FFilterBW; end;
+ MODE_DIGL: begin LoHz := -FController.FFilterBW; HiHz := 0; end;
+ MODE_DIGU: begin LoHz := 0; HiHz := FController.FFilterBW; end;
else begin LoHz := -FController.FFilterBW / 2; HiHz := FController.FFilterBW / 2; end;
end;
Ovls[n] := FVfoOverlay;
@@ -5275,15 +5318,27 @@ begin
end;
FController.FMode := Mode;
FController.FFilterBW := FilterBW;
- if Mode = MODE_FM then
- begin
- FController.FFilter := FILT_FM_DEF;
- for Idx := 0 to FILT_FM_COUNT - 1 do
- if FILT_FM_BW[Idx] = FilterBW then
+ case Mode of
+ MODE_FM:
begin
- FController.FFilter := Idx;
- FController.FFMDeviation := FILT_FM_DEV[Idx];
- Break;
+ FController.FFilter := FILT_FM_DEF;
+ for Idx := 0 to FILT_FM_COUNT - 1 do
+ if FILT_FM_BW[Idx] = FilterBW then
+ begin
+ FController.FFilter := Idx;
+ FController.FFMDeviation := FILT_FM_DEV[Idx];
+ Break;
+ end;
+ end;
+ MODE_WFM:
+ begin
+ FController.FFilter := FILT_WFM_DEF;
+ for Idx := 0 to FILT_WFM_COUNT - 1 do
+ if FILT_WFM_BW[Idx] = FilterBW then
+ begin
+ FController.FFilter := Idx;
+ Break;
+ end;
end;
end;
if FController.FWDSPReady then
@@ -5576,9 +5631,9 @@ begin
O := FindSliceFlag(SliceId);
if (O = nil) or not FController.GetSlice(SliceId, S) then Exit;
case S.Mode of
- MODE_LSB: BW := -S.FilterLo;
- MODE_USB: BW := S.FilterHi;
- else BW := S.FilterHi - S.FilterLo;
+ MODE_LSB, MODE_DIGL: BW := -S.FilterLo;
+ MODE_USB, MODE_DIGU: BW := S.FilterHi;
+ else BW := S.FilterHi - S.FilterLo;
end;
O.SetState(S.Mode, BW, S.TargetHz, FController.SliceSMeter(SliceId));
O.SetDSPState(0, 0, False, False, FController.FAGCMode);
diff --git a/RadioController.pas b/RadioController.pas
index 550731f..ce507a2 100644
--- a/RadioController.pas
+++ b/RadioController.pas
@@ -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,10 +1743,20 @@ 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;
- else begin FFilter := FILT_AM_DEF; FFilterBW := FILT_AM_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,11 +1765,16 @@ 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)];
- else Result := FILT_AM_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)
- else Idx := EnsureRange(Idx, 0, FILT_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,11 +2436,16 @@ 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_FM: for i := 0 to FILT_FM_COUNT-1 do BW_arr[i] := FILT_FM_BW[i];
- else for i := 0 to FILT_COUNT-1 do BW_arr[i] := FILT_AM_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;
for i := 0 to FILT_COUNT-1 do
diff --git a/Settings.pas b/Settings.pas
index 261f500..929e6df 100644
--- a/Settings.pas
+++ b/Settings.pas
@@ -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
diff --git a/SpectrumView.pas b/SpectrumView.pas
index b875d53..7c7d301 100644
--- a/SpectrumView.pas
+++ b/SpectrumView.pas
@@ -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)));
diff --git a/VfoOverlay.pas b/VfoOverlay.pas
index 2815043..f295872 100644
--- a/VfoOverlay.pas
+++ b/VfoOverlay.pas
@@ -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);
@@ -375,28 +386,36 @@ end;
function TVfoOverlay.GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer;
begin
case ModeIdx of
- 0,1: Result := SSB_BW[FilterIdx];
- 2: Result := DSB_BW[FilterIdx];
- 3,4: Result := CW_BW[FilterIdx];
- 5: Result := FM_BW[FilterIdx];
- else Result := AM_BW[FilterIdx];
+ 0,1: Result := SSB_BW[FilterIdx];
+ 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;
function TVfoOverlay.GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string;
begin
case ModeIdx of
- 0,1: Result := SSB_LBL[FilterIdx];
- 2: Result := DSB_LBL[FilterIdx];
- 3,4: Result := CW_LBL[FilterIdx];
- 5: Result := FM_LBL[FilterIdx];
- else Result := AM_LBL[FilterIdx];
+ 0,1: Result := SSB_LBL[FilterIdx];
+ 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;
diff --git a/WDSP.pas b/WDSP.pas
index bf355cd..21e57c7 100644
--- a/WDSP.pas
+++ b/WDSP.pas
@@ -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)
// ---------------------------------------------------------------------------
diff --git a/WDSPEngine.pas b/WDSPEngine.pas
index bdc5344..76da016 100644
--- a/WDSPEngine.pas
+++ b/WDSPEngine.pas
@@ -29,25 +29,55 @@ uses
WDSP;
const
- // WDSP mode integers (нет именованных констант в WDSP.pas)
- WDSP_LSB = 0;
- WDSP_USB = 1;
- WDSP_DSB = 2;
- WDSP_CWL = 3;
- WDSP_CWU = 4;
- WDSP_FM = 5;
- WDSP_AM = 6;
- WDSP_SAM = 12;
+ // 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;
+ WDSP_CWL = 3;
+ WDSP_CWU = 4;
+ WDSP_FM = 5;
+ WDSP_AM = 6;
+ WDSP_DIGU = 7;
+ WDSP_DIGL = 9;
+ WDSP_SAM = 10;
+ WDSP_RX_WFM = 12;
+ WDSP_TX_WFM = 14;
- // Наши индексы режимов (совпадают с кнопками MainForm)
- MODE_LSB = 0;
- MODE_USB = 1;
- MODE_DSB = 2;
- MODE_CWL = 3;
- MODE_CWU = 4;
- MODE_FM = 5;
- MODE_AM = 6;
- MODE_SAM = 7;
+ // Наши индексы режимов (совпадают с кнопками MainForm).
+ // Новые режимы дописываются В КОНЕЦ: индекс уходит в settings.json,
+ // band-cache, CAT (ZZMD/ZZMV) и web-API.
+ 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;
+
+ // Минимальная длина 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 + настройки
@@ -770,29 +814,156 @@ end;
function TWDSPEngine.ModeToWDSP(Mode: Integer): Integer;
begin
case Mode of
- MODE_LSB: Result := WDSP_LSB;
- MODE_USB: Result := WDSP_USB;
- MODE_DSB: Result := WDSP_DSB;
- MODE_CWL: Result := WDSP_CWL;
- MODE_CWU: Result := WDSP_CWU;
- MODE_FM: Result := WDSP_FM;
- MODE_AM: Result := WDSP_AM;
- MODE_SAM: Result := WDSP_SAM;
- else Result := WDSP_USB;
+ MODE_LSB: Result := WDSP_LSB;
+ MODE_USB: Result := WDSP_USB;
+ MODE_DSB: Result := WDSP_DSB;
+ MODE_CWL: Result := WDSP_CWL;
+ MODE_CWU: Result := WDSP_CWU;
+ 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
- MODE_LSB: SetFilter(-2400, -100);
- MODE_USB: SetFilter( 100, 2400);
- MODE_DSB: SetFilter(-2400, 2400);
- MODE_CWL: SetFilter( -800, -200);
- MODE_CWU: SetFilter( 200, 800);
- MODE_FM: SetFilter(-5000, 5000);
- MODE_AM: SetFilter(-4000, 4000);
- MODE_SAM: SetFilter(-4000, 4000);
+ MODE_LSB: SetFilter(-2400, -100);
+ MODE_USB: SetFilter( 100, 2400);
+ MODE_DSB: SetFilter(-2400, 2400);
+ MODE_CWL: SetFilter( -800, -200);
+ MODE_CWU: SetFilter( 200, 800);
+ 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;
diff --git a/WebPageHtml.pas b/WebPageHtml.pas
index 811ba7e..f4a273f 100644
--- a/WebPageHtml.pas
+++ b/WebPageHtml.pas
@@ -193,6 +193,7 @@ begin
'' +
'