mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37: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:
+147
@@ -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);
|
||||||
@@ -5487,6 +5528,7 @@ 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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user