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.