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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 16:59:27 +03:00
co-authored by Claude Opus 4.8
parent 388687d684
commit ba2a75bb04
11 changed files with 532 additions and 160 deletions
+268 -48
View File
@@ -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;