Files
ewsdr/SettingsForm.pas
T
2026-03-24 21:21:30 +03:00

1039 lines
34 KiB
ObjectPascal

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
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;
// ---- Close button ----
FBtnClose: TFlatButton;
// ---- Callbacks ----
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 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 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);
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');
{ 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;
// 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;
procedure TSettingsForm.BtnCloseClick(Sender: TObject);
begin
Close;
end;
end.