Files
ewsdr/SettingsForm.pas
T
ew8bakandClaude Opus 4.7 ce97ee486c WIP: TX implementation — per-device settings, separate analyzer, sound-card mic
- Settings: TTXSettings record + LoadTX/SaveTX, JSON section "tx" per MAC
- WDSPEngine: separate TX_DISP_ID analyzer with own FFT/Window/Det/Avg
  parameters, ApplyTXAnalyzerSettings + setters
- WDSPEngine: full TX chain setters (Filter/Compressor/Leveler/ALC/
  PhaseRot/EQ/AMCarrier/FM/CTCSS) + ApplyTXChainSettings
- WDSPEngine: ProcessTXBlock now feeds TX analyzer in DISPLAY_BLOCK_SIZE
  chunks (was passing whole FTXOut → analyzer ignored most of it)
- WDSPEngine: ActiveDisplayID switches RX/TX in UpdateSpectrum
- WDSPEngine: RX audio muted while TX active to avoid local feedback
- WDSPEngine: default EQ frequencies in Create (zero freqs broke TX chain)
- WDSPEngine: PushTXMicSampleD + OnPullMicSamples for sound-card path
- AudioInput: Available counter for non-blocking pull
- MainForm: PullSoundCardMic, BuildMicLineSelectByte, SendDUCSpecific
  with mic bias/boost/line/PTT bits, OnTXSettingsChange
- MainForm: auto-switch MicSource on TX device select, TX diagnostic
  in StatusBar panel [5]
- SettingsForm: new Transmit nav page with Microphone, Filter, Dynamic
  processing, EQ, AM/FM/CTCSS, TX Display sections

NOT YET WORKING: sound-card mic produces ring activity but signal still
not visible on TX spectrum/waterfall — needs further investigation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 08:06:58 +03:00

