Files
ewsdr/SettingsForm.pas
T
2026-03-26 22:50:55 +03:00

1212 lines
40 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, SpecAvgCount: Integer; SpecBackmult: Double) of object;
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode, WfAvgCount: Integer;
WfBackmult: 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;
FSpinSpecCount: TSpinEdit;
FEdSpecBackmult: TEdit;
// Waterfall group
FCmbWfDet: TComboBox;
FCmbWfAvg: TComboBox;
FSpinWfCount: TSpinEdit;
FEdWfBackmult: TEdit;
// 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 OnSpecCountChange(Sender: TObject);
procedure OnSpecBackmultChange(Sender: TObject);
procedure OnWfDetChange(Sender: TObject);
procedure OnWfAvgModeChange(Sender: TObject);
procedure OnWfCountChange(Sender: TObject);
procedure OnWfBackmultChange(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 GetSpecBackmult: Double;
function GetWfBackmult: 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, SpecAvgCount: Integer;
SpecBackmult: Double;
WfDet, WfAvgMode, WfAvgCount: Integer;
WfBackmult: 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 (index → value)
FFT_SIZES: array[0..5] of Integer = (1024, 2048, 4096, 8192, 16384, 32768);
FFT_NAMES: array[0..5] of string = ('1024','2048','4096','8192','16384','32768');
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..4] of string = (
'Peak Hold', 'None', 'Recursive', 'Time Window', 'Log Recursive');
// Note: AVERAGE_PEAK_HOLD=-1, NONE=0, RECURSIVE=1, TIME_WINDOW=2, LOG_RECURSIVE=3
// Index → WDSP value mapping: 0→-1, 1→0, 2→1, 3→2, 4→3
AVGMODE_VALUES: array[0..4] of Integer = (-1, 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..5] of Integer = (5, 10, 15, 20, 25, 30);
FPS_NAMES: array[0..5] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 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 := 700;
Height := 560;
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;
// Layout (alTop stack in FTabDispRX1):
// 8px top padding
// FFT group — full width
// 12px separator
// PnlMid — Spectrum (alLeft) | 10px gap | Waterfall (alClient), same height
// 12px separator
// Spectrum Grid group — full width
// hint (alBottom)
//
// Row step: STEP = BTN_H + 10 = 34px. Inner padding: R1 = 10.
const
R1 = 10; // top padding inside groups
LX = 10; // left margin inside group
LW = 88; // label column width
CX = LX + LW + 4; // combo/edit column start (= 102)
STEP = BTN_H + 10; // row step (= 34)
SEP = 12; // gap between groups
COLW = 328; // left column panel width
COLGAP = 10; // horizontal gap between columns
GH2 = 100; // GroupBox height for 2-row groups
GH3 = 136; // GroupBox height for 3-row groups
var
PnlFFT, PnlMid, PnlL, PnlSepH, PnlR: TPanel;
Grp: TGroupBox;
Lbl: TLabel;
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 MkEdit(AParent: TWinControl; AX, AY, AW: Integer;
AOnExit: TNotifyEvent): TEdit;
begin
Result := TEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.Color := CLR_PANEL;
Result.Font.Color := CLR_TEXT;
Result.Font.Name := 'Courier New';
Result.Font.Size := 8;
Result.OnExit := AOnExit;
end;
function MkSpin(AParent: TWinControl; AX, AY, AW: Integer;
AOnChange: TNotifyEvent): TSpinEdit;
begin
Result := TSpinEdit.Create(Self);
Result.Parent := AParent;
Result.SetBounds(AX, AY, AW, BTN_H);
Result.MinValue := 1;
Result.MaxValue := 256;
Result.Value := 2;
Result.Color := CLR_PANEL;
Result.Font.Color := CLR_TEXT;
Result.Font.Name := 'Courier New';
Result.Font.Size := 8;
Result.OnChange := AOnChange;
end;
begin
// Hint at bottom (added first so alBottom is honoured by later alTop panels)
Lbl := MakeLbl(FTabDispRX1,
'Backmult: higher value = slower decay (range 0.0 .. 1.0)', LX, 0, 0);
Lbl.Align := alBottom;
Lbl.Height := 22;
Lbl.Font.Color := CLR_TEXTDIM;
// ── Top padding ───────────────────────────────────────────────────────────
MkSep(FTabDispRX1, 8);
// ── FFT (full width, alTop) ───────────────────────────────────────────────
PnlFFT := TPanel.Create(Self);
PnlFFT.Parent := FTabDispRX1;
PnlFFT.BevelOuter := bvNone;
PnlFFT.Color := CLR_BG;
PnlFFT.Align := alTop;
PnlFFT.Height := GH2;
Grp := MkGrp(PnlFFT, 'FFT', GH2);
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]);
// ── Separator ─────────────────────────────────────────────────────────────
MkSep(FTabDispRX1, SEP);
// ── Middle row: Spectrum (left) | Waterfall (right) at the same level ─────
PnlMid := TPanel.Create(Self);
PnlMid.Parent := FTabDispRX1;
PnlMid.BevelOuter := bvNone;
PnlMid.Color := CLR_BG;
PnlMid.Align := alTop;
PnlMid.Height := GH3;
PnlL := TPanel.Create(Self);
PnlL.Parent := PnlMid;
PnlL.BevelOuter := bvNone;
PnlL.Color := CLR_BG;
PnlL.Align := alLeft;
PnlL.Width := COLW;
PnlSepH := TPanel.Create(Self);
PnlSepH.Parent := PnlMid;
PnlSepH.BevelOuter := bvNone;
PnlSepH.Color := CLR_BG;
PnlSepH.Align := alLeft;
PnlSepH.Width := COLGAP;
PnlR := TPanel.Create(Self);
PnlR.Parent := PnlMid;
PnlR.BevelOuter := bvNone;
PnlR.Color := CLR_BG;
PnlR.Align := alClient;
// ── Spectrum (left column) ────────────────────────────────────────────────
Grp := MkGrp(PnlL, 'Spectrum', GH3);
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 Count:', LX, R1+2*STEP+4, LW);
FSpinSpecCount := MkSpin(Grp, CX, R1+2*STEP, 72, OnSpecCountChange);
MakeLbl(Grp, 'Backmult:', CX+78, R1+2*STEP+4, 64);
FEdSpecBackmult := MkEdit(Grp, CX+78+68, R1+2*STEP, 74, OnSpecBackmultChange);
FEdSpecBackmult.Text := '0.45';
// ── Waterfall (right column) ──────────────────────────────────────────────
Grp := MkGrp(PnlR, 'Waterfall', GH3);
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(AVGMODE_NAMES) do FCmbWfAvg.Items.Add(AVGMODE_NAMES[i]);
MakeLbl(Grp, 'Avg Count:', LX, R1+2*STEP+4, LW);
FSpinWfCount := MkSpin(Grp, CX, R1+2*STEP, 72, OnWfCountChange);
MakeLbl(Grp, 'Backmult:', CX+78, R1+2*STEP+4, 64);
FEdWfBackmult := MkEdit(Grp, CX+78+68, R1+2*STEP, 74, OnWfBackmultChange);
FEdWfBackmult.Text := '0.45';
// ── Separator ─────────────────────────────────────────────────────────────
MkSep(FTabDispRX1, SEP);
// ── Spectrum Grid (full width, alTop) ─────────────────────────────────────
Grp := MkGrp(FTabDispRX1, 'Spectrum Grid', GH2);
MakeLbl(Grp, 'Ref Level (dBm):', LX, R1+4, 116);
FEdRefLevel := MkEdit(Grp, LX+120, R1, 80, OnRefLevelChange);
FEdRefLevel.Text := '-20';
MakeLbl(Grp, 'Range (dB):', LX, R1+STEP+4, LW);
FEdRange := MkEdit(Grp, CX, R1+STEP, 80, 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, SpecAvgCount: Integer;
SpecBackmult: Double;
WfDet, WfAvgMode, WfAvgCount: Integer;
WfBackmult: Double;
RefLevel, Range, GridStep: Double;
const AudioOutDevName: string;
const AudioInDevName: string);
var
i, Idx: Integer;
begin
FLoading := True;
try
// FFT Size
Idx := 2; // 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 := 5; // Kaiser
// 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 := 2; // default Recursive
for i := 0 to High(AVGMODE_VALUES) do
if AVGMODE_VALUES[i] = SpecAvgMode then begin Idx := i; Break; end;
FCmbSpecAvg.ItemIndex := Idx;
FSpinSpecCount.Value := EnsureRange(SpecAvgCount, 1, 256);
FEdSpecBackmult.Text := FormatFloat('0.##', SpecBackmult);
// Waterfall Detector
if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then
FCmbWfDet.ItemIndex := WfDet
else
FCmbWfDet.ItemIndex := 2; // Average
// Waterfall Avg Mode
Idx := 4; // default Log Recursive
for i := 0 to High(AVGMODE_VALUES) do
if AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end;
FCmbWfAvg.ItemIndex := Idx;
FSpinWfCount.Value := EnsureRange(WfAvgCount, 1, 256);
FEdWfBackmult.Text := FormatFloat('0.##', WfBackmult);
// 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.GetSpecBackmult: Double;
var V: Double; Err: Integer;
begin
Val(FEdSpecBackmult.Text, V, Err);
if Err <> 0 then V := 0.45;
Result := EnsureRange(V, 0.0, 1.0);
end;
function TSettingsForm.GetWfBackmult: Double;
var V: Double; Err: Integer;
begin
Val(FEdWfBackmult.Text, V, Err);
if Err <> 0 then V := 0.45;
Result := EnsureRange(V, 0.0, 1.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,
FSpinSpecCount.Value,
GetSpecBackmult);
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(AVGMODE_VALUES)) then
AvgVal := AVGMODE_VALUES[AvgIdx]
else
AvgVal := 3;
FOnWaterfallChange(
FCmbWfDet.ItemIndex,
AvgVal,
FSpinWfCount.Value,
GetWfBackmult);
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.OnSpecCountChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnSpecBackmultChange(Sender: TObject);
begin FireDisplayChange; end;
procedure TSettingsForm.OnWfDetChange(Sender: TObject);
begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfAvgModeChange(Sender: TObject);
begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfCountChange(Sender: TObject);
begin FireWaterfallChange; end;
procedure TSettingsForm.OnWfBackmultChange(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.