feat(tx): TX-профили + вкладка Transmit на подвкладках

Профиль — именованный снимок «как я звучу», переключаемый одним кликом
(идиома FlexRadio, состав по таблице TXProfile у Thetis).

Граница «профиль ↔ устройство»:
- в профиль: микрофонный вход (jack/boost/bias/PTT/Tip-Ring/Line-in gain),
  MicGain, кромки TX-фильтра, компрессор/leveler/ALC/phase rotator, EQ,
  AM carrier, Drive и Tune level;
- у устройства остаются ATT on TX, FIR/фаза/окно фильтра (это задержка
  тракта, а не тембр), TUNFreq, FM/CTCSS, TX Display, TX Grid и все
  параметры PureSignal — смена профиля не должна перерисовывать спектр
  и ломать калибровку PS.

Кнопки «сохранить» нет: активный профиль ведётся за живым состоянием
(StoreActiveTXProfile из OnTXSettingsChange и SetDrive) — как и все
остальные настройки в EWSDR, применяемые мгновенно.

Settings.pas: TTXProfile/TTXProfileList, TXProfileCapture/TXProfileApply
(единственные две точки, знающие состав профиля), DefaultTXProfiles
(Default/SSB DX/SSB Wide/ESSB 3.5k/ESSB 5k — наследуют mic-вход и
мощность от текущих настроек), Load/SaveTXProfiles (секция tx_profiles
под MAC). Секции нет — заводской набор строится из текущих настроек,
поэтому миграция старого конфига не меняет звук.

RadioController: FTXProfiles, rfTXProfile, SelectTXProfile (снимок → DSP
+ перепосыл DUC Specific при смене mic-байта + SetDrive + персист),
Add/Rename/Delete (последний профиль не удаляется).

MainForm: кнопка профиля в свободных колонках ряда DUP/RX MUTE — блок TX
не вырос по высоте; ЛКМ — список, ПКМ — Settings→Transmit→Profile.

SettingsForm: страница Transmit разбита на подвкладки Profile / EQ /
Hardware / Display + полоса профиля сверху; свиток 2000 px вместо этого
раскладывается на четыре коротких экрана.

