From ba2a75bb040a7ca6d9704940fdd603de2ae31856 Mon Sep 17 00:00:00 2001 From: Uladzimir Date: Fri, 10 Jul 2026 16:59:27 +0300 Subject: [PATCH] =?UTF-8?q?feat(dsp):=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC?= =?UTF-8?q?=D1=8B=20WFM,=20DIGI-U=20=D0=B8=20DIGI-L?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CATEngine.pas | 46 ++++--- ChannelsForm.pas | 4 +- MainForm.pas | 103 +++++++++++---- RadioController.pas | 75 +++++++---- Settings.pas | 10 ++ SpectrumView.pas | 6 +- VfoOverlay.pas | 87 +++++++----- WDSP.pas | 22 +++ WDSPEngine.pas | 316 +++++++++++++++++++++++++++++++++++++------- WebPageHtml.pas | 17 ++- WebServer.pas | 6 +- 11 files changed, 532 insertions(+), 160 deletions(-) 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 '' + '