mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-26 19:57:33 +00:00
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:
+333
-70
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user