mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
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:
+135
-21
@@ -31,6 +31,7 @@ type
|
|||||||
FBtnBands3: TFlatButton;
|
FBtnBands3: TFlatButton;
|
||||||
FBtnBands10: TFlatButton;
|
FBtnBands10: TFlatButton;
|
||||||
FButtonTheme: TAppTheme;
|
FButtonTheme: TAppTheme;
|
||||||
|
FRespDB: array of Double; // АЧХ по бинам (дБ), пересчитывается в Paint
|
||||||
function GetBandGain(Index: Integer): Double;
|
function GetBandGain(Index: Integer): Double;
|
||||||
function GetBandFreq(Index: Integer): Double;
|
function GetBandFreq(Index: Integer): Double;
|
||||||
procedure SetEnabledEQ(V: Boolean);
|
procedure SetEnabledEQ(V: Boolean);
|
||||||
@@ -47,7 +48,9 @@ type
|
|||||||
function BandX(Index: Integer; const R: TRect): Integer;
|
function BandX(Index: Integer; const R: TRect): Integer;
|
||||||
function XToFreq(X: Integer; const R: TRect): Double;
|
function XToFreq(X: Integer; const R: TRect): Double;
|
||||||
function BandColor(Index: Integer): TColor;
|
function BandColor(Index: Integer): TColor;
|
||||||
|
function GridBandFreq(Index: Integer): Double;
|
||||||
function InternalBandFreq(Index: Integer): Double;
|
function InternalBandFreq(Index: Integer): Double;
|
||||||
|
procedure BuildResponse;
|
||||||
function ResponseAtFreq(AFreq: Double): Double;
|
function ResponseAtFreq(AFreq: Double): Double;
|
||||||
function FormatFreq(AFreq: Double): string;
|
function FormatFreq(AFreq: Double): string;
|
||||||
function HitTestControl(X, Y: Integer): Integer;
|
function HitTestControl(X, Y: Integer): Integer;
|
||||||
@@ -93,6 +96,20 @@ const
|
|||||||
MAX_FREQ = 20000.0;
|
MAX_FREQ = 20000.0;
|
||||||
TOP_H = 44;
|
TOP_H = 44;
|
||||||
BOTTOM_H = 82;
|
BOTTOM_H = 82;
|
||||||
|
// Кривая = точное повторение расчёта WDSP (wdsp/eq.c, eq_impulse) под
|
||||||
|
// геометрию НАШЕГО TXA: канал открыт с dsp_rate = FAudioRate (48 кГц),
|
||||||
|
// eqp создаётся с nc = max(2048, dsp_size) и ctfmode = 0 (TXA.c) —
|
||||||
|
// SetTXAEQNC/SetTXAEQCtfmode мы не зовём. Отсюда N = 2048 ⇒ N/2 бинов
|
||||||
|
// до Найквиста. Рисовать «колокольчики», как принято у графических EQ,
|
||||||
|
// нельзя: WDSP строит ломаную по точкам и обрывает всё за крайними.
|
||||||
|
RESP_RATE = 48000.0;
|
||||||
|
RESP_MID = 1024; // N/2
|
||||||
|
RESP_BIN_HZ = RESP_RATE / 2.0 / RESP_MID; // 23.4375 Hz
|
||||||
|
RESP_FLOOR = -2000.0; // 1e-100 в амплитуде — пол WDSP
|
||||||
|
// Legacy-раскладка 3-полосного EQ у самого WDSP (SetTXAGrphEQ) — её же
|
||||||
|
// отдаёт движок (WDSPEngine.PushTXEQProfile). Частоты фиксированы, ручка
|
||||||
|
// Low держит полку 150..400 Гц, High тянется до 6 кГц.
|
||||||
|
EQ3_F: array[1..3] of Double = (150.0, 1500.0, 6000.0);
|
||||||
|
|
||||||
constructor TEqualizerControl.Create(AOwner: TComponent);
|
constructor TEqualizerControl.Create(AOwner: TComponent);
|
||||||
var
|
var
|
||||||
@@ -300,7 +317,8 @@ begin
|
|||||||
else Result := FClrAccent;
|
else Result := FClrAccent;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TEqualizerControl.InternalBandFreq(Index: Integer): Double;
|
function TEqualizerControl.GridBandFreq(Index: Integer): Double;
|
||||||
|
// Узел 10-полосной сетки профиля (то, что реально хранится и сохраняется).
|
||||||
begin
|
begin
|
||||||
if (Index >= 1) and (Index <= 10) and (FFreqs[Index] > 0.0) then
|
if (Index >= 1) and (Index <= 10) and (FFreqs[Index] > 0.0) then
|
||||||
Result := FFreqs[Index]
|
Result := FFreqs[Index]
|
||||||
@@ -308,24 +326,113 @@ begin
|
|||||||
Result := MIN_FREQ * Power(MAX_FREQ / MIN_FREQ, EnsureRange((Index - 1) / 9, 0.0, 1.0));
|
Result := MIN_FREQ * Power(MAX_FREQ / MIN_FREQ, EnsureRange((Index - 1) / 9, 0.0, 1.0));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TEqualizerControl.ResponseAtFreq(AFreq: Double): Double;
|
function TEqualizerControl.InternalBandFreq(Index: Integer): Double;
|
||||||
var
|
// Частота, на которой ручка ДЕЙСТВУЕТ. В 3-полосном режиме это не наша сетка,
|
||||||
i, N: Integer;
|
// а legacy-раскладка WDSP; FFreqs при этом не трогается, поэтому возврат к
|
||||||
D, W, Sum, LF, LBand: Double;
|
// 10 полосам отдаёт пользователю его же кривую.
|
||||||
begin
|
begin
|
||||||
Result := FPreampGain;
|
if FBandCount = 3 then
|
||||||
LF := Ln(EnsureRange(AFreq, MIN_FREQ, MAX_FREQ));
|
Result := EQ3_F[EnsureRange(Index, 1, 3)]
|
||||||
Sum := 0.0;
|
else
|
||||||
N := FBandCount;
|
Result := GridBandFreq(Index);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TEqualizerControl.BuildResponse;
|
||||||
|
// Порт eq_impulse (wdsp/eq.c) в дБ, без построения самого FIR. Точки F/G —
|
||||||
|
// узлы ломаной: между ними линейная интерполяция ПО ДЕЦИБЕЛАМ, ниже первой и
|
||||||
|
// выше последней точки ctfmode=0 даёт кумулятивный скат (f/f0)^4 НА КАЖДЫЙ
|
||||||
|
// бин — то есть фактически обрыв. Именно поэтому включённый EQ работает как
|
||||||
|
// второй полосовой фильтр, и верхняя точка обязана лежать за краем TX-фильтра.
|
||||||
|
var
|
||||||
|
N, i, j, k, Lo, Hi: Integer;
|
||||||
|
Fp, Gp: array[0..11] of Double;
|
||||||
|
TF, TG, F, Frac, Preamp, MagDB, RefDB: Double;
|
||||||
|
begin
|
||||||
|
if Length(FRespDB) <> RESP_MID then SetLength(FRespDB, RESP_MID);
|
||||||
|
Preamp := FPreampGain;
|
||||||
|
if FBandCount = 3 then
|
||||||
|
begin
|
||||||
|
// Ровно то, что уходит в WDSP из PushTXEQProfile: 4 узла, полка на низах.
|
||||||
|
N := 4;
|
||||||
|
Fp[1] := 2.0 * 150.0 / RESP_RATE; Gp[1] := FGains[1];
|
||||||
|
Fp[2] := 2.0 * 400.0 / RESP_RATE; Gp[2] := FGains[1];
|
||||||
|
Fp[3] := 2.0 * 1500.0 / RESP_RATE; Gp[3] := FGains[2];
|
||||||
|
Fp[4] := 2.0 * 6000.0 / RESP_RATE; Gp[4] := FGains[3];
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
N := EnsureRange(FBandCount, 1, 10);
|
||||||
for i := 1 to N do
|
for i := 1 to N do
|
||||||
begin
|
begin
|
||||||
LBand := Ln(InternalBandFreq(i));
|
Fp[i] := EnsureRange(2.0 * InternalBandFreq(i) / RESP_RATE, 0.0, 1.0);
|
||||||
D := (LF - LBand) / 0.30;
|
Gp[i] := FGains[i];
|
||||||
W := Exp(-0.5 * D * D);
|
end;
|
||||||
Sum := Sum + FGains[i] * W;
|
end;
|
||||||
|
// WDSP сортирует точки по частоте (qsort) — порядок ручек значения не имеет.
|
||||||
|
for i := 1 to N - 1 do
|
||||||
|
for j := 1 to N - i do
|
||||||
|
if Fp[j] > Fp[j + 1] then
|
||||||
|
begin
|
||||||
|
TF := Fp[j]; Fp[j] := Fp[j + 1]; Fp[j + 1] := TF;
|
||||||
|
TG := Gp[j]; Gp[j] := Gp[j + 1]; Gp[j + 1] := TG;
|
||||||
|
end;
|
||||||
|
Fp[0] := 0.0; Gp[0] := Gp[1];
|
||||||
|
Fp[N + 1] := 1.0; Gp[N + 1] := Gp[N];
|
||||||
|
j := 0;
|
||||||
|
for i := 0 to RESP_MID - 1 do
|
||||||
|
begin
|
||||||
|
F := (i + 0.5) / RESP_MID;
|
||||||
|
while (j < N) and (F > Fp[j + 1]) do Inc(j);
|
||||||
|
if Fp[j + 1] > Fp[j] then Frac := (F - Fp[j]) / (Fp[j + 1] - Fp[j])
|
||||||
|
else Frac := 0.0;
|
||||||
|
FRespDB[i] := Frac * Gp[j + 1] + (1.0 - Frac) * Gp[j] + Preamp;
|
||||||
|
end;
|
||||||
|
// Скаты за крайними точками (ctfmode = 0, чётная ветка N). Множитель на бин
|
||||||
|
// — (f/f0)^4 в АМПЛИТУДЕ, в децибелах 80*log10(f/f0), и он накапливается.
|
||||||
|
Lo := Trunc(Fp[1] * RESP_MID - 0.5);
|
||||||
|
Hi := Trunc(Fp[N] * RESP_MID - 0.5);
|
||||||
|
if (Lo > 0) and (Lo < RESP_MID) then
|
||||||
|
begin
|
||||||
|
MagDB := FRespDB[Lo];
|
||||||
|
RefDB := 80.0 * Log10(Lo / RESP_MID);
|
||||||
|
for k := Lo - 1 downto 0 do
|
||||||
|
begin
|
||||||
|
if k > 0 then MagDB := MagDB + 80.0 * Log10(k / RESP_MID) - RefDB
|
||||||
|
else MagDB := RESP_FLOOR;
|
||||||
|
if MagDB < RESP_FLOOR then MagDB := RESP_FLOOR;
|
||||||
|
FRespDB[k] := MagDB;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
if (Hi >= 0) and (Hi < RESP_MID - 1) then
|
||||||
|
begin
|
||||||
|
MagDB := FRespDB[Hi];
|
||||||
|
RefDB := 80.0 * Log10(Hi / RESP_MID);
|
||||||
|
for k := Hi + 1 to RESP_MID - 1 do
|
||||||
|
begin
|
||||||
|
MagDB := MagDB + RefDB - 80.0 * Log10(k / RESP_MID);
|
||||||
|
if MagDB < RESP_FLOOR then MagDB := RESP_FLOOR;
|
||||||
|
FRespDB[k] := MagDB;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TEqualizerControl.ResponseAtFreq(AFreq: Double): Double;
|
||||||
|
// Значение готовой АЧХ на частоте: линейная интерполяция между центрами бинов.
|
||||||
|
var
|
||||||
|
Pos: Double;
|
||||||
|
K: Integer;
|
||||||
|
begin
|
||||||
|
if Length(FRespDB) <> RESP_MID then BuildResponse;
|
||||||
|
Pos := AFreq / RESP_BIN_HZ - 0.5;
|
||||||
|
if Pos <= 0.0 then
|
||||||
|
Result := FRespDB[0]
|
||||||
|
else if Pos >= RESP_MID - 1 then
|
||||||
|
Result := FRespDB[RESP_MID - 1]
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
K := Trunc(Pos);
|
||||||
|
Result := FRespDB[K] + (Pos - K) * (FRespDB[K + 1] - FRespDB[K]);
|
||||||
end;
|
end;
|
||||||
Result := Result + Sum;
|
|
||||||
Result := EnsureRange(Result, -24.0, 24.0);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TEqualizerControl.FormatFreq(AFreq: Double): string;
|
function TEqualizerControl.FormatFreq(AFreq: Double): string;
|
||||||
@@ -387,7 +494,10 @@ begin
|
|||||||
|
|
||||||
Canvas.Brush.Style := bsClear;
|
Canvas.Brush.Style := bsClear;
|
||||||
Canvas.Font.Color := FClrTextDim;
|
Canvas.Font.Color := FClrTextDim;
|
||||||
S := FormatFreq(InternalBandFreq(i));
|
// Активные плитки подписаны рабочей частотой, погашенные — своим узлом
|
||||||
|
// сетки: иначе в 3-полосном режиме все семь показывали бы 6 кГц.
|
||||||
|
if i <= FBandCount then S := FormatFreq(InternalBandFreq(i))
|
||||||
|
else S := FormatFreq(GridBandFreq(i));
|
||||||
Canvas.TextOut(TR.Left + 6, TR.Top + 34, S);
|
Canvas.TextOut(TR.Left + 6, TR.Top + 34, S);
|
||||||
Canvas.Font.Color := C;
|
Canvas.Font.Color := C;
|
||||||
Canvas.TextOut(TR.Left + 6, TR.Top + 49, FormatFloat('0.0', FGains[i]));
|
Canvas.TextOut(TR.Left + 6, TR.Top + 49, FormatFloat('0.0', FGains[i]));
|
||||||
@@ -527,6 +637,7 @@ begin
|
|||||||
Canvas.Pen.Color := FClrBorder;
|
Canvas.Pen.Color := FClrBorder;
|
||||||
Canvas.Line(R.Left + 1, ZeroY, R.Right - 1, ZeroY);
|
Canvas.Line(R.Left + 1, ZeroY, R.Right - 1, ZeroY);
|
||||||
|
|
||||||
|
BuildResponse; // кривая считается заново на каждую отрисовку (1024 бина)
|
||||||
Canvas.Brush.Color := TColor($00304438);
|
Canvas.Brush.Color := TColor($00304438);
|
||||||
Canvas.Pen.Color := TColor($00304438);
|
Canvas.Pen.Color := TColor($00304438);
|
||||||
FillBottom := ZeroY;
|
FillBottom := ZeroY;
|
||||||
@@ -537,7 +648,9 @@ begin
|
|||||||
begin
|
begin
|
||||||
F := XToFreq(GX, R);
|
F := XToFreq(GX, R);
|
||||||
G := ResponseAtFreq(F);
|
G := ResponseAtFreq(F);
|
||||||
GY := GainToY(EnsureRange(G, MIN_GAIN, MAX_GAIN), R);
|
// Клампа по ±15 дБ нет намеренно: за крайними точками WDSP обрывает АЧХ,
|
||||||
|
// и кривая обязана уходить в пол графика — иначе обрыв не виден.
|
||||||
|
GY := GainToY(G, R);
|
||||||
if GX = R.Left then
|
if GX = R.Left then
|
||||||
LastFillY := GY
|
LastFillY := GY
|
||||||
else
|
else
|
||||||
@@ -547,13 +660,13 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
PrevX := CurveL;
|
PrevX := CurveL;
|
||||||
PrevY := GainToY(EnsureRange(ResponseAtFreq(XToFreq(CurveL, R)), MIN_GAIN, MAX_GAIN), R);
|
PrevY := GainToY(ResponseAtFreq(XToFreq(CurveL, R)), R);
|
||||||
Canvas.Pen.Color := FClrCurve;
|
Canvas.Pen.Color := FClrCurve;
|
||||||
Canvas.Pen.Width := 2;
|
Canvas.Pen.Width := 2;
|
||||||
for GX := CurveL + 1 to CurveR do
|
for GX := CurveL + 1 to CurveR do
|
||||||
begin
|
begin
|
||||||
F := XToFreq(GX, R);
|
F := XToFreq(GX, R);
|
||||||
GY := GainToY(EnsureRange(ResponseAtFreq(F), MIN_GAIN, MAX_GAIN), R);
|
GY := GainToY(ResponseAtFreq(F), R);
|
||||||
Canvas.Line(PrevX, PrevY, GX, GY);
|
Canvas.Line(PrevX, PrevY, GX, GY);
|
||||||
PrevX := GX;
|
PrevX := GX;
|
||||||
PrevY := GY;
|
PrevY := GY;
|
||||||
@@ -621,7 +734,8 @@ begin
|
|||||||
begin
|
begin
|
||||||
FDragging := H;
|
FDragging := H;
|
||||||
BandGain[H] := YToGain(Y, GraphRect);
|
BandGain[H] := YToGain(Y, GraphRect);
|
||||||
BandFreq[H] := XToFreq(X, GraphRect);
|
// В 3-полосном режиме частоты задаёт WDSP (EQ3_F) — двигаем только гейн.
|
||||||
|
if FBandCount <> 3 then BandFreq[H] := XToFreq(X, GraphRect);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -636,7 +750,7 @@ begin
|
|||||||
else if FDragging > 0 then
|
else if FDragging > 0 then
|
||||||
begin
|
begin
|
||||||
BandGain[FDragging] := YToGain(Y, GraphRect);
|
BandGain[FDragging] := YToGain(Y, GraphRect);
|
||||||
BandFreq[FDragging] := XToFreq(X, GraphRect);
|
if FBandCount <> 3 then BandFreq[FDragging] := XToFreq(X, GraphRect);
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
|
|||||||
@@ -532,6 +532,7 @@ type
|
|||||||
procedure OnTXProfileAddFromSettings(const AName: string);
|
procedure OnTXProfileAddFromSettings(const AName: string);
|
||||||
procedure OnTXProfileRenameFromSettings(Idx: Integer; const AName: string);
|
procedure OnTXProfileRenameFromSettings(Idx: Integer; const AName: string);
|
||||||
procedure OnTXProfileDeleteFromSettings(Idx: Integer);
|
procedure OnTXProfileDeleteFromSettings(Idx: Integer);
|
||||||
|
procedure OnTXProfileFactoryFromSettings(Sender: TObject);
|
||||||
procedure OnChannelStoreChanged(Sender: TObject);
|
procedure OnChannelStoreChanged(Sender: TObject);
|
||||||
procedure OnChannelActiveChanged(Idx: Integer; const Name: string; Active: Boolean);
|
procedure OnChannelActiveChanged(Idx: Integer; const Name: string; Active: Boolean);
|
||||||
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
procedure PbSpectrumMouseDown(Sender: TObject; Button: TMouseButton;
|
||||||
@@ -5769,6 +5770,11 @@ begin
|
|||||||
FController.DeleteTXProfile(Idx);
|
FController.DeleteTXProfile(Idx);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TMainForm.OnTXProfileFactoryFromSettings(Sender: TObject);
|
||||||
|
begin
|
||||||
|
FController.ResetTXProfilesToFactory;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TMainForm.OnChannelActiveChanged(Idx: Integer; const Name: string; Active: Boolean);
|
procedure TMainForm.OnChannelActiveChanged(Idx: Integer; const Name: string; Active: Boolean);
|
||||||
begin
|
begin
|
||||||
// Рендер кнопки Channel по событию адаптера каналов (Active=True → подсветка+имя).
|
// Рендер кнопки Channel по событию адаптера каналов (Active=True → подсветка+имя).
|
||||||
@@ -8778,6 +8784,7 @@ begin
|
|||||||
SF.OnTXProfileAdd := OnTXProfileAddFromSettings;
|
SF.OnTXProfileAdd := OnTXProfileAddFromSettings;
|
||||||
SF.OnTXProfileRename := OnTXProfileRenameFromSettings;
|
SF.OnTXProfileRename := OnTXProfileRenameFromSettings;
|
||||||
SF.OnTXProfileDelete := OnTXProfileDeleteFromSettings;
|
SF.OnTXProfileDelete := OnTXProfileDeleteFromSettings;
|
||||||
|
SF.OnTXProfileFactory := OnTXProfileFactoryFromSettings;
|
||||||
SF.OnAlexChange := OnAlexSettingsChange;
|
SF.OnAlexChange := OnAlexSettingsChange;
|
||||||
SF.OnXvtrChange := OnXvtrSettingsChange;
|
SF.OnXvtrChange := OnXvtrSettingsChange;
|
||||||
SF.OnOCChange := OnOCSettingsChange;
|
SF.OnOCChange := OnOCSettingsChange;
|
||||||
|
|||||||
@@ -595,6 +595,7 @@ type
|
|||||||
function AddTXProfile(const AName: string): Integer; // форк текущего звука
|
function AddTXProfile(const AName: string): Integer; // форк текущего звука
|
||||||
procedure RenameTXProfile(Idx: Integer; const AName: string);
|
procedure RenameTXProfile(Idx: Integer; const AName: string);
|
||||||
procedure DeleteTXProfile(Idx: Integer);
|
procedure DeleteTXProfile(Idx: Integer);
|
||||||
|
procedure ResetTXProfilesToFactory; // заводской набор заново
|
||||||
procedure SaveTXProfilesToConfig;
|
procedure SaveTXProfilesToConfig;
|
||||||
|
|
||||||
// ---- Управление диапазонами (состояние + DSP; рендер через события) ----
|
// ---- Управление диапазонами (состояние + DSP; рендер через события) ----
|
||||||
@@ -5046,6 +5047,22 @@ begin
|
|||||||
Changed(rfTXProfile);
|
Changed(rfTXProfile);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TRadioController.ResetTXProfilesToFactory;
|
||||||
|
// Пересобирает заводской набор поверх текущего списка. База — ЖИВЫЕ настройки,
|
||||||
|
// поэтому «Default» = микрофонный вход, полоса, динамика и мощность как сейчас,
|
||||||
|
// но с заводским (ровным) EQ. Единственный способ доехать до обновлённых
|
||||||
|
// заводских профилей с уже записанной секцией tx_profiles: она читается, а не
|
||||||
|
// пересоздаётся.
|
||||||
|
begin
|
||||||
|
DefaultTXProfiles(FTXSettings, FDrivePercent, FTXProfiles);
|
||||||
|
// ActiveIdx гасим перед Select: иначе StoreActiveTXProfile внутри него
|
||||||
|
// впишет в свежий «Default» живой EQ и сброс тембра тут же отменится.
|
||||||
|
FTXProfiles.ActiveIdx := -1;
|
||||||
|
// Применяем «Default» в эфир — иначе живой EQ остался бы кривым, а профиль
|
||||||
|
// говорил бы «ровный», и первая же правка TX-настроек вернула бы кривую.
|
||||||
|
SelectTXProfile(0); // сам сохранит и отрендерит
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TRadioController.ApplyTXSettingsToDSP;
|
procedure TRadioController.ApplyTXSettingsToDSP;
|
||||||
begin
|
begin
|
||||||
if not FWDSPReady then Exit;
|
if not FWDSPReady then Exit;
|
||||||
|
|||||||
+76
-13
@@ -1536,6 +1536,14 @@ begin
|
|||||||
end;
|
end;
|
||||||
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)
|
// TX settings (per-device, JSON-секция "tx" под MAC)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1543,12 +1551,6 @@ end;
|
|||||||
class procedure TSettingsManager.DefaultTX(out T: TTXSettings);
|
class procedure TSettingsManager.DefaultTX(out T: TTXSettings);
|
||||||
var
|
var
|
||||||
i: Integer;
|
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
|
begin
|
||||||
FillChar(T, SizeOf(T), 0);
|
FillChar(T, SizeOf(T), 0);
|
||||||
T.MicLineIn := False;
|
T.MicLineIn := False;
|
||||||
@@ -1590,7 +1592,7 @@ begin
|
|||||||
for i := 0 to 10 do
|
for i := 0 to 10 do
|
||||||
begin
|
begin
|
||||||
T.EQGains[i] := 0.0;
|
T.EQGains[i] := 0.0;
|
||||||
T.EQFreqs[i] := DefaultEQFreqs[i];
|
T.EQFreqs[i] := DEF_EQ_FREQS[i];
|
||||||
end;
|
end;
|
||||||
// AM
|
// AM
|
||||||
T.AMCarrierLevel := 0.5;
|
T.AMCarrierLevel := 0.5;
|
||||||
@@ -1862,6 +1864,36 @@ end;
|
|||||||
|
|
||||||
procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer;
|
procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer;
|
||||||
out L: TTXProfileList);
|
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;
|
procedure Add(const AName: string; ALow, AHigh: Integer;
|
||||||
AComp: Boolean; ACompGain: Double);
|
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].FilterHigh := AHigh;
|
||||||
L.Items[L.Count].CompressorOn := AComp;
|
L.Items[L.Count].CompressorOn := AComp;
|
||||||
L.Items[L.Count].CompressorGain := ACompGain;
|
L.Items[L.Count].CompressorGain := ACompGain;
|
||||||
|
FlatEQ(L.Items[L.Count]);
|
||||||
Inc(L.Count);
|
Inc(L.Count);
|
||||||
end;
|
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
|
begin
|
||||||
FillChar(L, SizeOf(L), 0);
|
FillChar(L, SizeOf(L), 0);
|
||||||
L.Count := 0;
|
L.Count := 0;
|
||||||
L.ActiveIdx := 0;
|
L.ActiveIdx := 0;
|
||||||
// «Default» — ровно текущее состояние устройства: после миграции старого
|
// «Default» — текущее состояние устройства (микрофонный вход, полоса,
|
||||||
// конфига звук в эфире не меняется ни на йоту.
|
// динамика, мощность), но с заводским EQ: после миграции старого конфига
|
||||||
|
// звук в эфире не меняется, а сброс к заводским возвращает ровный тембр.
|
||||||
TXProfileCapture(Base, DrivePct, L.Items[0]);
|
TXProfileCapture(Base, DrivePct, L.Items[0]);
|
||||||
L.Items[0].Name := 'Default';
|
L.Items[0].Name := 'Default';
|
||||||
|
FlatEQ(L.Items[0]);
|
||||||
L.Count := 1;
|
L.Count := 1;
|
||||||
// Остальные заводские отличаются от Default ТОЛЬКО полосой и компрессором;
|
// SSB-профили отличаются от Default только полосой и компрессором;
|
||||||
// микрофонный вход, EQ и мощность наследуются (см. коммент к DefaultTXProfiles).
|
// микрофонный вход и мощность наследуются, EQ у всех троих заводской-ровный.
|
||||||
|
// ESSB несут ещё и свою кривую: без неё «широкий» профиль звучит тем же
|
||||||
|
// телефонным тембром, а с чужой сеткой (верхний узел 3500) EQ обрезал бы
|
||||||
|
// верх ESSB 5k.
|
||||||
Add('SSB DX', 300, 2700, True, 8.0);
|
Add('SSB DX', 300, 2700, True, 8.0);
|
||||||
Add('SSB Wide', 200, 3100, False, 5.0);
|
Add('SSB Wide', 200, 3100, False, 5.0);
|
||||||
Add('ESSB 3.5k', 100, 3500, False, 5.0);
|
AddEQ('ESSB 3.5k', 100, 3500, False, 5.0, ESSB35_F, ESSB35_G, ESSB35_PRE);
|
||||||
Add('ESSB 5k', 100, 5000, False, 5.0);
|
AddEQ('ESSB 5k', 100, 5000, False, 5.0, ESSB50_F, ESSB50_G, ESSB50_PRE);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TSettingsManager.LoadTXProfiles(const MAC: array of Byte;
|
function TSettingsManager.LoadTXProfiles(const MAC: array of Byte;
|
||||||
|
|||||||
+34
-3
@@ -226,6 +226,7 @@ type
|
|||||||
FBtnTXProfNew: TFlatButton;
|
FBtnTXProfNew: TFlatButton;
|
||||||
FBtnTXProfRen: TFlatButton;
|
FBtnTXProfRen: TFlatButton;
|
||||||
FBtnTXProfDel: TFlatButton;
|
FBtnTXProfDel: TFlatButton;
|
||||||
|
FBtnTXProfFactory: TFlatButton;
|
||||||
FLblTXProfDrive: TLabel; // «Drive профиля: N %» (ползунок — в главном окне)
|
FLblTXProfDrive: TLabel; // «Drive профиля: N %» (ползунок — в главном окне)
|
||||||
FCmbTXMicJack: TFlatComboBox; // "Mic In" / "Line In"
|
FCmbTXMicJack: TFlatComboBox; // "Mic In" / "Line In"
|
||||||
FChkTXMicBoost: TFlatCheckBox; // +20 dB (только Mic In)
|
FChkTXMicBoost: TFlatCheckBox; // +20 dB (только Mic In)
|
||||||
@@ -438,6 +439,7 @@ type
|
|||||||
FOnTXProfileAdd: TOnTXProfileName;
|
FOnTXProfileAdd: TOnTXProfileName;
|
||||||
FOnTXProfileRename: TOnTXProfileIdxName;
|
FOnTXProfileRename: TOnTXProfileIdxName;
|
||||||
FOnTXProfileDelete: TOnTXProfileIdx;
|
FOnTXProfileDelete: TOnTXProfileIdx;
|
||||||
|
FOnTXProfileFactory: TNotifyEvent;
|
||||||
|
|
||||||
FLoading: Boolean;
|
FLoading: Boolean;
|
||||||
|
|
||||||
@@ -496,6 +498,7 @@ type
|
|||||||
procedure BtnTXProfNewClick(Sender: TObject);
|
procedure BtnTXProfNewClick(Sender: TObject);
|
||||||
procedure BtnTXProfRenClick(Sender: TObject);
|
procedure BtnTXProfRenClick(Sender: TObject);
|
||||||
procedure BtnTXProfDelClick(Sender: TObject);
|
procedure BtnTXProfDelClick(Sender: TObject);
|
||||||
|
procedure BtnTXProfFactoryClick(Sender: TObject);
|
||||||
procedure ApplyTheme(const T: TAppTheme);
|
procedure ApplyTheme(const T: TAppTheme);
|
||||||
procedure NavClick(Sender: TObject);
|
procedure NavClick(Sender: TObject);
|
||||||
procedure SelectPage(APage: TWinControl; ANav: TFlatButton);
|
procedure SelectPage(APage: TWinControl; ANav: TFlatButton);
|
||||||
@@ -655,6 +658,7 @@ type
|
|||||||
property OnTXProfileAdd: TOnTXProfileName read FOnTXProfileAdd write FOnTXProfileAdd;
|
property OnTXProfileAdd: TOnTXProfileName read FOnTXProfileAdd write FOnTXProfileAdd;
|
||||||
property OnTXProfileRename: TOnTXProfileIdxName read FOnTXProfileRename write FOnTXProfileRename;
|
property OnTXProfileRename: TOnTXProfileIdxName read FOnTXProfileRename write FOnTXProfileRename;
|
||||||
property OnTXProfileDelete: TOnTXProfileIdx read FOnTXProfileDelete write FOnTXProfileDelete;
|
property OnTXProfileDelete: TOnTXProfileIdx read FOnTXProfileDelete write FOnTXProfileDelete;
|
||||||
|
property OnTXProfileFactory: TNotifyEvent read FOnTXProfileFactory write FOnTXProfileFactory;
|
||||||
property OnWfAGCNFChange: TOnWfAGCNFChange read FOnWfAGCNFChange write FOnWfAGCNFChange;
|
property OnWfAGCNFChange: TOnWfAGCNFChange read FOnWfAGCNFChange write FOnWfAGCNFChange;
|
||||||
property OnADCChange: TOnADCChange read FOnADCChange write FOnADCChange;
|
property OnADCChange: TOnADCChange read FOnADCChange write FOnADCChange;
|
||||||
property OnWebSettingsChange: TOnWebSettingsChange read FOnWebSettingsChange write FOnWebSettingsChange;
|
property OnWebSettingsChange: TOnWebSettingsChange read FOnWebSettingsChange write FOnWebSettingsChange;
|
||||||
@@ -1514,10 +1518,12 @@ begin
|
|||||||
FBtnTXProfNew := MkBtn(Grp, PAD + 302, R1, 100, 'New...', BtnTXProfNewClick);
|
FBtnTXProfNew := MkBtn(Grp, PAD + 302, R1, 100, 'New...', BtnTXProfNewClick);
|
||||||
FBtnTXProfRen := MkBtn(Grp, PAD + 408, R1, 100, 'Rename...', BtnTXProfRenClick);
|
FBtnTXProfRen := MkBtn(Grp, PAD + 408, R1, 100, 'Rename...', BtnTXProfRenClick);
|
||||||
FBtnTXProfDel := MkBtn(Grp, PAD + 514, R1, 100, 'Delete', BtnTXProfDelClick);
|
FBtnTXProfDel := MkBtn(Grp, PAD + 514, R1, 100, 'Delete', BtnTXProfDelClick);
|
||||||
|
FBtnTXProfFactory := MkBtn(Grp, PAD + 620, R1, 100, 'Factory', BtnTXProfFactoryClick);
|
||||||
|
|
||||||
MakeLbl(Grp, 'Правки на вкладках Profile и EQ пишутся в активный профиль сразу — '
|
MakeLbl(Grp, 'Правки на вкладках Profile и EQ пишутся в активный профиль сразу — '
|
||||||
+ 'отдельной кнопки «сохранить» нет.', PAD, R1 + 30, GRP_W - 2 * PAD);
|
+ 'отдельной кнопки «сохранить» нет.', PAD, R1 + 30, GRP_W - 2 * PAD);
|
||||||
MakeLbl(Grp, 'New... создаёт копию текущего звука под новым именем.',
|
MakeLbl(Grp, 'New... создаёт копию текущего звука под новым именем. '
|
||||||
|
+ 'Factory — заводской набор заново, эквалайзер в ноль.',
|
||||||
PAD, R1 + 48, GRP_W - 2 * PAD);
|
PAD, R1 + 48, GRP_W - 2 * PAD);
|
||||||
|
|
||||||
// ---- Сегменты подвкладок ------------------------------------------------
|
// ---- Сегменты подвкладок ------------------------------------------------
|
||||||
@@ -1627,7 +1633,7 @@ begin
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
SubPage(1, SubTop);
|
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 := TEqualizerControl.Create(Self);
|
||||||
FTXEQControl.Parent := Grp;
|
FTXEQControl.Parent := Grp;
|
||||||
@@ -1638,7 +1644,17 @@ begin
|
|||||||
TColor($00383838), CLR_TEXT, CLR_TEXTDIM, CLR_ACCENT, TColor($0055DDEE));
|
TColor($00383838), CLR_TEXT, CLR_TEXTDIM, CLR_ACCENT, TColor($0055DDEE));
|
||||||
FTXEQControl.OnChange := OnTXAnyChange;
|
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: свойства устройства, профилем не переключаются
|
// Подвкладка 2 — Hardware: свойства устройства, профилем не переключаются
|
||||||
@@ -1953,6 +1969,21 @@ begin
|
|||||||
if Assigned(FOnTXProfileDelete) then FOnTXProfileDelete(Idx);
|
if Assigned(FOnTXProfileDelete) then FOnTXProfileDelete(Idx);
|
||||||
end;
|
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;
|
procedure TSettingsForm.LoadTXProfiles(const Names: array of string;
|
||||||
ActiveIdx, DrivePct: Integer);
|
ActiveIdx, DrivePct: Integer);
|
||||||
var
|
var
|
||||||
|
|||||||
+42
-17
@@ -554,6 +554,7 @@ type
|
|||||||
procedure ProcessTXBlock; // вызывается из TTXDSPThread
|
procedure ProcessTXBlock; // вызывается из TTXDSPThread
|
||||||
procedure FeedTXDisplaySample(const I, Q: Double);
|
procedure FeedTXDisplaySample(const I, Q: Double);
|
||||||
procedure ApplyTXChainSettings; // прокидывает все FTX* поля в WDSP
|
procedure ApplyTXChainSettings; // прокидывает все FTX* поля в WDSP
|
||||||
|
procedure PushTXEQProfile; // кривая EQ в WDSP (узлы + legacy 3 полосы)
|
||||||
procedure ApplyPSParams; // прокидывает FPS*-параметры в WDSP calcc
|
procedure ApplyPSParams; // прокидывает FPS*-параметры в WDSP calcc
|
||||||
procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID
|
procedure ApplyRXAnalyzerSettings; // применяет RX FFT/Window/Det/Avg к RX_DISP_ID
|
||||||
procedure ApplyTXAnalyzerSettings; // применяет TX FFT/Window/Det/Avg к TX_DISP_ID
|
procedure ApplyTXAnalyzerSettings; // применяет TX FFT/Window/Det/Avg к TX_DISP_ID
|
||||||
@@ -3542,8 +3543,6 @@ procedure TWDSPEngine.ApplyTXChainSettings;
|
|||||||
// Вызывается после OpenChannel(TXA) и при старте TX. Прокидывает все
|
// Вызывается после OpenChannel(TXA) и при старте TX. Прокидывает все
|
||||||
// FTX*-поля в WDSP. Идемпотентна, ничего не меняет если канал не открыт.
|
// FTX*-поля в WDSP. Идемпотентна, ничего не меняет если канал не открыт.
|
||||||
var
|
var
|
||||||
i: Integer;
|
|
||||||
Gains, Freqs, Qs: array[0..10] of Double;
|
|
||||||
BpLo, BpHi: Double;
|
BpLo, BpHi: Double;
|
||||||
begin
|
begin
|
||||||
if not FInitialized then Exit;
|
if not FInitialized then Exit;
|
||||||
@@ -3584,15 +3583,8 @@ begin
|
|||||||
SetTXAPHROTNstages(TXA_CHAN, FTXPhaseRotStages);
|
SetTXAPHROTNstages(TXA_CHAN, FTXPhaseRotStages);
|
||||||
SetTXAPHROTCorner(TXA_CHAN, FTXPhaseRotFreq);
|
SetTXAPHROTCorner(TXA_CHAN, FTXPhaseRotFreq);
|
||||||
|
|
||||||
// EQ TX (10-band: [0]=preamp, [1..10]=bands)
|
// EQ TX ([0]=preamp, [1..10]=точки кривой)
|
||||||
for i := 0 to 10 do
|
PushTXEQProfile;
|
||||||
begin
|
|
||||||
Gains[i] := FTXEQGains[i];
|
|
||||||
Freqs[i] := FTXEQFreqs[i];
|
|
||||||
Qs[i] := 0.707; // умолчание Q
|
|
||||||
end;
|
|
||||||
SetTXAEQProfile(TXA_CHAN, FTXEQNumBands, @Freqs[0], @Gains[0]);
|
|
||||||
SetTXAEQRun(TXA_CHAN, Ord(FTXEQOn));
|
|
||||||
|
|
||||||
// AM
|
// AM
|
||||||
SetTXAAMCarrierLevel(TXA_CHAN, FTXAMCarrier);
|
SetTXAAMCarrierLevel(TXA_CHAN, FTXAMCarrier);
|
||||||
@@ -3791,11 +3783,48 @@ begin
|
|||||||
SetTXAPHROTRun(TXA_CHAN, Ord(Enable));
|
SetTXAPHROTRun(TXA_CHAN, Ord(Enable));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TWDSPEngine.PushTXEQProfile;
|
||||||
|
// Единственная точка, отдающая кривую EQ в WDSP.
|
||||||
|
// F[1..n] — УЗЛЫ ломаной АЧХ (eq.c/eq_impulse), между ними интерполяция по
|
||||||
|
// децибелам, а за крайними узлами при ctfmode=0 (так создаётся eqp в TXA.c)
|
||||||
|
// идёт кумулятивный скат (f/f0)^4 на бин — фактически обрыв. Поэтому:
|
||||||
|
// • 10 полос — узлы берём из профиля как есть (за краями обязан быть запас,
|
||||||
|
// см. заводские ESSB-профили в Settings.DefaultTXProfiles);
|
||||||
|
// • 3 полосы — legacy-раскладка самого WDSP (SetTXAGrphEQ): 150/400/1500/
|
||||||
|
// 6000, ручка Low держит полку 150..400, High тянет до 6 кГц. Отдавать
|
||||||
|
// здесь первые три узла 10-полосной сетки (100/200/400) нельзя — весь
|
||||||
|
// голос выше 400 Гц ушёл бы в ноль.
|
||||||
|
var
|
||||||
|
F, G: array[0..10] of Double;
|
||||||
|
i, NF: Integer;
|
||||||
|
begin
|
||||||
|
if not FInitialized then Exit;
|
||||||
|
if FTXEQNumBands = 3 then
|
||||||
|
begin
|
||||||
|
NF := 4;
|
||||||
|
F[0] := 0.0; G[0] := FTXEQGains[0]; // preamp
|
||||||
|
F[1] := 150.0; G[1] := FTXEQGains[1]; // Low
|
||||||
|
F[2] := 400.0; G[2] := FTXEQGains[1];
|
||||||
|
F[3] := 1500.0; G[3] := FTXEQGains[2]; // Mid
|
||||||
|
F[4] := 6000.0; G[4] := FTXEQGains[3]; // High
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
NF := 10;
|
||||||
|
for i := 0 to 10 do
|
||||||
|
begin
|
||||||
|
F[i] := FTXEQFreqs[i];
|
||||||
|
G[i] := FTXEQGains[i];
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
SetTXAEQProfile(TXA_CHAN, NF, @F[0], @G[0]);
|
||||||
|
SetTXAEQRun(TXA_CHAN, Ord(FTXEQOn));
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.SetTXEQ(Enable: Boolean; NumBands: Integer;
|
procedure TWDSPEngine.SetTXEQ(Enable: Boolean; NumBands: Integer;
|
||||||
const Gains, Freqs: array of Double);
|
const Gains, Freqs: array of Double);
|
||||||
var
|
var
|
||||||
i, n: Integer;
|
i, n: Integer;
|
||||||
G, F: array[0..10] of Double;
|
|
||||||
begin
|
begin
|
||||||
FTXEQOn := Enable;
|
FTXEQOn := Enable;
|
||||||
FTXEQNumBands := NumBands;
|
FTXEQNumBands := NumBands;
|
||||||
@@ -3804,12 +3833,8 @@ begin
|
|||||||
begin
|
begin
|
||||||
FTXEQGains[i] := Gains[i];
|
FTXEQGains[i] := Gains[i];
|
||||||
FTXEQFreqs[i] := Freqs[i];
|
FTXEQFreqs[i] := Freqs[i];
|
||||||
G[i] := Gains[i];
|
|
||||||
F[i] := Freqs[i];
|
|
||||||
end;
|
end;
|
||||||
if not FInitialized then Exit;
|
PushTXEQProfile;
|
||||||
SetTXAEQProfile(TXA_CHAN, NumBands, @F[0], @G[0]);
|
|
||||||
SetTXAEQRun(TXA_CHAN, Ord(Enable));
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWDSPEngine.SetTXAMCarrierLevel(Level: Double);
|
procedure TWDSPEngine.SetTXAMCarrierLevel(Level: Double);
|
||||||
|
|||||||
Reference in New Issue
Block a user