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
+76 -13
View File
@@ -1536,6 +1536,14 @@ begin
end;
end;
const
// Заводская 10-полосная голосовая сетка EQ (Hz). [0]=слот преампа (частота
// не используется), [1..10]=узлы кривой. Подобрано под голос, а не под
// музыку: больше точек в 100..3500 Hz. Нужна и DefaultTX, и заводским
// профилям (сброс тембра в ноль), поэтому живёт на уровне модуля.
DEF_EQ_FREQS: array[0..10] of Double = (
0, 100, 200, 400, 700, 1100, 1500, 2000, 2500, 3000, 3500);
// ---------------------------------------------------------------------------
// TX settings (per-device, JSON-секция "tx" под MAC)
// ---------------------------------------------------------------------------
@@ -1543,12 +1551,6 @@ end;
class procedure TSettingsManager.DefaultTX(out T: TTXSettings);
var
i: Integer;
const
// 10-полосный голосовой EQ (Hz). [0]=preamp slot, [1..10]=bands.
// Подобрано под голос, а не под музыку: больше точек в 100..3500 Hz.
DefaultEQFreqs: array[0..10] of Double = (
0, // [0] preamp slot — частота не используется
100, 200, 400, 700, 1100, 1500, 2000, 2500, 3000, 3500);
begin
FillChar(T, SizeOf(T), 0);
T.MicLineIn := False;
@@ -1590,7 +1592,7 @@ begin
for i := 0 to 10 do
begin
T.EQGains[i] := 0.0;
T.EQFreqs[i] := DefaultEQFreqs[i];
T.EQFreqs[i] := DEF_EQ_FREQS[i];
end;
// AM
T.AMCarrierLevel := 0.5;
@@ -1862,6 +1864,36 @@ end;
procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer;
out L: TTXProfileList);
const
// Кривые EQ для заводских ESSB. Значения — УЗЛЫ ломаной АЧХ WDSP (eq.c),
// а не «полосы»: между узлами интерполяция по дБ, а ЗА крайними узлами
// ctfmode=0 обрывает АЧХ на нет. Отсюда железное правило, по которому
// подобраны частоты: нижний узел ≤ FilterLow, верхний ≥ FilterHigh, иначе
// включённый EQ сам срежет полосу, ради которой ESSB и включают.
// Форма: срез бубнения снизу, ровная середина, мягкий подъём присутствия
// к верху. Преамп в минус — EQ стоит ДО компрессора и ALC, и плюсовые
// гейны иначе просто загоняют ALC в ограничение.
ESSB35_F: array[1..10] of Double = (100, 160, 250, 400, 700, 1200, 1800, 2500, 3100, 3600);
ESSB35_G: array[1..10] of Double = ( -6, -4, -2, 0, 0, 1, 2, 3, 4, 4);
ESSB35_PRE = -2.0;
ESSB50_F: array[1..10] of Double = (100, 180, 300, 550, 950, 1600, 2400, 3300, 4200, 5100);
ESSB50_G: array[1..10] of Double = ( -5, -3, -1, 0, 0, 1, 2, 3.5, 5, 5);
ESSB50_PRE = -3.0;
// Тембр у заводского профиля — заводской: EQ выключен, все усиления и
// преамп в нуле, сетка узлов штатная. Наследовать сюда живой EQ нельзя —
// иначе «сброс к заводским» не сбрасывал бы то, ради чего его жмут.
procedure FlatEQ(var P: TTXProfile);
var i: Integer;
begin
P.EQOn := False;
P.EQNumBands := 10;
for i := 0 to 10 do
begin
P.EQGains[i] := 0.0;
P.EQFreqs[i] := DEF_EQ_FREQS[i];
end;
end;
procedure Add(const AName: string; ALow, AHigh: Integer;
AComp: Boolean; ACompGain: Double);
@@ -1873,24 +1905,55 @@ procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer;
L.Items[L.Count].FilterHigh := AHigh;
L.Items[L.Count].CompressorOn := AComp;
L.Items[L.Count].CompressorGain := ACompGain;
FlatEQ(L.Items[L.Count]);
Inc(L.Count);
end;
// То же плюс своя кривая EQ: у ESSB тембр — часть профиля, наследовать
// сетку 100..3500 от текущих настроек здесь нельзя (см. правило выше).
procedure AddEQ(const AName: string; ALow, AHigh: Integer;
AComp: Boolean; ACompGain: Double;
const AF, AG: array of Double; APreamp: Double);
var
i, Before: Integer;
begin
Before := L.Count;
Add(AName, ALow, AHigh, AComp, ACompGain);
if L.Count = Before then Exit; // упёрлись в TXPROF_MAX — ничего не добавлено
with L.Items[L.Count - 1] do
begin
EQOn := True;
EQNumBands := 10;
EQFreqs[0] := 0.0; // слот преампа — частота не используется
EQGains[0] := APreamp;
for i := 1 to 10 do
begin
EQFreqs[i] := AF[i - 1];
EQGains[i] := AG[i - 1];
end;
end;
end;
begin
FillChar(L, SizeOf(L), 0);
L.Count := 0;
L.ActiveIdx := 0;
// «Default» — ровно текущее состояние устройства: после миграции старого
// конфига звук в эфире не меняется ни на йоту.
// «Default» — текущее состояние устройства (микрофонный вход, полоса,
// динамика, мощность), но с заводским EQ: после миграции старого конфига
// звук в эфире не меняется, а сброс к заводским возвращает ровный тембр.
TXProfileCapture(Base, DrivePct, L.Items[0]);
L.Items[0].Name := 'Default';
FlatEQ(L.Items[0]);
L.Count := 1;
// Остальные заводские отличаются от Default ТОЛЬКО полосой и компрессором;
// микрофонный вход, EQ и мощность наследуются (см. коммент к DefaultTXProfiles).
// SSB-профили отличаются от Default только полосой и компрессором;
// микрофонный вход и мощность наследуются, EQ у всех троих заводской-ровный.
// ESSB несут ещё и свою кривую: без неё «широкий» профиль звучит тем же
// телефонным тембром, а с чужой сеткой (верхний узел 3500) EQ обрезал бы
// верх ESSB 5k.
Add('SSB DX', 300, 2700, True, 8.0);
Add('SSB Wide', 200, 3100, False, 5.0);
Add('ESSB 3.5k', 100, 3500, False, 5.0);
Add('ESSB 5k', 100, 5000, False, 5.0);
AddEQ('ESSB 3.5k', 100, 3500, False, 5.0, ESSB35_F, ESSB35_G, ESSB35_PRE);
AddEQ('ESSB 5k', 100, 5000, False, 5.0, ESSB50_F, ESSB50_G, ESSB50_PRE);
end;
function TSettingsManager.LoadTXProfiles(const MAC: array of Byte;