Проверено: round-trip персиста отдельным тестом (все поля, активный
индекс, усечение списка) и offscreen-снимки всех четырёх подвкладок.
На железе не проверено.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 14:21:09 +03:00
co-authored by Claude Opus 5
parent 24890ca8a7
commit 71a97b63f0
4 changed files with 940 additions and 71 deletions
+333 -70
View File
@@ -31,6 +31,13 @@ uses
FlatRadioButton,
EqualizerControl, AudioOutput, AudioInput, AppTheme, Settings, BoardUtils, DpiUtils;
const
// Подвкладки страницы Transmit: Profile / EQ / Hardware / Display.
TXTAB_COUNT = 4;
// Tag контейнера подвкладки: ApplyTheme красит такие панели фоном страницы,
// а не цветом группы (иначе подложка выглядела бы как одна гигантская группа).
TAG_PAGE_BG = 909;
type
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
TOnPASettingsChange = procedure(MaxPower: Double;
@@ -45,6 +52,10 @@ type
TOnCalibrationChange = procedure(const C: TCalibration) of object;
// TX-настройки целиком (любая правка → один callback с актуальной структурой)
TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
// TX-профили: выбор/переименование/удаление по индексу, создание — по имени.
TOnTXProfileIdx = procedure(Idx: Integer) of object;
TOnTXProfileName = procedure(const AName: string) of object;
TOnTXProfileIdxName = procedure(Idx: Integer; const AName: string) of object;
TOnFreqMhzDigitsChange = procedure(Digits: Integer) of object;
TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
@@ -204,6 +215,18 @@ type
// ---- Transmit tab controls ----
// Текущее состояние, отдаётся в OnTXChange целиком при любой правке.
FTX: TTXSettings;
// Подвкладки страницы Transmit: Profile / EQ / Hardware / Display.
// Первые две — то, что хранится в TX-профиле, вторые две — свойства
// устройства и экрана (от профиля не зависят). См. TTXProfile в Settings.
FTXSubPage: array[0..TXTAB_COUNT-1] of TPanel;
FTXSubBtn: array[0..TXTAB_COUNT-1] of TFlatButton;
FTXSubIdx: Integer;
// Полоса профиля наверху страницы
FCmbTXProfile: TFlatComboBox;
FBtnTXProfNew: TFlatButton;
FBtnTXProfRen: TFlatButton;
FBtnTXProfDel: TFlatButton;
FLblTXProfDrive: TLabel; // «Drive профиля: N %» (ползунок — в главном окне)
FCmbTXMicJack: TFlatComboBox; // "Mic In" / "Line In"
FChkTXMicBoost: TFlatCheckBox; // +20 dB (только Mic In)
FLblLineInGain: TLabel; // метка "Line In gain" (только Line In)
@@ -411,6 +434,10 @@ type
FOnWfAGCNFChange: TOnWfAGCNFChange;
FOnFreqMhzDigitsChange: TOnFreqMhzDigitsChange;
FOnTXChange: TOnTXSettingsChange;
FOnTXProfileSelect: TOnTXProfileIdx;
FOnTXProfileAdd: TOnTXProfileName;
FOnTXProfileRename: TOnTXProfileIdxName;
FOnTXProfileDelete: TOnTXProfileIdx;
FLoading: Boolean;
@@ -462,6 +489,13 @@ type
procedure OnTXAnyChange(Sender: TObject);
procedure OnMicJackChange(Sender: TObject);
procedure UpdateMicJackVisibility;
// Подвкладки и полоса профиля на странице Transmit
procedure SelectTXSubPage(Idx: Integer);
procedure TXSubTabClick(Sender: TObject);
procedure OnTXProfileComboChange(Sender: TObject);
procedure BtnTXProfNewClick(Sender: TObject);
procedure BtnTXProfRenClick(Sender: TObject);
procedure BtnTXProfDelClick(Sender: TObject);
procedure ApplyTheme(const T: TAppTheme);
procedure NavClick(Sender: TObject);
procedure SelectPage(APage: TWinControl; ANav: TFlatButton);
@@ -571,6 +605,15 @@ type
const SerAndromeda: array of Boolean;
TcpEnabled: Boolean; TcpPort: Integer);
procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
// Список TX-профилей + активный + drive активного (ползунок DRV живёт в
// главном окне, здесь показываем его значение как часть профиля).
procedure LoadTXProfiles(const Names: array of string;
ActiveIdx, DrivePct: Integer);
// Живое обновление подписи «Drive профиля» (ползунок DRV в главном окне).
procedure SetTXProfileDrive(DrivePct: Integer);
// Открыть диалог сразу на странице Transmit (ПКМ по кнопке профиля).
// SubIdx: 0=Profile, 1=EQ, 2=Hardware, 3=Display.
procedure ShowTransmitPage(SubIdx: Integer = 0);
procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
procedure LoadXvtrSettings(const X: TXvtrSettings);
procedure LoadOCSettings(const O: TOCSettings);
@@ -608,6 +651,10 @@ type
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
property OnFreqMhzDigitsChange: TOnFreqMhzDigitsChange read FOnFreqMhzDigitsChange write FOnFreqMhzDigitsChange;
property OnTXChange: TOnTXSettingsChange read FOnTXChange write FOnTXChange;
property OnTXProfileSelect: TOnTXProfileIdx read FOnTXProfileSelect write FOnTXProfileSelect;
property OnTXProfileAdd: TOnTXProfileName read FOnTXProfileAdd write FOnTXProfileAdd;
property OnTXProfileRename: TOnTXProfileIdxName read FOnTXProfileRename write FOnTXProfileRename;
property OnTXProfileDelete: TOnTXProfileIdx read FOnTXProfileDelete write FOnTXProfileDelete;
property OnWfAGCNFChange: TOnWfAGCNFChange read FOnWfAGCNFChange write FOnWfAGCNFChange;
property OnADCChange: TOnADCChange read FOnADCChange write FOnADCChange;
property OnWebSettingsChange: TOnWebSettingsChange read FOnWebSettingsChange write FOnWebSettingsChange;
@@ -1352,6 +1399,40 @@ const
var
Grp: TPanel;
Y, i: Integer;
SubTop: Integer; // общий (немасштабированный) Top всех четырёх подвкладок
// Подложка подвкладки: страница Transmit — четыре независимых экрана,
// переключаемых сегментами (свиток на 2000 px не листают, в нём ищут).
function SubPage(Idx: Integer; ATop: Integer): TPanel;
begin
Result := TPanel.Create(Self);
Result.Parent := FPageTransmit;
Result.BevelOuter := bvNone;
Result.Color := CLR_BG;
Result.Tag := TAG_PAGE_BG; // ApplyTheme: фон страницы, не группа
Result.SetBounds(0, DpiScale(ATop),
DpiScale(GRP_W + 2 * MARGIN), DpiScale(400));
Result.Visible := False;
FTXSubPage[Idx] := Result;
end;
function MkBtn(AParent: TWinControl; AX, AY, AW: Integer;
const ACap: string; AClick: TNotifyEvent): TFlatButton;
begin
Result := TFlatButton.Create(Self);
Result.Parent := AParent;
Result.Caption := ACap;
Result.SetBounds(DpiScale(AX), DpiScale(AY), DpiScale(AW), DpiScale(BTN_H));
Result.ClrNorm := CLR_PANEL;
Result.ClrBorder := CLR_BORDER;
Result.ClrHot := TColor($00242424);
Result.ClrActive := TColor($00204030);
Result.ClrText := CLR_TEXT;
Result.ClrTextAct := CLR_ACCENT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnClick := AClick;
end;
function MkSpin(AParent: TWinControl; AX, AY: Integer; AMin, AMax, AVal: Integer): TFlatSpinEdit;
begin
@@ -1417,17 +1498,46 @@ begin
with TLabel.Create(Self) do
begin
Parent := FPageTransmit;
Caption := 'TX-цепь WDSP: микрофон, фильтр, динамическая обработка, модуляция.';
SetBounds(DpiScale(MARGIN), DpiScale(48), DpiScale(700), DpiScale(20));
Caption := 'Profile и EQ — содержимое активного TX-профиля; '
+ 'Hardware и Display — свойства устройства, от профиля не зависят.';
SetBounds(DpiScale(MARGIN), DpiScale(48), DpiScale(GRP_W), DpiScale(20));
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
// ---- Mic options ----
// Mic source (Radio vs SoundCard) определяется автоматически по источнику PTT.
Y := 86;
Grp := MakeGroupPanel(FPageTransmit, 'Microphone', MARGIN, Y, GRP_W, 188);
// ---- Полоса профиля (над подвкладками, видна всегда) --------------------
Grp := MakeGroupPanel(FPageTransmit, 'TX Profile', MARGIN, 78, GRP_W, 124);
MakeLbl(Grp, 'Active', PAD, R1 + 5, 60);
FCmbTXProfile := MakeCombo(Grp, PAD + 60, R1, 230, OnTXProfileComboChange);
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);
MakeLbl(Grp, 'Правки на вкладках Profile и EQ пишутся в активный профиль сразу — '
+ 'отдельной кнопки «сохранить» нет.', PAD, R1 + 30, GRP_W - 2 * PAD);
MakeLbl(Grp, 'New... создаёт копию текущего звука под новым именем.',
PAD, R1 + 48, GRP_W - 2 * PAD);
// ---- Сегменты подвкладок ------------------------------------------------
Y := 78 + 124 + 12;
FTXSubBtn[0] := MkBtn(FPageTransmit, MARGIN, Y, 132, 'Profile', TXSubTabClick);
FTXSubBtn[1] := MkBtn(FPageTransmit, MARGIN + 136, Y, 132, 'EQ', TXSubTabClick);
FTXSubBtn[2] := MkBtn(FPageTransmit, MARGIN + 272, Y, 132, 'Hardware', TXSubTabClick);
FTXSubBtn[3] := MkBtn(FPageTransmit, MARGIN + 408, Y, 132, 'Display', TXSubTabClick);
for i := 0 to TXTAB_COUNT - 1 do FTXSubBtn[i].Tag := i;
Inc(Y, BTN_H + 14);
SubTop := Y; // ★ немасштабированный: FTXSubPage[].Top уже в пикселях
// =========================================================================
// Подвкладка 0 — Profile: всё, что определяет звук в эфире
// =========================================================================
SubPage(0, SubTop);
// ---- Микрофонный вход (часть профиля: гарнитура vs микшер-ESSB) ----
Grp := MakeGroupPanel(FTXSubPage[0], 'Microphone', MARGIN, 0, GRP_W, 176);
// Row 0: Mic jack selector | Boost (Mic In) или LineInGain (Line In) | Bias | PTT
MakeLbl(Grp, 'Mic jack', PAD, R1 + 5, 65);
@@ -1444,58 +1554,35 @@ begin
FChkTXMicBias := MkChk(Grp, PAD + 375, R1, 105, 'Mic bias');
FChkTXMicPTT := MkChk(Grp, PAD + 490, R1, 160, 'Mic-jack PTT');
// Row 1: Connector Tip/Ring (только Orion boards 4 и 5)
FLblTipRing := MakeLbl(Grp, 'Connector', PAD, R1 + STEP + 5, 70);
FCmbTXMicTR := MkCmb(Grp, PAD + 73, R1 + STEP, 90);
// Row 1: Mic gain
MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + STEP + 5, LW);
FEdTXMicGain := MkFloat(Grp, CX, R1 + STEP, 10.0, -20.0, 60.0, 0.5);
// Row 2: Connector Tip/Ring (только Orion boards 4 и 5). Стоит последним —
// на не-Orion строка скрыта и не оставляет дыру посреди группы.
FLblTipRing := MakeLbl(Grp, 'Connector', PAD, R1 + 2 * STEP + 5, 70);
FCmbTXMicTR := MkCmb(Grp, PAD + 73, R1 + 2 * STEP, 90);
FCmbTXMicTR.Items.Add('Tip'); // MicTipRing=False, bit3=0
FCmbTXMicTR.Items.Add('Ring'); // MicTipRing=True, bit3=1
FCmbTXMicTR.ItemIndex := 0;
FLblTipRing.Visible := False;
FCmbTXMicTR.Visible := False;
// Row 2: Mic gain | ATT on TX
MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + 2 * STEP + 5, LW);
FEdTXMicGain := MkFloat(Grp, CX, R1 + 2 * STEP, 10.0, -20.0, 60.0, 0.5);
MakeLbl(Grp, 'ATT on TX (dB)', PAD + 320, R1 + 2 * STEP + 5, 110);
FEdTXAttOnTX := MkSpin(Grp, PAD + 320 + 120, R1 + 2 * STEP, 0, 31, 31);
// ---- Tune (TUN button) ----
Inc(Y, 188 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'Tune (TUN)', MARGIN, Y, GRP_W, 100);
MakeLbl(Grp, 'Tune level (%)', PAD, R1 + 5, LW);
FEdTUNLevel := MkSpin(Grp, CX, R1, 0, 100, 10);
MakeLbl(Grp, 'Tone freq (Hz)', PAD + 320, R1 + 5, 110);
FEdTUNFreq := MkSpin(Grp, PAD + 320 + 120, R1, 100, 5000, 750);
// ---- TX bandpass filter ----
Inc(Y, 100 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Filter (bandpass)', MARGIN, Y, GRP_W, 220);
// ---- Кромки TX-фильтра (FIR/фаза/окно — на вкладке Hardware) ----
Y := 176 + 12;
Grp := MakeGroupPanel(FTXSubPage[0], 'TX Filter (bandpass)', MARGIN, Y, GRP_W, 108);
MakeLbl(Grp, 'Low cut (Hz)', PAD, R1 + 5, LW);
FEdTXFiltLow := MkSpin(Grp, CX, R1, 0, 8000, 200);
MakeLbl(Grp, 'High cut (Hz)', PAD + 320, R1 + 5, 90);
FEdTXFiltHigh := MkSpin(Grp, PAD + 320 + 100, R1, 100, 12000, 3100);
MakeLbl(Grp, 'Ширина полосы голоса. ESSB — просто High cut 3500..5000; '
+ 'знак боковой ставит движок, здесь всегда магнитуда.',
PAD, R1 + 32, GRP_W - 2 * PAD);
MakeLbl(Grp, 'FIR length', PAD, R1 + STEP + 5, LW);
FCmbTXFiltNC := MkCmb(Grp, CX, R1 + STEP, 140);
FCmbTXFiltNC.Items.AddStrings(['256','512','1024','2048','4096']);
FCmbTXFiltNC.ItemIndex := 2;
MakeLbl(Grp, 'Phase mode', PAD, R1 + 2 * STEP + 5, LW);
FCmbTXFiltMP := MkCmb(Grp, CX, R1 + 2 * STEP, 200);
FCmbTXFiltMP.Items.AddStrings(['Linear Phase','Low Latency']);
FCmbTXFiltMP.ItemIndex := 1;
MakeLbl(Grp, 'Window', PAD, R1 + 3 * STEP + 5, LW);
FCmbTXFiltWindow := MkCmb(Grp, CX, R1 + 3 * STEP, 240);
for i := 0 to High(WINDOW_NAMES) do
FCmbTXFiltWindow.Items.Add(WINDOW_NAMES[i]);
FCmbTXFiltWindow.ItemIndex := 1;
// ---- Compressor / Leveler / ALC / Phase Rotator ----
Inc(Y, 220 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'Dynamic processing', MARGIN, Y, GRP_W, 220);
// ---- Динамическая обработка ----
Inc(Y, 108 + 12);
Grp := MakeGroupPanel(FTXSubPage[0], 'Dynamic processing', MARGIN, Y, GRP_W, 220);
FChkTXCompOn := MkChk(Grp, PAD, R1, 220, 'Compressor');
MakeLbl(Grp, 'Gain (dB)', PAD + 230, R1 + 5, 80);
@@ -1519,9 +1606,28 @@ begin
MakeLbl(Grp, 'Corner (Hz)', PAD + 430, R1 + 3 * STEP + 5, 80);
FEdTXPHRFreq := MkFloat(Grp, PAD + 520, R1 + 3 * STEP, 338.0, 50.0, 3000.0, 1.0);
// ---- TX EQ ----
// ---- Мощность и несущая AM (тоже часть профиля) ----
Inc(Y, 220 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Equalizer', MARGIN, Y, GRP_W, 390);
Grp := MakeGroupPanel(FTXSubPage[0], 'Power / AM carrier', MARGIN, Y, GRP_W, 148);
MakeLbl(Grp, 'Tune level (%)', PAD, R1 + 5, LW);
FEdTUNLevel := MkSpin(Grp, CX, R1, 0, 100, 10);
MakeLbl(Grp, 'AM carrier level', PAD + 320, R1 + 5, 110);
FEdTXAMCarrier := MkFloat(Grp, PAD + 320 + 120, R1, 0.5, 0.0, 1.0, 0.05);
FLblTXProfDrive := MakeLbl(Grp, 'Drive профиля: —', PAD, R1 + STEP + 5, 300);
MakeLbl(Grp, 'Мощность — часть профиля, но крутится ползунком DRV в главном окне: '
+ 'текущее значение уходит в активный профиль само.',
PAD, R1 + STEP + 28, GRP_W - 2 * PAD);
FTXSubPage[0].Height := DpiScale(Y + 148 + 12);
// =========================================================================
// Подвкладка 1 — EQ (тоже профильный, но занимает целый экран)
// =========================================================================
SubPage(1, SubTop);
Grp := MakeGroupPanel(FTXSubPage[1], 'TX Equalizer', MARGIN, 0, GRP_W, 390);
FTXEQControl := TEqualizerControl.Create(Self);
FTXEQControl.Parent := Grp;
@@ -1532,33 +1638,75 @@ begin
TColor($00383838), CLR_TEXT, CLR_TEXTDIM, CLR_ACCENT, TColor($0055DDEE));
FTXEQControl.OnChange := OnTXAnyChange;
// ---- AM / FM / CTCSS ----
Inc(Y, 390 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'AM / FM / CTCSS', MARGIN, Y, GRP_W, 260);
FTXSubPage[1].Height := DpiScale(390 + 12);
MakeLbl(Grp, 'AM carrier level', PAD, R1 + 5, LW);
FEdTXAMCarrier := MkFloat(Grp, CX, R1, 0.5, 0.0, 1.0, 0.05);
// =========================================================================
// Подвкладка 2 — Hardware: свойства устройства, профилем не переключаются
// =========================================================================
SubPage(2, SubTop);
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + STEP + 5, LW);
FEdTXFMDev := MkFloat(Grp, CX, R1 + STEP, 5000.0, 1000.0, 25000.0, 100.0);
Grp := MakeGroupPanel(FTXSubPage[2], 'Radio', MARGIN, 0, GRP_W, 108);
MakeLbl(Grp, 'FM AF low cut', PAD, R1 + 2 * STEP + 5, LW);
FEdTXFMLow := MkSpin(Grp, CX, R1 + 2 * STEP, 0, 5000, 300);
MakeLbl(Grp, 'FM AF high cut', PAD + 320, R1 + 2 * STEP + 5, 100);
FEdTXFMHigh := MkSpin(Grp, PAD + 320 + 110, R1 + 2 * STEP, 100, 8000, 3000);
MakeLbl(Grp, 'ATT on TX (dB)', PAD, R1 + 5, LW);
FEdTXAttOnTX := MkSpin(Grp, CX, R1, 0, 31, 31);
MakeLbl(Grp, 'Tune tone (Hz)', PAD + 320, R1 + 5, 110);
FEdTUNFreq := MkSpin(Grp, PAD + 320 + 120, R1, 100, 5000, 750);
MakeLbl(Grp, 'ATT on TX — ослабление RX-ADC на передаче (защита входа), '
+ 'не путать с мощностью.', PAD, R1 + 32, GRP_W - 2 * PAD);
MakeLbl(Grp, 'FM emphasis', PAD, R1 + 3 * STEP + 5, LW);
FCmbTXFMEmph := MkCmb(Grp, CX, R1 + 3 * STEP, 180);
Y := 108 + 12;
Grp := MakeGroupPanel(FTXSubPage[2], 'TX Filter (DSP)', MARGIN, Y, GRP_W, 190);
MakeLbl(Grp, 'FIR length', PAD, R1 + 5, LW);
FCmbTXFiltNC := MkCmb(Grp, CX, R1, 140);
FCmbTXFiltNC.Items.AddStrings(['256','512','1024','2048','4096']);
FCmbTXFiltNC.ItemIndex := 2;
MakeLbl(Grp, 'Phase mode', PAD, R1 + STEP + 5, LW);
FCmbTXFiltMP := MkCmb(Grp, CX, R1 + STEP, 200);
FCmbTXFiltMP.Items.AddStrings(['Linear Phase','Low Latency']);
FCmbTXFiltMP.ItemIndex := 1;
MakeLbl(Grp, 'Window', PAD, R1 + 2 * STEP + 5, LW);
FCmbTXFiltWindow := MkCmb(Grp, CX, R1 + 2 * STEP, 240);
for i := 0 to High(WINDOW_NAMES) do
FCmbTXFiltWindow.Items.Add(WINDOW_NAMES[i]);
FCmbTXFiltWindow.ItemIndex := 1;
MakeLbl(Grp, 'Крутизна и задержка тракта, а не тембр — поэтому вне профиля.',
PAD, R1 + 3 * STEP + 5, GRP_W - 2 * PAD);
Inc(Y, 190 + 12);
Grp := MakeGroupPanel(FTXSubPage[2], 'FM / CTCSS', MARGIN, Y, GRP_W, 250);
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + 5, LW);
FEdTXFMDev := MkFloat(Grp, CX, R1, 5000.0, 1000.0, 25000.0, 100.0);
MakeLbl(Grp, 'FM AF low cut', PAD, R1 + STEP + 5, LW);
FEdTXFMLow := MkSpin(Grp, CX, R1 + STEP, 0, 5000, 300);
MakeLbl(Grp, 'FM AF high cut', PAD + 320, R1 + STEP + 5, 100);
FEdTXFMHigh := MkSpin(Grp, PAD + 320 + 110, R1 + STEP, 100, 8000, 3000);
MakeLbl(Grp, 'FM emphasis', PAD, R1 + 2 * STEP + 5, LW);
FCmbTXFMEmph := MkCmb(Grp, CX, R1 + 2 * STEP, 180);
FCmbTXFMEmph.Items.AddStrings(['Pre-EQ','Post-EQ']);
FCmbTXFMEmph.ItemIndex := 1;
FChkTXCTCSSOn := MkChk(Grp, PAD, R1 + 4 * STEP, 220, 'CTCSS tone');
MakeLbl(Grp, 'Freq (Hz)', PAD + 230, R1 + 4 * STEP + 5, 80);
FEdTXCTCSSFreq := MkFloat(Grp, PAD + 320, R1 + 4 * STEP, 100.0, 50.0, 300.0, 0.1);
FChkTXCTCSSOn := MkChk(Grp, PAD, R1 + 3 * STEP, 220, 'CTCSS tone');
MakeLbl(Grp, 'Freq (Hz)', PAD + 230, R1 + 3 * STEP + 5, 80);
FEdTXCTCSSFreq := MkFloat(Grp, PAD + 320, R1 + 3 * STEP, 100.0, 50.0, 300.0, 0.1);
// ---- TX Display (отдельный analyzer для TX-спектра/водопада) ----
Inc(Y, 260 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Display (Analyzer)', MARGIN, Y, GRP_W, 318);
MakeLbl(Grp, 'Свойство канала/репитера — профилем голоса не переключается.',
PAD, R1 + 4 * STEP + 5, GRP_W - 2 * PAD);
FTXSubPage[2].Height := DpiScale(Y + 250 + 12);
// =========================================================================
// Подвкладка 3 — Display: анализатор и сетка TX-спектра
// =========================================================================
SubPage(3, SubTop);
Grp := MakeGroupPanel(FTXSubPage[3], 'TX Display (Analyzer)', MARGIN, 0, GRP_W, 318);
MakeLbl(Grp, 'FFT size', PAD, R1 + 5, LW);
FCmbTXFFT := MkCmb(Grp, CX, R1, 140);
@@ -1595,8 +1743,8 @@ begin
FEdTXWfAvgTime := MkFloat(Grp, PAD + 540, R1 + 5 * STEP, 120.0, 1.0, 5000.0, 1.0);
// ---- TX Grid (RefLevel / Range / GridStep — отдельные от RX) ----
Inc(Y, 318 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Grid', MARGIN, Y, GRP_W, 170);
Y := 318 + 12;
Grp := MakeGroupPanel(FTXSubPage[3], 'TX Grid', MARGIN, Y, GRP_W, 170);
MakeLbl(Grp, 'Reference level', PAD, R1 + 5, LW);
FEdTXRefLevel := TFlatEdit.Create(Self);
@@ -1624,8 +1772,12 @@ begin
FCmbTXGridStep := MkCmb(Grp, CX, R1 + 2 * STEP, 130);
for i := 0 to High(GRID_STEP_NAMES) do FCmbTXGridStep.Items.Add(GRID_STEP_NAMES[i]);
FCmbTXGridStep.ItemIndex := 1;
end;
FTXSubPage[3].Height := DpiScale(Y + 170 + 12);
FTXSubIdx := -1;
SelectTXSubPage(0);
end;
procedure TSettingsForm.CollectTXFromUI;
var i: Integer;
begin
@@ -1729,6 +1881,115 @@ begin
FireTXChange;
end;
// ---------------------------------------------------------------------------
// Transmit: подвкладки и полоса TX-профиля
// ---------------------------------------------------------------------------
procedure TSettingsForm.SelectTXSubPage(Idx: Integer);
var i: Integer;
begin
if (Idx < 0) or (Idx >= TXTAB_COUNT) then Exit;
FTXSubIdx := Idx;
for i := 0 to TXTAB_COUNT - 1 do
begin
if FTXSubPage[i] <> nil then FTXSubPage[i].Visible := (i = Idx);
if FTXSubBtn[i] <> nil then FTXSubBtn[i].Active := (i = Idx);
end;
end;
procedure TSettingsForm.TXSubTabClick(Sender: TObject);
begin
if Sender is TFlatButton then SelectTXSubPage(TFlatButton(Sender).Tag);
end;
procedure TSettingsForm.OnTXProfileComboChange(Sender: TObject);
begin
if FLoading then Exit;
if (FCmbTXProfile = nil) or (FCmbTXProfile.ItemIndex < 0) then Exit;
// Применение снимка в эфир — снаружи (контроллер); назад придёт LoadTXSettings
// с уже новыми значениями, форма перерисуется целиком.
if Assigned(FOnTXProfileSelect) then FOnTXProfileSelect(FCmbTXProfile.ItemIndex);
end;
procedure TSettingsForm.BtnTXProfNewClick(Sender: TObject);
var S: string;
begin
if FCmbTXProfile = nil then Exit;
if FCmbTXProfile.Items.Count >= TXPROF_MAX then
begin
MessageDlg('TX Profile',
Format('Больше %d профилей на устройство не поддерживается.', [TXPROF_MAX]),
mtInformation, [mbOK], 0);
Exit;
end;
S := 'Profile ' + IntToStr(FCmbTXProfile.Items.Count + 1);
if not InputQuery('TX Profile', 'Имя нового профиля (копия текущего звука):', S) then Exit;
if Trim(S) = '' then Exit;
if Assigned(FOnTXProfileAdd) then FOnTXProfileAdd(Trim(S));
end;
procedure TSettingsForm.BtnTXProfRenClick(Sender: TObject);
var S: string; Idx: Integer;
begin
if FCmbTXProfile = nil then Exit;
Idx := FCmbTXProfile.ItemIndex;
if (Idx < 0) or (Idx >= FCmbTXProfile.Items.Count) then Exit;
S := FCmbTXProfile.Items[Idx];
if not InputQuery('TX Profile', 'Новое имя профиля:', S) then Exit;
if Trim(S) = '' then Exit;
if Assigned(FOnTXProfileRename) then FOnTXProfileRename(Idx, Trim(S));
end;
procedure TSettingsForm.BtnTXProfDelClick(Sender: TObject);
var Idx: Integer;
begin
if FCmbTXProfile = nil then Exit;
Idx := FCmbTXProfile.ItemIndex;
// Последний профиль не удаляем — активному профилю всегда есть куда писать.
if (Idx < 0) or (FCmbTXProfile.Items.Count <= 1) then Exit;
if MessageDlg('TX Profile',
'Удалить профиль «' + FCmbTXProfile.Items[Idx] + '»?',
mtConfirmation, [mbYes, mbNo], 0) <> mrYes then Exit;
if Assigned(FOnTXProfileDelete) then FOnTXProfileDelete(Idx);
end;
procedure TSettingsForm.LoadTXProfiles(const Names: array of string;
ActiveIdx, DrivePct: Integer);
var
i: Integer;
WasLoading: Boolean;
begin
if FCmbTXProfile = nil then Exit;
WasLoading := FLoading;
FLoading := True; // заполнение списка не должно шибать OnChange
try
FCmbTXProfile.Items.Clear;
for i := 0 to High(Names) do FCmbTXProfile.Items.Add(Names[i]);
if (ActiveIdx >= 0) and (ActiveIdx < FCmbTXProfile.Items.Count) then
FCmbTXProfile.ItemIndex := ActiveIdx;
if FBtnTXProfDel <> nil then
FBtnTXProfDel.Enabled := FCmbTXProfile.Items.Count > 1;
if FLblTXProfDrive <> nil then
FLblTXProfDrive.Caption := Format('Drive профиля: %d %%', [DrivePct]);
finally
FLoading := WasLoading;
end;
end;
procedure TSettingsForm.SetTXProfileDrive(DrivePct: Integer);
begin
if FLblTXProfDrive <> nil then
FLblTXProfDrive.Caption := Format('Drive профиля: %d %%', [DrivePct]);
end;
procedure TSettingsForm.ShowTransmitPage(SubIdx: Integer = 0);
begin
SelectPage(FPageTransmit, FNavTransmit);
SelectTXSubPage(SubIdx);
Show;
BringToFront;
end;
procedure TSettingsForm.LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
var i: Integer; IsOrion: Boolean;
begin
@@ -2708,6 +2969,8 @@ procedure TSettingsForm.ApplyTheme(const T: TAppTheme);
begin
if TPanel(Ctrl).Height = 1 then
TPanel(Ctrl).Color := T.Border
else if TPanel(Ctrl).Tag = TAG_PAGE_BG then
TPanel(Ctrl).Color := T.BG // подложка подвкладки, не группа
else
TPanel(Ctrl).Color := T.Panel;
WalkControls(TPanel(Ctrl));