feat(tx): заводские EQ-кривые ESSB, честная АЧХ в редакторе, фикс 3-полосного EQ

Ключ ко всему — семантика EQ в WDSP (wdsp/eq.c, eq_impulse + TXA.c):
F[1..n] это УЗЛЫ ломаной АЧХ, между ними интерполяция по децибелам, а за
крайними узлами при ctfmode=0 (так создаётся eqp у TXA, мы его не меняем)
идёт кумулятивный скат (f/f0)^4 на каждый бин — фактически обрыв. Значит
включённый EQ работает как второй полосовой фильтр, и верхний узел обязан
лежать за верхней кромкой TX-фильтра.

Settings: заводские ESSB несут свою кривую (AddEQ) — ESSB 3.5k узлы
100..3600, ESSB 5k узлы 100..5100, преамп в минус (EQ стоит до компрессора
и ALC). EQ у Default/SSB DX/SSB Wide сбрасывается в заводской ноль (FlatEQ),
а не наследует живой, иначе «сброс к заводским» не сбрасывал бы тембр.
Сетка узлов вынесена в модульную DEF_EQ_FREQS.

WDSPEngine: единая точка пуша EQ (PushTXEQProfile). Починен 3-полосный
режим: раньше в WDSP уходили первые три узла 10-полосной сетки (100/200/400)
и включённый EQ убивал весь голос выше 400 Гц; теперь legacy-раскладка
самого WDSP (150/400/1500/6000, как SetTXAGrphEQ).

EqualizerControl: кривая — точный порт eq_impulse вместо гауссовых горбов
(сверено с C численно, расхождение 0.0 дБ), рисуется без клампа ±15, чтобы
обрыв за крайними узлами был виден. В 3-полосном режиме ручки стоят на
150/1500/6000 и по частоте не таскаются, сетка профиля при этом цела.

