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
+148 -1
View File
@@ -356,6 +356,10 @@ type
TrkAtt: TFlatSlider; TrkAtt: TFlatSlider;
BtnChannels: TFlatButton; BtnChannels: TFlatButton;
FChannelsDropDown: TFlatDropDown; FChannelsDropDown: TFlatDropDown;
// TX-профиль: кнопка с именем активного (блок TX, ряд DUP) + список.
// ЛКМ — список, ПКМ — вкладка Transmit в настройках.
BtnTXProfile: TFlatButton;
FTXProfileDropDown: TFlatDropDown;
FChannelsForm: TObject; // TChannelsForm (cast при использовании) FChannelsForm: TObject; // TChannelsForm (cast при использовании)
FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON) FBeaconScopeForm: TBeaconScopeForm; // окно констелляции маяка (ПКМ по BEACON)
@@ -517,6 +521,17 @@ type
Shift: TShiftState; X, Y: Integer); Shift: TShiftState; X, Y: Integer);
procedure OnChannelsDropDownSelect(Sender: TObject; Idx: Integer); procedure OnChannelsDropDownSelect(Sender: TObject; Idx: Integer);
procedure RefreshChannelsDropDown; procedure RefreshChannelsDropDown;
// TX-профили: рендер кнопки/списка + обработчики блока TX и вкладки Transmit.
procedure RenderTXProfile;
procedure PushTXProfilesToSettings;
procedure BtnTXProfileClick(Sender: TObject);
procedure BtnTXProfileMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure OnTXProfileDropDownSelect(Sender: TObject; Idx: Integer);
procedure OnTXProfileSelectFromSettings(Idx: Integer);
procedure OnTXProfileAddFromSettings(const AName: string);
procedure OnTXProfileRenameFromSettings(Idx: Integer; const AName: string);
procedure OnTXProfileDeleteFromSettings(Idx: Integer);
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;
@@ -1245,6 +1260,9 @@ begin
FController.FNetwork.OnWideband := OnWidebandCB; FController.FNetwork.OnWideband := OnWidebandCB;
// Дефолты TX, Alex и XVTR (на случай если устройство ещё не выбрано). // Дефолты TX, Alex и XVTR (на случай если устройство ещё не выбрано).
TSettingsManager.DefaultTX(FController.FTXSettings); TSettingsManager.DefaultTX(FController.FTXSettings);
// Заводской набор TX-профилей до подключения (ConnectDevice перечитает свой).
DefaultTXProfiles(FController.FTXSettings, FController.FDrivePercent,
FController.FTXProfiles);
TSettingsManager.DefaultAlex(FController.FAlexSettings); TSettingsManager.DefaultAlex(FController.FAlexSettings);
TSettingsManager.DefaultOC(FController.FOCSettings); TSettingsManager.DefaultOC(FController.FOCSettings);
TSettingsManager.DefaultXvtr(FController.FXvtrSettings); TSettingsManager.DefaultXvtr(FController.FXvtrSettings);
@@ -1582,7 +1600,12 @@ begin
TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive% TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive%
UpdateDriveLabel; // % / дБ рядом с ползунком UpdateDriveLabel; // % / дБ рядом с ползунком
FChannelController.OnDriveChanged; // внешнее изменение снимает память канала FChannelController.OnDriveChanged; // внешнее изменение снимает память канала
// Мощность — часть TX-профиля: держим подпись в настройках свежей.
if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then
TSettingsForm(FSettingsForm).SetTXProfileDrive(FController.FDrivePercent);
end; end;
rfTXProfile:
RenderTXProfile;
rfAGCTop: rfAGCTop:
begin begin
TrkAGC.Position := FController.FAGCTop; TrkAGC.Position := FController.FAGCTop;
@@ -1739,6 +1762,7 @@ begin
FWidebandView.SetFillSpectrum(FController.FWidebandFill); FWidebandView.SetFillSpectrum(FController.FWidebandFill);
FSpecView.FillSpectrum := FController.FSpectrumFill; FSpecView.FillSpectrum := FController.FSpectrumFill;
if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex); if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex);
RenderTXProfile; // список профилей этого устройства (загружен по MAC)
if FController.FLoadedGlobal.DisplayFPS > 0 then if FController.FLoadedGlobal.DisplayFPS > 0 then
ApplyFPS(FController.FLoadedGlobal.DisplayFPS); ApplyFPS(FController.FLoadedGlobal.DisplayFPS);
ApplyFreqMhzDigits(EnsureRange(FController.FLoadedGlobal.FreqMhzDigits, 3, 5)); ApplyFreqMhzDigits(EnsureRange(FController.FLoadedGlobal.FreqMhzDigits, 3, 5));
@@ -2093,6 +2117,21 @@ begin
StyleButton(BtnRxMute, FController.FRxMuteOnTx); StyleButton(BtnRxMute, FController.FRxMuteOnTx);
UpdateRxMuteButton; // видимость/активность по DUP UpdateRxMuteButton; // видимость/активность по DUP
// TX-профиль — свободные колонки 2..3 того же ряда (панель не растёт по
// высоте). Подпись = имя активного профиля, ЛКМ — список, ПКМ — настройки.
BtnTXProfile := MakeBtn(PanelTX, 'Default',
LeftPanelButtonLeft(LEFT_W, 4, 2), 44,
LeftPanelButtonLeft(LEFT_W, 4, 3)
+ LeftPanelButtonWidth(LEFT_W, 4, 3)
- LeftPanelButtonLeft(LEFT_W, 4, 2),
BTN_SM, BtnTXProfileClick);
BtnTXProfile.OnMouseDown := BtnTXProfileMouseDown;
StyleButton(BtnTXProfile, False);
FTXProfileDropDown := TFlatDropDown.Create(BtnTXProfile, PanelLeft, 1, BTN_SM);
FTXProfileDropDown.OnSelect := OnTXProfileDropDownSelect;
RenderTXProfile;
LblDrv := TLabel.Create(Self); LblDrv := TLabel.Create(Self);
LblDrv.Parent := PanelTX; LblDrv.Left := 4; LblDrv.Top := 77; LblDrv.Parent := PanelTX; LblDrv.Left := 4; LblDrv.Top := 77;
LblDrv.Caption := 'DRV'; LblDrv.Font.Color := CLR_TEXTDIM; LblDrv.Caption := 'DRV'; LblDrv.Font.Color := CLR_TEXTDIM;
@@ -3188,6 +3227,8 @@ begin
end; end;
if BtnChannels <> nil then StyleButton(BtnChannels, False); if BtnChannels <> nil then StyleButton(BtnChannels, False);
if FChannelsDropDown <> nil then FChannelsDropDown.ApplyStyle(StyleButton, T.Panel); if FChannelsDropDown <> nil then FChannelsDropDown.ApplyStyle(StyleButton, T.Panel);
if BtnTXProfile <> nil then StyleButton(BtnTXProfile, False);
if FTXProfileDropDown <> nil then FTXProfileDropDown.ApplyStyle(StyleButton, T.Panel);
StyleButton(BtnNR, BtnNR.Active); StyleButton(BtnNR, BtnNR.Active);
StyleButton(BtnNB, BtnNB.Active); StyleButton(BtnNB, BtnNB.Active);
StyleButton(BtnSNB, BtnSNB.Active); StyleButton(BtnSNB, BtnSNB.Active);
@@ -5486,7 +5527,8 @@ procedure TMainForm.CloseCTCSSPopup;
begin begin
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup; if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
CloseFMStepPopup; CloseFMStepPopup;
if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup; if FChannelsDropDown <> nil then FChannelsDropDown.ClosePopup;
if FTXProfileDropDown <> nil then FTXProfileDropDown.ClosePopup;
end; end;
procedure TMainForm.BtnFMRptMinusClick(Sender: TObject); procedure TMainForm.BtnFMRptMinusClick(Sender: TObject);
@@ -5592,6 +5634,7 @@ begin
if FController.FChannelStore.Count = 0 then Exit; if FController.FChannelStore.Count = 0 then Exit;
FCTCSSDropDown.ClosePopup; FCTCSSDropDown.ClosePopup;
FStepDropDown.ClosePopup; FStepDropDown.ClosePopup;
if FTXProfileDropDown <> nil then FTXProfileDropDown.ClosePopup;
FChannelsDropDown.Toggle; FChannelsDropDown.Toggle;
end; end;
@@ -5627,6 +5670,101 @@ begin
FChannelController.Select(Idx); FChannelController.Select(Idx);
end; end;
// ---------------------------------------------------------------------------
// TX-профили (кнопка блока TX + список)
// ---------------------------------------------------------------------------
procedure TMainForm.RenderTXProfile;
// Единая точка рендера: подпись кнопки, состав списка и — если открыт диалог
// настроек — его вкладка Transmit (профиль переписал живые TX-настройки).
var
Names: array of string;
i, Act, Cnt: Integer;
begin
if BtnTXProfile = nil then Exit;
Cnt := FController.TXProfileCount;
Act := FController.ActiveTXProfile;
if (Act >= 0) and (Act < Cnt) then
BtnTXProfile.Caption := FController.TXProfileName(Act)
else
BtnTXProfile.Caption := 'TX Profile';
BtnTXProfile.Invalidate;
if FTXProfileDropDown <> nil then
begin
SetLength(Names, Cnt);
for i := 0 to Cnt - 1 do Names[i] := FController.TXProfileName(i);
FTXProfileDropDown.SetItems(Names);
if (Act >= 0) and (Act < Cnt) then FTXProfileDropDown.SetItemIndex(Act);
end;
if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then
begin
TSettingsForm(FSettingsForm).LoadTXSettings(FController.FTXSettings,
IfThen(FController.FNetwork.Connected,
FController.FNetwork.Device.BoardType, FPendingBoardType));
PushTXProfilesToSettings;
end;
end;
procedure TMainForm.PushTXProfilesToSettings;
var
Names: array of string;
i: Integer;
begin
if FSettingsForm = nil then Exit;
SetLength(Names, FController.TXProfileCount);
for i := 0 to FController.TXProfileCount - 1 do
Names[i] := FController.TXProfileName(i);
TSettingsForm(FSettingsForm).LoadTXProfiles(Names, FController.ActiveTXProfile,
FController.FDrivePercent);
end;
procedure TMainForm.BtnTXProfileClick(Sender: TObject);
begin
if FController.TXProfileCount = 0 then Exit;
// Чужие списки гасим поимённо: общий CloseCTCSSPopup закрыл бы и наш, и
// повторный клик по кнопке никогда не сворачивал бы список.
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
CloseFMStepPopup;
FTXProfileDropDown.Toggle;
end;
procedure TMainForm.BtnTXProfileMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button <> mbRight then Exit;
// ПКМ — прямо в редактор профиля (вкладка Transmit), как ПКМ по кнопке фильтра.
if FTXProfileDropDown <> nil then FTXProfileDropDown.ClosePopup;
BtnSettingsClick(nil);
if FSettingsForm <> nil then TSettingsForm(FSettingsForm).ShowTransmitPage;
end;
procedure TMainForm.OnTXProfileDropDownSelect(Sender: TObject; Idx: Integer);
begin
// Применение (WDSP + mic-байт + drive) и персист — в контроллере;
// рендер придёт назад событием rfTXProfile.
FController.SelectTXProfile(Idx);
end;
procedure TMainForm.OnTXProfileSelectFromSettings(Idx: Integer);
begin
FController.SelectTXProfile(Idx);
end;
procedure TMainForm.OnTXProfileAddFromSettings(const AName: string);
begin
FController.AddTXProfile(AName);
end;
procedure TMainForm.OnTXProfileRenameFromSettings(Idx: Integer; const AName: string);
begin
FController.RenameTXProfile(Idx, AName);
end;
procedure TMainForm.OnTXProfileDeleteFromSettings(Idx: Integer);
begin
FController.DeleteTXProfile(Idx);
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 → подсветка+имя).
@@ -7812,10 +7950,14 @@ begin
end; end;
end; end;
end; end;
// Правка «звуковых» полей — это правка активного профиля: отдельной кнопки
// «сохранить» нет, профиль ведётся за живым состоянием (см. TTXProfile).
FController.StoreActiveTXProfile;
// Сохраняем сразу — настройки per-device, попадают в hpsdr_settings.json // Сохраняем сразу — настройки per-device, попадают в hpsdr_settings.json
if FController.FDevConnected then if FController.FDevConnected then
begin begin
FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings); FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings);
FController.FSettings.SaveTXProfiles(FController.FDevMAC, FController.FTXProfiles);
FController.FSettings.Save; FController.FSettings.Save;
end; end;
end; end;
@@ -8624,6 +8766,10 @@ begin
SF.OnThemeChange := SetLightTheme; SF.OnThemeChange := SetLightTheme;
SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits; SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits;
SF.OnTXChange := OnTXSettingsChange; SF.OnTXChange := OnTXSettingsChange;
SF.OnTXProfileSelect := OnTXProfileSelectFromSettings;
SF.OnTXProfileAdd := OnTXProfileAddFromSettings;
SF.OnTXProfileRename := OnTXProfileRenameFromSettings;
SF.OnTXProfileDelete := OnTXProfileDeleteFromSettings;
SF.OnAlexChange := OnAlexSettingsChange; SF.OnAlexChange := OnAlexSettingsChange;
SF.OnXvtrChange := OnXvtrSettingsChange; SF.OnXvtrChange := OnXvtrSettingsChange;
SF.OnOCChange := OnOCSettingsChange; SF.OnOCChange := OnOCSettingsChange;
@@ -8632,6 +8778,7 @@ begin
SF.OnWebSettingsChange := ApplyWebSettings; SF.OnWebSettingsChange := ApplyWebSettings;
end; end;
SF := TSettingsForm(FSettingsForm); SF := TSettingsForm(FSettingsForm);
PushTXProfilesToSettings;
SF.LoadTXSettings(FController.FTXSettings, SF.LoadTXSettings(FController.FTXSettings,
IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType)); IfThen(FController.FNetwork.Connected, FController.FNetwork.Device.BoardType, FPendingBoardType));
SF.LoadAlexSettings(FController.FAlexSettings, SF.LoadAlexSettings(FController.FAlexSettings,
+152
View File
@@ -80,6 +80,7 @@ type
rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel, rfFMDeviation, rfFMCTCSS, rfFMCTCSSTone, rfFMSQ, rfFMSQLevel,
rfFMStep, rfFMStepIdx, rfFMRpt, rfFMStep, rfFMStepIdx, rfFMRpt,
rfChannel, rfChannel,
rfTXProfile, // сменился активный TX-профиль
rfSMeter, rfPower, rfSWR, rfSupply, rfPLL, // телеметрия rfSMeter, rfPower, rfSWR, rfSupply, rfPLL, // телеметрия
rfADCOverload, // ADC overload (рендер индикатора) rfADCOverload, // ADC overload (рендер индикатора)
rfBeaconLock, // QO-100 beacon lock: статус/коррекция rfBeaconLock, // QO-100 beacon lock: статус/коррекция
@@ -424,6 +425,10 @@ type
// ---- Конфиги / устройство (per-MAC) ---- // ---- Конфиги / устройство (per-MAC) ----
FTXSettings: TTXSettings; FTXSettings: TTXSettings;
// Именованные TX-профили («звуковая» часть FTXSettings + drive, см.
// TTXProfile). Истина для эфира — всегда живые FTXSettings/FDrivePercent;
// активный профиль ведётся за ними (StoreActiveTXProfile).
FTXProfiles: TTXProfileList;
FAlexSettings: TAlexSettings; FAlexSettings: TAlexSettings;
FOCSettings: TOCSettings; // OC Control (Open Collector, openHPSDR-only) FOCSettings: TOCSettings; // OC Control (Open Collector, openHPSDR-only)
// Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер. // Антенные разъёмы AD936x (Pluto/LibreSDR) на диапазон/трансвертер.
@@ -577,6 +582,21 @@ type
procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP procedure ApplyTXSettingsToDSP; // TX-цепь (фильтр/gain/EQ/comp/...) → WDSP
function PSOutlierSigmaEff: Double; // σ outlier-фильтра calcc (авто по плате) function PSOutlierSigmaEff: Double; // σ outlier-фильтра calcc (авто по плате)
// ---- TX-профили --------------------------------------------------------
// Профиль — именованный снимок «звуковой» части TX-цепи (состав в
// TTXProfile). Отдельной кнопки «сохранить» нет: правки ручками пишутся в
// активный профиль сразу (StoreActiveTXProfile), как и всё остальное в
// EWSDR. Переключение применяет снимок в эфир немедленно.
function TXProfileCount: Integer;
function TXProfileName(Idx: Integer): string;
function ActiveTXProfile: Integer;
procedure SelectTXProfile(Idx: Integer);
procedure StoreActiveTXProfile(Persist: Boolean = False);
function AddTXProfile(const AName: string): Integer; // форк текущего звука
procedure RenameTXProfile(Idx: Integer; const AName: string);
procedure DeleteTXProfile(Idx: Integer);
procedure SaveTXProfilesToConfig;
// ---- Управление диапазонами (состояние + DSP; рендер через события) ---- // ---- Управление диапазонами (состояние + DSP; рендер через события) ----
// MakeBandSettings — снимок текущего состояния в запись диапазона. // MakeBandSettings — снимок текущего состояния в запись диапазона.
// SaveCurrentBand — сохраняет текущее состояние в кэш+конфиг (или в XVTR-слот). // SaveCurrentBand — сохраняет текущее состояние в кэш+конфиг (или в XVTR-слот).
@@ -4222,6 +4242,10 @@ procedure TRadioController.SetDrive(V: Integer);
begin begin
if V < 0 then V := 0 else if V > 100 then V := 100; if V < 0 then V := 0 else if V > 100 then V := 100;
FDrivePercent := V; FDrivePercent := V;
// Мощность входит в профиль: ведём её в активном без записи на диск (слайдер
// шлёт события пачками; на диск профили уходят на переключении/остановке).
if (FTXProfiles.ActiveIdx >= 0) and (FTXProfiles.ActiveIdx < FTXProfiles.Count) then
FTXProfiles.Items[FTXProfiles.ActiveIdx].DrivePercent := FDrivePercent;
FDriveLevel := CalcDriveByte; FDriveLevel := CalcDriveByte;
ApplyWDSPDrive(FDrivePercent); ApplyWDSPDrive(FDrivePercent);
ApplyPlutoTxAtten; // Pluto: drive живьём меняет TX-аттенюацию во время передачи ApplyPlutoTxAtten; // Pluto: drive живьём меняет TX-аттенюацию во время передачи
@@ -4904,6 +4928,124 @@ begin
FNetwork.SendDUCSpecific(DUCPkt); FNetwork.SendDUCSpecific(DUCPkt);
end; end;
// ---------------------------------------------------------------------------
// TX-профили
// ---------------------------------------------------------------------------
function TRadioController.TXProfileCount: Integer;
begin
Result := FTXProfiles.Count;
end;
function TRadioController.TXProfileName(Idx: Integer): string;
begin
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Result := ''
else Result := FTXProfiles.Items[Idx].Name;
end;
function TRadioController.ActiveTXProfile: Integer;
begin
Result := FTXProfiles.ActiveIdx;
end;
procedure TRadioController.SaveTXProfilesToConfig;
begin
if (not FDevConnected) or (not Assigned(FSettings)) then Exit;
FSettings.SaveTXProfiles(FDevMAC, FTXProfiles);
FSettings.Save;
end;
procedure TRadioController.StoreActiveTXProfile(Persist: Boolean = False);
// Живое состояние → активный профиль. Зовётся на каждой правке TX-настроек и
// на смене drive; Persist=False не трогает диск (drive ездит слайдером).
var Idx: Integer;
begin
Idx := FTXProfiles.ActiveIdx;
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Exit;
TXProfileCapture(FTXSettings, FDrivePercent, FTXProfiles.Items[Idx]);
if Persist then SaveTXProfilesToConfig;
end;
procedure TRadioController.SelectTXProfile(Idx: Integer);
var
OldMic: Byte;
NewDrive: Integer;
begin
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Exit;
// Уходящий профиль дописываем живыми значениями — ручные правки не теряются.
StoreActiveTXProfile;
OldMic := BuildMicLineSelectByte;
NewDrive := FDrivePercent;
FTXProfiles.ActiveIdx := Idx;
TXProfileApply(FTXProfiles.Items[Idx], FTXSettings, NewDrive);
ApplyTXSettingsToDSP;
// Маршрутизация микрофона живёт в железе (DUC Specific byte 50) — при смене
// Mic In/Line In/boost/bias пакет надо перепослать, WDSP тут ни при чём.
if BuildMicLineSelectByte <> OldMic then SendDUCSpecificFromSettings;
// SetDrive пересчитает калиброванный байт, толкнёт WDSP/Pluto и HP-кадр;
// зовём безусловно — при активном TUN уровень берётся из TUNLevel профиля.
SetDrive(NewDrive);
if FTuning and FWDSPReady then
FDSPEngine.SetDriveLevel(EnsureRange(FTXSettings.TUNLevel, 0, 100) / 100.0);
if FDevConnected and Assigned(FSettings) then
begin
FSettings.SaveTX(FDevMAC, FTXSettings);
FSettings.SaveTXProfiles(FDevMAC, FTXProfiles);
FSettings.Save;
end;
Changed(rfTXProfile);
end;
function TRadioController.AddTXProfile(const AName: string): Integer;
// Форк: новый профиль = текущий звук под новым именем, он же становится активным.
begin
Result := -1;
if FTXProfiles.Count >= TXPROF_MAX then Exit;
Result := FTXProfiles.Count;
TXProfileCapture(FTXSettings, FDrivePercent, FTXProfiles.Items[Result]);
FTXProfiles.Items[Result].Name := Copy(Trim(AName), 1, 31);
if FTXProfiles.Items[Result].Name = '' then
FTXProfiles.Items[Result].Name := 'Profile ' + IntToStr(Result + 1);
Inc(FTXProfiles.Count);
FTXProfiles.ActiveIdx := Result;
SaveTXProfilesToConfig;
Changed(rfTXProfile);
end;
procedure TRadioController.RenameTXProfile(Idx: Integer; const AName: string);
var S: string;
begin
if (Idx < 0) or (Idx >= FTXProfiles.Count) then Exit;
S := Copy(Trim(AName), 1, 31);
if S = '' then Exit;
FTXProfiles.Items[Idx].Name := S;
SaveTXProfilesToConfig;
Changed(rfTXProfile);
end;
procedure TRadioController.DeleteTXProfile(Idx: Integer);
// Последний профиль не удаляем: список без активного элемента сломал бы
// авто-сохранение (StoreActiveTXProfile писать некуда).
var i: Integer;
begin
if (Idx < 0) or (Idx >= FTXProfiles.Count) or (FTXProfiles.Count <= 1) then Exit;
for i := Idx to FTXProfiles.Count - 2 do
FTXProfiles.Items[i] := FTXProfiles.Items[i + 1];
Dec(FTXProfiles.Count);
if FTXProfiles.ActiveIdx > Idx then Dec(FTXProfiles.ActiveIdx)
else if FTXProfiles.ActiveIdx = Idx then
begin
// Удалили активный: встаём на соседний и применяем его в эфир. ActiveIdx
// гасим — иначе StoreActiveTXProfile внутри Select затрёт соседа живым
// звуком удалённого профиля.
FTXProfiles.ActiveIdx := -1;
SelectTXProfile(EnsureRange(Idx, 0, FTXProfiles.Count - 1));
Exit; // SelectTXProfile уже сохранил и отрендерил
end;
SaveTXProfilesToConfig;
Changed(rfTXProfile);
end;
procedure TRadioController.ApplyTXSettingsToDSP; procedure TRadioController.ApplyTXSettingsToDSP;
begin begin
if not FWDSPReady then Exit; if not FWDSPReady then Exit;
@@ -5847,6 +5989,12 @@ begin
// drive% (0..100) — раньше грузился через TrkDrive.Position; теперь напрямую, // drive% (0..100) — раньше грузился через TrkDrive.Position; теперь напрямую,
// слайдер выставит rfDevice-рендер. Калиброванный байт считаем после FCurrentBand. // слайдер выставит rfDevice-рендер. Калиброванный байт считаем после FCurrentBand.
FDrivePercent := EnsureRange(FLoadedGlobal.DriveLevel, 0, 100); FDrivePercent := EnsureRange(FLoadedGlobal.DriveLevel, 0, 100);
// TX-профили. Секции нет (конфиг до этой версии) — строим заводской набор из
// ТЕКУЩИХ настроек: «Default» повторяет их один в один, звук после
// обновления не меняется. Активный профиль на старте не применяем — живая
// секция "tx" уже хранит последнее состояние, оно и есть истина.
if not FSettings.LoadTXProfiles(FDevMAC, FTXProfiles) then
DefaultTXProfiles(FTXSettings, FDrivePercent, FTXProfiles);
// Клампим LastBand — иначе битый/старый конфиг → RestoreBand выйдет по гарду, // Клампим LastBand — иначе битый/старый конфиг → RestoreBand выйдет по гарду,
// а последующие SetMode/SetFilterIdx запишут в FBandCache[невалид] за массив. // а последующие SetMode/SetFilterIdx запишут в FBandCache[невалид] за массив.
FCurrentBand := EnsureRange(FLoadedGlobal.LastBand, 0, CFG_BAND_COUNT - 1); FCurrentBand := EnsureRange(FLoadedGlobal.LastBand, 0, CFG_BAND_COUNT - 1);
@@ -5999,6 +6147,10 @@ begin
FSettings.SaveAnt936x(FDevMAC, FAnt936x); FSettings.SaveAnt936x(FDevMAC, FAnt936x);
FSettings.SaveOC(FDevMAC, FOCSettings); FSettings.SaveOC(FDevMAC, FOCSettings);
FSettings.SaveFilters(FDevMAC, FFilterTable); FSettings.SaveFilters(FDevMAC, FFilterTable);
// Активный TX-профиль дописываем живыми значениями (drive/громкость микро-
// фона ездят мимо диалога настроек) и пишем список целиком.
StoreActiveTXProfile;
FSettings.SaveTXProfiles(FDevMAC, FTXProfiles);
// XVTR LastFreq → актуальная видимая частота в момент остановки. // XVTR LastFreq → актуальная видимая частота в момент остановки.
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA; FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA;
+307
View File
@@ -28,6 +28,7 @@ const
CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS
CFG_CAT_SLICE_COUNT = 6; // слотов доп. слайсов (B..G) — = WDSPEngine.MAX_SLICES CFG_CAT_SLICE_COUNT = 6; // слотов доп. слайсов (B..G) — = WDSPEngine.MAX_SLICES
CFG_CAT_SLICE_PORT0 = 19091; // порт слайса B по умолчанию (главный — 19090) CFG_CAT_SLICE_PORT0 = 19091; // порт слайса B по умолчанию (главный — 19090)
TXPROF_MAX = 16; // потолок именованных TX-профилей на устройство
QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100 QO100_SLOT = CFG_XVTR_COUNT - 1; // последний слот зарезервирован под QO-100
// (split-LO транспондер; настраивается на // (split-LO транспондер; настраивается на
// отдельной странице, не в общей XVTR-таблице) // отдельной странице, не в общей XVTR-таблице)
@@ -298,6 +299,59 @@ type
// (Orion MkII → 5.0, иначе выкл.), 0 = выкл. // (Orion MkII → 5.0, иначе выкл.), 0 = выкл.
end; end;
// ---- TX-профили (именованные снимки «как я звучу») ----------------------
// Профиль хранит ТОЛЬКО то, что определяет звук в эфире: маршрутизацию
// микрофонного входа, усиление, кромки TX-фильтра, динамическую обработку,
// эквалайзер, уровень несущей AM и мощность. Всё остальное из TTXSettings
// (FIR-длина/фаза/окно фильтра, ATT on TX, FM/CTCSS, параметры анализатора
// и сетки, калибровка PureSignal) — свойство устройства, от профиля НЕ
// зависит: переключение профиля не должно перерисовывать спектр, менять
// задержку тракта или ломать калибровку PS.
// Разбиение — по образцу таблицы TXProfile у Thetis (database.cs), но без
// её хвоста из VAC/DSP-буферов, которых у нас нет.
TTXProfile = record
Name: string[31];
// Микрофонный вход (гарнитура / микшер-ESSB — физически разные входы)
MicLineIn: Boolean;
MicBoost: Boolean;
MicPTTEnabled: Boolean;
MicTipRing: Boolean;
MicBias: Boolean;
LineInGainDB: Double;
MicGainDB: Double;
// Кромки TX-фильтра (FIR-длина/фаза/окно остаются у устройства)
FilterLow: Integer;
FilterHigh: Integer;
// Динамическая обработка
CompressorOn: Boolean;
CompressorGain: Double;
LevelerOn: Boolean;
LevelerTop: Double;
LevelerDecay: Integer;
ALCOn: Boolean;
ALCMaxGain: Double;
ALCDecay: Integer;
PhaseRotOn: Boolean;
PhaseRotStages: Integer;
PhaseRotFreq: Double;
// Эквалайзер
EQOn: Boolean;
EQNumBands: Integer;
EQGains: array[0..10] of Double;
EQFreqs: array[0..10] of Double;
// Модуляция AM (несущая — часть «звука», в отличие от девиации FM)
AMCarrierLevel: Double;
// Мощность (Thetis держит Power/Tune_Power в профиле)
DrivePercent: Integer; // 0..100
TUNLevel: Integer; // 0..100
end;
TTXProfileList = record
Count: Integer;
ActiveIdx: Integer;
Items: array[0..TXPROF_MAX-1] of TTXProfile;
end;
// Web-сервер — глобальные настройки (не привязаны к устройству, секция "web"). // Web-сервер — глобальные настройки (не привязаны к устройству, секция "web").
TWebSettings = record TWebSettings = record
Enabled: Boolean; Enabled: Boolean;
@@ -625,8 +679,25 @@ type
// только режимы, отличающиеся от заводских, — конфиг не пухнет. // только режимы, отличающиеся от заводских, — конфиг не пухнет.
function LoadFilters(const MAC: array of Byte; out T: TFilterTable): Boolean; function LoadFilters(const MAC: array of Byte; out T: TFilterTable): Boolean;
procedure SaveFilters(const MAC: array of Byte; const T: TFilterTable); procedure SaveFilters(const MAC: array of Byte; const T: TFilterTable);
// TX-профили per-device (JSON-секция "tx_profiles" под MAC). LoadTXProfiles
// возвращает False, если секции ещё нет: вызывающий строит заводской набор
// из текущих TX-настроек (DefaultTXProfiles) — миграция старого конфига.
function LoadTXProfiles(const MAC: array of Byte; out L: TTXProfileList): Boolean;
procedure SaveTXProfiles(const MAC: array of Byte; const L: TTXProfileList);
end; end;
// ---- TX-профили ------------------------------------------------------------
// Снимок «звуковых» полей живых настроек в профиль (имя не трогается) и
// обратное применение. Единственные две точки, знающие состав профиля, —
// добавляя поле в TTXProfile, правь обе.
procedure TXProfileCapture(const T: TTXSettings; DrivePct: Integer; var P: TTXProfile);
procedure TXProfileApply(const P: TTXProfile; var T: TTXSettings; var DrivePct: Integer);
// Заводской набор профилей. Base — текущие настройки устройства: из них
// наследуются микрофонный вход, мощность и всё, что профиль не переопределяет
// (иначе переключение на «ESSB» сбрасывало бы Mic In/Line In и drive).
procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer;
out L: TTXProfileList);
// Сколько пресетов реально есть у режима (VAR1/VAR2 идут сверх этого числа). // Сколько пресетов реально есть у режима (VAR1/VAR2 идут сверх этого числа).
function FilterPresetCount(Mode: Integer): Integer; function FilterPresetCount(Mode: Integer): Integer;
// Заводской индекс фильтра режима. // Заводской индекс фильтра режима.
@@ -1715,6 +1786,242 @@ begin
JW(O,'ps_outlier_sigma',T.PSOutlierSigma); JW(O,'ps_outlier_sigma',T.PSOutlierSigma);
end; end;
// ---------------------------------------------------------------------------
// TX-профили: снимок/применение + заводской набор + персист
// ---------------------------------------------------------------------------
procedure TXProfileCapture(const T: TTXSettings; DrivePct: Integer; var P: TTXProfile);
var i: Integer;
begin
P.MicLineIn := T.MicLineIn;
P.MicBoost := T.MicBoost;
P.MicPTTEnabled := T.MicPTTEnabled;
P.MicTipRing := T.MicTipRing;
P.MicBias := T.MicBias;
P.LineInGainDB := T.LineInGainDB;
P.MicGainDB := T.MicGainDB;
P.FilterLow := T.FilterLow;
P.FilterHigh := T.FilterHigh;
P.CompressorOn := T.CompressorOn;
P.CompressorGain := T.CompressorGain;
P.LevelerOn := T.LevelerOn;
P.LevelerTop := T.LevelerTop;
P.LevelerDecay := T.LevelerDecay;
P.ALCOn := T.ALCOn;
P.ALCMaxGain := T.ALCMaxGain;
P.ALCDecay := T.ALCDecay;
P.PhaseRotOn := T.PhaseRotOn;
P.PhaseRotStages := T.PhaseRotStages;
P.PhaseRotFreq := T.PhaseRotFreq;
P.EQOn := T.EQOn;
P.EQNumBands := T.EQNumBands;
for i := 0 to 10 do
begin
P.EQGains[i] := T.EQGains[i];
P.EQFreqs[i] := T.EQFreqs[i];
end;
P.AMCarrierLevel := T.AMCarrierLevel;
P.DrivePercent := EnsureRange(DrivePct, 0, 100);
P.TUNLevel := EnsureRange(T.TUNLevel, 0, 100);
end;
procedure TXProfileApply(const P: TTXProfile; var T: TTXSettings; var DrivePct: Integer);
var i: Integer;
begin
T.MicLineIn := P.MicLineIn;
T.MicBoost := P.MicBoost;
T.MicPTTEnabled := P.MicPTTEnabled;
T.MicTipRing := P.MicTipRing;
T.MicBias := P.MicBias;
T.LineInGainDB := EnsureRange(P.LineInGainDB, -34.5, 12.0);
T.MicGainDB := P.MicGainDB;
T.FilterLow := P.FilterLow;
T.FilterHigh := P.FilterHigh;
T.CompressorOn := P.CompressorOn;
T.CompressorGain := P.CompressorGain;
T.LevelerOn := P.LevelerOn;
T.LevelerTop := P.LevelerTop;
T.LevelerDecay := P.LevelerDecay;
T.ALCOn := P.ALCOn;
T.ALCMaxGain := P.ALCMaxGain;
T.ALCDecay := P.ALCDecay;
T.PhaseRotOn := P.PhaseRotOn;
T.PhaseRotStages := P.PhaseRotStages;
T.PhaseRotFreq := P.PhaseRotFreq;
T.EQOn := P.EQOn;
T.EQNumBands := P.EQNumBands;
for i := 0 to 10 do
begin
T.EQGains[i] := P.EQGains[i];
T.EQFreqs[i] := P.EQFreqs[i];
end;
T.AMCarrierLevel := P.AMCarrierLevel;
T.TUNLevel := EnsureRange(P.TUNLevel, 0, 100);
DrivePct := EnsureRange(P.DrivePercent, 0, 100);
end;
procedure DefaultTXProfiles(const Base: TTXSettings; DrivePct: Integer;
out L: TTXProfileList);
procedure Add(const AName: string; ALow, AHigh: Integer;
AComp: Boolean; ACompGain: Double);
begin
if L.Count >= TXPROF_MAX then Exit;
TXProfileCapture(Base, DrivePct, L.Items[L.Count]);
L.Items[L.Count].Name := AName;
L.Items[L.Count].FilterLow := ALow;
L.Items[L.Count].FilterHigh := AHigh;
L.Items[L.Count].CompressorOn := AComp;
L.Items[L.Count].CompressorGain := ACompGain;
Inc(L.Count);
end;
begin
FillChar(L, SizeOf(L), 0);
L.Count := 0;
L.ActiveIdx := 0;
// «Default» — ровно текущее состояние устройства: после миграции старого
// конфига звук в эфире не меняется ни на йоту.
TXProfileCapture(Base, DrivePct, L.Items[0]);
L.Items[0].Name := 'Default';
L.Count := 1;
// Остальные заводские отличаются от Default ТОЛЬКО полосой и компрессором;
// микрофонный вход, EQ и мощность наследуются (см. коммент к DefaultTXProfiles).
Add('SSB DX', 300, 2700, True, 8.0);
Add('SSB Wide', 200, 3100, False, 5.0);
Add('ESSB 3.5k', 100, 3500, False, 5.0);
Add('ESSB 5k', 100, 5000, False, 5.0);
end;
function TSettingsManager.LoadTXProfiles(const MAC: array of Byte;
out L: TTXProfileList): Boolean;
var
MacStr: string;
DevObj, Root, O: TJSONObject;
Node: TJSONData;
Arr: TJSONArray;
BaseTX: TTXSettings;
DefP: TTXProfile;
i, j: Integer;
begin
FillChar(L, SizeOf(L), 0);
L.Count := 0;
L.ActiveIdx := 0;
Result := False;
MacStr := MacToStr(MAC);
if FRoot.Find(MacStr) = nil then Exit;
DevObj := GetDevObj(MacStr);
if DevObj.Find('tx_profiles') = nil then Exit;
Root := EnsureObj(DevObj, 'tx_profiles');
Node := Root.Find('list');
if not (Node is TJSONArray) then Exit;
Arr := TJSONArray(Node);
// Заводской профиль как источник значений по умолчанию для отсутствующих
// ключей: конфиг, записанный старой версией, дочитывается без дыр.
DefaultTX(BaseTX);
TXProfileCapture(BaseTX, 50, DefP);
DefP.Name := 'Profile';
for i := 0 to Arr.Count - 1 do
begin
if L.Count >= TXPROF_MAX then Break;
if not (Arr.Items[i] is TJSONObject) then Continue;
O := TJSONObject(Arr.Items[i]);
L.Items[L.Count] := DefP;
with L.Items[L.Count] do
begin
Name := Copy(JS(O,'name', DefP.Name), 1, 31);
MicLineIn := JB(O,'mic_line_in', MicLineIn);
MicBoost := JB(O,'mic_boost', MicBoost);
MicPTTEnabled := JB(O,'mic_ptt', MicPTTEnabled);
MicTipRing := JB(O,'mic_tip_ring', MicTipRing);
MicBias := JB(O,'mic_bias', MicBias);
LineInGainDB := EnsureRange(JD(O,'line_in_gain_db', LineInGainDB), -34.5, 12.0);
MicGainDB := JD(O,'mic_gain_db', MicGainDB);
FilterLow := JI(O,'filter_low', FilterLow);
FilterHigh := JI(O,'filter_high', FilterHigh);
CompressorOn := JB(O,'comp_on', CompressorOn);
CompressorGain := JD(O,'comp_gain', CompressorGain);
LevelerOn := JB(O,'lev_on', LevelerOn);
LevelerTop := JD(O,'lev_top', LevelerTop);
LevelerDecay := JI(O,'lev_decay', LevelerDecay);
ALCOn := JB(O,'alc_on', ALCOn);
ALCMaxGain := JD(O,'alc_max_gain', ALCMaxGain);
ALCDecay := JI(O,'alc_decay', ALCDecay);
PhaseRotOn := JB(O,'phr_on', PhaseRotOn);
PhaseRotStages := JI(O,'phr_stages', PhaseRotStages);
PhaseRotFreq := JD(O,'phr_freq', PhaseRotFreq);
EQOn := JB(O,'eq_on', EQOn);
EQNumBands := JI(O,'eq_bands', EQNumBands);
for j := 0 to 10 do
begin
EQGains[j] := JD(O,'eq_g_'+IntToStr(j), EQGains[j]);
EQFreqs[j] := JD(O,'eq_f_'+IntToStr(j), EQFreqs[j]);
end;
AMCarrierLevel := JD(O,'am_carrier', AMCarrierLevel);
DrivePercent := EnsureRange(JI(O,'drive', DrivePercent), 0, 100);
TUNLevel := EnsureRange(JI(O,'tun_level', TUNLevel), 0, 100);
if Name = '' then Name := 'Profile ' + IntToStr(L.Count + 1);
end;
Inc(L.Count);
end;
if L.Count = 0 then Exit; // пустой список = как будто секции нет
L.ActiveIdx := JI(Root, 'active', 0);
if (L.ActiveIdx < 0) or (L.ActiveIdx >= L.Count) then L.ActiveIdx := 0;
Result := True;
end;
procedure TSettingsManager.SaveTXProfiles(const MAC: array of Byte;
const L: TTXProfileList);
var
Root, O: TJSONObject;
Arr: TJSONArray;
i, j, Idx: Integer;
begin
Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'tx_profiles');
JW(Root, 'active', EnsureRange(L.ActiveIdx, 0, Max(0, L.Count - 1)));
// Список пересоздаётся целиком: удаление профиля не должно оставлять хвост.
Idx := Root.IndexOfName('list');
if Idx >= 0 then Root.Delete(Idx);
Arr := TJSONArray.Create;
Root.Add('list', Arr);
for i := 0 to L.Count - 1 do
begin
O := TJSONObject.Create;
Arr.Add(O);
JWS(O,'name', L.Items[i].Name);
JW(O,'mic_line_in', L.Items[i].MicLineIn);
JW(O,'mic_boost', L.Items[i].MicBoost);
JW(O,'mic_ptt', L.Items[i].MicPTTEnabled);
JW(O,'mic_tip_ring', L.Items[i].MicTipRing);
JW(O,'mic_bias', L.Items[i].MicBias);
JW(O,'line_in_gain_db', L.Items[i].LineInGainDB);
JW(O,'mic_gain_db', L.Items[i].MicGainDB);
JW(O,'filter_low', L.Items[i].FilterLow);
JW(O,'filter_high', L.Items[i].FilterHigh);
JW(O,'comp_on', L.Items[i].CompressorOn);
JW(O,'comp_gain', L.Items[i].CompressorGain);
JW(O,'lev_on', L.Items[i].LevelerOn);
JW(O,'lev_top', L.Items[i].LevelerTop);
JW(O,'lev_decay', L.Items[i].LevelerDecay);
JW(O,'alc_on', L.Items[i].ALCOn);
JW(O,'alc_max_gain', L.Items[i].ALCMaxGain);
JW(O,'alc_decay', L.Items[i].ALCDecay);
JW(O,'phr_on', L.Items[i].PhaseRotOn);
JW(O,'phr_stages', L.Items[i].PhaseRotStages);
JW(O,'phr_freq', L.Items[i].PhaseRotFreq);
JW(O,'eq_on', L.Items[i].EQOn);
JW(O,'eq_bands', L.Items[i].EQNumBands);
for j := 0 to 10 do
begin
JW(O,'eq_g_'+IntToStr(j), L.Items[i].EQGains[j]);
JW(O,'eq_f_'+IntToStr(j), L.Items[i].EQFreqs[j]);
end;
JW(O,'am_carrier', L.Items[i].AMCarrierLevel);
JW(O,'drive', L.Items[i].DrivePercent);
JW(O,'tun_level', L.Items[i].TUNLevel);
end;
end;
class procedure TSettingsManager.DefaultAlex(out A: TAlexSettings); class procedure TSettingsManager.DefaultAlex(out A: TAlexSettings);
var i: Integer; var i: Integer;
begin begin
+333 -70
View File
@@ -31,6 +31,13 @@ uses
FlatRadioButton, FlatRadioButton,
EqualizerControl, AudioOutput, AudioInput, AppTheme, Settings, BoardUtils, DpiUtils; EqualizerControl, AudioOutput, AudioInput, AppTheme, Settings, BoardUtils, DpiUtils;
const
// Подвкладки страницы Transmit: Profile / EQ / Hardware / Display.
TXTAB_COUNT = 4;
// Tag контейнера подвкладки: ApplyTheme красит такие панели фоном страницы,
// а не цветом группы (иначе подложка выглядела бы как одна гигантская группа).
TAG_PAGE_BG = 909;
type type
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон // PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
TOnPASettingsChange = procedure(MaxPower: Double; TOnPASettingsChange = procedure(MaxPower: Double;
@@ -45,6 +52,10 @@ type
TOnCalibrationChange = procedure(const C: TCalibration) of object; TOnCalibrationChange = procedure(const C: TCalibration) of object;
// TX-настройки целиком (любая правка → один callback с актуальной структурой) // TX-настройки целиком (любая правка → один callback с актуальной структурой)
TOnTXSettingsChange = procedure(const T: TTXSettings) of object; 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; TOnFreqMhzDigitsChange = procedure(Digits: Integer) of object;
TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector, TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
@@ -204,6 +215,18 @@ type
// ---- Transmit tab controls ---- // ---- Transmit tab controls ----
// Текущее состояние, отдаётся в OnTXChange целиком при любой правке. // Текущее состояние, отдаётся в OnTXChange целиком при любой правке.
FTX: TTXSettings; 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" FCmbTXMicJack: TFlatComboBox; // "Mic In" / "Line In"
FChkTXMicBoost: TFlatCheckBox; // +20 dB (только Mic In) FChkTXMicBoost: TFlatCheckBox; // +20 dB (только Mic In)
FLblLineInGain: TLabel; // метка "Line In gain" (только Line In) FLblLineInGain: TLabel; // метка "Line In gain" (только Line In)
@@ -411,6 +434,10 @@ type
FOnWfAGCNFChange: TOnWfAGCNFChange; FOnWfAGCNFChange: TOnWfAGCNFChange;
FOnFreqMhzDigitsChange: TOnFreqMhzDigitsChange; FOnFreqMhzDigitsChange: TOnFreqMhzDigitsChange;
FOnTXChange: TOnTXSettingsChange; FOnTXChange: TOnTXSettingsChange;
FOnTXProfileSelect: TOnTXProfileIdx;
FOnTXProfileAdd: TOnTXProfileName;
FOnTXProfileRename: TOnTXProfileIdxName;
FOnTXProfileDelete: TOnTXProfileIdx;
FLoading: Boolean; FLoading: Boolean;
@@ -462,6 +489,13 @@ type
procedure OnTXAnyChange(Sender: TObject); procedure OnTXAnyChange(Sender: TObject);
procedure OnMicJackChange(Sender: TObject); procedure OnMicJackChange(Sender: TObject);
procedure UpdateMicJackVisibility; 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 ApplyTheme(const T: TAppTheme);
procedure NavClick(Sender: TObject); procedure NavClick(Sender: TObject);
procedure SelectPage(APage: TWinControl; ANav: TFlatButton); procedure SelectPage(APage: TWinControl; ANav: TFlatButton);
@@ -571,6 +605,15 @@ type
const SerAndromeda: array of Boolean; const SerAndromeda: array of Boolean;
TcpEnabled: Boolean; TcpPort: Integer); TcpEnabled: Boolean; TcpPort: Integer);
procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0); 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 LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
procedure LoadXvtrSettings(const X: TXvtrSettings); procedure LoadXvtrSettings(const X: TXvtrSettings);
procedure LoadOCSettings(const O: TOCSettings); procedure LoadOCSettings(const O: TOCSettings);
@@ -608,6 +651,10 @@ type
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange; property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
property OnFreqMhzDigitsChange: TOnFreqMhzDigitsChange read FOnFreqMhzDigitsChange write FOnFreqMhzDigitsChange; property OnFreqMhzDigitsChange: TOnFreqMhzDigitsChange read FOnFreqMhzDigitsChange write FOnFreqMhzDigitsChange;
property OnTXChange: TOnTXSettingsChange read FOnTXChange write FOnTXChange; 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 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;
@@ -1352,6 +1399,40 @@ const
var var
Grp: TPanel; Grp: TPanel;
Y, i: Integer; 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; function MkSpin(AParent: TWinControl; AX, AY: Integer; AMin, AMax, AVal: Integer): TFlatSpinEdit;
begin begin
@@ -1417,17 +1498,46 @@ begin
with TLabel.Create(Self) do with TLabel.Create(Self) do
begin begin
Parent := FPageTransmit; Parent := FPageTransmit;
Caption := 'TX-цепь WDSP: микрофон, фильтр, динамическая обработка, модуляция.'; Caption := 'Profile и EQ — содержимое активного TX-профиля; '
SetBounds(DpiScale(MARGIN), DpiScale(48), DpiScale(700), DpiScale(20)); + 'Hardware и Display — свойства устройства, от профиля не зависят.';
SetBounds(DpiScale(MARGIN), DpiScale(48), DpiScale(GRP_W), DpiScale(20));
Font.Name := UI_FONT; Font.Name := UI_FONT;
Font.Size := 9; Font.Size := 9;
Font.Color := CLR_TEXTDIM; Font.Color := CLR_TEXTDIM;
end; end;
// ---- Mic options ---- // ---- Полоса профиля (над подвкладками, видна всегда) --------------------
// Mic source (Radio vs SoundCard) определяется автоматически по источнику PTT. Grp := MakeGroupPanel(FPageTransmit, 'TX Profile', MARGIN, 78, GRP_W, 124);
Y := 86;
Grp := MakeGroupPanel(FPageTransmit, 'Microphone', MARGIN, Y, GRP_W, 188); 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 // Row 0: Mic jack selector | Boost (Mic In) или LineInGain (Line In) | Bias | PTT
MakeLbl(Grp, 'Mic jack', PAD, R1 + 5, 65); MakeLbl(Grp, 'Mic jack', PAD, R1 + 5, 65);
@@ -1444,58 +1554,35 @@ begin
FChkTXMicBias := MkChk(Grp, PAD + 375, R1, 105, 'Mic bias'); FChkTXMicBias := MkChk(Grp, PAD + 375, R1, 105, 'Mic bias');
FChkTXMicPTT := MkChk(Grp, PAD + 490, R1, 160, 'Mic-jack PTT'); FChkTXMicPTT := MkChk(Grp, PAD + 490, R1, 160, 'Mic-jack PTT');
// Row 1: Connector Tip/Ring (только Orion boards 4 и 5) // Row 1: Mic gain
FLblTipRing := MakeLbl(Grp, 'Connector', PAD, R1 + STEP + 5, 70); MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + STEP + 5, LW);
FCmbTXMicTR := MkCmb(Grp, PAD + 73, R1 + STEP, 90); 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('Tip'); // MicTipRing=False, bit3=0
FCmbTXMicTR.Items.Add('Ring'); // MicTipRing=True, bit3=1 FCmbTXMicTR.Items.Add('Ring'); // MicTipRing=True, bit3=1
FCmbTXMicTR.ItemIndex := 0; FCmbTXMicTR.ItemIndex := 0;
FLblTipRing.Visible := False; FLblTipRing.Visible := False;
FCmbTXMicTR.Visible := False; FCmbTXMicTR.Visible := False;
// Row 2: Mic gain | ATT on TX // ---- Кромки TX-фильтра (FIR/фаза/окно — на вкладке Hardware) ----
MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + 2 * STEP + 5, LW); Y := 176 + 12;
FEdTXMicGain := MkFloat(Grp, CX, R1 + 2 * STEP, 10.0, -20.0, 60.0, 0.5); Grp := MakeGroupPanel(FTXSubPage[0], 'TX Filter (bandpass)', MARGIN, Y, GRP_W, 108);
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);
MakeLbl(Grp, 'Low cut (Hz)', PAD, R1 + 5, LW); MakeLbl(Grp, 'Low cut (Hz)', PAD, R1 + 5, LW);
FEdTXFiltLow := MkSpin(Grp, CX, R1, 0, 8000, 200); FEdTXFiltLow := MkSpin(Grp, CX, R1, 0, 8000, 200);
MakeLbl(Grp, 'High cut (Hz)', PAD + 320, R1 + 5, 90); MakeLbl(Grp, 'High cut (Hz)', PAD + 320, R1 + 5, 90);
FEdTXFiltHigh := MkSpin(Grp, PAD + 320 + 100, R1, 100, 12000, 3100); 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); Inc(Y, 108 + 12);
FCmbTXFiltNC.Items.AddStrings(['256','512','1024','2048','4096']); Grp := MakeGroupPanel(FTXSubPage[0], 'Dynamic processing', MARGIN, Y, GRP_W, 220);
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);
FChkTXCompOn := MkChk(Grp, PAD, R1, 220, 'Compressor'); FChkTXCompOn := MkChk(Grp, PAD, R1, 220, 'Compressor');
MakeLbl(Grp, 'Gain (dB)', PAD + 230, R1 + 5, 80); 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); 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); FEdTXPHRFreq := MkFloat(Grp, PAD + 520, R1 + 3 * STEP, 338.0, 50.0, 3000.0, 1.0);
// ---- TX EQ ---- // ---- Мощность и несущая AM (тоже часть профиля) ----
Inc(Y, 220 + 12); 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 := TEqualizerControl.Create(Self);
FTXEQControl.Parent := Grp; FTXEQControl.Parent := Grp;
@@ -1532,33 +1638,75 @@ 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;
// ---- AM / FM / CTCSS ---- FTXSubPage[1].Height := DpiScale(390 + 12);
Inc(Y, 390 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'AM / FM / CTCSS', MARGIN, Y, GRP_W, 260);
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); Grp := MakeGroupPanel(FTXSubPage[2], 'Radio', MARGIN, 0, GRP_W, 108);
FEdTXFMDev := MkFloat(Grp, CX, R1 + STEP, 5000.0, 1000.0, 25000.0, 100.0);
MakeLbl(Grp, 'FM AF low cut', PAD, R1 + 2 * STEP + 5, LW); MakeLbl(Grp, 'ATT on TX (dB)', PAD, R1 + 5, LW);
FEdTXFMLow := MkSpin(Grp, CX, R1 + 2 * STEP, 0, 5000, 300); FEdTXAttOnTX := MkSpin(Grp, CX, R1, 0, 31, 31);
MakeLbl(Grp, 'FM AF high cut', PAD + 320, R1 + 2 * STEP + 5, 100); MakeLbl(Grp, 'Tune tone (Hz)', PAD + 320, R1 + 5, 110);
FEdTXFMHigh := MkSpin(Grp, PAD + 320 + 110, R1 + 2 * STEP, 100, 8000, 3000); 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); Y := 108 + 12;
FCmbTXFMEmph := MkCmb(Grp, CX, R1 + 3 * STEP, 180); 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.Items.AddStrings(['Pre-EQ','Post-EQ']);
FCmbTXFMEmph.ItemIndex := 1; FCmbTXFMEmph.ItemIndex := 1;
FChkTXCTCSSOn := MkChk(Grp, PAD, R1 + 4 * STEP, 220, 'CTCSS tone'); FChkTXCTCSSOn := MkChk(Grp, PAD, R1 + 3 * STEP, 220, 'CTCSS tone');
MakeLbl(Grp, 'Freq (Hz)', PAD + 230, R1 + 4 * STEP + 5, 80); MakeLbl(Grp, 'Freq (Hz)', PAD + 230, R1 + 3 * STEP + 5, 80);
FEdTXCTCSSFreq := MkFloat(Grp, PAD + 320, R1 + 4 * STEP, 100.0, 50.0, 300.0, 0.1); FEdTXCTCSSFreq := MkFloat(Grp, PAD + 320, R1 + 3 * STEP, 100.0, 50.0, 300.0, 0.1);
// ---- TX Display (отдельный analyzer для TX-спектра/водопада) ---- MakeLbl(Grp, 'Свойство канала/репитера — профилем голоса не переключается.',
Inc(Y, 260 + 12); PAD, R1 + 4 * STEP + 5, GRP_W - 2 * PAD);
Grp := MakeGroupPanel(FPageTransmit, 'TX Display (Analyzer)', MARGIN, Y, GRP_W, 318);
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); MakeLbl(Grp, 'FFT size', PAD, R1 + 5, LW);
FCmbTXFFT := MkCmb(Grp, CX, R1, 140); 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); FEdTXWfAvgTime := MkFloat(Grp, PAD + 540, R1 + 5 * STEP, 120.0, 1.0, 5000.0, 1.0);
// ---- TX Grid (RefLevel / Range / GridStep — отдельные от RX) ---- // ---- TX Grid (RefLevel / Range / GridStep — отдельные от RX) ----
Inc(Y, 318 + 12); Y := 318 + 12;
Grp := MakeGroupPanel(FPageTransmit, 'TX Grid', MARGIN, Y, GRP_W, 170); Grp := MakeGroupPanel(FTXSubPage[3], 'TX Grid', MARGIN, Y, GRP_W, 170);
MakeLbl(Grp, 'Reference level', PAD, R1 + 5, LW); MakeLbl(Grp, 'Reference level', PAD, R1 + 5, LW);
FEdTXRefLevel := TFlatEdit.Create(Self); FEdTXRefLevel := TFlatEdit.Create(Self);
@@ -1624,8 +1772,12 @@ begin
FCmbTXGridStep := MkCmb(Grp, CX, R1 + 2 * STEP, 130); FCmbTXGridStep := MkCmb(Grp, CX, R1 + 2 * STEP, 130);
for i := 0 to High(GRID_STEP_NAMES) do FCmbTXGridStep.Items.Add(GRID_STEP_NAMES[i]); for i := 0 to High(GRID_STEP_NAMES) do FCmbTXGridStep.Items.Add(GRID_STEP_NAMES[i]);
FCmbTXGridStep.ItemIndex := 1; FCmbTXGridStep.ItemIndex := 1;
end;
FTXSubPage[3].Height := DpiScale(Y + 170 + 12);
FTXSubIdx := -1;
SelectTXSubPage(0);
end;
procedure TSettingsForm.CollectTXFromUI; procedure TSettingsForm.CollectTXFromUI;
var i: Integer; var i: Integer;
begin begin
@@ -1729,6 +1881,115 @@ begin
FireTXChange; FireTXChange;
end; 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); procedure TSettingsForm.LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
var i: Integer; IsOrion: Boolean; var i: Integer; IsOrion: Boolean;
begin begin
@@ -2708,6 +2969,8 @@ procedure TSettingsForm.ApplyTheme(const T: TAppTheme);
begin begin
if TPanel(Ctrl).Height = 1 then if TPanel(Ctrl).Height = 1 then
TPanel(Ctrl).Color := T.Border TPanel(Ctrl).Color := T.Border
else if TPanel(Ctrl).Tag = TAG_PAGE_BG then
TPanel(Ctrl).Color := T.BG // подложка подвкладки, не группа
else else
TPanel(Ctrl).Color := T.Panel; TPanel(Ctrl).Color := T.Panel;
WalkControls(TPanel(Ctrl)); WalkControls(TPanel(Ctrl));