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, 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 // ---- Layout ---- FNavPanel: TPanel; FContentPanel: TPanel; FPageAudio: TScrollBox; FPageDisplay: TScrollBox; FPageSpectrum: TScrollBox; FPageWaterfall: TScrollBox; FPagePA: TScrollBox; FPageAdvanced: TScrollBox; FNavAudio: TFlatButton; FNavDisplay: TFlatButton; FNavSpectrum: TFlatButton; FNavWaterfall: TFlatButton; FNavPA: TFlatButton; FNavAdvanced: TFlatButton; // ---- Audio tab controls ---- FCmbRXDev: TComboBox; FRXDevIndices: array[0..63] of Integer; FRXDevCount: Integer; FCmbTXDev: TComboBox; FTXDevIndices: array[0..63] of Integer; FTXDevCount: Integer; // ---- RX1 sub-tab controls ---- FCmbFFTSize: TComboBox; FCmbWindow: TComboBox; FCmbSpecDet: TComboBox; FCmbSpecAvg: TComboBox; FSpecAvgTime: TFloatSpinEdit; FCmbWfDet: TComboBox; FCmbWfAvg: TComboBox; FWfAvgTime: TFloatSpinEdit; FWfHigh: TFloatSpinEdit; FWfLow: TFloatSpinEdit; FWfAGCOffset: TFloatSpinEdit; 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; FLoading: Boolean; procedure BuildUI; procedure BuildAudioTab; procedure BuildDisplayTab; procedure BuildRX1Tab; procedure BuildPATab; procedure ApplyTheme; procedure NavClick(Sender: TObject); procedure SelectPage(APage: TWinControl; ANav: TFlatButton); function MakeLbl(AParent: TWinControl; const Cap: string; ALeft, ATop, AW: Integer): TLabel; function MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer; OnChange: TNotifyEvent): TComboBox; function MakeGroupPanel(AParent: TWinControl; const Cap: string; ALeft, ATop, AW, AH: Integer): TPanel; function MakeScrollPage: TScrollBox; function MakeNavButton(const Cap: string; ATop: Integer): TFlatButton; 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; 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); procedure RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput); procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean); procedure LoadFPS(FPS: Integer); 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_TEXT = TColor($00E0E0E0); CLR_TEXTDIM = TColor($00888888); CLR_BORDER = TColor($00303030); CLR_ACCENT = TColor($0040FF80); CLR_INPUT = TColor($00FFFFFF); CLR_INPUT_TEXT = TColor($00202020); {$IFDEF MSWINDOWS} UI_FONT = 'Segoe UI'; {$ELSE} UI_FONT = 'Sans'; {$ENDIF} BTN_H = 24; GRP_TITLE_H = 22; // высота заголовка в group-панелях (title label + divider) 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 := 900; Height := 660; Position := poScreenCenter; BorderStyle := bsSizeable; Color := CLR_BG; Font.Name := UI_FONT; Font.Size := 9; 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 := UI_FONT; Result.Font.Size := 9; 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_INPUT; Result.Font.Color := CLR_INPUT_TEXT; Result.Font.Name := UI_FONT; Result.Font.Size := 9; Result.OnChange := OnChange; end; // Создаёт панель-группу с заголовком и разделителем (вместо TGroupBox). // На Windows TGroupBox рисует фрейм системными цветами, TPanel — нет. function TSettingsForm.MakeGroupPanel(AParent: TWinControl; const Cap: string; ALeft, ATop, AW, AH: Integer): TPanel; var TitleLbl: TLabel; Divider: TPanel; begin Result := TPanel.Create(Self); Result.Parent := AParent; Result.SetBounds(ALeft, ATop, AW, AH); Result.BevelOuter := bvNone; Result.Color := CLR_PANEL; TitleLbl := TLabel.Create(Self); TitleLbl.Parent := Result; TitleLbl.Caption := Cap; TitleLbl.SetBounds(8, 5, AW - 16, 14); TitleLbl.Font.Color := CLR_ACCENT; TitleLbl.Font.Name := UI_FONT; TitleLbl.Font.Size := 9; TitleLbl.Font.Style := [fsBold]; Divider := TPanel.Create(Self); Divider.Parent := Result; Divider.SetBounds(0, GRP_TITLE_H, AW + 200, 1); Divider.BevelOuter := bvNone; Divider.Color := CLR_BORDER; end; function TSettingsForm.MakeScrollPage: TScrollBox; begin Result := TScrollBox.Create(Self); Result.Parent := FContentPanel; Result.Align := alClient; Result.BorderStyle := bsNone; Result.Color := CLR_BG; Result.AutoScroll := True; Result.Visible := False; end; function TSettingsForm.MakeNavButton(const Cap: string; ATop: Integer): TFlatButton; begin Result := TFlatButton.Create(Self); Result.Parent := FNavPanel; Result.Caption := Cap; Result.SetBounds(10, ATop, 166, 32); Result.Anchors := [akLeft, akTop, akRight]; Result.OnClick := NavClick; Result.ClrNorm := CLR_PANEL; Result.ClrBorder := CLR_PANEL; Result.ClrHot := TColor($00242424); Result.ClrActive := TColor($00204030); Result.ClrText := CLR_TEXT; Result.ClrTextAct := CLR_ACCENT; Result.Font.Name := UI_FONT; Result.Font.Size := 9; Result.Font.Style := []; end; // --------------------------------------------------------------------------- // BuildUI // --------------------------------------------------------------------------- procedure TSettingsForm.BuildUI; begin FNavPanel := TPanel.Create(Self); FNavPanel.Parent := Self; FNavPanel.Align := alLeft; FNavPanel.Width := 188; FNavPanel.BevelOuter := bvNone; FNavPanel.Color := TColor($00151515); with TLabel.Create(Self) do begin Parent := FNavPanel; Caption := 'Settings'; SetBounds(14, 14, 160, 24); Font.Name := UI_FONT; Font.Size := 14; Font.Style := [fsBold]; Font.Color := CLR_TEXT; end; with TLabel.Create(Self) do begin Parent := FNavPanel; Caption := 'EWSDR'; SetBounds(14, 39, 160, 18); Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM; end; FNavAudio := MakeNavButton('Audio', 74); FNavDisplay := MakeNavButton('Display', 110); FNavSpectrum := MakeNavButton('Spectrum', 146); FNavWaterfall := MakeNavButton('Waterfall', 182); FNavPA := MakeNavButton('Power Amplifier', 218); FNavAdvanced := MakeNavButton('Advanced', 254); FContentPanel := TPanel.Create(Self); FContentPanel.Parent := Self; FContentPanel.Align := alClient; FContentPanel.BevelOuter := bvNone; FContentPanel.Color := CLR_BG; FPageAudio := MakeScrollPage; FPageDisplay := MakeScrollPage; FPageSpectrum := MakeScrollPage; FPageWaterfall := MakeScrollPage; FPagePA := MakeScrollPage; FPageAdvanced := MakeScrollPage; BuildAudioTab; BuildDisplayTab; BuildRX1Tab; BuildPATab; FBtnClose := TFlatButton.Create(Self); FBtnClose.Parent := Self; FBtnClose.Caption := 'Close'; FBtnClose.SetBounds(Width - 106, Height - 40, 92, 28); 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; FBtnClose.Font.Name := UI_FONT; FBtnClose.Font.Size := 9; SelectPage(FPageAudio, FNavAudio); end; procedure TSettingsForm.NavClick(Sender: TObject); begin if Sender = FNavAudio then SelectPage(FPageAudio, FNavAudio) else if Sender = FNavDisplay then SelectPage(FPageDisplay, FNavDisplay) else if Sender = FNavSpectrum then SelectPage(FPageSpectrum, FNavSpectrum) else if Sender = FNavWaterfall then SelectPage(FPageWaterfall, FNavWaterfall) else if Sender = FNavPA then SelectPage(FPagePA, FNavPA) else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced); end; procedure TSettingsForm.SelectPage(APage: TWinControl; ANav: TFlatButton); begin FPageAudio.Visible := APage = FPageAudio; FPageDisplay.Visible := APage = FPageDisplay; FPageSpectrum.Visible := APage = FPageSpectrum; FPageWaterfall.Visible := APage = FPageWaterfall; FPagePA.Visible := APage = FPagePA; FPageAdvanced.Visible := APage = FPageAdvanced; FNavAudio.Active := ANav = FNavAudio; FNavDisplay.Active := ANav = FNavDisplay; FNavSpectrum.Active := ANav = FNavSpectrum; FNavWaterfall.Active := ANav = FNavWaterfall; FNavPA.Active := ANav = FNavPA; FNavAdvanced.Active := ANav = FNavAdvanced; end; // --------------------------------------------------------------------------- // BuildAudioTab // --------------------------------------------------------------------------- procedure TSettingsForm.BuildAudioTab; const MARGIN = 22; PAD = 18; R1 = 42; STEP = 42; LW = 150; CX = PAD + LW + 12; CMB_W = 460; var Grp: TPanel; begin with TLabel.Create(Self) do begin Parent := FPageAudio; Caption := 'Audio'; SetBounds(MARGIN, 18, 400, 28); Font.Name := UI_FONT; Font.Size := 16; Font.Style := [fsBold]; Font.Color := CLR_TEXT; end; with TLabel.Create(Self) do begin Parent := FPageAudio; Caption := 'Input and output devices used by the receiver and transmitter.'; SetBounds(MARGIN, 48, 560, 20); Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM; end; Grp := MakeGroupPanel(FPageAudio, 'Audio Devices', MARGIN, 86, 650, 142); MakeLbl(Grp, 'TX input device', PAD, R1 + 5, LW); FCmbTXDev := MakeCombo(Grp, CX, R1, CMB_W, OnTXDevChange); FCmbTXDev.Items.Add('None'); MakeLbl(Grp, 'RX output device', PAD, R1 + STEP + 5, LW); FCmbRXDev := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnRXDevChange); FCmbRXDev.Items.Add('Default'); end; // --------------------------------------------------------------------------- // BuildDisplayTab // --------------------------------------------------------------------------- procedure TSettingsForm.BuildDisplayTab; const MARGIN = 22; PAD = 18; R1 = 42; CHKW = 220; LW = 150; CX = PAD + LW + 12; var i: Integer; Grp: TPanel; begin with TLabel.Create(Self) do begin Parent := FPageDisplay; Caption := 'Display'; SetBounds(MARGIN, 18, 400, 28); Font.Name := UI_FONT; Font.Size := 16; Font.Style := [fsBold]; Font.Color := CLR_TEXT; end; with TLabel.Create(Self) do begin Parent := FPageDisplay; Caption := 'General visual options. Spectrum and waterfall processing are split into their own pages.'; SetBounds(MARGIN, 48, 680, 20); Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM; end; Grp := MakeGroupPanel(FPageDisplay, 'Visible Panes', MARGIN, 86, 650, 86); FChkShowSpectrum := TCheckBox.Create(Self); FChkShowSpectrum.Parent := Grp; FChkShowSpectrum.Caption := 'Spectrum'; FChkShowSpectrum.SetBounds(PAD, R1, CHKW, 22); FChkShowSpectrum.Checked := True; FChkShowSpectrum.Font.Color := CLR_TEXT; FChkShowSpectrum.Font.Name := UI_FONT; FChkShowSpectrum.Font.Size := 9; FChkShowSpectrum.OnChange := OnVisibilityChkChange; FChkShowWaterfall := TCheckBox.Create(Self); FChkShowWaterfall.Parent := Grp; FChkShowWaterfall.Caption := 'Waterfall'; FChkShowWaterfall.SetBounds(PAD + CHKW + 20, R1, CHKW, 22); FChkShowWaterfall.Checked := True; FChkShowWaterfall.Font.Color := CLR_TEXT; FChkShowWaterfall.Font.Name := UI_FONT; FChkShowWaterfall.Font.Size := 9; FChkShowWaterfall.OnChange := OnVisibilityChkChange; Grp := MakeGroupPanel(FPageDisplay, 'Performance', MARGIN, 186, 650, 92); MakeLbl(Grp, 'Refresh rate', PAD, R1 + 5, LW); FCmbFPS := MakeCombo(Grp, CX, R1, 140, OnFPSCmbChange); for i := 0 to High(FPS_NAMES) do FCmbFPS.Items.Add(FPS_NAMES[i]); FCmbFPS.ItemIndex := 8; end; // --------------------------------------------------------------------------- // BuildRX1Tab // --------------------------------------------------------------------------- procedure TSettingsForm.BuildRX1Tab; const MARGIN = 22; PAD = 18; LW = 150; CX = PAD + LW + 12; CMB_W = 230; CMB_W_LONG = 300; SPW = 96; EDT_W = 96; STEP = 42; R1 = 42; var Grp: TPanel; i: Integer; function MkFloat(AX, AY, AW: Integer; AValue, AMin, AMax, AInc: Double; AOnChange: TNotifyEvent): TFloatSpinEdit; begin Result := TFloatSpinEdit.Create(Self); Result.Parent := Grp; 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_INPUT; Result.Font.Color := CLR_INPUT_TEXT; Result.Font.Name := UI_FONT; Result.Font.Size := 9; Result.OnChange := AOnChange; end; function MkEdit(AX, AY, AW: Integer; const AText: string; AOnExit: TNotifyEvent): TEdit; begin Result := TEdit.Create(Self); Result.Parent := Grp; Result.SetBounds(AX, AY, AW, BTN_H); Result.Color := CLR_INPUT; Result.Font.Color := CLR_INPUT_TEXT; Result.Font.Name := UI_FONT; Result.Font.Size := 9; Result.Text := AText; Result.OnExit := AOnExit; end; begin with TLabel.Create(Self) do begin Parent := FPageSpectrum; Caption := 'Spectrum'; SetBounds(MARGIN, 18, 400, 28); Font.Name := UI_FONT; Font.Size := 16; Font.Style := [fsBold]; Font.Color := CLR_TEXT; end; with TLabel.Create(Self) do begin Parent := FPageSpectrum; Caption := 'Analyzer, detector, averaging and grid parameters for RX1.'; SetBounds(MARGIN, 48, 620, 20); Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM; end; Grp := MakeGroupPanel(FPageSpectrum, 'Analyzer', MARGIN, 86, 650, 128); MakeLbl(Grp, 'FFT Size:', PAD, R1 + 5, LW); FCmbFFTSize := MakeCombo(Grp, CX, R1, CMB_W, OnFFTSizeChange); for i := 0 to High(FFT_NAMES) do FCmbFFTSize.Items.Add(FFT_NAMES[i]); MakeLbl(Grp, 'Window:', PAD, R1 + STEP + 5, LW); FCmbWindow := MakeCombo(Grp, CX, R1 + STEP, CMB_W_LONG, OnWindowChange); for i := 0 to High(WINDOW_NAMES) do FCmbWindow.Items.Add(WINDOW_NAMES[i]); Grp := MakeGroupPanel(FPageSpectrum, 'Detection && Averaging', MARGIN, 230, 650, 170); MakeLbl(Grp, 'Detector:', PAD, R1 + 5, LW); FCmbSpecDet := MakeCombo(Grp, CX, R1, CMB_W, OnSpecDetChange); for i := 0 to High(DETECTOR_NAMES) do FCmbSpecDet.Items.Add(DETECTOR_NAMES[i]); MakeLbl(Grp, 'Avg Mode:', PAD, R1 + STEP + 5, LW); FCmbSpecAvg := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnSpecAvgModeChange); for i := 0 to High(AVGMODE_NAMES) do FCmbSpecAvg.Items.Add(AVGMODE_NAMES[i]); MakeLbl(Grp, 'Avg Time (ms):', PAD, R1 + 2 * STEP + 5, LW); FSpecAvgTime := MkFloat(CX, R1 + 2 * STEP, SPW, 30.0, 0.0, 5000.0, 10.0, OnSpecAvgTimeChange); Grp := MakeGroupPanel(FPageSpectrum, 'Grid', MARGIN, 416, 650, 170); MakeLbl(Grp, 'Reference level:', PAD, R1 + 5, LW); FEdRefLevel := MkEdit(CX, R1, EDT_W, '-20', OnRefLevelChange); MakeLbl(Grp, 'Range (dB):', PAD, R1 + STEP + 5, LW); FEdRange := MkEdit(CX, R1 + STEP, EDT_W, '110', OnRangeChange); MakeLbl(Grp, 'Grid step:', PAD, R1 + 2 * STEP + 5, LW); FCmbGridStep := MakeCombo(Grp, CX, R1 + 2 * STEP, 130, OnGridStepChange); for i := 0 to High(GRID_STEP_NAMES) do FCmbGridStep.Items.Add(GRID_STEP_NAMES[i]); FCmbGridStep.ItemIndex := 1; with TLabel.Create(Self) do begin Parent := FPageWaterfall; Caption := 'Waterfall'; SetBounds(MARGIN, 18, 400, 28); Font.Name := UI_FONT; Font.Size := 16; Font.Style := [fsBold]; Font.Color := CLR_TEXT; end; with TLabel.Create(Self) do begin Parent := FPageWaterfall; Caption := 'Waterfall detector, smoothing and color scale thresholds.'; SetBounds(MARGIN, 48, 620, 20); Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM; end; Grp := MakeGroupPanel(FPageWaterfall, 'Detection && Averaging', MARGIN, 86, 650, 170); MakeLbl(Grp, 'Detector:', PAD, R1 + 5, LW); FCmbWfDet := MakeCombo(Grp, CX, R1, CMB_W, OnWfDetChange); for i := 0 to High(DETECTOR_NAMES) do FCmbWfDet.Items.Add(DETECTOR_NAMES[i]); MakeLbl(Grp, 'Avg Mode:', PAD, R1 + STEP + 5, LW); FCmbWfAvg := MakeCombo(Grp, CX, R1 + STEP, CMB_W, OnWfAvgModeChange); for i := 0 to High(WF_AVGMODE_NAMES) do FCmbWfAvg.Items.Add(WF_AVGMODE_NAMES[i]); MakeLbl(Grp, 'Avg Time (ms):', PAD, R1 + 2 * STEP + 5, LW); FWfAvgTime := MkFloat(CX, R1 + 2 * STEP, SPW, 120.0, 0.0, 5000.0, 10.0, OnWfAvgTimeChange); Grp := MakeGroupPanel(FPageWaterfall, 'Color Scale', MARGIN, 272, 650, 170); MakeLbl(Grp, 'High (dBm):', PAD, R1 + 5, LW); FWfHigh := MkFloat(CX, R1, SPW, -80.0, -160.0, 20.0, 1.0, OnWfThresholdChange); MakeLbl(Grp, 'Low (dBm):', PAD, R1 + STEP + 5, LW); FWfLow := MkFloat(CX, R1 + STEP, SPW, -130.0, -180.0, 0.0, 1.0, OnWfThresholdChange); MakeLbl(Grp, 'AGC Offset (dB):', PAD, R1 + 2 * STEP + 5, LW); FWfAGCOffset := MkFloat(CX, R1 + 2 * STEP, SPW, 0.0, -40.0, 40.0, 0.5, OnWfThresholdChange); end; // --------------------------------------------------------------------------- // BuildPATab // --------------------------------------------------------------------------- procedure TSettingsForm.BuildPATab; const MARGIN = 22; LBL_W = 46; ED_W = 72; COL_W = LBL_W + ED_W + 8; COLS = 4; ROW_H = 32; GRP_PAD = 18; R1 = 42; GRP_H = 222; 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_INPUT; E.Font.Color := CLR_INPUT_TEXT; E.Font.Name := UI_FONT; E.Font.Size := 9; 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: TPanel; Lbl: TLabel; Spin: TSpinEdit; i, r, c, X, Y: Integer; begin with TLabel.Create(Self) do begin Parent := FPagePA; Caption := 'Power Amplifier'; SetBounds(MARGIN, 18, 400, 28); Font.Name := UI_FONT; Font.Size := 16; Font.Style := [fsBold]; Font.Color := CLR_TEXT; end; with TLabel.Create(Self) do begin Parent := FPagePA; Caption := 'Output limit and per-band calibration values.'; SetBounds(MARGIN, 48, 620, 20); Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM; end; Grp := MakeGroupPanel(FPagePA, 'Power && Calibration', MARGIN, 86, 650, GRP_H); // Max Power Lbl := TLabel.Create(Self); Lbl.Parent := Grp; Lbl.Caption := 'Max power (W)'; Lbl.SetBounds(GRP_PAD, R1 + 6, 132, 18); Lbl.Font.Color := CLR_TEXTDIM; Lbl.Font.Name := UI_FONT; Lbl.Font.Size := 9; Spin := TSpinEdit.Create(Self); Spin.Parent := Grp; Spin.SetBounds(GRP_PAD + 150, R1, ED_W, BTN_H + 2); Spin.Color := CLR_INPUT; Spin.Font.Color := CLR_INPUT_TEXT; Spin.Font.Name := UI_FONT; Spin.Font.Size := 9; Spin.MinValue := 5; Spin.MaxValue := 200; Spin.Increment := 1; Spin.Value := 100; Spin.OnChange := OnMaxPowerChange; FEdMaxPower := Spin; // Calibration label Lbl := TLabel.Create(Self); Lbl.Parent := Grp; Lbl.Caption := 'Calibration per band, 38.8 ... 100.0'; Lbl.SetBounds(GRP_PAD, R1 + ROW_H + 12, 320, 18); Lbl.Font.Color := CLR_TEXTDIM; Lbl.Font.Name := UI_FONT; Lbl.Font.Size := 9; // Band grid 4 columns × 3 rows for i := 0 to 10 do begin r := i div COLS; c := i mod COLS; X := GRP_PAD + c * (COL_W + 4); Y := R1 + ROW_H + 34 + 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 := UI_FONT; Lbl.Font.Size := 9; FEdBandCal[i] := MakeFloatSpin(Grp, X + LBL_W, Y, OnBandCalChange); FEdBandCal[i].Tag := i; end; with TLabel.Create(Self) do begin Parent := FPageAdvanced; Caption := 'Advanced'; SetBounds(MARGIN, 18, 400, 28); Font.Name := UI_FONT; Font.Size := 16; Font.Style := [fsBold]; Font.Color := CLR_TEXT; end; with TLabel.Create(Self) do begin Parent := FPageAdvanced; Caption := 'Transmit, CAT and DSP-only settings will appear here when they are wired to runtime settings.'; SetBounds(MARGIN, 48, 680, 20); Font.Name := UI_FONT; Font.Size := 9; Font.Color := CLR_TEXTDIM; end; end; procedure TSettingsForm.ApplyTheme; begin Color := CLR_BG; if Assigned(FNavPanel) then FNavPanel.Color := TColor($00151515); if Assigned(FContentPanel) then FContentPanel.Color := CLR_BG; if Assigned(FPageAudio) then FPageAudio.Color := CLR_BG; if Assigned(FPageDisplay) then FPageDisplay.Color := CLR_BG; if Assigned(FPageSpectrum) then FPageSpectrum.Color := CLR_BG; if Assigned(FPageWaterfall) then FPageWaterfall.Color := CLR_BG; if Assigned(FPagePA) then FPagePA.Color := CLR_BG; if Assigned(FPageAdvanced) then FPageAdvanced.Color := CLR_BG; end; // --------------------------------------------------------------------------- // LoadValues // --------------------------------------------------------------------------- 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 Idx := 5; for i := 0 to High(FFT_SIZES) do if FFT_SIZES[i] = FFTSize then begin Idx := i; Break; end; FCmbFFTSize.ItemIndex := Idx; if (WinType >= 0) and (WinType < FCmbWindow.Items.Count) then FCmbWindow.ItemIndex := WinType else FCmbWindow.ItemIndex := 2; if (SpecDet >= 0) and (SpecDet < FCmbSpecDet.Items.Count) then FCmbSpecDet.ItemIndex := SpecDet else FCmbSpecDet.ItemIndex := 0; Idx := 3; 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); if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then FCmbWfDet.ItemIndex := WfDet else FCmbWfDet.ItemIndex := 0; Idx := 3; 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); FEdRefLevel.Text := FormatFloat('0.#', RefLevel); FEdRange.Text := FormatFloat('0.#', Range); Idx := 1; 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; if AudioOutDevName = '' then FCmbRXDev.ItemIndex := 0 else begin Idx := FCmbRXDev.Items.IndexOf(AudioOutDevName); if Idx >= 0 then FCmbRXDev.ItemIndex := Idx else FCmbRXDev.ItemIndex := 0; end; if AudioInDevName = '' then FCmbTXDev.ItemIndex := 0 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 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; if (FRXDevCount = 0) and (AudioOut.LastError <> '') then FCmbRXDev.Items.Add('PortAudio: ' + AudioOut.LastError); i := FCmbRXDev.Items.IndexOf(OldRXSel); if i >= 0 then FCmbRXDev.ItemIndex := i else FCmbRXDev.ItemIndex := 0; end; 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; if (FTXDevCount = 0) and (AudioIn.LastError <> '') then FCmbTXDev.Items.Add('PortAudio: ' + AudioIn.LastError); 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 FOnAudioDevChange(-1, '') else begin DevName := FCmbRXDev.Items[Idx]; 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 FOnAudioInDevChange(-1, '') 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 := 8; 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.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.