RadioController/SettingsForm/MainForm: кнопка Factory — пересборка заводского
набора с применением Default. Без неё обновлённые заводские профили не доедут
до тех, у кого секция tx_profiles уже записана: она читается, а не создаётся.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 15:47:12 +03:00
co-authored by Claude Opus 5
parent c8781bee6a
commit 8563c4e4cc
6 changed files with 312 additions and 55 deletions
+34 -3
View File
@@ -226,6 +226,7 @@ type
FBtnTXProfNew: TFlatButton;
FBtnTXProfRen: TFlatButton;
FBtnTXProfDel: TFlatButton;
FBtnTXProfFactory: TFlatButton;
FLblTXProfDrive: TLabel; // «Drive профиля: N %» (ползунок — в главном окне)
FCmbTXMicJack: TFlatComboBox; // "Mic In" / "Line In"
FChkTXMicBoost: TFlatCheckBox; // +20 dB (только Mic In)
@@ -438,6 +439,7 @@ type
FOnTXProfileAdd: TOnTXProfileName;
FOnTXProfileRename: TOnTXProfileIdxName;
FOnTXProfileDelete: TOnTXProfileIdx;
FOnTXProfileFactory: TNotifyEvent;
FLoading: Boolean;
@@ -496,6 +498,7 @@ type
procedure BtnTXProfNewClick(Sender: TObject);
procedure BtnTXProfRenClick(Sender: TObject);
procedure BtnTXProfDelClick(Sender: TObject);
procedure BtnTXProfFactoryClick(Sender: TObject);
procedure ApplyTheme(const T: TAppTheme);
procedure NavClick(Sender: TObject);
procedure SelectPage(APage: TWinControl; ANav: TFlatButton);
@@ -655,6 +658,7 @@ type
property OnTXProfileAdd: TOnTXProfileName read FOnTXProfileAdd write FOnTXProfileAdd;
property OnTXProfileRename: TOnTXProfileIdxName read FOnTXProfileRename write FOnTXProfileRename;
property OnTXProfileDelete: TOnTXProfileIdx read FOnTXProfileDelete write FOnTXProfileDelete;
property OnTXProfileFactory: TNotifyEvent read FOnTXProfileFactory write FOnTXProfileFactory;
property OnWfAGCNFChange: TOnWfAGCNFChange read FOnWfAGCNFChange write FOnWfAGCNFChange;
property OnADCChange: TOnADCChange read FOnADCChange write FOnADCChange;
property OnWebSettingsChange: TOnWebSettingsChange read FOnWebSettingsChange write FOnWebSettingsChange;
@@ -1514,10 +1518,12 @@ begin
FBtnTXProfNew := MkBtn(Grp, PAD + 302, R1, 100, 'New...', BtnTXProfNewClick);
FBtnTXProfRen := MkBtn(Grp, PAD + 408, R1, 100, 'Rename...', BtnTXProfRenClick);
FBtnTXProfDel := MkBtn(Grp, PAD + 514, R1, 100, 'Delete', BtnTXProfDelClick);
FBtnTXProfFactory := MkBtn(Grp, PAD + 620, R1, 100, 'Factory', BtnTXProfFactoryClick);
MakeLbl(Grp, 'Правки на вкладках Profile и EQ пишутся в активный профиль сразу — '
+ 'отдельной кнопки «сохранить» нет.', PAD, R1 + 30, GRP_W - 2 * PAD);
MakeLbl(Grp, 'New... создаёт копию текущего звука под новым именем.',
MakeLbl(Grp, 'New... создаёт копию текущего звука под новым именем. '
+ 'Factory — заводской набор заново, эквалайзер в ноль.',
PAD, R1 + 48, GRP_W - 2 * PAD);
// ---- Сегменты подвкладок ------------------------------------------------
@@ -1627,7 +1633,7 @@ begin
// =========================================================================
SubPage(1, SubTop);
Grp := MakeGroupPanel(FTXSubPage[1], 'TX Equalizer', MARGIN, 0, GRP_W, 390);
Grp := MakeGroupPanel(FTXSubPage[1], 'TX Equalizer', MARGIN, 0, GRP_W, 434);
FTXEQControl := TEqualizerControl.Create(Self);
FTXEQControl.Parent := Grp;
@@ -1638,7 +1644,17 @@ begin
TColor($00383838), CLR_TEXT, CLR_TEXTDIM, CLR_ACCENT, TColor($0055DDEE));
FTXEQControl.OnChange := OnTXAnyChange;
FTXSubPage[1].Height := DpiScale(390 + 12);
// Главное, что надо знать про этот EQ: точки — не «полосы», а узлы кривой,
// и за крайними узлами WDSP обрывает АЧХ (см. WDSPEngine.PushTXEQProfile).
MakeLbl(Grp, 'Точки — узлы кривой: между ними интерполяция по дБ, '
+ 'за крайними точками АЧХ обрывается.', PAD, R1 + 332, GRP_W - 2 * PAD);
MakeLbl(Grp, 'Нижняя точка должна быть не выше нижней кромки TX-фильтра, '
+ 'верхняя — не ниже верхней, иначе EQ сам срежет полосу.',
PAD, R1 + 352, GRP_W - 2 * PAD);
MakeLbl(Grp, 'В режиме 3 полос частоты задаёт WDSP (150 / 1500 / 6000 Гц) — '
+ 'двигается только усиление.', PAD, R1 + 372, GRP_W - 2 * PAD);
FTXSubPage[1].Height := DpiScale(434 + 12);
// =========================================================================
// Подвкладка 2 — Hardware: свойства устройства, профилем не переключаются
@@ -1953,6 +1969,21 @@ begin
if Assigned(FOnTXProfileDelete) then FOnTXProfileDelete(Idx);
end;
procedure TSettingsForm.BtnTXProfFactoryClick(Sender: TObject);
// Пересоздать заводской набор. Нужен не только «на случай чего»: заводские
// профили пишутся в конфиг один раз при первом подключении устройства, и без
// этой кнопки обновлённые заводские настройки (например кривые EQ у ESSB)
// не доехали бы до тех, у кого секция tx_profiles уже записана.
begin
if MessageDlg('TX Profile',
'Заменить весь список профилей заводским набором?'#13#10
+ 'Свои профили и правки заводских будут потеряны.'#13#10
+ 'Активным станет «Default»: микрофон, полоса, динамика и мощность '
+ 'останутся как сейчас, эквалайзер вернётся в ноль.',
mtConfirmation, [mbYes, mbNo], 0) <> mrYes then Exit;
if Assigned(FOnTXProfileFactory) then FOnTXProfileFactory(Self);
end;
procedure TSettingsForm.LoadTXProfiles(const Names: array of string;
ActiveIdx, DrivePct: Integer);
var