Files
ewsdr/SettingsForm.pas
T
2026-04-22 22:09:42 +03:00

1186 lines
38 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, ComCtrls, Spin,
FlatButton, AudioOutput, AudioInput;
type
// PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон
TOnPASettingsChange = procedure(MaxPower: Double;
const BandCal: array of Double) 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;
{ TSettingsForm }
TSettingsForm = class(TForm)
private
// ---- Tabs ----
FPageCtrl: TPageControl;
FTabGeneral: TTabSheet;
FTabAudio: TTabSheet;
FTabDisplay: TTabSheet;
FTabDSP: TTabSheet;
FTabTransmit: TTabSheet;
FTabPA: TTabSheet;
FTabCAT: TTabSheet;
// ---- Audio tab controls ----
FCmbRXDev: TComboBox;
FRXDevIndices: array[0..63] of Integer;
FRXDevCount: Integer;
FCmbTXDev: TComboBox;
FTXDevIndices: array[0..63] of Integer;
FTXDevCount: Integer;
// ---- Display sub-tabs ----
FDispPageCtrl: TPageControl;
FTabDispGeneral: TTabSheet;
FTabDispRX1: TTabSheet;
FTabDispRX2: TTabSheet;
FTabDispTX: TTabSheet;
// ---- RX1 sub-tab controls ----
// FFT group
FCmbFFTSize: TComboBox;
FCmbWindow: TComboBox;
// Spectrum group
FCmbSpecDet: TComboBox;
FCmbSpecAvg: TComboBox;
FSpecAvgTime: TFloatSpinEdit;
// Waterfall group
FCmbWfDet: TComboBox;
FCmbWfAvg: TComboBox;
FWfAvgTime: TFloatSpinEdit;
FWfHigh: TFloatSpinEdit;
FWfLow: TFloatSpinEdit;
FWfAGCOffset: TFloatSpinEdit;
// Grid group
FEdRefLevel: TEdit;
FEdRange: TEdit;
FCmbGridStep: TComboBox;
// ---- General display tab controls ----
FChkShowSpectrum: TCheckBox;
FChkShowWaterfall: TCheckBox;
FCmbFPS: TComboBox;
// ---- PA Settings tab controls ----
FEdMaxPower: TSpinEdit;
FEdBandCal: array[0..10] of TFloatSpinEdit;
// ---- Close button ----
FBtnClose: TFlatButton;
// ---- Callbacks ----
FOnPAChange: TOnPASettingsChange;
FOnDisplayChange: TOnDisplayParamChange;
FOnWaterfallChange: TOnWaterfallParamChange;
FOnGridChange: TOnGridParamChange;
FOnAudioDevChange: TOnAudioDevChange;
FOnAudioInDevChange: TOnAudioInDevChange;
FOnVisibilityChange: TOnVisibilityChange;
FOnFPSChange: TOnFPSChange;
// ---- Suppress change events during load ----
FLoading: Boolean;
procedure BuildUI;
procedure BuildAudioTab;
procedure BuildDisplayTab;
procedure BuildRX1Tab;
procedure BuildPATab;
procedure ApplyTheme;
// Helpers
function MakeLbl(AParent: TWinControl; const Cap: string;
ALeft, ATop, AW: Integer): TLabel;
function MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer;
OnChange: TNotifyEvent): TComboBox;
function MakeGroupBox(AParent: TWinControl; const Cap: string;
ALeft, ATop, AW, AH: Integer): TGroupBox;
// Event handlers
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 OnMaxPowerChange(Sender: TObject);
procedure OnBandCalChange(Sender: TObject);
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;
// Заполнить элементы управления текущими значениями (без firing events)
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);
// Перечислить PA устройства ввода и вывода
procedure RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput);
// Загрузить текущие значения видимости (без fire событий)
procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean);
// Загрузить текущий FPS (без fire события)
procedure LoadFPS(FPS: Integer);
// Загрузить PA настройки (без fire событий)
procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double);
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;
end;
implementation
const
CLR_BG = TColor($00121212);
CLR_PANEL = TColor($001A1A1A);
CLR_GROUP = TColor($00202020);
CLR_TEXT = TColor($00E0E0E0);
CLR_TEXTDIM = TColor($00888888);
CLR_BORDER = TColor($00303030);
CLR_ACCENT = TColor($0040FF80);
BTN_H = 24;
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 := 760;
Height := 640;
Position := poScreenCenter;
BorderStyle := bsSizeable;
Color := CLR_BG;
Font.Name := 'Courier New';
Font.Size := 8;
Font.Color := CLR_TEXT;
FLoading := False;
FRXDevCount := 0;
BuildUI;
ApplyTheme;
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 := 'Courier New';
Result.Font.Size := 8;
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_PANEL;
Result.Font.Color := CLR_TEXT;
Result.Font.Name := 'Courier New';
Result.Font.Size := 8;
Result.OnChange := OnChange;
end;
function TSettingsForm.MakeGroupBox(AParent: TWinControl; const Cap: string;
ALeft, ATop, AW, AH: Integer): TGroupBox;
begin
Result := TGroupBox.Create(Self);
Result.Parent := AParent;
Result.Caption := Cap;
Result.SetBounds(ALeft, ATop, AW, AH);
Result.Color := CLR_GROUP;
Result.Font.Color := CLR_ACCENT;
Result.Font.Name := 'Courier New';
Result.Font.Size := 8;
end;
// ---------------------------------------------------------------------------
// BuildUI
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildUI;
var
i: Integer;
begin
FPageCtrl := TPageControl.Create(Self);
FPageCtrl.Parent := Self;
FPageCtrl.SetBounds(4, 4, Width - 12, Height - 48);
FPageCtrl.Anchors := [akLeft, akTop, akRight, akBottom];
FPageCtrl.Color := CLR_BG;
FPageCtrl.Font.Color := CLR_TEXT;
FTabGeneral := FPageCtrl.AddTabSheet; FTabGeneral.Caption := 'General';
FTabAudio := FPageCtrl.AddTabSheet; FTabAudio.Caption := 'Audio';
FTabDisplay := FPageCtrl.AddTabSheet; FTabDisplay.Caption := 'Display';
FTabDSP := FPageCtrl.AddTabSheet; FTabDSP.Caption := 'DSP';
FTabTransmit := FPageCtrl.AddTabSheet; FTabTransmit.Caption := 'Transmit';
FTabPA := FPageCtrl.AddTabSheet; FTabPA.Caption := 'PA Settings';
FTabCAT := FPageCtrl.AddTabSheet; FTabCAT.Caption := 'CAT';
for i := 0 to FPageCtrl.PageCount - 1 do
FPageCtrl.Pages[i].Color := CLR_BG;
BuildAudioTab;
BuildDisplayTab;
BuildPATab;
// Close button
FBtnClose := TFlatButton.Create(Self);
FBtnClose.Parent := Self;
FBtnClose.Caption := 'CLOSE';
FBtnClose.SetBounds(Width - 90, Height - 38, 82, BTN_H);
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;
end;
procedure TSettingsForm.BuildAudioTab;
const
R1 = 10; // top padding inside groups
LW = 110; // label width
var
Grp: TGroupBox;
procedure MkGrp(out G: TGroupBox; const Cap: string);
begin
G := TGroupBox.Create(Self);
G.Parent := FTabAudio;
G.Caption := Cap;
G.Color := CLR_GROUP;
G.Font.Color := CLR_ACCENT;
G.Font.Name := 'Courier New';
G.Font.Size := 8;
G.AutoSize := True;
G.Align := alTop;
end;
procedure MkSep;
var S: TPanel;
begin
S := TPanel.Create(Self);
S.Parent := FTabAudio;
S.BevelOuter := bvNone;
S.Color := CLR_BG;
S.Align := alTop;
S.Height := 12;
end;
begin
// ── Top padding ───────────────────────────────────────────────────────────
MkSep;
// ── TX Audio Input ────────────────────────────────────────────────────────
MkGrp(Grp, 'TX Audio Input');
MakeLbl(Grp, 'Input Device:', 10, R1+4, LW);
FCmbTXDev := MakeCombo(Grp, LW+14, R1, 440, OnTXDevChange);
FCmbTXDev.Items.Add('None');
MkSep;
// ── RX Audio Output ───────────────────────────────────────────────────────
MkGrp(Grp, 'RX Audio Output');
MakeLbl(Grp, 'Output Device:', 10, R1+4, LW);
FCmbRXDev := MakeCombo(Grp, LW+14, R1, 440, OnRXDevChange);
FCmbRXDev.Items.Add('Default');
end;
procedure TSettingsForm.BuildDisplayTab;
var
i: Integer;
begin
FDispPageCtrl := TPageControl.Create(Self);
FDispPageCtrl.Parent := FTabDisplay;
FDispPageCtrl.Align := alClient;
FDispPageCtrl.Color := CLR_BG;
FDispPageCtrl.Font.Color := CLR_TEXT;
FTabDispGeneral := FDispPageCtrl.AddTabSheet; FTabDispGeneral.Caption := 'General';
FTabDispRX1 := FDispPageCtrl.AddTabSheet; FTabDispRX1.Caption := 'RX1';
FTabDispRX2 := FDispPageCtrl.AddTabSheet; FTabDispRX2.Caption := 'RX2';
FTabDispTX := FDispPageCtrl.AddTabSheet; FTabDispTX.Caption := 'TX';
FTabDispGeneral.Color := CLR_BG;
FTabDispRX1.Color := CLR_BG;
FTabDispRX2.Color := CLR_BG;
FTabDispTX.Color := CLR_BG;
// ---- General sub-tab: visibility checkboxes ----
FChkShowSpectrum := TCheckBox.Create(Self);
FChkShowSpectrum.Parent := FTabDispGeneral;
FChkShowSpectrum.Caption := 'Show Spectrum';
FChkShowSpectrum.SetBounds(12, 16, 200, 22);
FChkShowSpectrum.Checked := True;
FChkShowSpectrum.Font.Color := CLR_TEXT;
FChkShowSpectrum.Font.Name := 'Courier New';
FChkShowSpectrum.Font.Size := 8;
FChkShowSpectrum.OnChange := OnVisibilityChkChange;
FChkShowWaterfall := TCheckBox.Create(Self);
FChkShowWaterfall.Parent := FTabDispGeneral;
FChkShowWaterfall.Caption := 'Show Waterfall';
FChkShowWaterfall.SetBounds(12, 44, 200, 22);
FChkShowWaterfall.Checked := True;
FChkShowWaterfall.Font.Color := CLR_TEXT;
FChkShowWaterfall.Font.Name := 'Courier New';
FChkShowWaterfall.Font.Size := 8;
FChkShowWaterfall.OnChange := OnVisibilityChkChange;
// FPS label + combo
with TLabel.Create(Self) do
begin
Parent := FTabDispGeneral;
Caption := 'Display FPS:';
SetBounds(12, 76, 100, 22);
Font.Color := CLR_TEXTDIM;
Font.Name := 'Courier New';
Font.Size := 8;
end;
FCmbFPS := MakeCombo(FTabDispGeneral, 116, 74, 100, OnFPSCmbChange);
for i := 0 to High(FPS_NAMES) do
FCmbFPS.Items.Add(FPS_NAMES[i]);
FCmbFPS.ItemIndex := 3; // 20 fps default
BuildRX1Tab;
end;
procedure TSettingsForm.BuildRX1Tab;
const
R1 = 12;
LX = 12;
LW = 112;
CX = LX + LW + 8;
STEP = 34;
SEP = 12;
GH_FFT = 100;
GH_SPEC = 136;
GH_WF = 170;
GH_GRID = 100;
var
PnlFFT, PnlSpec, PnlWF: TPanel;
Grp: TGroupBox;
i: Integer;
function MkGrp(AParent: TWinControl; const Cap: string; AH: Integer): TGroupBox;
begin
Result := TGroupBox.Create(Self);
Result.Parent := AParent;
Result.Caption := Cap;
Result.Color := CLR_GROUP;
Result.Font.Color := CLR_ACCENT;
Result.Font.Name := 'Courier New';
Result.Font.Size := 8;
Result.Align := alTop;
Result.Height := AH;
end;
procedure MkSep(AParent: TWinControl; AH: Integer);
var S: TPanel;
begin
S := TPanel.Create(Self);
S.Parent := AParent;
S.BevelOuter := bvNone;
S.Color := CLR_BG;
S.Align := alTop;
S.Height := AH;
end;
function MkFloat(AParent: TWinControl; AX, AY, AW: Integer; AValue: Double;
AMin, AMax, AInc: Double; AOnChange: TNotifyEvent): TFloatSpinEdit;
begin
Result := TFloatSpinEdit.Create(Self);
Result.Parent := AParent;
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_PANEL;
Result.Font.Color := CLR_TEXT;
Result.Font.Name := 'Courier New';
Result.Font.Size := 8;
Result.OnChange := AOnChange;
end;
begin
MkSep(FTabDispRX1, 8);
PnlFFT := TPanel.Create(Self);
PnlFFT.Parent := FTabDispRX1;
PnlFFT.BevelOuter := bvNone;
PnlFFT.Color := CLR_BG;
PnlFFT.Align := alTop;
PnlFFT.Height := GH_FFT;
Grp := MkGrp(PnlFFT, 'FFT / Analyzer', GH_FFT);
MakeLbl(Grp, 'FFT Size:', LX, R1+4, LW);
FCmbFFTSize := MakeCombo(Grp, CX, R1, 130, OnFFTSizeChange);
for i := 0 to High(FFT_NAMES) do FCmbFFTSize.Items.Add(FFT_NAMES[i]);
MakeLbl(Grp, 'Window:', LX, R1+STEP+4, LW);
FCmbWindow := MakeCombo(Grp, CX, R1+STEP, 250, OnWindowChange);
for i := 0 to High(WINDOW_NAMES) do FCmbWindow.Items.Add(WINDOW_NAMES[i]);
MkSep(FTabDispRX1, SEP);
PnlSpec := TPanel.Create(Self);
PnlSpec.Parent := FTabDispRX1;
PnlSpec.BevelOuter := bvNone;
PnlSpec.Color := CLR_BG;
PnlSpec.Align := alTop;
PnlSpec.Height := GH_SPEC;
Grp := MkGrp(PnlSpec, 'Spectrum', GH_SPEC);
MakeLbl(Grp, 'Detector:', LX, R1+4, LW);
FCmbSpecDet := MakeCombo(Grp, CX, R1, 172, OnSpecDetChange);
for i := 0 to High(DETECTOR_NAMES) do FCmbSpecDet.Items.Add(DETECTOR_NAMES[i]);
MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW);
FCmbSpecAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnSpecAvgModeChange);
for i := 0 to High(AVGMODE_NAMES) do FCmbSpecAvg.Items.Add(AVGMODE_NAMES[i]);
MakeLbl(Grp, 'Avg Time (ms):', LX, R1+2*STEP+4, LW);
FSpecAvgTime := MkFloat(Grp, CX, R1+2*STEP, 92, 120.0, 0.0, 5000.0, 10.0, OnSpecAvgTimeChange);
MkSep(FTabDispRX1, SEP);
PnlWF := TPanel.Create(Self);
PnlWF.Parent := FTabDispRX1;
PnlWF.BevelOuter := bvNone;
PnlWF.Color := CLR_BG;
PnlWF.Align := alTop;
PnlWF.Height := GH_WF;
Grp := MkGrp(PnlWF, 'Waterfall', GH_WF);
MakeLbl(Grp, 'Detector:', LX, R1+4, LW);
FCmbWfDet := MakeCombo(Grp, CX, R1, 172, OnWfDetChange);
for i := 0 to High(DETECTOR_NAMES) do FCmbWfDet.Items.Add(DETECTOR_NAMES[i]);
MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW);
FCmbWfAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnWfAvgModeChange);
for i := 0 to High(WF_AVGMODE_NAMES) do FCmbWfAvg.Items.Add(WF_AVGMODE_NAMES[i]);
MakeLbl(Grp, 'Avg Time (ms):', LX, R1+2*STEP+4, LW);
FWfAvgTime := MkFloat(Grp, CX, R1+2*STEP, 92, 120.0, 0.0, 5000.0, 10.0, OnWfAvgTimeChange);
MakeLbl(Grp, 'High (dBm):', LX, R1+3*STEP+4, LW);
FWfHigh := MkFloat(Grp, CX, R1+3*STEP, 92, -80.0, -160.0, 20.0, 1.0, OnWfThresholdChange);
MakeLbl(Grp, 'Low (dBm):', CX+120, R1+3*STEP+4, 72);
FWfLow := MkFloat(Grp, CX+196, R1+3*STEP, 92, -130.0, -180.0, 0.0, 1.0, OnWfThresholdChange);
MakeLbl(Grp, 'AGC Offset:', LX, R1+4*STEP+4, LW);
FWfAGCOffset := MkFloat(Grp, CX, R1+4*STEP, 92, 0.0, -40.0, 40.0, 0.5, OnWfThresholdChange);
MkSep(FTabDispRX1, SEP);
Grp := MkGrp(FTabDispRX1, 'Spectrum Grid', GH_GRID);
MakeLbl(Grp, 'Ref Level (dBm):', LX, R1+4, 116);
FEdRefLevel := TEdit.Create(Self);
FEdRefLevel.Parent := Grp;
FEdRefLevel.SetBounds(LX+120, R1, 80, BTN_H);
FEdRefLevel.Color := CLR_PANEL;
FEdRefLevel.Font.Color := CLR_TEXT;
FEdRefLevel.Font.Name := 'Courier New';
FEdRefLevel.Font.Size := 8;
FEdRefLevel.OnExit := OnRefLevelChange;
FEdRefLevel.Text := '-20';
MakeLbl(Grp, 'Range (dB):', LX, R1+STEP+4, LW);
FEdRange := TEdit.Create(Self);
FEdRange.Parent := Grp;
FEdRange.SetBounds(CX, R1+STEP, 80, BTN_H);
FEdRange.Color := CLR_PANEL;
FEdRange.Font.Color := CLR_TEXT;
FEdRange.Font.Name := 'Courier New';
FEdRange.Font.Size := 8;
FEdRange.OnExit := OnRangeChange;
FEdRange.Text := '110';
MakeLbl(Grp, 'Grid Step:', CX+90, R1+STEP+4, 72);
FCmbGridStep := MakeCombo(Grp, CX+90+76, R1+STEP, 80, OnGridStepChange);
for i := 0 to High(GRID_STEP_NAMES) do FCmbGridStep.Items.Add(GRID_STEP_NAMES[i]);
FCmbGridStep.ItemIndex := 1;
end;
procedure TSettingsForm.ApplyTheme;
var
i: Integer;
begin
Color := CLR_BG;
for i := 0 to FPageCtrl.PageCount - 1 do
FPageCtrl.Pages[i].Color := CLR_BG;
FDispPageCtrl.Color := CLR_BG;
for i := 0 to FDispPageCtrl.PageCount - 1 do
FDispPageCtrl.Pages[i].Color := CLR_BG;
end;
// ---------------------------------------------------------------------------
// LoadValues — заполняет элементы без fire событий
// ---------------------------------------------------------------------------
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
// FFT Size
Idx := 0; // default 4096
for i := 0 to High(FFT_SIZES) do
if FFT_SIZES[i] = FFTSize then begin Idx := i; Break; end;
FCmbFFTSize.ItemIndex := Idx;
// Window Type
if (WinType >= 0) and (WinType < FCmbWindow.Items.Count) then
FCmbWindow.ItemIndex := WinType
else
FCmbWindow.ItemIndex := 2; // Hann
// Spectrum Detector
if (SpecDet >= 0) and (SpecDet < FCmbSpecDet.Items.Count) then
FCmbSpecDet.ItemIndex := SpecDet
else
FCmbSpecDet.ItemIndex := 0;
// Spectrum Avg Mode (WDSP value → combo index)
Idx := 0; // default None
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);
// Waterfall Detector
if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then
FCmbWfDet.ItemIndex := WfDet
else
FCmbWfDet.ItemIndex := 2; // Average
// Waterfall Avg Mode
Idx := 1; // default Recursive
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);
// Grid
FEdRefLevel.Text := FormatFloat('0.#', RefLevel);
FEdRange.Text := FormatFloat('0.#', Range);
Idx := 1; // 10 dB default
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;
// RX Audio Output
if AudioOutDevName = '' then
FCmbRXDev.ItemIndex := 0 // 'Default'
else
begin
Idx := FCmbRXDev.Items.IndexOf(AudioOutDevName);
if Idx >= 0 then FCmbRXDev.ItemIndex := Idx
else FCmbRXDev.ItemIndex := 0;
end;
// TX Audio Input
if AudioInDevName = '' then
FCmbTXDev.ItemIndex := 0 // 'None'
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
// ── RX Output devices ─────────────────────────────────────────────────
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;
i := FCmbRXDev.Items.IndexOf(OldRXSel);
if i >= 0 then FCmbRXDev.ItemIndex := i
else FCmbRXDev.ItemIndex := 0;
end;
// ── TX Input devices ──────────────────────────────────────────────────
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;
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
begin
// "Default"
FOnAudioDevChange(-1, '');
end
else
begin
DevName := FCmbRXDev.Items[Idx];
// Map combo index to PA device index
// Combo: 0=Default, 1..N = real devices
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
begin
// 'None'
FOnAudioInDevChange(-1, '');
end
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.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 := 3; // 20 fps default
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;
// ---------------------------------------------------------------------------
// BuildPATab — вкладка PA Settings: MaxPower + per-band calibration
// ---------------------------------------------------------------------------
procedure TSettingsForm.BuildPATab;
const
LBL_W = 46; // ширина метки диапазона
ED_W = 72; // ширина SpinEdit (с кнопками)
COL_W = LBL_W + ED_W + 8; // ширина одной пары
COLS = 4; // колонок на строку
ROW_H = 32; // высота строки (spin edits чуть выше plain edit)
GRP_PAD = 12; // отступ внутри группы
GRP_TOP = 24; // высота caption группы
ROWS = 3; // строк для 11 диапазонов (ceil(11/4))
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_PANEL;
E.Font.Color := CLR_TEXT;
E.Font.Name := 'Courier New';
E.Font.Size := 8;
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: TGroupBox;
Sep: TPanel;
Lbl: TLabel;
Spin: TSpinEdit;
i, r, c, X, Y: Integer;
begin
// ── Top padding ───────────────────────────────────────────────────────────
Sep := TPanel.Create(Self);
Sep.Parent := FTabPA;
Sep.BevelOuter := bvNone;
Sep.Color := CLR_BG;
Sep.Align := alTop;
Sep.Height := 10;
// ── Group Power & Calibration ─────────────────────────────────────────────
Grp := TGroupBox.Create(Self);
Grp.Parent := FTabPA;
Grp.Caption := 'Power & Calibration';
Grp.Color := CLR_GROUP;
Grp.Font.Color := CLR_ACCENT;
Grp.Font.Name := 'Courier New';
Grp.Font.Size := 8;
Grp.Align := alTop;
Grp.Height := GRP_TOP + ROW_H + 20 + ROWS * ROW_H + GRP_PAD + 16;
Grp.AutoSize := False;
// Max Power строка
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := 'Max Power (W):';
Lbl.SetBounds(GRP_PAD, GRP_TOP + 6, 120, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := 'Courier New';
Lbl.Font.Size := 8;
Spin := TSpinEdit.Create(Self);
Spin.Parent := Grp;
Spin.SetBounds(GRP_PAD + 124, GRP_TOP, ED_W, BTN_H + 2);
Spin.Color := CLR_PANEL;
Spin.Font.Color := CLR_TEXT;
Spin.Font.Name := 'Courier New';
Spin.Font.Size := 8;
Spin.MinValue := 5;
Spin.MaxValue := 200;
Spin.Increment := 1;
Spin.Value := 100;
Spin.OnChange := OnMaxPowerChange;
FEdMaxPower := Spin;
// Метка для калибровки
Lbl := TLabel.Create(Self);
Lbl.Parent := Grp;
Lbl.Caption := 'Calibration per band (38.8 ... 100.0):';
Lbl.SetBounds(GRP_PAD, GRP_TOP + ROW_H + 8, 280, 18);
Lbl.Font.Color := CLR_TEXTDIM;
Lbl.Font.Name := 'Courier New';
Lbl.Font.Size := 8;
// Grid 4 columns × 3 rows for 11 bands
for i := 0 to 10 do
begin
r := i div COLS;
c := i mod COLS;
X := GRP_PAD + c * (COL_W + 4);
Y := GRP_TOP + ROW_H + 26 + 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 := 'Courier New';
Lbl.Font.Size := 8;
FEdBandCal[i] := MakeFloatSpin(Grp, X + LBL_W, Y, OnBandCalChange);
FEdBandCal[i].Tag := i;
end;
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;
end.