2114 lines
71 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit SettingsForm;
{
SettingsForm.pas — диалог настроек EWSDR SDR приложения.
Вкладки: General | Audio | Display | DSP | Transmit | PA Settings | CAT
Display имеет подвкладки: General | RX1 | RX2 | TX
Все изменения применяются МГНОВЕННО через events:
OnDisplayChange — изменены FFT/Window/Detector/Avg параметры
OnWaterfallChange — изменены параметры водопада
OnGridChange — изменена сетка спектра
OnAudioChange — изменено аудио устройство/буфер
OnSampleRateChange — изменён audio output sample rate (только PortAudio, DDC не трогается)
Для применения настроек: создайте форму, зарегистрируйте callbacks,
потом вызовите Show или ShowModal.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Math,
Forms, Controls, Graphics, Dialogs,
StdCtrls, ExtCtrls, Spin,
FlatButton, AudioOutput, AudioInput, AppTheme, Settings;
type
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
TOnPASettingsChange = procedure(MaxPower: Double;
const BandCal: array of Double) of object;
// TX-настройки целиком (любая правка → один callback с актуальной структурой)
TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object;
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode: Integer;
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double) of object;
TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object;
TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object;
TOnVisibilityChange = procedure(ShowSpectrum, ShowWaterfall: Boolean) of object;
TOnFPSChange = procedure(FPS: Integer) of object;
TOnThemeChange = procedure(LightTheme: Boolean) of object;
TOnCATChange = procedure(
const SerEnabled: array of Boolean;
const SerPort: array of string;
const SerBaud: array of Integer;
const SerDataBits: array of Integer;
const SerStopBits: array of Integer;
const SerParity: array of Integer;
TcpEnabled: Boolean; TcpPort: Integer) of object;
{ TSettingsForm }
TSettingsForm = class(TForm)
private
// ---- Layout ----
FNavPanel: TPanel;
FContentPanel: TPanel;
FPageAudio: TScrollBox;
FPageDisplay: TScrollBox;
FPageSpectrum: TScrollBox;
FPageWaterfall: TScrollBox;
FPagePA: TScrollBox;
FPageAdvanced: TScrollBox;
FPageCAT: TScrollBox;
FPageTransmit: TScrollBox;
FNavAudio: TFlatButton;
FNavDisplay: TFlatButton;
FNavSpectrum: TFlatButton;
FNavWaterfall: TFlatButton;
FNavPA: TFlatButton;
FNavAdvanced: TFlatButton;
FNavCAT: TFlatButton;
FNavTransmit: TFlatButton;
// ---- Audio tab controls ----
FCmbRXDev: TComboBox;
FRXDevIndices: array[0..63] of Integer;
FRXDevCount: Integer;
FCmbTXDev: TComboBox;
FTXDevIndices: array[0..63] of Integer;
FTXDevCount: Integer;
// ---- RX1 sub-tab controls ----
FCmbFFTSize: TComboBox;
FCmbWindow: TComboBox;
FCmbSpecDet: TComboBox;
FCmbSpecAvg: TComboBox;
FSpecAvgTime: TFloatSpinEdit;
FCmbWfDet: TComboBox;
FCmbWfAvg: TComboBox;
FWfAvgTime: TFloatSpinEdit;
FWfHigh: TFloatSpinEdit;
FWfLow: TFloatSpinEdit;
FWfAGCOffset: TFloatSpinEdit;
FEdRefLevel: TEdit;
FEdRange: TEdit;
FCmbGridStep: TComboBox;
// ---- General display tab controls ----
FChkShowSpectrum: TCheckBox;
FChkShowWaterfall: TCheckBox;
FCmbFPS: TComboBox;
FChkLightTheme: TCheckBox;
// ---- PA Settings tab controls ----
FEdMaxPower: TSpinEdit;
FEdBandCal: array[0..10] of TFloatSpinEdit;
// ---- Transmit tab controls ----
// Текущее состояние, отдаётся в OnTXChange целиком при любой правке.
FTX: TTXSettings;
// Mic source / options
FCmbTXMicSource: TComboBox;
FChkTXMicBoost: TCheckBox;
FChkTXMicBias: TCheckBox;
FChkTXMicLineIn: TCheckBox;
FChkTXMicPTT: TCheckBox;
FEdTXMicGain: TFloatSpinEdit;
// Filter
FEdTXFiltLow: TSpinEdit;
FEdTXFiltHigh: TSpinEdit;
FCmbTXFiltNC: TComboBox;
FCmbTXFiltMP: TComboBox;
FCmbTXFiltWindow: TComboBox;
// Compressor
FChkTXCompOn: TCheckBox;
FEdTXCompGain: TFloatSpinEdit;
// Leveler
FChkTXLevOn: TCheckBox;
FEdTXLevTop: TFloatSpinEdit;
FEdTXLevDecay: TSpinEdit;
// ALC
FChkTXALCOn: TCheckBox;
FEdTXALCMaxGain: TFloatSpinEdit;
FEdTXALCDecay: TSpinEdit;
// Phase Rotator
FChkTXPHROn: TCheckBox;
FEdTXPHRStages: TSpinEdit;
FEdTXPHRFreq: TFloatSpinEdit;
// EQ
FChkTXEQOn: TCheckBox;
FCmbTXEQBands: TComboBox;
FEdTXEQGain: array[0..10] of TFloatSpinEdit;
FEdTXEQFreq: array[0..10] of TFloatSpinEdit;
// AM
FEdTXAMCarrier: TFloatSpinEdit;
// FM
FEdTXFMDev: TFloatSpinEdit;
FEdTXFMLow: TSpinEdit;
FEdTXFMHigh: TSpinEdit;
FCmbTXFMEmph: TComboBox;
// CTCSS
FChkTXCTCSSOn: TCheckBox;
FEdTXCTCSSFreq: TFloatSpinEdit;
// TX Display (отдельный analyzer)
FCmbTXFFT: TComboBox;
FCmbTXWindow: TComboBox;
FCmbTXSpecDet: TComboBox;
FCmbTXSpecAvg: TComboBox;
FEdTXSpecAvgTime: TFloatSpinEdit;
FCmbTXWfDet: TComboBox;
FCmbTXWfAvg: TComboBox;
FEdTXWfAvgTime: TFloatSpinEdit;
// ---- CAT tab controls ----
FCATSerialEn: array[0..3] of TCheckBox;
FCATSerialPort: array[0..3] of TEdit;
FCATSerialBaud: array[0..3] of TComboBox;
FCATSerialDataBits: array[0..3] of TComboBox;
FCATSerialStopBits: array[0..3] of TComboBox;
FCATSerialParity: array[0..3] of TComboBox;
FCATTcpEn: TCheckBox;
FCATTcpPort: TSpinEdit;
// ---- Close button ----
FBtnClose: TFlatButton;
// ---- Callbacks ----
FOnCATChange: TOnCATChange;
FOnPAChange: TOnPASettingsChange;
FOnDisplayChange: TOnDisplayParamChange;
FOnWaterfallChange: TOnWaterfallParamChange;
FOnGridChange: TOnGridParamChange;
FOnAudioDevChange: TOnAudioDevChange;
FOnAudioInDevChange: TOnAudioInDevChange;
FOnVisibilityChange: TOnVisibilityChange;
FOnFPSChange: TOnFPSChange;
FOnThemeChange: TOnThemeChange;
FOnTXChange: TOnTXSettingsChange;
FLoading: Boolean;
procedure BuildUI;
procedure BuildAudioTab;
procedure BuildDisplayTab;
procedure BuildRX1Tab;
procedure BuildPATab;
procedure BuildCATTab;
procedure BuildTransmitTab;
procedure CollectTXFromUI;
procedure FireTXChange;
procedure OnTXAnyChange(Sender: TObject);
procedure ApplyTheme(const T: TAppTheme);
procedure NavClick(Sender: TObject);
procedure SelectPage(APage: TWinControl; ANav: TFlatButton);
function MakeLbl(AParent: TWinControl; const Cap: string;
ALeft, ATop, AW: Integer): TLabel;
function MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer;
OnChange: TNotifyEvent): TComboBox;
function MakeGroupPanel(AParent: TWinControl; const Cap: string;
ALeft, ATop, AW, AH: Integer): TPanel;
function MakeScrollPage: TScrollBox;
function MakeNavButton(const Cap: string; ATop: Integer): TFlatButton;
procedure OnFFTSizeChange(Sender: TObject);
procedure OnWindowChange(Sender: TObject);
procedure OnSpecDetChange(Sender: TObject);
procedure OnSpecAvgModeChange(Sender: TObject);
procedure OnSpecAvgTimeChange(Sender: TObject);
procedure OnWfDetChange(Sender: TObject);
procedure OnWfAvgModeChange(Sender: TObject);
procedure OnWfAvgTimeChange(Sender: TObject);
procedure OnWfThresholdChange(Sender: TObject);
procedure OnRefLevelChange(Sender: TObject);
procedure OnRangeChange(Sender: TObject);
procedure OnGridStepChange(Sender: TObject);
procedure OnRXDevChange(Sender: TObject);
procedure OnTXDevChange(Sender: TObject);
procedure OnVisibilityChkChange(Sender: TObject);
procedure OnFPSCmbChange(Sender: TObject);
procedure OnLightThemeChkChange(Sender: TObject);
procedure OnMaxPowerChange(Sender: TObject);
procedure OnBandCalChange(Sender: TObject);
procedure OnCATSerialChange(Sender: TObject);
procedure OnCATTcpChange(Sender: TObject);
procedure FireCATChange;
procedure BtnCloseClick(Sender: TObject);
procedure FireDisplayChange;
procedure FireWaterfallChange;
procedure FireGridChange;
function GetSpecAvgTimeMS: Double;
function GetWfAvgTimeMS: Double;
function GetWfHigh: Double;
function GetWfLow: Double;
function GetWfAGCOffset: Double;
function GetRefLevel: Double;
function GetRange: Double;
function GetGridStep: Double;
function GetFFTSize: Integer;
public
constructor Create(AOwner: TComponent); override;
procedure LoadValues(
FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
SpecAvgTimeMS: Double;
WfDet, WfAvgMode: Integer;
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double;
RefLevel, Range, GridStep: Double;
const AudioOutDevName: string;
const AudioInDevName: string);
procedure RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput);
procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean);
procedure LoadFPS(FPS: Integer);
procedure LoadLightTheme(ALight: Boolean);
procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double);
procedure LoadCATSettings(
const SerEnabled: array of Boolean;
const SerPort: array of string;
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
TcpEnabled: Boolean; TcpPort: Integer);
procedure LoadTXSettings(const T: TTXSettings);
property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange;
property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange;
property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange;
property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange;
property OnGridChange: TOnGridParamChange read FOnGridChange write FOnGridChange;
property OnAudioDevChange: TOnAudioDevChange read FOnAudioDevChange write FOnAudioDevChange;
property OnAudioInDevChange: TOnAudioInDevChange read FOnAudioInDevChange write FOnAudioInDevChange;
property OnVisibilityChange: TOnVisibilityChange read FOnVisibilityChange write FOnVisibilityChange;
property OnFPSChange: TOnFPSChange read FOnFPSChange write FOnFPSChange;
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
property OnTXChange: TOnTXSettingsChange read FOnTXChange write FOnTXChange;
end;
implementation
const
CLR_BG = TColor($00121212);
CLR_PANEL = TColor($001A1A1A);
CLR_TEXT = TColor($00E0E0E0);
CLR_TEXTDIM = TColor($00888888);
CLR_BORDER = TColor($00303030);
CLR_ACCENT = TColor($0040FF80);
CLR_INPUT = TColor($00FFFFFF);
CLR_INPUT_TEXT = TColor($00202020);
{$IFDEF MSWINDOWS}
UI_FONT = 'Segoe UI';
{$ELSE}
UI_FONT = 'Sans';
{$ENDIF}
BTN_H = 24;
GRP_TITLE_H = 22; // высота заголовка в group-панелях (title label + divider)
CAT_BAUD_VALS: array[0..7] of Integer = (1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200);
CAT_BAUD_NAMES: array[0..7] of string = ('1200','2400','4800','9600','19200','38400','57600','115200');
FFT_SIZES: array[0..6] of Integer = (4096, 8192, 16384, 32768, 65536, 131072, 262144);
FFT_NAMES: array[0..6] of string = ('4096','8192','16384','32768','65536','131072','262144');
WINDOW_NAMES: array[0..6] of string = (
'Rectangular', '4-term Blackman-Harris', 'Hann',
'Flat-top', 'Hamming', 'Kaiser', '7-term Blackman-Harris');
DETECTOR_NAMES: array[0..4] of string = (
'Peak', 'Rosenfell', 'Average', 'Sample', 'RMS');
AVGMODE_NAMES: array[0..3] of string = (
'None', 'Recursive', 'Time Window', 'Log Recursive');
AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3);
WF_AVGMODE_NAMES: array[0..3] of string = (
'None', 'Recursive', 'Time Window', 'Log Recursive');
WF_AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3);
GRID_STEPS: array[0..4] of Double = (5.0, 10.0, 20.0, 30.0, 40.0);
GRID_STEP_NAMES: array[0..4] of string = ('5 dB','10 dB','20 dB','30 dB','40 dB');
FPS_VALUES: array[0..12] of Integer = (5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 80, 90, 100);
FPS_NAMES: array[0..12] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps',
'40 fps','50 fps','60 fps','75 fps','80 fps','90 fps','100 fps');
PA_BAND_NAMES: array[0..10] of string =
('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
{ TSettingsForm }
constructor TSettingsForm.Create(AOwner: TComponent);
begin
inherited CreateNew(AOwner);
Caption := 'Settings';
Width := 900;
Height := 660;
Position := poScreenCenter;
BorderStyle := bsSizeable;
Color := CLR_BG;
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXT;
FLoading := False;
FRXDevCount := 0;
BuildUI;
ApplyTheme(DarkTheme);
end;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function TSettingsForm.MakeLbl(AParent: TWinControl; const Cap: string;
ALeft, ATop, AW: Integer): TLabel;
begin
Result := TLabel.Create(Self);
Result.Parent := AParent;
Result.Caption := Cap;
Result.Left := ALeft;
Result.Top := ATop;
if AW > 0 then Result.Width := AW;
Result.Font.Color := CLR_TEXTDIM;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
end;
function TSettingsForm.MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer;
OnChange: TNotifyEvent): TComboBox;
begin
Result := TComboBox.Create(Self);
Result.Parent := AParent;
Result.Left := ALeft;
Result.Top := ATop;
Result.Width := AW;
Result.Height := BTN_H;
Result.Style := csDropDownList;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnChange;
end;
// Создаёт панель-группу с заголовком и разделителем (вместо TGroupBox).
// На Windows TGroupBox рисует фрейм системными цветами, TPanel — нет.
function TSettingsForm.MakeGroupPanel(AParent: TWinControl; const Cap: string;
ALeft, ATop, AW, AH: Integer): TPanel;
var
TitleLbl: TLabel;
Divider: TPanel;
begin
Result := TPanel.Create(Self);
Result.Parent := AParent;
Result.SetBounds(ALeft, ATop, AW, AH);
Result.BevelOuter := bvNone;
Result.Color := CLR_PANEL;
TitleLbl := TLabel.Create(Self);
TitleLbl.Parent := Result;
TitleLbl.Caption := Cap;
TitleLbl.SetBounds(8, 5, AW - 16, 14);
TitleLbl.Font.Color := CLR_ACCENT;
TitleLbl.Font.Name := UI_FONT;
TitleLbl.Font.Size := 9;
TitleLbl.Font.Style := [fsBold];
Divider := TPanel.Create(Self);
Divider.Parent := Result;
Divider.SetBounds(0, GRP_TITLE_H, AW + 200, 1);
Divider.BevelOuter := bvNone;
Divider.Color := CLR_BORDER;
end;
function TSettingsForm.MakeScrollPage: TScrollBox;
begin
Result := TScrollBox.Create(Self);
Result.Parent := FContentPanel;
Result.Align := alClient;
Result.BorderStyle := bsNone;
Result.Color := CLR_BG;
Result.AutoScroll := True;
Result.Visible := False;
end;
function TSettingsForm.MakeNavButton(const Cap: string; ATop: Integer): TFlatButton;
begin
Result := TFlatButton.Create(Self);
Result.Parent := FNavPanel;
Result.Caption := Cap;
Result.SetBounds(10, ATop, 166, 32);
Result.Anchors := [akLeft, akTop, akRight];
Result.OnClick := NavClick;
Result.ClrNorm := CLR_PANEL;
Result.ClrBorder := CLR_PANEL;
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.Font.Style := [];
end;
// ---------------------------------------------------------------------------
// BuildUI
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildUI;
begin
FNavPanel := TPanel.Create(Self);
FNavPanel.Parent := Self;
FNavPanel.Align := alLeft;
FNavPanel.Width := 188;
FNavPanel.BevelOuter := bvNone;
FNavPanel.Color := TColor($00151515);
with TLabel.Create(Self) do
begin
Parent := FNavPanel;
Caption := 'Settings';
SetBounds(14, 14, 160, 24);
Font.Name := UI_FONT;
Font.Size := 14;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FNavPanel;
Caption := 'EWSDR';
SetBounds(14, 39, 160, 18);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
FNavAudio := MakeNavButton('Audio', 74);
FNavDisplay := MakeNavButton('Display', 110);
FNavSpectrum := MakeNavButton('Spectrum', 146);
FNavWaterfall := MakeNavButton('Waterfall', 182);
FNavTransmit := MakeNavButton('Transmit', 218);
FNavPA := MakeNavButton('Power Amplifier', 254);
FNavAdvanced := MakeNavButton('Advanced', 290);
FNavCAT := MakeNavButton('CAT', 326);
FContentPanel := TPanel.Create(Self);
FContentPanel.Parent := Self;
FContentPanel.Align := alClient;
FContentPanel.BevelOuter := bvNone;
FContentPanel.Color := CLR_BG;
FPageAudio := MakeScrollPage;
FPageDisplay := MakeScrollPage;
FPageSpectrum := MakeScrollPage;
FPageWaterfall := MakeScrollPage;
FPageTransmit := MakeScrollPage;
FPagePA := MakeScrollPage;
FPageAdvanced := MakeScrollPage;
FPageCAT := MakeScrollPage;
BuildAudioTab;
BuildDisplayTab;
BuildRX1Tab;
BuildTransmitTab;
BuildPATab;
BuildCATTab;
FBtnClose := TFlatButton.Create(Self);
FBtnClose.Parent := Self;
FBtnClose.Caption := 'Close';
FBtnClose.SetBounds(Width - 106, Height - 40, 92, 28);
FBtnClose.Anchors := [akRight, akBottom];
FBtnClose.OnClick := BtnCloseClick;
FBtnClose.ClrNorm := CLR_PANEL;
FBtnClose.ClrBorder := TColor($00404040);
FBtnClose.ClrHot := TColor($00303030);
FBtnClose.ClrActive := TColor($00003300);
FBtnClose.ClrText := CLR_TEXT;
FBtnClose.ClrTextAct:= CLR_ACCENT;
FBtnClose.Font.Name := UI_FONT;
FBtnClose.Font.Size := 9;
SelectPage(FPageAudio, FNavAudio);
end;
procedure TSettingsForm.NavClick(Sender: TObject);
begin
if Sender = FNavAudio then SelectPage(FPageAudio, FNavAudio)
else if Sender = FNavDisplay then SelectPage(FPageDisplay, FNavDisplay)
else if Sender = FNavSpectrum then SelectPage(FPageSpectrum, FNavSpectrum)
else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall)
else if Sender = FNavTransmit then SelectPage(FPageTransmit, FNavTransmit)
else if Sender = FNavPA then SelectPage(FPagePA, FNavPA)
else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced)
else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT);
end;
procedure TSettingsForm.SelectPage(APage: TWinControl; ANav: TFlatButton);
begin
FPageAudio.Visible := APage = FPageAudio;
FPageDisplay.Visible := APage = FPageDisplay;
FPageSpectrum.Visible := APage = FPageSpectrum;
FPageWaterfall.Visible := APage = FPageWaterfall;
FPageTransmit.Visible := APage = FPageTransmit;
FPagePA.Visible := APage = FPagePA;
FPageAdvanced.Visible := APage = FPageAdvanced;
FPageCAT.Visible := APage = FPageCAT;
FNavAudio.Active := ANav = FNavAudio;
FNavDisplay.Active := ANav = FNavDisplay;
FNavSpectrum.Active := ANav = FNavSpectrum;
FNavWaterfall.Active := ANav = FNavWaterfall;
FNavTransmit.Active := ANav = FNavTransmit;
FNavPA.Active := ANav = FNavPA;
FNavAdvanced.Active := ANav = FNavAdvanced;
FNavCAT.Active := ANav = FNavCAT;
end;
// ---------------------------------------------------------------------------
// BuildAudioTab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildAudioTab;
const
MARGIN = 22;
PAD = 18;
R1 = 42;
STEP = 42;
LW = 150;
CX = PAD + LW + 12;
CMB_W = 460;
var
Grp: TPanel;
begin
with TLabel.Create(Self) do
begin
Parent := FPageAudio;
Caption := 'Audio';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageAudio;
Caption := 'Input and output devices used by the receiver and transmitter.';
SetBounds(MARGIN, 48, 560, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
Grp := MakeGroupPanel(FPageAudio, 'Audio Devices', MARGIN, 86, 650, 142);
MakeLbl(Grp, 'TX input device', PAD, R1 + 5, LW);
FCmbTXDev := MakeCombo(Grp, CX, R1, CMB_W, OnTXDevChange);
FCmbTXDev.Items.Add('None');
MakeLbl(Grp, 'RX output device', PAD, R1 + STEP + 5, LW);
FCmbRXDev := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnRXDevChange);
FCmbRXDev.Items.Add('Default');
end;
// ---------------------------------------------------------------------------
// BuildDisplayTab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildDisplayTab;
const
MARGIN = 22;
PAD = 18;
R1 = 42;
CHKW = 220;
LW = 150;
CX = PAD + LW + 12;
var
i: Integer;
Grp: TPanel;
begin
with TLabel.Create(Self) do
begin
Parent := FPageDisplay;
Caption := 'Display';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageDisplay;
Caption := 'General visual options. Spectrum and waterfall processing are split into their own pages.';
SetBounds(MARGIN, 48, 680, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
Grp := MakeGroupPanel(FPageDisplay, 'Visible Panes', MARGIN, 86, 650, 86);
FChkShowSpectrum := TCheckBox.Create(Self);
FChkShowSpectrum.Parent := Grp;
FChkShowSpectrum.Caption := 'Spectrum';
FChkShowSpectrum.SetBounds(PAD, R1, CHKW, 22);
FChkShowSpectrum.Checked := True;
FChkShowSpectrum.Font.Color := CLR_TEXT;
FChkShowSpectrum.Font.Name := UI_FONT;
FChkShowSpectrum.Font.Size := 9;
FChkShowSpectrum.OnChange := OnVisibilityChkChange;
FChkShowWaterfall := TCheckBox.Create(Self);
FChkShowWaterfall.Parent := Grp;
FChkShowWaterfall.Caption := 'Waterfall';
FChkShowWaterfall.SetBounds(PAD + CHKW + 20, R1, CHKW, 22);
FChkShowWaterfall.Checked := True;
FChkShowWaterfall.Font.Color := CLR_TEXT;
FChkShowWaterfall.Font.Name := UI_FONT;
FChkShowWaterfall.Font.Size := 9;
FChkShowWaterfall.OnChange := OnVisibilityChkChange;
Grp := MakeGroupPanel(FPageDisplay, 'Performance', MARGIN, 186, 650, 92);
MakeLbl(Grp, 'Refresh rate', PAD, R1 + 5, LW);
FCmbFPS := MakeCombo(Grp, CX, R1, 140, OnFPSCmbChange);
for i := 0 to High(FPS_NAMES) do
FCmbFPS.Items.Add(FPS_NAMES[i]);
FCmbFPS.ItemIndex := 8;
Grp := MakeGroupPanel(FPageDisplay, 'Appearance', MARGIN, 292, 650, 68);
FChkLightTheme := TCheckBox.Create(Self);
FChkLightTheme.Parent := Grp;
FChkLightTheme.Caption := 'Light theme';
FChkLightTheme.SetBounds(PAD, R1, CHKW, 22);
FChkLightTheme.Checked := False;
FChkLightTheme.Font.Color := CLR_TEXT;
FChkLightTheme.Font.Name := UI_FONT;
FChkLightTheme.Font.Size := 9;
FChkLightTheme.OnChange := OnLightThemeChkChange;
end;
// ---------------------------------------------------------------------------
// BuildRX1Tab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildRX1Tab;
const
MARGIN = 22;
PAD = 18;
LW = 150;
CX = PAD + LW + 12;
CMB_W = 230;
CMB_W_LONG = 300;
SPW = 96;
EDT_W = 96;
STEP = 42;
R1 = 42;
var
Grp: TPanel;
i: Integer;
function MkFloat(AX, AY, AW: Integer; AValue, AMin, AMax, AInc: Double;
AOnChange: TNotifyEvent): TFloatSpinEdit;
begin
Result := TFloatSpinEdit.Create(Self);
Result.Parent := Grp;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.Value := AValue; Result.MinValue := AMin;
Result.MaxValue := AMax; Result.Increment := AInc;
Result.DecimalPlaces := 1;
Result.Color := CLR_INPUT; Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT; Result.Font.Size := 9;
Result.OnChange := AOnChange;
end;
function MkEdit(AX, AY, AW: Integer; const AText: string;
AOnExit: TNotifyEvent): TEdit;
begin
Result := TEdit.Create(Self);
Result.Parent := Grp;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.Color := CLR_INPUT; Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT; Result.Font.Size := 9;
Result.Text := AText; Result.OnExit := AOnExit;
end;
begin
with TLabel.Create(Self) do
begin
Parent := FPageSpectrum;
Caption := 'Spectrum';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageSpectrum;
Caption := 'Analyzer, detector, averaging and grid parameters for RX1.';
SetBounds(MARGIN, 48, 620, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
Grp := MakeGroupPanel(FPageSpectrum, 'Analyzer', MARGIN, 86, 650, 128);
MakeLbl(Grp, 'FFT Size:', PAD, R1 + 5, LW);
FCmbFFTSize := MakeCombo(Grp, CX, R1, CMB_W, OnFFTSizeChange);
for i := 0 to High(FFT_NAMES) do FCmbFFTSize.Items.Add(FFT_NAMES[i]);
MakeLbl(Grp, 'Window:', PAD, R1 + STEP + 5, LW);
FCmbWindow := MakeCombo(Grp, CX, R1 + STEP, CMB_W_LONG, OnWindowChange);
for i := 0 to High(WINDOW_NAMES) do FCmbWindow.Items.Add(WINDOW_NAMES[i]);
Grp := MakeGroupPanel(FPageSpectrum, 'Detection && Averaging', MARGIN, 230, 650, 170);
MakeLbl(Grp, 'Detector:', PAD, R1 + 5, LW);
FCmbSpecDet := MakeCombo(Grp, CX, R1, CMB_W, OnSpecDetChange);
for i := 0 to High(DETECTOR_NAMES) do FCmbSpecDet.Items.Add(DETECTOR_NAMES[i]);
MakeLbl(Grp, 'Avg Mode:', PAD, R1 + STEP + 5, LW);
FCmbSpecAvg := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnSpecAvgModeChange);
for i := 0 to High(AVGMODE_NAMES) do FCmbSpecAvg.Items.Add(AVGMODE_NAMES[i]);
MakeLbl(Grp, 'Avg Time (ms):', PAD, R1 + 2 * STEP + 5, LW);
FSpecAvgTime := MkFloat(CX, R1 + 2 * STEP, SPW, 30.0, 0.0, 5000.0, 10.0, OnSpecAvgTimeChange);
Grp := MakeGroupPanel(FPageSpectrum, 'Grid', MARGIN, 416, 650, 170);
MakeLbl(Grp, 'Reference level:', PAD, R1 + 5, LW);
FEdRefLevel := MkEdit(CX, R1, EDT_W, '-20', OnRefLevelChange);
MakeLbl(Grp, 'Range (dB):', PAD, R1 + STEP + 5, LW);
FEdRange := MkEdit(CX, R1 + STEP, EDT_W, '110', OnRangeChange);
MakeLbl(Grp, 'Grid step:', PAD, R1 + 2 * STEP + 5, LW);
FCmbGridStep := MakeCombo(Grp, CX, R1 + 2 * STEP, 130, OnGridStepChange);
for i := 0 to High(GRID_STEP_NAMES) do FCmbGridStep.Items.Add(GRID_STEP_NAMES[i]);
FCmbGridStep.ItemIndex := 1;
with TLabel.Create(Self) do
begin
Parent := FPageWaterfall;
Caption := 'Waterfall';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageWaterfall;
Caption := 'Waterfall detector, smoothing and color scale thresholds.';
SetBounds(MARGIN, 48, 620, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
Grp := MakeGroupPanel(FPageWaterfall, 'Detection && Averaging', MARGIN, 86, 650, 170);
MakeLbl(Grp, 'Detector:', PAD, R1 + 5, LW);
FCmbWfDet := MakeCombo(Grp, CX, R1, CMB_W, OnWfDetChange);
for i := 0 to High(DETECTOR_NAMES) do FCmbWfDet.Items.Add(DETECTOR_NAMES[i]);
MakeLbl(Grp, 'Avg Mode:', PAD, R1 + STEP + 5, LW);
FCmbWfAvg := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnWfAvgModeChange);
for i := 0 to High(WF_AVGMODE_NAMES) do FCmbWfAvg.Items.Add(WF_AVGMODE_NAMES[i]);
MakeLbl(Grp, 'Avg Time (ms):', PAD, R1 + 2 * STEP + 5, LW);
FWfAvgTime := MkFloat(CX, R1 + 2 * STEP, SPW, 120.0, 0.0, 5000.0, 10.0, OnWfAvgTimeChange);
Grp := MakeGroupPanel(FPageWaterfall, 'Color Scale', MARGIN, 272, 650, 170);
MakeLbl(Grp, 'High (dBm):', PAD, R1 + 5, LW);
FWfHigh := MkFloat(CX, R1, SPW, -80.0, -160.0, 20.0, 1.0, OnWfThresholdChange);
MakeLbl(Grp, 'Low (dBm):', PAD, R1 + STEP + 5, LW);
FWfLow := MkFloat(CX, R1 + STEP, SPW, -130.0, -180.0, 0.0, 1.0, OnWfThresholdChange);
MakeLbl(Grp, 'AGC Offset (dB):', PAD, R1 + 2 * STEP + 5, LW);
FWfAGCOffset := MkFloat(CX, R1 + 2 * STEP, SPW, 0.0, -40.0, 40.0, 0.5, OnWfThresholdChange);
end;
// ---------------------------------------------------------------------------
// BuildTransmitTab — все настройки TX-тракта (по образцу Thetis Setup→Transmit)
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildTransmitTab;
const
MARGIN = 22;
PAD = 18;
LW = 150;
CX = PAD + LW + 12;
STEP = 32;
R1 = 42;
SPW = 96;
CMB_W = 200;
var
Grp: TPanel;
Y, i: Integer;
function MkSpin(AParent: TWinControl; AX, AY: Integer; AMin, AMax, AVal: Integer): TSpinEdit;
begin
Result := TSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, SPW, BTN_H);
Result.MinValue := AMin;
Result.MaxValue := AMax;
Result.Value := AVal;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnTXAnyChange;
end;
function MkFloat(AParent: TWinControl; AX, AY: Integer;
AVal, AMin, AMax, AInc: Double): TFloatSpinEdit;
begin
Result := TFloatSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, SPW, BTN_H);
Result.MinValue := AMin;
Result.MaxValue := AMax;
Result.Increment := AInc;
Result.Value := AVal;
Result.DecimalPlaces := 2;
Result.Color := CLR_INPUT;
Result.Font.Color := CLR_INPUT_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnTXAnyChange;
end;
function MkChk(AParent: TWinControl; AX, AY, AW: Integer; const ACap: string): TCheckBox;
begin
Result := TCheckBox.Create(Self);
Result.Parent := AParent;
Result.Caption := ACap;
Result.SetBounds(AX, AY, AW, 22);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnTXAnyChange;
end;
function MkCmb(AParent: TWinControl; AX, AY, AW: Integer): TComboBox;
begin
Result := MakeCombo(AParent, AX, AY, AW, OnTXAnyChange);
end;
begin
with TLabel.Create(Self) do
begin
Parent := FPageTransmit;
Caption := 'Transmit';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageTransmit;
Caption := 'TX-цепь WDSP: микрофон, фильтр, динамическая обработка, модуляция.';
SetBounds(MARGIN, 48, 700, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
// ---- Mic source / options ----
Y := 86;
Grp := MakeGroupPanel(FPageTransmit, 'Microphone', MARGIN, Y, 700, 220);
MakeLbl(Grp, 'Mic source', PAD, R1 + 5, LW);
FCmbTXMicSource := MkCmb(Grp, CX, R1, CMB_W);
FCmbTXMicSource.Items.Add('Radio (HW mic)');
FCmbTXMicSource.Items.Add('Sound card');
FCmbTXMicSource.ItemIndex := 0;
FChkTXMicBoost := MkChk(Grp, PAD, R1 + STEP, 200, 'Mic boost +20 dB');
FChkTXMicBias := MkChk(Grp, PAD + 200, R1 + STEP, 200, 'Mic bias');
FChkTXMicLineIn := MkChk(Grp, PAD + 400, R1 + STEP, 160, 'Line in');
FChkTXMicPTT := MkChk(Grp, PAD, R1 + 2 * STEP, 220, 'Mic-jack PTT');
MakeLbl(Grp, 'Mic gain (dB)', PAD, R1 + 3 * STEP + 5, LW);
FEdTXMicGain := MkFloat(Grp, CX, R1 + 3 * STEP, 10.0, -20.0, 60.0, 0.5);
// ---- TX bandpass filter ----
Inc(Y, 220 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Filter (bandpass)', MARGIN, Y, 700, 200);
MakeLbl(Grp, 'Low cut (Hz)', PAD, R1 + 5, LW);
FEdTXFiltLow := MkSpin(Grp, CX, R1, 0, 8000, 100);
MakeLbl(Grp, 'High cut (Hz)', PAD + 320, R1 + 5, 90);
FEdTXFiltHigh := MkSpin(Grp, PAD + 320 + 100, R1, 100, 12000, 2800);
MakeLbl(Grp, 'FIR length', PAD, R1 + STEP + 5, LW);
FCmbTXFiltNC := MkCmb(Grp, CX, R1 + STEP, 140);
FCmbTXFiltNC.Items.AddStrings(['256','512','1024','2048','4096']);
FCmbTXFiltNC.ItemIndex := 2;
MakeLbl(Grp, 'Phase mode', PAD, R1 + 2 * STEP + 5, LW);
FCmbTXFiltMP := MkCmb(Grp, CX, R1 + 2 * STEP, 200);
FCmbTXFiltMP.Items.AddStrings(['Linear Phase','Low Latency']);
FCmbTXFiltMP.ItemIndex := 1;
MakeLbl(Grp, 'Window', PAD, R1 + 3 * STEP + 5, LW);
FCmbTXFiltWindow := MkCmb(Grp, CX, R1 + 3 * STEP, 240);
for i := 0 to High(WINDOW_NAMES) do
FCmbTXFiltWindow.Items.Add(WINDOW_NAMES[i]);
FCmbTXFiltWindow.ItemIndex := 1;
// ---- Compressor / Leveler / ALC / Phase Rotator ----
Inc(Y, 200 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'Dynamic processing', MARGIN, Y, 700, 220);
FChkTXCompOn := MkChk(Grp, PAD, R1, 220, 'Compressor');
MakeLbl(Grp, 'Gain (dB)', PAD + 230, R1 + 5, 80);
FEdTXCompGain := MkFloat(Grp, PAD + 320, R1, 5.0, 0.0, 20.0, 0.5);
FChkTXLevOn := MkChk(Grp, PAD, R1 + STEP, 220, 'Leveler');
MakeLbl(Grp, 'Top (dB)', PAD + 230, R1 + STEP + 5, 80);
FEdTXLevTop := MkFloat(Grp, PAD + 320, R1 + STEP, 5.0, 0.0, 20.0, 0.5);
MakeLbl(Grp, 'Decay (ms)', PAD + 430, R1 + STEP + 5, 80);
FEdTXLevDecay := MkSpin(Grp, PAD + 520, R1 + STEP, 1, 5000, 500);
FChkTXALCOn := MkChk(Grp, PAD, R1 + 2 * STEP, 220, 'ALC');
MakeLbl(Grp, 'Max gain', PAD + 230, R1 + 2 * STEP + 5, 80);
FEdTXALCMaxGain := MkFloat(Grp, PAD + 320, R1 + 2 * STEP, 0.0, -20.0, 20.0, 0.5);
MakeLbl(Grp, 'Decay (ms)', PAD + 430, R1 + 2 * STEP + 5, 80);
FEdTXALCDecay := MkSpin(Grp, PAD + 520, R1 + 2 * STEP, 1, 5000, 10);
FChkTXPHROn := MkChk(Grp, PAD, R1 + 3 * STEP, 220, 'Phase Rotator');
MakeLbl(Grp, 'Stages', PAD + 230, R1 + 3 * STEP + 5, 80);
FEdTXPHRStages := MkSpin(Grp, PAD + 320, R1 + 3 * STEP, 2, 16, 8);
MakeLbl(Grp, 'Corner (Hz)', PAD + 430, R1 + 3 * STEP + 5, 80);
FEdTXPHRFreq := MkFloat(Grp, PAD + 520, R1 + 3 * STEP, 338.0, 50.0, 3000.0, 1.0);
// ---- TX EQ ----
Inc(Y, 220 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Equalizer', MARGIN, Y, 700, 320);
FChkTXEQOn := MkChk(Grp, PAD, R1, 200, 'Enable EQ');
MakeLbl(Grp, 'Bands', PAD + 230, R1 + 5, 60);
FCmbTXEQBands := MkCmb(Grp, PAD + 290, R1, 80);
FCmbTXEQBands.Items.AddStrings(['3','10']);
FCmbTXEQBands.ItemIndex := 1;
// 11 строк: [0]=Preamp, [1..10]=полосы. Freq+Gain в одной строке.
for i := 0 to 10 do
begin
if i = 0 then
MakeLbl(Grp, 'Preamp (dB)', PAD, R1 + STEP + i * 22 + 3, 100)
else
MakeLbl(Grp, Format('Band %d', [i]), PAD, R1 + STEP + i * 22 + 3, 80);
FEdTXEQFreq[i] := MkFloat(Grp, PAD + 90, R1 + STEP + i * 22, 0.0, 0.0, 20000.0, 10.0);
FEdTXEQFreq[i].Width := 100;
MakeLbl(Grp, 'gain', PAD + 200, R1 + STEP + i * 22 + 3, 30);
FEdTXEQGain[i] := MkFloat(Grp, PAD + 230, R1 + STEP + i * 22, 0.0, -15.0, 15.0, 0.5);
FEdTXEQGain[i].Width := 80;
end;
// ---- AM / FM / CTCSS ----
Inc(Y, 320 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'AM / FM / CTCSS', MARGIN, Y, 700, 220);
MakeLbl(Grp, 'AM carrier level', PAD, R1 + 5, LW);
FEdTXAMCarrier := MkFloat(Grp, CX, R1, 0.5, 0.0, 1.0, 0.05);
MakeLbl(Grp, 'FM deviation (Hz)', PAD, R1 + STEP + 5, LW);
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);
FEdTXFMLow := MkSpin(Grp, CX, R1 + 2 * STEP, 0, 5000, 300);
MakeLbl(Grp, 'FM AF high cut', PAD + 320, R1 + 2 * STEP + 5, 100);
FEdTXFMHigh := MkSpin(Grp, PAD + 320 + 110, R1 + 2 * STEP, 100, 8000, 3000);
MakeLbl(Grp, 'FM emphasis', PAD, R1 + 3 * STEP + 5, LW);
FCmbTXFMEmph := MkCmb(Grp, CX, R1 + 3 * STEP, 180);
FCmbTXFMEmph.Items.AddStrings(['Pre-EQ','Post-EQ']);
FCmbTXFMEmph.ItemIndex := 1;
FChkTXCTCSSOn := MkChk(Grp, PAD, R1 + 4 * STEP, 220, 'CTCSS tone');
MakeLbl(Grp, 'Freq (Hz)', PAD + 230, R1 + 4 * STEP + 5, 80);
FEdTXCTCSSFreq := MkFloat(Grp, PAD + 320, R1 + 4 * STEP, 100.0, 50.0, 300.0, 0.1);
// ---- TX Display (отдельный analyzer для TX-спектра/водопада) ----
Inc(Y, 220 + 12);
Grp := MakeGroupPanel(FPageTransmit, 'TX Display (Analyzer)', MARGIN, Y, 700, 290);
MakeLbl(Grp, 'FFT size', PAD, R1 + 5, LW);
FCmbTXFFT := MkCmb(Grp, CX, R1, 140);
for i := 0 to High(FFT_NAMES) do FCmbTXFFT.Items.Add(FFT_NAMES[i]);
FCmbTXFFT.ItemIndex := 2;
MakeLbl(Grp, 'Window', PAD, R1 + STEP + 5, LW);
FCmbTXWindow := MkCmb(Grp, CX, R1 + STEP, 240);
for i := 0 to High(WINDOW_NAMES) do FCmbTXWindow.Items.Add(WINDOW_NAMES[i]);
FCmbTXWindow.ItemIndex := 2;
MakeLbl(Grp, 'Spec detector',PAD, R1 + 2 * STEP + 5, LW);
FCmbTXSpecDet := MkCmb(Grp, CX, R1 + 2 * STEP, 200);
for i := 0 to High(DETECTOR_NAMES) do FCmbTXSpecDet.Items.Add(DETECTOR_NAMES[i]);
FCmbTXSpecDet.ItemIndex := 0;
MakeLbl(Grp, 'Spec average', PAD, R1 + 3 * STEP + 5, LW);
FCmbTXSpecAvg := MkCmb(Grp, CX, R1 + 3 * STEP, 200);
for i := 0 to High(AVGMODE_NAMES) do FCmbTXSpecAvg.Items.Add(AVGMODE_NAMES[i]);
FCmbTXSpecAvg.ItemIndex := 3;
MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 3 * STEP + 5, 100);
FEdTXSpecAvgTime := MkFloat(Grp, PAD + 540, R1 + 3 * STEP, 30.0, 1.0, 5000.0, 1.0);
MakeLbl(Grp, 'WF detector', PAD, R1 + 4 * STEP + 5, LW);
FCmbTXWfDet := MkCmb(Grp, CX, R1 + 4 * STEP, 200);
for i := 0 to High(DETECTOR_NAMES) do FCmbTXWfDet.Items.Add(DETECTOR_NAMES[i]);
FCmbTXWfDet.ItemIndex := 0;
MakeLbl(Grp, 'WF average', PAD, R1 + 5 * STEP + 5, LW);
FCmbTXWfAvg := MkCmb(Grp, CX, R1 + 5 * STEP, 200);
for i := 0 to High(WF_AVGMODE_NAMES) do FCmbTXWfAvg.Items.Add(WF_AVGMODE_NAMES[i]);
FCmbTXWfAvg.ItemIndex := 3;
MakeLbl(Grp, 'Avg time (ms)', PAD + 430, R1 + 5 * STEP + 5, 100);
FEdTXWfAvgTime := MkFloat(Grp, PAD + 540, R1 + 5 * STEP, 120.0, 1.0, 5000.0, 1.0);
end;
procedure TSettingsForm.CollectTXFromUI;
var i: Integer;
begin
if FCmbTXMicSource <> nil then
FTX.MicSource := Max(0, FCmbTXMicSource.ItemIndex);
if FChkTXMicBoost <> nil then FTX.MicBoost := FChkTXMicBoost.Checked;
if FChkTXMicBias <> nil then FTX.MicBias := FChkTXMicBias.Checked;
if FChkTXMicLineIn <> nil then FTX.MicLineIn := FChkTXMicLineIn.Checked;
if FChkTXMicPTT <> nil then FTX.MicPTTEnabled := FChkTXMicPTT.Checked;
if FEdTXMicGain <> nil then FTX.MicGainDB := FEdTXMicGain.Value;
if FEdTXFiltLow <> nil then FTX.FilterLow := FEdTXFiltLow.Value;
if FEdTXFiltHigh <> nil then FTX.FilterHigh := FEdTXFiltHigh.Value;
if FCmbTXFiltNC <> nil then
FTX.FilterNC := StrToIntDef(FCmbTXFiltNC.Text, 1024);
if FCmbTXFiltMP <> nil then FTX.FilterMP := Max(0, FCmbTXFiltMP.ItemIndex);
if FCmbTXFiltWindow<> nil then FTX.FilterWindow := Max(0, FCmbTXFiltWindow.ItemIndex);
if FChkTXCompOn <> nil then FTX.CompressorOn := FChkTXCompOn.Checked;
if FEdTXCompGain <> nil then FTX.CompressorGain := FEdTXCompGain.Value;
if FChkTXLevOn <> nil then FTX.LevelerOn := FChkTXLevOn.Checked;
if FEdTXLevTop <> nil then FTX.LevelerTop := FEdTXLevTop.Value;
if FEdTXLevDecay <> nil then FTX.LevelerDecay := FEdTXLevDecay.Value;
if FChkTXALCOn <> nil then FTX.ALCOn := FChkTXALCOn.Checked;
if FEdTXALCMaxGain <> nil then FTX.ALCMaxGain := FEdTXALCMaxGain.Value;
if FEdTXALCDecay <> nil then FTX.ALCDecay := FEdTXALCDecay.Value;
if FChkTXPHROn <> nil then FTX.PhaseRotOn := FChkTXPHROn.Checked;
if FEdTXPHRStages <> nil then FTX.PhaseRotStages := FEdTXPHRStages.Value;
if FEdTXPHRFreq <> nil then FTX.PhaseRotFreq := FEdTXPHRFreq.Value;
if FChkTXEQOn <> nil then FTX.EQOn := FChkTXEQOn.Checked;
if FCmbTXEQBands <> nil then
if FCmbTXEQBands.ItemIndex = 0 then FTX.EQNumBands := 3 else FTX.EQNumBands := 10;
for i := 0 to 10 do
begin
if FEdTXEQGain[i] <> nil then FTX.EQGains[i] := FEdTXEQGain[i].Value;
if FEdTXEQFreq[i] <> nil then FTX.EQFreqs[i] := FEdTXEQFreq[i].Value;
end;
if FEdTXAMCarrier <> nil then FTX.AMCarrierLevel := FEdTXAMCarrier.Value;
if FEdTXFMDev <> nil then FTX.FMDeviation := FEdTXFMDev.Value;
if FEdTXFMLow <> nil then FTX.FMLowCut := FEdTXFMLow.Value;
if FEdTXFMHigh <> nil then FTX.FMHighCut := FEdTXFMHigh.Value;
if FCmbTXFMEmph <> nil then FTX.FMEmphPosition := Max(0, FCmbTXFMEmph.ItemIndex);
if FChkTXCTCSSOn <> nil then FTX.CTCSSOn := FChkTXCTCSSOn.Checked;
if FEdTXCTCSSFreq <> nil then FTX.CTCSSFreq := FEdTXCTCSSFreq.Value;
if (FCmbTXFFT <> nil) and (FCmbTXFFT.ItemIndex >= 0) then
FTX.TXFFTSize := FFT_SIZES[FCmbTXFFT.ItemIndex];
if FCmbTXWindow <> nil then FTX.TXWindowType := Max(0, FCmbTXWindow.ItemIndex);
if FCmbTXSpecDet <> nil then FTX.TXSpecDetector := Max(0, FCmbTXSpecDet.ItemIndex);
if (FCmbTXSpecAvg <> nil) and (FCmbTXSpecAvg.ItemIndex >= 0) then
FTX.TXSpecAvgMode := AVGMODE_VALUES[FCmbTXSpecAvg.ItemIndex];
if FEdTXSpecAvgTime <> nil then FTX.TXSpecAvgTimeMS := FEdTXSpecAvgTime.Value;
if FCmbTXWfDet <> nil then FTX.TXWfDetector := Max(0, FCmbTXWfDet.ItemIndex);
if (FCmbTXWfAvg <> nil) and (FCmbTXWfAvg.ItemIndex >= 0) then
FTX.TXWfAvgMode := WF_AVGMODE_VALUES[FCmbTXWfAvg.ItemIndex];
if FEdTXWfAvgTime <> nil then FTX.TXWfAvgTimeMS := FEdTXWfAvgTime.Value;
end;
procedure TSettingsForm.FireTXChange;
begin
if FLoading then Exit;
CollectTXFromUI;
if Assigned(FOnTXChange) then
FOnTXChange(FTX);
end;
procedure TSettingsForm.OnTXAnyChange(Sender: TObject);
begin
FireTXChange;
end;
procedure TSettingsForm.LoadTXSettings(const T: TTXSettings);
var i: Integer;
begin
FLoading := True;
try
FTX := T;
if FCmbTXMicSource <> nil then
FCmbTXMicSource.ItemIndex := EnsureRange(T.MicSource, 0, FCmbTXMicSource.Items.Count - 1);
if FChkTXMicBoost <> nil then FChkTXMicBoost.Checked := T.MicBoost;
if FChkTXMicBias <> nil then FChkTXMicBias.Checked := T.MicBias;
if FChkTXMicLineIn <> nil then FChkTXMicLineIn.Checked := T.MicLineIn;
if FChkTXMicPTT <> nil then FChkTXMicPTT.Checked := T.MicPTTEnabled;
if FEdTXMicGain <> nil then FEdTXMicGain.Value := T.MicGainDB;
if FEdTXFiltLow <> nil then FEdTXFiltLow.Value := T.FilterLow;
if FEdTXFiltHigh <> nil then FEdTXFiltHigh.Value := T.FilterHigh;
if FCmbTXFiltNC <> nil then FCmbTXFiltNC.Text := IntToStr(T.FilterNC);
if FCmbTXFiltMP <> nil then
FCmbTXFiltMP.ItemIndex := EnsureRange(T.FilterMP, 0, FCmbTXFiltMP.Items.Count - 1);
if FCmbTXFiltWindow<> nil then
FCmbTXFiltWindow.ItemIndex := EnsureRange(T.FilterWindow, 0, FCmbTXFiltWindow.Items.Count - 1);
if FChkTXCompOn <> nil then FChkTXCompOn.Checked := T.CompressorOn;
if FEdTXCompGain <> nil then FEdTXCompGain.Value := T.CompressorGain;
if FChkTXLevOn <> nil then FChkTXLevOn.Checked := T.LevelerOn;
if FEdTXLevTop <> nil then FEdTXLevTop.Value := T.LevelerTop;
if FEdTXLevDecay <> nil then FEdTXLevDecay.Value := T.LevelerDecay;
if FChkTXALCOn <> nil then FChkTXALCOn.Checked := T.ALCOn;
if FEdTXALCMaxGain <> nil then FEdTXALCMaxGain.Value := T.ALCMaxGain;
if FEdTXALCDecay <> nil then FEdTXALCDecay.Value := T.ALCDecay;
if FChkTXPHROn <> nil then FChkTXPHROn.Checked := T.PhaseRotOn;
if FEdTXPHRStages <> nil then FEdTXPHRStages.Value := T.PhaseRotStages;
if FEdTXPHRFreq <> nil then FEdTXPHRFreq.Value := T.PhaseRotFreq;
if FChkTXEQOn <> nil then FChkTXEQOn.Checked := T.EQOn;
if FCmbTXEQBands <> nil then
if T.EQNumBands <= 3 then FCmbTXEQBands.ItemIndex := 0
else FCmbTXEQBands.ItemIndex := 1;
for i := 0 to 10 do
begin
if FEdTXEQGain[i] <> nil then FEdTXEQGain[i].Value := T.EQGains[i];
if FEdTXEQFreq[i] <> nil then FEdTXEQFreq[i].Value := T.EQFreqs[i];
end;
if FEdTXAMCarrier <> nil then FEdTXAMCarrier.Value := T.AMCarrierLevel;
if FEdTXFMDev <> nil then FEdTXFMDev.Value := T.FMDeviation;
if FEdTXFMLow <> nil then FEdTXFMLow.Value := T.FMLowCut;
if FEdTXFMHigh <> nil then FEdTXFMHigh.Value := T.FMHighCut;
if FCmbTXFMEmph <> nil then
FCmbTXFMEmph.ItemIndex := EnsureRange(T.FMEmphPosition, 0, FCmbTXFMEmph.Items.Count - 1);
if FChkTXCTCSSOn <> nil then FChkTXCTCSSOn.Checked := T.CTCSSOn;
if FEdTXCTCSSFreq <> nil then FEdTXCTCSSFreq.Value := T.CTCSSFreq;
if FCmbTXFFT <> nil then
begin
FCmbTXFFT.ItemIndex := 2;
for i := 0 to High(FFT_SIZES) do
if FFT_SIZES[i] = T.TXFFTSize then
begin
FCmbTXFFT.ItemIndex := i;
Break;
end;
end;
if FCmbTXWindow <> nil then
FCmbTXWindow.ItemIndex := EnsureRange(T.TXWindowType, 0, FCmbTXWindow.Items.Count - 1);
if FCmbTXSpecDet <> nil then
FCmbTXSpecDet.ItemIndex := EnsureRange(T.TXSpecDetector, 0, FCmbTXSpecDet.Items.Count - 1);
if FCmbTXSpecAvg <> nil then
begin
FCmbTXSpecAvg.ItemIndex := 3;
for i := 0 to High(AVGMODE_VALUES) do
if AVGMODE_VALUES[i] = T.TXSpecAvgMode then
begin
FCmbTXSpecAvg.ItemIndex := i;
Break;
end;
end;
if FEdTXSpecAvgTime <> nil then FEdTXSpecAvgTime.Value := T.TXSpecAvgTimeMS;
if FCmbTXWfDet <> nil then
FCmbTXWfDet.ItemIndex := EnsureRange(T.TXWfDetector, 0, FCmbTXWfDet.Items.Count - 1);
if FCmbTXWfAvg <> nil then
begin
FCmbTXWfAvg.ItemIndex := 3;
for i := 0 to High(WF_AVGMODE_VALUES) do
if WF_AVGMODE_VALUES[i] = T.TXWfAvgMode then
begin
FCmbTXWfAvg.ItemIndex := i;
Break;
end;
end;
if FEdTXWfAvgTime <> nil then FEdTXWfAvgTime.Value := T.TXWfAvgTimeMS;
finally
FLoading := False;
end;
end;
// ---------------------------------------------------------------------------
// BuildPATab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildPATab;
const
MARGIN = 22;
LBL_W = 46;
ED_W = 72;
COL_W = LBL_W + ED_W + 8;
COLS = 4;
ROW_H = 32;
GRP_PAD = 18;
R1 = 42;
GRP_H = 222;
function MakeFloatSpin(AParent: TWinControl; ALeft, ATop: Integer;
AChange: TNotifyEvent): TFloatSpinEdit;
var E: TFloatSpinEdit;
begin
E := TFloatSpinEdit.Create(Self);
E.Parent := AParent;
E.SetBounds(ALeft, ATop, ED_W, BTN_H + 2);
E.Color := CLR_INPUT;
E.Font.Color := CLR_INPUT_TEXT;
E.Font.Name := UI_FONT;
E.Font.Size := 9;
E.MinValue := 38.8;
E.MaxValue := 100.0;
E.Increment := 0.1;
E.DecimalPlaces := 1;
E.Value := 100.0;
E.OnChange := AChange;
Result := E;
end;
var
Grp: TPanel;
Lbl: TLabel;
Spin: TSpinEdit;
i, r, c, X, Y: Integer;
begin
with TLabel.Create(Self) do
begin
Parent := FPagePA;
Caption := 'Power Amplifier';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPagePA;
Caption := 'Output limit and per-band calibration values.';
SetBounds(MARGIN, 48, 620, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
Grp := MakeGroupPanel(FPagePA, 'Power && Calibration', MARGIN, 86, 650, GRP_H);
// Max Power
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := 'Max power (W)';
Lbl.SetBounds(GRP_PAD, R1 + 6, 132, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := UI_FONT;
Lbl.Font.Size := 9;
Spin := TSpinEdit.Create(Self);
Spin.Parent := Grp;
Spin.SetBounds(GRP_PAD + 150, R1, ED_W, BTN_H + 2);
Spin.Color := CLR_INPUT;
Spin.Font.Color := CLR_INPUT_TEXT;
Spin.Font.Name := UI_FONT;
Spin.Font.Size := 9;
Spin.MinValue := 5;
Spin.MaxValue := 200;
Spin.Increment := 1;
Spin.Value := 100;
Spin.OnChange := OnMaxPowerChange;
FEdMaxPower := Spin;
// Calibration label
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := 'Calibration per band, 38.8 ... 100.0';
Lbl.SetBounds(GRP_PAD, R1 + ROW_H + 12, 320, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := UI_FONT;
Lbl.Font.Size := 9;
// Band grid 4 columns × 3 rows
for i := 0 to 10 do
begin
r := i div COLS;
c := i mod COLS;
X := GRP_PAD + c * (COL_W + 4);
Y := R1 + ROW_H + 34 + r * ROW_H;
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := PA_BAND_NAMES[i] + ':';
Lbl.SetBounds(X, Y + 5, LBL_W, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := UI_FONT;
Lbl.Font.Size := 9;
FEdBandCal[i] := MakeFloatSpin(Grp, X + LBL_W, Y, OnBandCalChange);
FEdBandCal[i].Tag := i;
end;
with TLabel.Create(Self) do
begin
Parent := FPageAdvanced;
Caption := 'Advanced';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT;
Font.Size := 16;
Font.Style := [fsBold];
Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageAdvanced;
Caption := 'Transmit, CAT and DSP-only settings will appear here when they are wired to runtime settings.';
SetBounds(MARGIN, 48, 680, 20);
Font.Name := UI_FONT;
Font.Size := 9;
Font.Color := CLR_TEXTDIM;
end;
end;
procedure TSettingsForm.ApplyTheme(const T: TAppTheme);
procedure StyleBtn(B: TFlatButton);
begin
B.ClrNorm := T.Panel;
B.ClrBorder := T.Panel;
B.ClrHot := T.BG;
B.ClrActive := T.Panel;
B.ClrText := T.Text;
B.ClrTextAct := T.FreqActive;
B.Invalidate;
end;
procedure WalkControls(C: TWinControl);
var i: Integer; Ctrl: TControl;
begin
for i := 0 to C.ControlCount - 1 do
begin
Ctrl := C.Controls[i];
if Ctrl is TFlatButton then
StyleBtn(TFlatButton(Ctrl))
else if Ctrl is TScrollBox then
begin
TScrollBox(Ctrl).Color := T.BG;
WalkControls(TScrollBox(Ctrl));
end
else if Ctrl is TPanel then
begin
if TPanel(Ctrl).Height = 1 then
TPanel(Ctrl).Color := T.Border
else
TPanel(Ctrl).Color := T.Panel;
WalkControls(TPanel(Ctrl));
end
else if Ctrl is TLabel then
begin
if Ctrl.Font.Size >= 12 then
TLabel(Ctrl).Font.Color := T.Text
else if fsBold in Ctrl.Font.Style then
TLabel(Ctrl).Font.Color := T.FreqActive
else
TLabel(Ctrl).Font.Color := T.TextDim;
end
else if Ctrl is TCheckBox then
TCheckBox(Ctrl).Font.Color := T.Text
else if Ctrl is TListBox then
begin
TListBox(Ctrl).Color := T.BG;
TListBox(Ctrl).Font.Color := T.Text;
end
else if Ctrl is TWinControl then
WalkControls(TWinControl(Ctrl));
end;
end;
begin
Color := T.BG;
Font.Color := T.Text;
WalkControls(Self);
Invalidate;
end;
// ---------------------------------------------------------------------------
// LoadValues
// ---------------------------------------------------------------------------
procedure TSettingsForm.LoadValues(
FFTSize, WinType, SpecDet, SpecAvgMode: Integer;
SpecAvgTimeMS: Double;
WfDet, WfAvgMode: Integer;
WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double;
RefLevel, Range, GridStep: Double;
const AudioOutDevName: string;
const AudioInDevName: string);
var
i, Idx: Integer;
begin
FLoading := True;
try
Idx := 5;
for i := 0 to High(FFT_SIZES) do
if FFT_SIZES[i] = FFTSize then begin Idx := i; Break; end;
FCmbFFTSize.ItemIndex := Idx;
if (WinType >= 0) and (WinType < FCmbWindow.Items.Count) then
FCmbWindow.ItemIndex := WinType
else
FCmbWindow.ItemIndex := 2;
if (SpecDet >= 0) and (SpecDet < FCmbSpecDet.Items.Count) then
FCmbSpecDet.ItemIndex := SpecDet
else
FCmbSpecDet.ItemIndex := 0;
Idx := 3;
for i := 0 to High(AVGMODE_VALUES) do
if AVGMODE_VALUES[i] = SpecAvgMode then begin Idx := i; Break; end;
FCmbSpecAvg.ItemIndex := Idx;
FSpecAvgTime.Value := EnsureRange(SpecAvgTimeMS, 0.0, 5000.0);
if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then
FCmbWfDet.ItemIndex := WfDet
else
FCmbWfDet.ItemIndex := 0;
Idx := 3;
for i := 0 to High(WF_AVGMODE_VALUES) do
if WF_AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end;
FCmbWfAvg.ItemIndex := Idx;
FWfAvgTime.Value := EnsureRange(WfAvgTimeMS, 0.0, 5000.0);
FWfHigh.Value := EnsureRange(WfHigh, -160.0, 20.0);
FWfLow.Value := EnsureRange(WfLow, -180.0, 0.0);
FWfAGCOffset.Value := EnsureRange(WfAGCOffset, -40.0, 40.0);
FEdRefLevel.Text := FormatFloat('0.#', RefLevel);
FEdRange.Text := FormatFloat('0.#', Range);
Idx := 1;
for i := 0 to High(GRID_STEPS) do
if Abs(GRID_STEPS[i] - GridStep) < 0.5 then begin Idx := i; Break; end;
FCmbGridStep.ItemIndex := Idx;
if AudioOutDevName = '' then
FCmbRXDev.ItemIndex := 0
else
begin
Idx := FCmbRXDev.Items.IndexOf(AudioOutDevName);
if Idx >= 0 then FCmbRXDev.ItemIndex := Idx
else FCmbRXDev.ItemIndex := 0;
end;
if AudioInDevName = '' then
FCmbTXDev.ItemIndex := 0
else
begin
Idx := FCmbTXDev.Items.IndexOf(AudioInDevName);
if Idx >= 0 then FCmbTXDev.ItemIndex := Idx
else FCmbTXDev.ItemIndex := 0;
end;
finally
FLoading := False;
end;
end;
// ---------------------------------------------------------------------------
// RefreshAudioDevices
// ---------------------------------------------------------------------------
procedure TSettingsForm.RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput);
var
Names: TStringList;
Cnt, i: Integer;
OldRXSel: string;
OldTXSel: string;
begin
Names := TStringList.Create;
try
if AudioOut <> nil then
begin
OldRXSel := FCmbRXDev.Text;
FCmbRXDev.Items.Clear;
FCmbRXDev.Items.Add('Default');
FRXDevCount := 0;
Cnt := 0;
Names.Clear;
if AudioOut.EnumOutputDevices(Names, FRXDevIndices, Cnt) then
begin
FRXDevCount := Cnt;
for i := 0 to Cnt - 1 do
FCmbRXDev.Items.Add(Names[i]);
end;
if (FRXDevCount = 0) and (AudioOut.LastError <> '') then
FCmbRXDev.Items.Add('PortAudio: ' + AudioOut.LastError);
i := FCmbRXDev.Items.IndexOf(OldRXSel);
if i >= 0 then FCmbRXDev.ItemIndex := i
else FCmbRXDev.ItemIndex := 0;
end;
if AudioIn <> nil then
begin
OldTXSel := FCmbTXDev.Text;
FCmbTXDev.Items.Clear;
FCmbTXDev.Items.Add('None');
FTXDevCount := 0;
Cnt := 0;
Names.Clear;
if AudioIn.EnumInputDevices(Names, FTXDevIndices, Cnt) then
begin
FTXDevCount := Cnt;
for i := 0 to Cnt - 1 do
FCmbTXDev.Items.Add(Names[i]);
end;
if (FTXDevCount = 0) and (AudioIn.LastError <> '') then
FCmbTXDev.Items.Add('PortAudio: ' + AudioIn.LastError);
i := FCmbTXDev.Items.IndexOf(OldTXSel);
if i >= 0 then FCmbTXDev.ItemIndex := i
else FCmbTXDev.ItemIndex := 0;
end;
finally
Names.Free;
end;
end;
// ---------------------------------------------------------------------------
// Value accessors
// ---------------------------------------------------------------------------
function TSettingsForm.GetFFTSize: Integer;
var Idx: Integer;
begin
Idx := FCmbFFTSize.ItemIndex;
if (Idx >= 0) and (Idx <= High(FFT_SIZES)) then
Result := FFT_SIZES[Idx]
else
Result := 4096;
end;
function TSettingsForm.GetSpecAvgTimeMS: Double;
begin
Result := EnsureRange(FSpecAvgTime.Value, 0.0, 5000.0);
end;
function TSettingsForm.GetWfAvgTimeMS: Double;
begin
Result := EnsureRange(FWfAvgTime.Value, 0.0, 5000.0);
end;
function TSettingsForm.GetWfHigh: Double;
begin
Result := EnsureRange(FWfHigh.Value, -160.0, 20.0);
end;
function TSettingsForm.GetWfLow: Double;
begin
Result := EnsureRange(FWfLow.Value, -180.0, 0.0);
end;
function TSettingsForm.GetWfAGCOffset: Double;
begin
Result := EnsureRange(FWfAGCOffset.Value, -40.0, 40.0);
end;
function TSettingsForm.GetRefLevel: Double;
var V: Double; Err: Integer;
begin
Val(FEdRefLevel.Text, V, Err);
if Err <> 0 then V := -20.0;
Result := EnsureRange(V, -200.0, 0.0);
end;
function TSettingsForm.GetRange: Double;
var V: Double; Err: Integer;
begin
Val(FEdRange.Text, V, Err);
if Err <> 0 then V := 110.0;
Result := EnsureRange(V, 10.0, 200.0);
end;
function TSettingsForm.GetGridStep: Double;
var Idx: Integer;
begin
Idx := FCmbGridStep.ItemIndex;
if (Idx >= 0) and (Idx <= High(GRID_STEPS)) then
Result := GRID_STEPS[Idx]
else
Result := 10.0;
end;
// ---------------------------------------------------------------------------
// Fire helpers
// ---------------------------------------------------------------------------
procedure TSettingsForm.FireDisplayChange;
var
AvgIdx: Integer;
AvgVal: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnDisplayChange) then Exit;
AvgIdx := FCmbSpecAvg.ItemIndex;
if (AvgIdx >= 0) and (AvgIdx <= High(AVGMODE_VALUES)) then
AvgVal := AVGMODE_VALUES[AvgIdx]
else
AvgVal := 3;
FOnDisplayChange(
GetFFTSize,
FCmbWindow.ItemIndex,
FCmbSpecDet.ItemIndex,
AvgVal,
GetSpecAvgTimeMS);
end;
procedure TSettingsForm.FireWaterfallChange;
var
AvgIdx: Integer;
AvgVal: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnWaterfallChange) then Exit;
AvgIdx := FCmbWfAvg.ItemIndex;
if (AvgIdx >= 0) and (AvgIdx <= High(WF_AVGMODE_VALUES)) then
AvgVal := WF_AVGMODE_VALUES[AvgIdx]
else
AvgVal := 1;
FOnWaterfallChange(
FCmbWfDet.ItemIndex,
AvgVal,
GetWfAvgTimeMS,
GetWfHigh,
GetWfLow,
GetWfAGCOffset);
end;
procedure TSettingsForm.FireGridChange;
begin
if FLoading then Exit;
if not Assigned(FOnGridChange) then Exit;
FOnGridChange(GetRefLevel, GetRange, GetGridStep);
end;
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
procedure TSettingsForm.OnFFTSizeChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnWindowChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnSpecDetChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnSpecAvgModeChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnSpecAvgTimeChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnWfDetChange(Sender: TObject);
begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfAvgModeChange(Sender: TObject);
begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfAvgTimeChange(Sender: TObject);
begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfThresholdChange(Sender: TObject);
begin FireWaterfallChange; end;
procedure TSettingsForm.OnRefLevelChange(Sender: TObject);
begin FireGridChange; end;
procedure TSettingsForm.OnRangeChange(Sender: TObject);
begin FireGridChange; end;
procedure TSettingsForm.OnGridStepChange(Sender: TObject);
begin FireGridChange; end;
procedure TSettingsForm.OnRXDevChange(Sender: TObject);
var
Idx: Integer;
DevIdx: Integer;
DevName: string;
begin
if FLoading then Exit;
if not Assigned(FOnAudioDevChange) then Exit;
Idx := FCmbRXDev.ItemIndex;
if Idx <= 0 then
FOnAudioDevChange(-1, '')
else
begin
DevName := FCmbRXDev.Items[Idx];
DevIdx := Idx - 1;
if (DevIdx >= 0) and (DevIdx < FRXDevCount) then
FOnAudioDevChange(FRXDevIndices[DevIdx], DevName)
else
FOnAudioDevChange(-1, '');
end;
end;
procedure TSettingsForm.OnTXDevChange(Sender: TObject);
var
Idx: Integer;
DevIdx: Integer;
DevName: string;
begin
if FLoading then Exit;
if not Assigned(FOnAudioInDevChange) then Exit;
Idx := FCmbTXDev.ItemIndex;
if Idx <= 0 then
FOnAudioInDevChange(-1, '')
else
begin
DevName := FCmbTXDev.Items[Idx];
DevIdx := Idx - 1;
if (DevIdx >= 0) and (DevIdx < FTXDevCount) then
FOnAudioInDevChange(FTXDevIndices[DevIdx], DevName)
else
FOnAudioInDevChange(-1, '');
end;
end;
procedure TSettingsForm.LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean);
begin
FLoading := True;
try
FChkShowSpectrum.Checked := ShowSpectrum;
FChkShowWaterfall.Checked := ShowWaterfall;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.OnVisibilityChkChange(Sender: TObject);
begin
if FLoading then Exit;
if not Assigned(FOnVisibilityChange) then Exit;
FOnVisibilityChange(FChkShowSpectrum.Checked, FChkShowWaterfall.Checked);
end;
procedure TSettingsForm.OnLightThemeChkChange(Sender: TObject);
begin
if FLoading then Exit;
if FChkLightTheme.Checked then ApplyTheme(LightTheme)
else ApplyTheme(DarkTheme);
if Assigned(FOnThemeChange) then
FOnThemeChange(FChkLightTheme.Checked);
end;
procedure TSettingsForm.OnFPSCmbChange(Sender: TObject);
var
Idx: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnFPSChange) then Exit;
Idx := FCmbFPS.ItemIndex;
if (Idx >= 0) and (Idx <= High(FPS_VALUES)) then
FOnFPSChange(FPS_VALUES[Idx]);
end;
procedure TSettingsForm.LoadFPS(FPS: Integer);
var
i, Idx: Integer;
begin
FLoading := True;
try
Idx := 8;
for i := 0 to High(FPS_VALUES) do
if FPS_VALUES[i] = FPS then begin Idx := i; Break; end;
FCmbFPS.ItemIndex := Idx;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.LoadLightTheme(ALight: Boolean);
begin
FLoading := True;
try
FChkLightTheme.Checked := ALight;
finally
FLoading := False;
end;
if ALight then ApplyTheme(LightTheme)
else ApplyTheme(DarkTheme);
end;
procedure TSettingsForm.LoadPASettings(MaxPower: Double;
const BandCal: array of Double);
var i, n: Integer;
begin
FLoading := True;
try
FEdMaxPower.Value := Round(EnsureRange(MaxPower, 5.0, 200.0));
n := Min(10, High(BandCal));
for i := 0 to n do
FEdBandCal[i].Value := EnsureRange(BandCal[i], 38.8, 100.0);
finally
FLoading := False;
end;
end;
procedure TSettingsForm.OnMaxPowerChange(Sender: TObject);
var
BandCal: array[0..10] of Double;
i: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnPAChange) then Exit;
for i := 0 to 10 do
BandCal[i] := FEdBandCal[i].Value;
FOnPAChange(FEdMaxPower.Value, BandCal);
end;
procedure TSettingsForm.OnBandCalChange(Sender: TObject);
var
BandCal: array[0..10] of Double;
i: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnPAChange) then Exit;
for i := 0 to 10 do
BandCal[i] := FEdBandCal[i].Value;
FOnPAChange(FEdMaxPower.Value, BandCal);
end;
procedure TSettingsForm.BtnCloseClick(Sender: TObject);
begin
Close;
end;
// ---------------------------------------------------------------------------
// BuildCATTab
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildCATTab;
const
MARGIN = 22;
PAD = 18;
LW = 120;
CX = PAD + LW + 8;
GRP_W = 320;
GRP_H = 240;
GRP_GAP = 16;
CMB_W = 140;
EDT_W = 160;
R1 = 42;
STEP = 32;
var
i, col, row, gx, gy, j: Integer;
Grp: TPanel;
Chk: TCheckBox;
Ed: TEdit;
Cmb: TComboBox;
Spin: TSpinEdit;
begin
with TLabel.Create(Self) do
begin
Parent := FPageCAT;
Caption := 'CAT';
SetBounds(MARGIN, 18, 400, 28);
Font.Name := UI_FONT; Font.Size := 16;
Font.Style := [fsBold]; Font.Color := CLR_TEXT;
end;
with TLabel.Create(Self) do
begin
Parent := FPageCAT;
Caption := 'Computer Aided Transceiver — serial port and TCP server settings.';
SetBounds(MARGIN, 48, 680, 20);
Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM;
end;
for i := 0 to 3 do
begin
row := i div 2;
col := i mod 2;
gx := MARGIN + col * (GRP_W + GRP_GAP);
gy := 86 + row * (GRP_H + GRP_GAP);
Grp := MakeGroupPanel(FPageCAT, 'Serial Port ' + IntToStr(i + 1), gx, gy, GRP_W, GRP_H);
Chk := TCheckBox.Create(Self);
Chk.Parent := Grp;
Chk.Caption := 'Enable';
Chk.SetBounds(PAD, R1, GRP_W - PAD * 2, 22);
Chk.Font.Color := CLR_TEXT;
Chk.Font.Name := UI_FONT; Chk.Font.Size := 9;
Chk.Tag := i;
Chk.OnChange := OnCATSerialChange;
FCATSerialEn[i] := Chk;
MakeLbl(Grp, 'Port:', PAD, R1 + STEP + 6, LW);
Ed := TEdit.Create(Self);
Ed.Parent := Grp;
Ed.SetBounds(CX, R1 + STEP, EDT_W, BTN_H);
Ed.Color := CLR_INPUT; Ed.Font.Color := CLR_INPUT_TEXT;
Ed.Font.Name := UI_FONT; Ed.Font.Size := 9;
Ed.Tag := i;
Ed.OnChange := OnCATSerialChange;
{$IFDEF MSWINDOWS}
Ed.Text := 'COM' + IntToStr(i + 1);
{$ELSE}
Ed.Text := '/dev/ttyS' + IntToStr(i);
{$ENDIF}
FCATSerialPort[i] := Ed;
MakeLbl(Grp, 'Baud rate:', PAD, R1 + 2 * STEP + 6, LW);
Cmb := MakeCombo(Grp, CX, R1 + 2 * STEP, CMB_W, OnCATSerialChange);
Cmb.Tag := i;
for j := 0 to High(CAT_BAUD_NAMES) do Cmb.Items.Add(CAT_BAUD_NAMES[j]);
Cmb.ItemIndex := 3; // 9600
FCATSerialBaud[i] := Cmb;
MakeLbl(Grp, 'Data bits:', PAD, R1 + 3 * STEP + 6, LW);
Cmb := MakeCombo(Grp, CX, R1 + 3 * STEP, CMB_W, OnCATSerialChange);
Cmb.Tag := i;
Cmb.Items.Add('7'); Cmb.Items.Add('8');
Cmb.ItemIndex := 1; // 8
FCATSerialDataBits[i] := Cmb;
MakeLbl(Grp, 'Stop bits:', PAD, R1 + 4 * STEP + 6, LW);
Cmb := MakeCombo(Grp, CX, R1 + 4 * STEP, CMB_W, OnCATSerialChange);
Cmb.Tag := i;
Cmb.Items.Add('1'); Cmb.Items.Add('2');
Cmb.ItemIndex := 0; // 1
FCATSerialStopBits[i] := Cmb;
MakeLbl(Grp, 'Parity:', PAD, R1 + 5 * STEP + 6, LW);
Cmb := MakeCombo(Grp, CX, R1 + 5 * STEP, CMB_W, OnCATSerialChange);
Cmb.Tag := i;
Cmb.Items.Add('None'); Cmb.Items.Add('Odd'); Cmb.Items.Add('Even');
Cmb.ItemIndex := 0; // None
FCATSerialParity[i] := Cmb;
end;
// TCP server group below the two rows of serial port panels
gy := 86 + 2 * (GRP_H + GRP_GAP);
Grp := MakeGroupPanel(FPageCAT, 'TCP CAT Server', MARGIN, gy, GRP_W * 2 + GRP_GAP, 116);
FCATTcpEn := TCheckBox.Create(Self);
FCATTcpEn.Parent := Grp;
FCATTcpEn.Caption := 'Enable TCP server (default port 19090)';
FCATTcpEn.SetBounds(PAD, R1, 300, 22);
FCATTcpEn.Font.Color := CLR_TEXT;
FCATTcpEn.Font.Name := UI_FONT; FCATTcpEn.Font.Size := 9;
FCATTcpEn.OnChange := OnCATTcpChange;
MakeLbl(Grp, 'Port:', PAD, R1 + STEP + 6, LW);
Spin := TSpinEdit.Create(Self);
Spin.Parent := Grp;
Spin.SetBounds(CX, R1 + STEP, 100, BTN_H + 2);
Spin.Color := CLR_INPUT; Spin.Font.Color := CLR_INPUT_TEXT;
Spin.Font.Name := UI_FONT; Spin.Font.Size := 9;
Spin.MinValue := 1; Spin.MaxValue := 65535;
Spin.Value := 19090;
Spin.OnChange := OnCATTcpChange;
FCATTcpPort := Spin;
end;
// ---------------------------------------------------------------------------
// CAT event handlers
// ---------------------------------------------------------------------------
procedure TSettingsForm.FireCATChange;
var
SerEn: array[0..3] of Boolean;
SerPort: array[0..3] of string;
SerBaud, SerData, SerStop, SerPar: array[0..3] of Integer;
i, Idx: Integer;
begin
if FLoading then Exit;
if not Assigned(FOnCATChange) then Exit;
for i := 0 to 3 do
begin
SerEn[i] := FCATSerialEn[i].Checked;
SerPort[i] := FCATSerialPort[i].Text;
Idx := FCATSerialBaud[i].ItemIndex;
if (Idx >= 0) and (Idx <= High(CAT_BAUD_VALS)) then
SerBaud[i] := CAT_BAUD_VALS[Idx]
else
SerBaud[i] := 9600;
if FCATSerialDataBits[i].ItemIndex = 0 then SerData[i] := 7 else SerData[i] := 8;
if FCATSerialStopBits[i].ItemIndex = 1 then SerStop[i] := 2 else SerStop[i] := 1;
SerPar[i] := FCATSerialParity[i].ItemIndex;
end;
FOnCATChange(SerEn, SerPort, SerBaud, SerData, SerStop, SerPar,
FCATTcpEn.Checked, FCATTcpPort.Value);
end;
procedure TSettingsForm.OnCATSerialChange(Sender: TObject);
begin FireCATChange; end;
procedure TSettingsForm.OnCATTcpChange(Sender: TObject);
begin FireCATChange; end;
// ---------------------------------------------------------------------------
// LoadCATSettings
// ---------------------------------------------------------------------------
procedure TSettingsForm.LoadCATSettings(
const SerEnabled: array of Boolean;
const SerPort: array of string;
const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer;
TcpEnabled: Boolean; TcpPort: Integer);
var
i, j, Idx: Integer;
begin
FLoading := True;
try
for i := 0 to 3 do
begin
if i <= High(SerEnabled) then FCATSerialEn[i].Checked := SerEnabled[i];
if i <= High(SerPort) then FCATSerialPort[i].Text := SerPort[i];
if i <= High(SerBaud) then
begin
Idx := 3;
for j := 0 to High(CAT_BAUD_VALS) do
if CAT_BAUD_VALS[j] = SerBaud[i] then begin Idx := j; Break; end;
FCATSerialBaud[i].ItemIndex := Idx;
end;
if i <= High(SerDataBits) then
begin
if SerDataBits[i] = 7 then FCATSerialDataBits[i].ItemIndex := 0
else FCATSerialDataBits[i].ItemIndex := 1;
end;
if i <= High(SerStopBits) then
begin
if SerStopBits[i] = 2 then FCATSerialStopBits[i].ItemIndex := 1
else FCATSerialStopBits[i].ItemIndex := 0;
end;
if i <= High(SerParity) then
FCATSerialParity[i].ItemIndex := EnsureRange(SerParity[i], 0, 2);
end;
FCATTcpEn.Checked := TcpEnabled;
FCATTcpPort.Value := EnsureRange(TcpPort, 1, 65535);
finally
FLoading := False;
end;
end;
end.