unit SettingsForm; { SettingsForm.pas — диалог настроек EWSDR SDR приложения. Вкладки: General | Audio | Display | DSP | Transmit | PA Settings | CAT Display имеет подвкладки: General | RX1 | RX2 | TX Все изменения применяются МГНОВЕННО через events: OnDisplayChange — изменены FFT/Window/Detector/Avg параметры OnWaterfallChange — изменены параметры водопада OnGridChange — изменена сетка спектра OnAudioChange — изменено аудио устройство/буфер OnSampleRateChange — изменён audio output sample rate (только PortAudio, DDC не трогается) Для применения настроек: создайте форму, зарегистрируйте callbacks, потом вызовите Show или ShowModal. } {$IFDEF FPC} {$MODE Delphi} {$ENDIF} interface uses Classes, SysUtils, Math, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, ComCtrls, Spin, FlatButton, AudioOutput, AudioInput; type // PA: MaxPower в Вт, BandCal[0..10] — калибровка 38.8..100.0 на диапазон TOnPASettingsChange = procedure(MaxPower: Double; const BandCal: array of Double) of object; TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector, SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object; TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode: Integer; WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double) of object; TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object; TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object; TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object; TOnVisibilityChange = procedure(ShowSpectrum, ShowWaterfall: Boolean) of object; TOnFPSChange = procedure(FPS: Integer) of object; { TSettingsForm } TSettingsForm = class(TForm) private // ---- Tabs ---- FPageCtrl: TPageControl; FTabGeneral: TTabSheet; FTabAudio: TTabSheet; FTabDisplay: TTabSheet; FTabDSP: TTabSheet; FTabTransmit: TTabSheet; FTabPA: TTabSheet; FTabCAT: TTabSheet; // ---- Audio tab controls ---- FCmbRXDev: TComboBox; FRXDevIndices: array[0..63] of Integer; FRXDevCount: Integer; FCmbTXDev: TComboBox; FTXDevIndices: array[0..63] of Integer; FTXDevCount: Integer; // ---- Display sub-tabs ---- FDispPageCtrl: TPageControl; FTabDispGeneral: TTabSheet; FTabDispRX1: TTabSheet; FTabDispRX2: TTabSheet; FTabDispTX: TTabSheet; // ---- RX1 sub-tab controls ---- // FFT group FCmbFFTSize: TComboBox; FCmbWindow: TComboBox; // Spectrum group FCmbSpecDet: TComboBox; FCmbSpecAvg: TComboBox; FSpecAvgTime: TFloatSpinEdit; // Waterfall group FCmbWfDet: TComboBox; FCmbWfAvg: TComboBox; FWfAvgTime: TFloatSpinEdit; FWfHigh: TFloatSpinEdit; FWfLow: TFloatSpinEdit; FWfAGCOffset: TFloatSpinEdit; // Grid group FEdRefLevel: TEdit; FEdRange: TEdit; FCmbGridStep: TComboBox; // ---- General display tab controls ---- FChkShowSpectrum: TCheckBox; FChkShowWaterfall: TCheckBox; FCmbFPS: TComboBox; // ---- PA Settings tab controls ---- FEdMaxPower: TSpinEdit; FEdBandCal: array[0..10] of TFloatSpinEdit; // ---- Close button ---- FBtnClose: TFlatButton; // ---- Callbacks ---- FOnPAChange: TOnPASettingsChange; FOnDisplayChange: TOnDisplayParamChange; FOnWaterfallChange: TOnWaterfallParamChange; FOnGridChange: TOnGridParamChange; FOnAudioDevChange: TOnAudioDevChange; FOnAudioInDevChange: TOnAudioInDevChange; FOnVisibilityChange: TOnVisibilityChange; FOnFPSChange: TOnFPSChange; // ---- Suppress change events during load ---- FLoading: Boolean; procedure BuildUI; procedure BuildAudioTab; procedure BuildDisplayTab; procedure BuildRX1Tab; procedure BuildPATab; procedure ApplyTheme; // Helpers function MakeLbl(AParent: TWinControl; const Cap: string; ALeft, ATop, AW: Integer): TLabel; function MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer; OnChange: TNotifyEvent): TComboBox; function MakeGroupBox(AParent: TWinControl; const Cap: string; ALeft, ATop, AW, AH: Integer): TGroupBox; // Event handlers procedure OnFFTSizeChange(Sender: TObject); procedure OnWindowChange(Sender: TObject); procedure OnSpecDetChange(Sender: TObject); procedure OnSpecAvgModeChange(Sender: TObject); procedure OnSpecAvgTimeChange(Sender: TObject); procedure OnWfDetChange(Sender: TObject); procedure OnWfAvgModeChange(Sender: TObject); procedure OnWfAvgTimeChange(Sender: TObject); procedure OnWfThresholdChange(Sender: TObject); procedure OnRefLevelChange(Sender: TObject); procedure OnRangeChange(Sender: TObject); procedure OnGridStepChange(Sender: TObject); procedure OnRXDevChange(Sender: TObject); procedure OnTXDevChange(Sender: TObject); procedure OnVisibilityChkChange(Sender: TObject); procedure OnFPSCmbChange(Sender: TObject); procedure OnMaxPowerChange(Sender: TObject); procedure OnBandCalChange(Sender: TObject); procedure BtnCloseClick(Sender: TObject); procedure FireDisplayChange; procedure FireWaterfallChange; procedure FireGridChange; function GetSpecAvgTimeMS: Double; function GetWfAvgTimeMS: Double; function GetWfHigh: Double; function GetWfLow: Double; function GetWfAGCOffset: Double; function GetRefLevel: Double; function GetRange: Double; function GetGridStep: Double; function GetFFTSize: Integer; public constructor Create(AOwner: TComponent); override; // Заполнить элементы управления текущими значениями (без firing events) procedure LoadValues( FFTSize, WinType, SpecDet, SpecAvgMode: Integer; SpecAvgTimeMS: Double; WfDet, WfAvgMode: Integer; WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double; RefLevel, Range, GridStep: Double; const AudioOutDevName: string; const AudioInDevName: string); // Перечислить PA устройства ввода и вывода procedure RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput); // Загрузить текущие значения видимости (без fire событий) procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean); // Загрузить текущий FPS (без fire события) procedure LoadFPS(FPS: Integer); // Загрузить PA настройки (без fire событий) procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double); property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange; property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange; property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange; property OnGridChange: TOnGridParamChange read FOnGridChange write FOnGridChange; property OnAudioDevChange: TOnAudioDevChange read FOnAudioDevChange write FOnAudioDevChange; property OnAudioInDevChange: TOnAudioInDevChange read FOnAudioInDevChange write FOnAudioInDevChange; property OnVisibilityChange: TOnVisibilityChange read FOnVisibilityChange write FOnVisibilityChange; property OnFPSChange: TOnFPSChange read FOnFPSChange write FOnFPSChange; end; implementation const CLR_BG = TColor($00121212); CLR_PANEL = TColor($001A1A1A); CLR_GROUP = TColor($00202020); CLR_TEXT = TColor($00E0E0E0); CLR_TEXTDIM = TColor($00888888); CLR_BORDER = TColor($00303030); CLR_ACCENT = TColor($0040FF80); BTN_H = 24; FFT_SIZES: array[0..6] of Integer = (4096, 8192, 16384, 32768, 65536, 131072, 262144); FFT_NAMES: array[0..6] of string = ('4096','8192','16384','32768','65536','131072','262144'); WINDOW_NAMES: array[0..6] of string = ( 'Rectangular', '4-term Blackman-Harris', 'Hann', 'Flat-top', 'Hamming', 'Kaiser', '7-term Blackman-Harris'); DETECTOR_NAMES: array[0..4] of string = ( 'Peak', 'Rosenfell', 'Average', 'Sample', 'RMS'); AVGMODE_NAMES: array[0..3] of string = ( 'None', 'Recursive', 'Time Window', 'Log Recursive'); AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3); WF_AVGMODE_NAMES: array[0..3] of string = ( 'None', 'Recursive', 'Time Window', 'Log Recursive'); WF_AVGMODE_VALUES: array[0..3] of Integer = (0, 1, 2, 3); GRID_STEPS: array[0..4] of Double = (5.0, 10.0, 20.0, 30.0, 40.0); GRID_STEP_NAMES: array[0..4] of string = ('5 dB','10 dB','20 dB','30 dB','40 dB'); FPS_VALUES: array[0..12] of Integer = (5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 80, 90, 100); FPS_NAMES: array[0..12] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps', '40 fps','50 fps','60 fps','75 fps','80 fps','90 fps','100 fps'); PA_BAND_NAMES: array[0..10] of string = ('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m'); { TSettingsForm } constructor TSettingsForm.Create(AOwner: TComponent); begin inherited CreateNew(AOwner); Caption := 'Settings'; Width := 760; Height := 640; Position := poScreenCenter; BorderStyle := bsSizeable; Color := CLR_BG; Font.Name := 'Courier New'; Font.Size := 8; Font.Color := CLR_TEXT; FLoading := False; FRXDevCount := 0; BuildUI; ApplyTheme; end; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function TSettingsForm.MakeLbl(AParent: TWinControl; const Cap: string; ALeft, ATop, AW: Integer): TLabel; begin Result := TLabel.Create(Self); Result.Parent := AParent; Result.Caption := Cap; Result.Left := ALeft; Result.Top := ATop; if AW > 0 then Result.Width := AW; Result.Font.Color := CLR_TEXTDIM; Result.Font.Name := 'Courier New'; Result.Font.Size := 8; end; function TSettingsForm.MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer; OnChange: TNotifyEvent): TComboBox; begin Result := TComboBox.Create(Self); Result.Parent := AParent; Result.Left := ALeft; Result.Top := ATop; Result.Width := AW; Result.Height := BTN_H; Result.Style := csDropDownList; Result.Color := CLR_PANEL; Result.Font.Color := CLR_TEXT; Result.Font.Name := 'Courier New'; Result.Font.Size := 8; Result.OnChange := OnChange; end; function TSettingsForm.MakeGroupBox(AParent: TWinControl; const Cap: string; ALeft, ATop, AW, AH: Integer): TGroupBox; begin Result := TGroupBox.Create(Self); Result.Parent := AParent; Result.Caption := Cap; Result.SetBounds(ALeft, ATop, AW, AH); Result.Color := CLR_GROUP; Result.Font.Color := CLR_ACCENT; Result.Font.Name := 'Courier New'; Result.Font.Size := 8; end; // --------------------------------------------------------------------------- // BuildUI // --------------------------------------------------------------------------- procedure TSettingsForm.BuildUI; var i: Integer; begin FPageCtrl := TPageControl.Create(Self); FPageCtrl.Parent := Self; FPageCtrl.SetBounds(4, 4, Width - 12, Height - 48); FPageCtrl.Anchors := [akLeft, akTop, akRight, akBottom]; FPageCtrl.Color := CLR_BG; FPageCtrl.Font.Color := CLR_TEXT; FTabGeneral := FPageCtrl.AddTabSheet; FTabGeneral.Caption := 'General'; FTabAudio := FPageCtrl.AddTabSheet; FTabAudio.Caption := 'Audio'; FTabDisplay := FPageCtrl.AddTabSheet; FTabDisplay.Caption := 'Display'; FTabDSP := FPageCtrl.AddTabSheet; FTabDSP.Caption := 'DSP'; FTabTransmit := FPageCtrl.AddTabSheet; FTabTransmit.Caption := 'Transmit'; FTabPA := FPageCtrl.AddTabSheet; FTabPA.Caption := 'PA Settings'; FTabCAT := FPageCtrl.AddTabSheet; FTabCAT.Caption := 'CAT'; for i := 0 to FPageCtrl.PageCount - 1 do FPageCtrl.Pages[i].Color := CLR_BG; BuildAudioTab; BuildDisplayTab; BuildPATab; // Close button FBtnClose := TFlatButton.Create(Self); FBtnClose.Parent := Self; FBtnClose.Caption := 'CLOSE'; FBtnClose.SetBounds(Width - 90, Height - 38, 82, BTN_H); FBtnClose.Anchors := [akRight, akBottom]; FBtnClose.OnClick := BtnCloseClick; FBtnClose.ClrNorm := CLR_PANEL; FBtnClose.ClrBorder := TColor($00404040); FBtnClose.ClrHot := TColor($00303030); FBtnClose.ClrActive := TColor($00003300); FBtnClose.ClrText := CLR_TEXT; FBtnClose.ClrTextAct:= CLR_ACCENT; end; procedure TSettingsForm.BuildAudioTab; const R1 = 10; // top padding inside groups LW = 110; // label width var Grp: TGroupBox; procedure MkGrp(out G: TGroupBox; const Cap: string); begin G := TGroupBox.Create(Self); G.Parent := FTabAudio; G.Caption := Cap; G.Color := CLR_GROUP; G.Font.Color := CLR_ACCENT; G.Font.Name := 'Courier New'; G.Font.Size := 8; G.AutoSize := True; G.Align := alTop; end; procedure MkSep; var S: TPanel; begin S := TPanel.Create(Self); S.Parent := FTabAudio; S.BevelOuter := bvNone; S.Color := CLR_BG; S.Align := alTop; S.Height := 12; end; begin // ── Top padding ─────────────────────────────────────────────────────────── MkSep; // ── TX Audio Input ──────────────────────────────────────────────────────── MkGrp(Grp, 'TX Audio Input'); MakeLbl(Grp, 'Input Device:', 10, R1+4, LW); FCmbTXDev := MakeCombo(Grp, LW+14, R1, 440, OnTXDevChange); FCmbTXDev.Items.Add('None'); MkSep; // ── RX Audio Output ─────────────────────────────────────────────────────── MkGrp(Grp, 'RX Audio Output'); MakeLbl(Grp, 'Output Device:', 10, R1+4, LW); FCmbRXDev := MakeCombo(Grp, LW+14, R1, 440, OnRXDevChange); FCmbRXDev.Items.Add('Default'); end; procedure TSettingsForm.BuildDisplayTab; var i: Integer; begin FDispPageCtrl := TPageControl.Create(Self); FDispPageCtrl.Parent := FTabDisplay; FDispPageCtrl.Align := alClient; FDispPageCtrl.Color := CLR_BG; FDispPageCtrl.Font.Color := CLR_TEXT; FTabDispGeneral := FDispPageCtrl.AddTabSheet; FTabDispGeneral.Caption := 'General'; FTabDispRX1 := FDispPageCtrl.AddTabSheet; FTabDispRX1.Caption := 'RX1'; FTabDispRX2 := FDispPageCtrl.AddTabSheet; FTabDispRX2.Caption := 'RX2'; FTabDispTX := FDispPageCtrl.AddTabSheet; FTabDispTX.Caption := 'TX'; FTabDispGeneral.Color := CLR_BG; FTabDispRX1.Color := CLR_BG; FTabDispRX2.Color := CLR_BG; FTabDispTX.Color := CLR_BG; // ---- General sub-tab: visibility checkboxes ---- FChkShowSpectrum := TCheckBox.Create(Self); FChkShowSpectrum.Parent := FTabDispGeneral; FChkShowSpectrum.Caption := 'Show Spectrum'; FChkShowSpectrum.SetBounds(12, 16, 200, 22); FChkShowSpectrum.Checked := True; FChkShowSpectrum.Font.Color := CLR_TEXT; FChkShowSpectrum.Font.Name := 'Courier New'; FChkShowSpectrum.Font.Size := 8; FChkShowSpectrum.OnChange := OnVisibilityChkChange; FChkShowWaterfall := TCheckBox.Create(Self); FChkShowWaterfall.Parent := FTabDispGeneral; FChkShowWaterfall.Caption := 'Show Waterfall'; FChkShowWaterfall.SetBounds(12, 44, 200, 22); FChkShowWaterfall.Checked := True; FChkShowWaterfall.Font.Color := CLR_TEXT; FChkShowWaterfall.Font.Name := 'Courier New'; FChkShowWaterfall.Font.Size := 8; FChkShowWaterfall.OnChange := OnVisibilityChkChange; // FPS label + combo with TLabel.Create(Self) do begin Parent := FTabDispGeneral; Caption := 'Display FPS:'; SetBounds(12, 76, 100, 22); Font.Color := CLR_TEXTDIM; Font.Name := 'Courier New'; Font.Size := 8; end; FCmbFPS := MakeCombo(FTabDispGeneral, 116, 74, 100, OnFPSCmbChange); for i := 0 to High(FPS_NAMES) do FCmbFPS.Items.Add(FPS_NAMES[i]); FCmbFPS.ItemIndex := 3; // 20 fps default BuildRX1Tab; end; procedure TSettingsForm.BuildRX1Tab; const R1 = 12; LX = 12; LW = 112; CX = LX + LW + 8; STEP = 34; SEP = 12; GH_FFT = 100; GH_SPEC = 136; GH_WF = 170; GH_GRID = 100; var PnlFFT, PnlSpec, PnlWF: TPanel; Grp: TGroupBox; i: Integer; function MkGrp(AParent: TWinControl; const Cap: string; AH: Integer): TGroupBox; begin Result := TGroupBox.Create(Self); Result.Parent := AParent; Result.Caption := Cap; Result.Color := CLR_GROUP; Result.Font.Color := CLR_ACCENT; Result.Font.Name := 'Courier New'; Result.Font.Size := 8; Result.Align := alTop; Result.Height := AH; end; procedure MkSep(AParent: TWinControl; AH: Integer); var S: TPanel; begin S := TPanel.Create(Self); S.Parent := AParent; S.BevelOuter := bvNone; S.Color := CLR_BG; S.Align := alTop; S.Height := AH; end; function MkFloat(AParent: TWinControl; AX, AY, AW: Integer; AValue: Double; AMin, AMax, AInc: Double; AOnChange: TNotifyEvent): TFloatSpinEdit; begin Result := TFloatSpinEdit.Create(Self); Result.Parent := AParent; Result.SetBounds(AX, AY, AW, BTN_H); Result.Value := AValue; Result.MinValue := AMin; Result.MaxValue := AMax; Result.Increment := AInc; Result.DecimalPlaces := 1; Result.Color := CLR_PANEL; Result.Font.Color := CLR_TEXT; Result.Font.Name := 'Courier New'; Result.Font.Size := 8; Result.OnChange := AOnChange; end; begin MkSep(FTabDispRX1, 8); PnlFFT := TPanel.Create(Self); PnlFFT.Parent := FTabDispRX1; PnlFFT.BevelOuter := bvNone; PnlFFT.Color := CLR_BG; PnlFFT.Align := alTop; PnlFFT.Height := GH_FFT; Grp := MkGrp(PnlFFT, 'FFT / Analyzer', GH_FFT); MakeLbl(Grp, 'FFT Size:', LX, R1+4, LW); FCmbFFTSize := MakeCombo(Grp, CX, R1, 130, OnFFTSizeChange); for i := 0 to High(FFT_NAMES) do FCmbFFTSize.Items.Add(FFT_NAMES[i]); MakeLbl(Grp, 'Window:', LX, R1+STEP+4, LW); FCmbWindow := MakeCombo(Grp, CX, R1+STEP, 250, OnWindowChange); for i := 0 to High(WINDOW_NAMES) do FCmbWindow.Items.Add(WINDOW_NAMES[i]); MkSep(FTabDispRX1, SEP); PnlSpec := TPanel.Create(Self); PnlSpec.Parent := FTabDispRX1; PnlSpec.BevelOuter := bvNone; PnlSpec.Color := CLR_BG; PnlSpec.Align := alTop; PnlSpec.Height := GH_SPEC; Grp := MkGrp(PnlSpec, 'Spectrum', GH_SPEC); MakeLbl(Grp, 'Detector:', LX, R1+4, LW); FCmbSpecDet := MakeCombo(Grp, CX, R1, 172, OnSpecDetChange); for i := 0 to High(DETECTOR_NAMES) do FCmbSpecDet.Items.Add(DETECTOR_NAMES[i]); MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW); FCmbSpecAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnSpecAvgModeChange); for i := 0 to High(AVGMODE_NAMES) do FCmbSpecAvg.Items.Add(AVGMODE_NAMES[i]); MakeLbl(Grp, 'Avg Time (ms):', LX, R1+2*STEP+4, LW); FSpecAvgTime := MkFloat(Grp, CX, R1+2*STEP, 92, 120.0, 0.0, 5000.0, 10.0, OnSpecAvgTimeChange); MkSep(FTabDispRX1, SEP); PnlWF := TPanel.Create(Self); PnlWF.Parent := FTabDispRX1; PnlWF.BevelOuter := bvNone; PnlWF.Color := CLR_BG; PnlWF.Align := alTop; PnlWF.Height := GH_WF; Grp := MkGrp(PnlWF, 'Waterfall', GH_WF); MakeLbl(Grp, 'Detector:', LX, R1+4, LW); FCmbWfDet := MakeCombo(Grp, CX, R1, 172, OnWfDetChange); for i := 0 to High(DETECTOR_NAMES) do FCmbWfDet.Items.Add(DETECTOR_NAMES[i]); MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW); FCmbWfAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnWfAvgModeChange); for i := 0 to High(WF_AVGMODE_NAMES) do FCmbWfAvg.Items.Add(WF_AVGMODE_NAMES[i]); MakeLbl(Grp, 'Avg Time (ms):', LX, R1+2*STEP+4, LW); FWfAvgTime := MkFloat(Grp, CX, R1+2*STEP, 92, 120.0, 0.0, 5000.0, 10.0, OnWfAvgTimeChange); MakeLbl(Grp, 'High (dBm):', LX, R1+3*STEP+4, LW); FWfHigh := MkFloat(Grp, CX, R1+3*STEP, 92, -80.0, -160.0, 20.0, 1.0, OnWfThresholdChange); MakeLbl(Grp, 'Low (dBm):', CX+120, R1+3*STEP+4, 72); FWfLow := MkFloat(Grp, CX+196, R1+3*STEP, 92, -130.0, -180.0, 0.0, 1.0, OnWfThresholdChange); MakeLbl(Grp, 'AGC Offset:', LX, R1+4*STEP+4, LW); FWfAGCOffset := MkFloat(Grp, CX, R1+4*STEP, 92, 0.0, -40.0, 40.0, 0.5, OnWfThresholdChange); MkSep(FTabDispRX1, SEP); Grp := MkGrp(FTabDispRX1, 'Spectrum Grid', GH_GRID); MakeLbl(Grp, 'Ref Level (dBm):', LX, R1+4, 116); FEdRefLevel := TEdit.Create(Self); FEdRefLevel.Parent := Grp; FEdRefLevel.SetBounds(LX+120, R1, 80, BTN_H); FEdRefLevel.Color := CLR_PANEL; FEdRefLevel.Font.Color := CLR_TEXT; FEdRefLevel.Font.Name := 'Courier New'; FEdRefLevel.Font.Size := 8; FEdRefLevel.OnExit := OnRefLevelChange; FEdRefLevel.Text := '-20'; MakeLbl(Grp, 'Range (dB):', LX, R1+STEP+4, LW); FEdRange := TEdit.Create(Self); FEdRange.Parent := Grp; FEdRange.SetBounds(CX, R1+STEP, 80, BTN_H); FEdRange.Color := CLR_PANEL; FEdRange.Font.Color := CLR_TEXT; FEdRange.Font.Name := 'Courier New'; FEdRange.Font.Size := 8; FEdRange.OnExit := OnRangeChange; FEdRange.Text := '110'; MakeLbl(Grp, 'Grid Step:', CX+90, R1+STEP+4, 72); FCmbGridStep := MakeCombo(Grp, CX+90+76, R1+STEP, 80, OnGridStepChange); for i := 0 to High(GRID_STEP_NAMES) do FCmbGridStep.Items.Add(GRID_STEP_NAMES[i]); FCmbGridStep.ItemIndex := 1; end; procedure TSettingsForm.ApplyTheme; var i: Integer; begin Color := CLR_BG; for i := 0 to FPageCtrl.PageCount - 1 do FPageCtrl.Pages[i].Color := CLR_BG; FDispPageCtrl.Color := CLR_BG; for i := 0 to FDispPageCtrl.PageCount - 1 do FDispPageCtrl.Pages[i].Color := CLR_BG; end; // --------------------------------------------------------------------------- // LoadValues — заполняет элементы без fire событий // --------------------------------------------------------------------------- procedure TSettingsForm.LoadValues( FFTSize, WinType, SpecDet, SpecAvgMode: Integer; SpecAvgTimeMS: Double; WfDet, WfAvgMode: Integer; WfAvgTimeMS, WfHigh, WfLow, WfAGCOffset: Double; RefLevel, Range, GridStep: Double; const AudioOutDevName: string; const AudioInDevName: string); var i, Idx: Integer; begin FLoading := True; try // FFT Size Idx := 0; // default 4096 for i := 0 to High(FFT_SIZES) do if FFT_SIZES[i] = FFTSize then begin Idx := i; Break; end; FCmbFFTSize.ItemIndex := Idx; // Window Type if (WinType >= 0) and (WinType < FCmbWindow.Items.Count) then FCmbWindow.ItemIndex := WinType else FCmbWindow.ItemIndex := 2; // Hann // Spectrum Detector if (SpecDet >= 0) and (SpecDet < FCmbSpecDet.Items.Count) then FCmbSpecDet.ItemIndex := SpecDet else FCmbSpecDet.ItemIndex := 0; // Spectrum Avg Mode (WDSP value → combo index) Idx := 0; // default None for i := 0 to High(AVGMODE_VALUES) do if AVGMODE_VALUES[i] = SpecAvgMode then begin Idx := i; Break; end; FCmbSpecAvg.ItemIndex := Idx; FSpecAvgTime.Value := EnsureRange(SpecAvgTimeMS, 0.0, 5000.0); // Waterfall Detector if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then FCmbWfDet.ItemIndex := WfDet else FCmbWfDet.ItemIndex := 2; // Average // Waterfall Avg Mode Idx := 1; // default Recursive for i := 0 to High(WF_AVGMODE_VALUES) do if WF_AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end; FCmbWfAvg.ItemIndex := Idx; FWfAvgTime.Value := EnsureRange(WfAvgTimeMS, 0.0, 5000.0); FWfHigh.Value := EnsureRange(WfHigh, -160.0, 20.0); FWfLow.Value := EnsureRange(WfLow, -180.0, 0.0); FWfAGCOffset.Value := EnsureRange(WfAGCOffset, -40.0, 40.0); // Grid FEdRefLevel.Text := FormatFloat('0.#', RefLevel); FEdRange.Text := FormatFloat('0.#', Range); Idx := 1; // 10 dB default for i := 0 to High(GRID_STEPS) do if Abs(GRID_STEPS[i] - GridStep) < 0.5 then begin Idx := i; Break; end; FCmbGridStep.ItemIndex := Idx; // RX Audio Output if AudioOutDevName = '' then FCmbRXDev.ItemIndex := 0 // 'Default' else begin Idx := FCmbRXDev.Items.IndexOf(AudioOutDevName); if Idx >= 0 then FCmbRXDev.ItemIndex := Idx else FCmbRXDev.ItemIndex := 0; end; // TX Audio Input if AudioInDevName = '' then FCmbTXDev.ItemIndex := 0 // 'None' else begin Idx := FCmbTXDev.Items.IndexOf(AudioInDevName); if Idx >= 0 then FCmbTXDev.ItemIndex := Idx else FCmbTXDev.ItemIndex := 0; end; finally FLoading := False; end; end; // --------------------------------------------------------------------------- // RefreshAudioDevices // --------------------------------------------------------------------------- procedure TSettingsForm.RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput); var Names: TStringList; Cnt, i: Integer; OldRXSel: string; OldTXSel: string; begin Names := TStringList.Create; try // ── RX Output devices ───────────────────────────────────────────────── if AudioOut <> nil then begin OldRXSel := FCmbRXDev.Text; FCmbRXDev.Items.Clear; FCmbRXDev.Items.Add('Default'); FRXDevCount := 0; Cnt := 0; Names.Clear; if AudioOut.EnumOutputDevices(Names, FRXDevIndices, Cnt) then begin FRXDevCount := Cnt; for i := 0 to Cnt - 1 do FCmbRXDev.Items.Add(Names[i]); end; i := FCmbRXDev.Items.IndexOf(OldRXSel); if i >= 0 then FCmbRXDev.ItemIndex := i else FCmbRXDev.ItemIndex := 0; end; // ── TX Input devices ────────────────────────────────────────────────── if AudioIn <> nil then begin OldTXSel := FCmbTXDev.Text; FCmbTXDev.Items.Clear; FCmbTXDev.Items.Add('None'); FTXDevCount := 0; Cnt := 0; Names.Clear; if AudioIn.EnumInputDevices(Names, FTXDevIndices, Cnt) then begin FTXDevCount := Cnt; for i := 0 to Cnt - 1 do FCmbTXDev.Items.Add(Names[i]); end; i := FCmbTXDev.Items.IndexOf(OldTXSel); if i >= 0 then FCmbTXDev.ItemIndex := i else FCmbTXDev.ItemIndex := 0; end; finally Names.Free; end; end; // --------------------------------------------------------------------------- // Value accessors // --------------------------------------------------------------------------- function TSettingsForm.GetFFTSize: Integer; var Idx: Integer; begin Idx := FCmbFFTSize.ItemIndex; if (Idx >= 0) and (Idx <= High(FFT_SIZES)) then Result := FFT_SIZES[Idx] else Result := 4096; end; function TSettingsForm.GetSpecAvgTimeMS: Double; begin Result := EnsureRange(FSpecAvgTime.Value, 0.0, 5000.0); end; function TSettingsForm.GetWfAvgTimeMS: Double; begin Result := EnsureRange(FWfAvgTime.Value, 0.0, 5000.0); end; function TSettingsForm.GetWfHigh: Double; begin Result := EnsureRange(FWfHigh.Value, -160.0, 20.0); end; function TSettingsForm.GetWfLow: Double; begin Result := EnsureRange(FWfLow.Value, -180.0, 0.0); end; function TSettingsForm.GetWfAGCOffset: Double; begin Result := EnsureRange(FWfAGCOffset.Value, -40.0, 40.0); end; function TSettingsForm.GetRefLevel: Double; var V: Double; Err: Integer; begin Val(FEdRefLevel.Text, V, Err); if Err <> 0 then V := -20.0; Result := EnsureRange(V, -200.0, 0.0); end; function TSettingsForm.GetRange: Double; var V: Double; Err: Integer; begin Val(FEdRange.Text, V, Err); if Err <> 0 then V := 110.0; Result := EnsureRange(V, 10.0, 200.0); end; function TSettingsForm.GetGridStep: Double; var Idx: Integer; begin Idx := FCmbGridStep.ItemIndex; if (Idx >= 0) and (Idx <= High(GRID_STEPS)) then Result := GRID_STEPS[Idx] else Result := 10.0; end; // --------------------------------------------------------------------------- // Fire helpers // --------------------------------------------------------------------------- procedure TSettingsForm.FireDisplayChange; var AvgIdx: Integer; AvgVal: Integer; begin if FLoading then Exit; if not Assigned(FOnDisplayChange) then Exit; AvgIdx := FCmbSpecAvg.ItemIndex; if (AvgIdx >= 0) and (AvgIdx <= High(AVGMODE_VALUES)) then AvgVal := AVGMODE_VALUES[AvgIdx] else AvgVal := 3; FOnDisplayChange( GetFFTSize, FCmbWindow.ItemIndex, FCmbSpecDet.ItemIndex, AvgVal, GetSpecAvgTimeMS); end; procedure TSettingsForm.FireWaterfallChange; var AvgIdx: Integer; AvgVal: Integer; begin if FLoading then Exit; if not Assigned(FOnWaterfallChange) then Exit; AvgIdx := FCmbWfAvg.ItemIndex; if (AvgIdx >= 0) and (AvgIdx <= High(WF_AVGMODE_VALUES)) then AvgVal := WF_AVGMODE_VALUES[AvgIdx] else AvgVal := 1; FOnWaterfallChange( FCmbWfDet.ItemIndex, AvgVal, GetWfAvgTimeMS, GetWfHigh, GetWfLow, GetWfAGCOffset); end; procedure TSettingsForm.FireGridChange; begin if FLoading then Exit; if not Assigned(FOnGridChange) then Exit; FOnGridChange(GetRefLevel, GetRange, GetGridStep); end; // --------------------------------------------------------------------------- // Event handlers // --------------------------------------------------------------------------- procedure TSettingsForm.OnFFTSizeChange(Sender: TObject); begin FireDisplayChange; end; procedure TSettingsForm.OnWindowChange(Sender: TObject); begin FireDisplayChange; end; procedure TSettingsForm.OnSpecDetChange(Sender: TObject); begin FireDisplayChange; end; procedure TSettingsForm.OnSpecAvgModeChange(Sender: TObject); begin FireDisplayChange; end; procedure TSettingsForm.OnSpecAvgTimeChange(Sender: TObject); begin FireDisplayChange; end; procedure TSettingsForm.OnWfDetChange(Sender: TObject); begin FireWaterfallChange; end; procedure TSettingsForm.OnWfAvgModeChange(Sender: TObject); begin FireWaterfallChange; end; procedure TSettingsForm.OnWfAvgTimeChange(Sender: TObject); begin FireWaterfallChange; end; procedure TSettingsForm.OnWfThresholdChange(Sender: TObject); begin FireWaterfallChange; end; procedure TSettingsForm.OnRefLevelChange(Sender: TObject); begin FireGridChange; end; procedure TSettingsForm.OnRangeChange(Sender: TObject); begin FireGridChange; end; procedure TSettingsForm.OnGridStepChange(Sender: TObject); begin FireGridChange; end; procedure TSettingsForm.OnRXDevChange(Sender: TObject); var Idx: Integer; DevIdx: Integer; DevName: string; begin if FLoading then Exit; if not Assigned(FOnAudioDevChange) then Exit; Idx := FCmbRXDev.ItemIndex; if Idx <= 0 then begin // "Default" FOnAudioDevChange(-1, ''); end else begin DevName := FCmbRXDev.Items[Idx]; // Map combo index to PA device index // Combo: 0=Default, 1..N = real devices DevIdx := Idx - 1; if (DevIdx >= 0) and (DevIdx < FRXDevCount) then FOnAudioDevChange(FRXDevIndices[DevIdx], DevName) else FOnAudioDevChange(-1, ''); end; end; procedure TSettingsForm.OnTXDevChange(Sender: TObject); var Idx: Integer; DevIdx: Integer; DevName: string; begin if FLoading then Exit; if not Assigned(FOnAudioInDevChange) then Exit; Idx := FCmbTXDev.ItemIndex; if Idx <= 0 then begin // 'None' FOnAudioInDevChange(-1, ''); end else begin DevName := FCmbTXDev.Items[Idx]; DevIdx := Idx - 1; if (DevIdx >= 0) and (DevIdx < FTXDevCount) then FOnAudioInDevChange(FTXDevIndices[DevIdx], DevName) else FOnAudioInDevChange(-1, ''); end; end; procedure TSettingsForm.LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean); begin FLoading := True; try FChkShowSpectrum.Checked := ShowSpectrum; FChkShowWaterfall.Checked := ShowWaterfall; finally FLoading := False; end; end; procedure TSettingsForm.OnVisibilityChkChange(Sender: TObject); begin if FLoading then Exit; if not Assigned(FOnVisibilityChange) then Exit; FOnVisibilityChange(FChkShowSpectrum.Checked, FChkShowWaterfall.Checked); end; procedure TSettingsForm.OnFPSCmbChange(Sender: TObject); var Idx: Integer; begin if FLoading then Exit; if not Assigned(FOnFPSChange) then Exit; Idx := FCmbFPS.ItemIndex; if (Idx >= 0) and (Idx <= High(FPS_VALUES)) then FOnFPSChange(FPS_VALUES[Idx]); end; procedure TSettingsForm.LoadFPS(FPS: Integer); var i, Idx: Integer; begin FLoading := True; try Idx := 3; // 20 fps default for i := 0 to High(FPS_VALUES) do if FPS_VALUES[i] = FPS then begin Idx := i; Break; end; FCmbFPS.ItemIndex := Idx; finally FLoading := False; end; end; // --------------------------------------------------------------------------- // BuildPATab — вкладка PA Settings: MaxPower + per-band calibration // --------------------------------------------------------------------------- procedure TSettingsForm.BuildPATab; const LBL_W = 46; // ширина метки диапазона ED_W = 72; // ширина SpinEdit (с кнопками) COL_W = LBL_W + ED_W + 8; // ширина одной пары COLS = 4; // колонок на строку ROW_H = 32; // высота строки (spin edits чуть выше plain edit) GRP_PAD = 12; // отступ внутри группы GRP_TOP = 24; // высота caption группы ROWS = 3; // строк для 11 диапазонов (ceil(11/4)) function MakeFloatSpin(AParent: TWinControl; ALeft, ATop: Integer; AChange: TNotifyEvent): TFloatSpinEdit; var E: TFloatSpinEdit; begin E := TFloatSpinEdit.Create(Self); E.Parent := AParent; E.SetBounds(ALeft, ATop, ED_W, BTN_H + 2); E.Color := CLR_PANEL; E.Font.Color := CLR_TEXT; E.Font.Name := 'Courier New'; E.Font.Size := 8; E.MinValue := 38.8; E.MaxValue := 100.0; E.Increment := 0.1; E.DecimalPlaces := 1; E.Value := 100.0; E.OnChange := AChange; Result := E; end; var Grp: TGroupBox; Sep: TPanel; Lbl: TLabel; Spin: TSpinEdit; i, r, c, X, Y: Integer; begin // ── Top padding ─────────────────────────────────────────────────────────── Sep := TPanel.Create(Self); Sep.Parent := FTabPA; Sep.BevelOuter := bvNone; Sep.Color := CLR_BG; Sep.Align := alTop; Sep.Height := 10; // ── Group Power & Calibration ───────────────────────────────────────────── Grp := TGroupBox.Create(Self); Grp.Parent := FTabPA; Grp.Caption := 'Power & Calibration'; Grp.Color := CLR_GROUP; Grp.Font.Color := CLR_ACCENT; Grp.Font.Name := 'Courier New'; Grp.Font.Size := 8; Grp.Align := alTop; Grp.Height := GRP_TOP + ROW_H + 20 + ROWS * ROW_H + GRP_PAD + 16; Grp.AutoSize := False; // Max Power строка Lbl := TLabel.Create(Self); Lbl.Parent := Grp; Lbl.Caption := 'Max Power (W):'; Lbl.SetBounds(GRP_PAD, GRP_TOP + 6, 120, 18); Lbl.Font.Color := CLR_TEXTDIM; Lbl.Font.Name := 'Courier New'; Lbl.Font.Size := 8; Spin := TSpinEdit.Create(Self); Spin.Parent := Grp; Spin.SetBounds(GRP_PAD + 124, GRP_TOP, ED_W, BTN_H + 2); Spin.Color := CLR_PANEL; Spin.Font.Color := CLR_TEXT; Spin.Font.Name := 'Courier New'; Spin.Font.Size := 8; Spin.MinValue := 5; Spin.MaxValue := 200; Spin.Increment := 1; Spin.Value := 100; Spin.OnChange := OnMaxPowerChange; FEdMaxPower := Spin; // Метка для калибровки Lbl := TLabel.Create(Self); Lbl.Parent := Grp; Lbl.Caption := 'Calibration per band (38.8 ... 100.0):'; Lbl.SetBounds(GRP_PAD, GRP_TOP + ROW_H + 8, 280, 18); Lbl.Font.Color := CLR_TEXTDIM; Lbl.Font.Name := 'Courier New'; Lbl.Font.Size := 8; // Grid 4 columns × 3 rows for 11 bands for i := 0 to 10 do begin r := i div COLS; c := i mod COLS; X := GRP_PAD + c * (COL_W + 4); Y := GRP_TOP + ROW_H + 26 + r * ROW_H; Lbl := TLabel.Create(Self); Lbl.Parent := Grp; Lbl.Caption := PA_BAND_NAMES[i] + ':'; Lbl.SetBounds(X, Y + 5, LBL_W, 18); Lbl.Font.Color := CLR_TEXTDIM; Lbl.Font.Name := 'Courier New'; Lbl.Font.Size := 8; FEdBandCal[i] := MakeFloatSpin(Grp, X + LBL_W, Y, OnBandCalChange); FEdBandCal[i].Tag := i; end; end; procedure TSettingsForm.LoadPASettings(MaxPower: Double; const BandCal: array of Double); var i, n: Integer; begin FLoading := True; try FEdMaxPower.Value := Round(EnsureRange(MaxPower, 5.0, 200.0)); n := Min(10, High(BandCal)); for i := 0 to n do FEdBandCal[i].Value := EnsureRange(BandCal[i], 38.8, 100.0); finally FLoading := False; end; end; procedure TSettingsForm.OnMaxPowerChange(Sender: TObject); var BandCal: array[0..10] of Double; i: Integer; begin if FLoading then Exit; if not Assigned(FOnPAChange) then Exit; for i := 0 to 10 do BandCal[i] := FEdBandCal[i].Value; FOnPAChange(FEdMaxPower.Value, BandCal); end; procedure TSettingsForm.OnBandCalChange(Sender: TObject); var BandCal: array[0..10] of Double; i: Integer; begin if FLoading then Exit; if not Assigned(FOnPAChange) then Exit; for i := 0 to 10 do BandCal[i] := FEdBandCal[i].Value; FOnPAChange(FEdMaxPower.Value, BandCal); end; procedure TSettingsForm.BtnCloseClick(Sender: TObject); begin Close; end; end.