diff --git a/AudioInput.pas b/AudioInput.pas index 900ec2a..89489a6 100644 --- a/AudioInput.pas +++ b/AudioInput.pas @@ -15,6 +15,7 @@ unit AudioInput; {$IFDEF FPC} {$MODE Delphi} + {$PACKRECORDS C} {$ENDIF} interface @@ -156,9 +157,13 @@ const 'libportaudio.so.0' ); {$ELSE} - PA_IN_LIBS: array[0..1] of AnsiString = ( + PA_IN_LIBS: array[0..5] of AnsiString = ( 'portaudio_x64.dll', - 'portaudio.dll' + 'portaudio_x86.dll', + 'portaudio.dll', + 'libportaudio-2.dll', + 'libportaudio.dll', + 'libportaudio64.dll' ); {$ENDIF} PA_NO_ERROR = 0; @@ -265,7 +270,11 @@ begin if FLibHandle = NilHandle then begin +{$IFDEF WINDOWS} + FLastError := 'PortAudio DLL not found. Put portaudio_x64.dll, portaudio.dll, or libportaudio-2.dll next to ewsdr.exe'; +{$ELSE} FLastError := 'libportaudio not found'; +{$ENDIF} Exit; end; @@ -279,7 +288,15 @@ begin FPa_CloseStream := TPaMic_CloseStream (GetProcAddress(FLibHandle, 'Pa_CloseStream')); FPa_GetErrorText := TPaMic_GetErrorText(GetProcAddress(FLibHandle, 'Pa_GetErrorText')); - if not Assigned(FPa_Initialize) or not Assigned(FPa_OpenStream) then + if not Assigned(FPa_Initialize) or + not Assigned(FPa_Terminate) or + not Assigned(FPa_GetDeviceCount) or + not Assigned(FPa_GetDeviceInfo) or + not Assigned(FPa_OpenStream) or + not Assigned(FPa_StartStream) or + not Assigned(FPa_StopStream) or + not Assigned(FPa_CloseStream) or + not Assigned(FPa_GetErrorText) then begin FLastError := 'libportaudio: symbols not found'; FreeLibrary(FLibHandle); @@ -329,10 +346,27 @@ begin Count := 0; Result := False; if not Initialize then Exit; - if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then Exit; + if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then + begin + FLastError := 'PortAudio device enumeration is not available'; + Exit; + end; N := FPa_GetDeviceCount(); - if N <= 0 then Exit; + if N < 0 then + begin + FLastError := 'Pa_GetDeviceCount: '; + if Assigned(FPa_GetErrorText) then + FLastError := FLastError + string(FPa_GetErrorText(N)) + else + FLastError := FLastError + IntToStr(N); + Exit; + end; + if N = 0 then + begin + FLastError := 'No PortAudio devices found'; + Exit; + end; for I := 0 to N - 1 do begin @@ -410,8 +444,10 @@ begin InParam.device := FDeviceIndex; InParam.sampleFormat := PA_FLOAT32; - // suggestedLatency = defaultLowInputLatency устройства (как в piHPSDR) Info := FPa_GetDeviceInfo(FDeviceIndex); +{$IFDEF WINDOWS} + // На Windows latency=0 часто ломает WASAPI/MME; берём значение от устройства. +{$ENDIF} if Info <> nil then InParam.suggestedLatency := Info^.defaultLowInputLatency else diff --git a/AudioOutput.pas b/AudioOutput.pas index c17c705..45bc72b 100644 --- a/AudioOutput.pas +++ b/AudioOutput.pas @@ -15,6 +15,7 @@ unit AudioOutput; {$IFDEF FPC} {$MODE Delphi} + {$PACKRECORDS C} {$ENDIF} interface @@ -166,9 +167,13 @@ const 'libportaudio.so.0' ); {$ELSE} - PA_LIBS: array[0..1] of AnsiString = ( + PA_LIBS: array[0..5] of AnsiString = ( 'portaudio_x64.dll', - 'portaudio.dll' + 'portaudio_x86.dll', + 'portaudio.dll', + 'libportaudio-2.dll', + 'libportaudio.dll', + 'libportaudio64.dll' ); {$ENDIF} PA_NO_ERROR = 0; @@ -282,7 +287,11 @@ begin if FLibHandle = NilHandle then begin +{$IFDEF WINDOWS} + FLastError := 'PortAudio DLL not found. Put portaudio_x64.dll, portaudio.dll, or libportaudio-2.dll next to ewsdr.exe'; +{$ELSE} FLastError := 'libportaudio not found. sudo apt install libportaudio2'; +{$ENDIF} Exit; end; @@ -297,7 +306,16 @@ begin FPa_CloseStream := TPa_CloseStream(GetProcAddress(FLibHandle, 'Pa_CloseStream')); FPa_GetErrorText := TPa_GetErrorText(GetProcAddress(FLibHandle, 'Pa_GetErrorText')); - if not Assigned(FPa_Initialize) or not Assigned(FPa_OpenStream) then + if not Assigned(FPa_Initialize) or + not Assigned(FPa_Terminate) or + not Assigned(FPa_GetDefaultOutputDevice) or + not Assigned(FPa_GetDeviceCount) or + not Assigned(FPa_GetDeviceInfo) or + not Assigned(FPa_OpenStream) or + not Assigned(FPa_StartStream) or + not Assigned(FPa_StopStream) or + not Assigned(FPa_CloseStream) or + not Assigned(FPa_GetErrorText) then begin FLastError := 'libportaudio: symbols not found'; FreeLibrary(FLibHandle); @@ -350,10 +368,27 @@ begin Count := 0; Result := False; if not Initialize then Exit; - if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then Exit; + if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then + begin + FLastError := 'PortAudio device enumeration is not available'; + Exit; + end; N := FPa_GetDeviceCount(); - if N <= 0 then Exit; + if N < 0 then + begin + FLastError := 'Pa_GetDeviceCount: '; + if Assigned(FPa_GetErrorText) then + FLastError := FLastError + string(FPa_GetErrorText(N)) + else + FLastError := FLastError + IntToStr(N); + Exit; + end; + if N = 0 then + begin + FLastError := 'No PortAudio devices found'; + Exit; + end; for I := 0 to N - 1 do begin @@ -416,6 +451,7 @@ var OutParam: TPaStreamParameters; Err: TPaError; Dev: TPaDeviceIndex; + Info: PPaDeviceInfo; begin Result := False; if FOpen then begin Result := True; Exit; end; @@ -441,8 +477,12 @@ begin OutParam.hostApiSpecificStreamInfo := nil; OutParam.sampleFormat := PA_FLOAT32; {$IFDEF WINDOWS} - // На Windows latency=0 вызывает фризы — используем разумный минимум - OutParam.suggestedLatency := 0.050; // 50ms — стабильно на Windows WASAPI/MME + // На Windows latency=0 часто ломает WASAPI/MME; берём значение от устройства. + Info := FPa_GetDeviceInfo(Dev); + if (Info <> nil) and (Info^.defaultLowOutputLatency > 0.0) then + OutParam.suggestedLatency := Info^.defaultLowOutputLatency + else + OutParam.suggestedLatency := 0.050; {$ELSE} OutParam.suggestedLatency := 0.0; // на Linux ALSA справляется с минимумом {$ENDIF} diff --git a/MainForm.pas b/MainForm.pas index 4a0741c..1b02590 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -952,7 +952,7 @@ begin end else begin - Result.FFTSize := 4096; + Result.FFTSize := 131072; Result.WindowType := 2; Result.SpecDetector := 0; Result.SpecAvgMode := 3; @@ -1177,7 +1177,7 @@ begin FPanelHidden := False; FShowSpectrum := True; FShowWaterfall := True; - FDisplayFPS := 20; + FDisplayFPS := 60; FWaterfallDirty := True; FSplitterRatio := 0.40; // 40% спектр, 60% водопад по умолчанию FSplitterDrag := False; @@ -5815,7 +5815,7 @@ begin FAudioOutDevName, FAudioInDevName) else SF.LoadValues( - 4096, 2, 0, 3, 30.0, + 131072, 2, 0, 3, 30.0, 0, 3, 120.0, FWfManualHigh, FWfManualLow, FWfAGCOffset, FSpecRefLevel, FSpecRange, FSpecGridStep, FAudioOutDevName, FAudioInDevName); diff --git a/Settings.pas b/Settings.pas index b2ef484..62e8b61 100644 --- a/Settings.pas +++ b/Settings.pas @@ -49,7 +49,7 @@ type WindowWidth: Integer; WindowHeight: Integer; // --- Display settings --- - FFTSize: Integer; // 1024,2048,4096,8192,16384 + FFTSize: Integer; // 4096..262144 WindowType: Integer; // 0=Rect,1=BH4,2=Hann,3=FlatTop,4=Hamming,5=Kaiser,6=BH7 SpecDetector: Integer; // 0=Peak,1=Rosenfell,2=Average,3=Sample,4=RMS SpecAvgMode: Integer; // 0=None,1=Recursive,2=TimeWindow,3=LogRecursive @@ -171,7 +171,7 @@ begin G.PAMaxPower := 100.0; for i := 0 to CFG_BAND_COUNT-1 do G.PABandCal[i] := 100.0; // Display defaults - G.FFTSize := 4096; + G.FFTSize := 131072; G.WindowType := 2; // Hann G.SpecDetector := 0; // Peak G.SpecAvgMode := 3; // Log Recursive @@ -190,7 +190,7 @@ begin G.AudioInDevice := ''; G.ShowSpectrum := True; G.ShowWaterfall := True; - G.DisplayFPS := 20; + G.DisplayFPS := 60; end; constructor TSettingsManager.Create(const FilePath: string); @@ -336,30 +336,40 @@ begin G.LastBand := JI(GObj,'last_band',5); G.SampleRate := JI(GObj,'sample_rate',192000); // Display settings - G.FFTSize := JI(GObj,'fft_size',4096); - G.WindowType := JI(GObj,'win_type',5); + G.FFTSize := JI(GObj,'fft_size',131072); + G.WindowType := JI(GObj,'win_type',2); G.SpecDetector := JI(GObj,'spec_detector',0); - G.SpecAvgMode := JI(GObj,'spec_avg_mode',0); + G.SpecAvgMode := JI(GObj,'spec_avg_mode',3); if GObj.Find('spec_avg_time_ms') <> nil then - G.SpecAvgTimeMS := JD(GObj,'spec_avg_time_ms',120.0) - else + G.SpecAvgTimeMS := JD(GObj,'spec_avg_time_ms',30.0) + else if (GObj.Find('spec_avg_count') <> nil) or + (GObj.Find('spec_backmult') <> nil) then + begin G.SpecAvgTimeMS := LegacyAverageTimeMS( - JI(GObj,'display_fps',20), + JI(GObj,'display_fps',60), G.SpecAvgMode, JI(GObj,'spec_avg_count',1), JD(GObj,'spec_backmult',0.0), - 120.0); - G.WfDetector := JI(GObj,'wf_detector',2); - G.WfAvgMode := JI(GObj,'wf_avg_mode',1); + 30.0); + end + else + G.SpecAvgTimeMS := 30.0; + G.WfDetector := JI(GObj,'wf_detector',0); + G.WfAvgMode := JI(GObj,'wf_avg_mode',3); if GObj.Find('wf_avg_time_ms') <> nil then G.WfAvgTimeMS := JD(GObj,'wf_avg_time_ms',120.0) - else + else if (GObj.Find('wf_avg_count') <> nil) or + (GObj.Find('wf_backmult') <> nil) then + begin G.WfAvgTimeMS := LegacyAverageTimeMS( - JI(GObj,'display_fps',20), + JI(GObj,'display_fps',60), G.WfAvgMode, JI(GObj,'wf_avg_count',2), JD(GObj,'wf_backmult',0.25), 120.0); + end + else + G.WfAvgTimeMS := 120.0; G.WfManualHigh := JD(GObj,'wf_manual_high',-80.0); G.WfManualLow := JD(GObj,'wf_manual_low',-130.0); G.WfAGCOffset := JD(GObj,'wf_agc_offset',0.0); @@ -373,7 +383,7 @@ begin // Visibility settings G.ShowSpectrum := JB(GObj,'show_spectrum',True); G.ShowWaterfall := JB(GObj,'show_waterfall',True); - G.DisplayFPS := JI(GObj,'display_fps',20); + G.DisplayFPS := JI(GObj,'display_fps',60); // PA settings G.PAMaxPower := JD(GObj,'pa_max_power',100.0); for i := 0 to CFG_BAND_COUNT-1 do diff --git a/SettingsForm.pas b/SettingsForm.pas index 2430a87..1bc1814 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -26,7 +26,7 @@ interface uses Classes, SysUtils, Math, Forms, Controls, Graphics, Dialogs, - StdCtrls, ExtCtrls, ComCtrls, Spin, + StdCtrls, ExtCtrls, Spin, FlatButton, AudioOutput, AudioInput; type @@ -47,15 +47,21 @@ type { TSettingsForm } TSettingsForm = class(TForm) private - // ---- Tabs ---- - FPageCtrl: TPageControl; - FTabGeneral: TTabSheet; - FTabAudio: TTabSheet; - FTabDisplay: TTabSheet; - FTabDSP: TTabSheet; - FTabTransmit: TTabSheet; - FTabPA: TTabSheet; - FTabCAT: TTabSheet; + // ---- 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; @@ -65,29 +71,18 @@ type 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; @@ -114,7 +109,6 @@ type FOnVisibilityChange: TOnVisibilityChange; FOnFPSChange: TOnFPSChange; - // ---- Suppress change events during load ---- FLoading: Boolean; procedure BuildUI; @@ -123,16 +117,18 @@ type procedure BuildRX1Tab; procedure BuildPATab; procedure ApplyTheme; + procedure NavClick(Sender: TObject); + procedure SelectPage(APage: TWinControl; ANav: TFlatButton); - // 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; + function MakeGroupPanel(AParent: TWinControl; const Cap: string; + ALeft, ATop, AW, AH: Integer): TPanel; + function MakeScrollPage: TScrollBox; + function MakeNavButton(const Cap: string; ATop: Integer): TFlatButton; - // Event handlers procedure OnFFTSizeChange(Sender: TObject); procedure OnWindowChange(Sender: TObject); procedure OnSpecDetChange(Sender: TObject); @@ -170,7 +166,6 @@ type public constructor Create(AOwner: TComponent); override; - // Заполнить элементы управления текущими значениями (без firing events) procedure LoadValues( FFTSize, WinType, SpecDet, SpecAvgMode: Integer; SpecAvgTimeMS: Double; @@ -180,13 +175,9 @@ type 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; @@ -204,12 +195,19 @@ 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); + 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'); @@ -244,13 +242,13 @@ constructor TSettingsForm.Create(AOwner: TComponent); begin inherited CreateNew(AOwner); Caption := 'Settings'; - Width := 760; - Height := 640; + Width := 900; + Height := 660; Position := poScreenCenter; BorderStyle := bsSizeable; Color := CLR_BG; - Font.Name := 'Courier New'; - Font.Size := 8; + Font.Name := UI_FONT; + Font.Size := 9; Font.Color := CLR_TEXT; FLoading := False; FRXDevCount := 0; @@ -272,8 +270,8 @@ begin Result.Top := ATop; if AW > 0 then Result.Width := AW; Result.Font.Color := CLR_TEXTDIM; - Result.Font.Name := 'Courier New'; - Result.Font.Size := 8; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; end; function TSettingsForm.MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer; @@ -286,24 +284,71 @@ begin 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.Color := CLR_INPUT; + Result.Font.Color := CLR_INPUT_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; Result.OnChange := OnChange; end; -function TSettingsForm.MakeGroupBox(AParent: TWinControl; const Cap: string; - ALeft, ATop, AW, AH: Integer): TGroupBox; +// Создаёт панель-группу с заголовком и разделителем (вместо 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 := TGroupBox.Create(Self); - Result.Parent := AParent; - Result.Caption := Cap; + Result := TPanel.Create(Self); + Result.Parent := AParent; Result.SetBounds(ALeft, ATop, AW, AH); - Result.Color := CLR_GROUP; - Result.Font.Color := CLR_ACCENT; - Result.Font.Name := 'Courier New'; - Result.Font.Size := 8; + 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; // --------------------------------------------------------------------------- @@ -311,36 +356,64 @@ end; // --------------------------------------------------------------------------- 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; + FNavPanel := TPanel.Create(Self); + FNavPanel.Parent := Self; + FNavPanel.Align := alLeft; + FNavPanel.Width := 188; + FNavPanel.BevelOuter := bvNone; + FNavPanel.Color := TColor($00151515); - 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'; + 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; - for i := 0 to FPageCtrl.PageCount - 1 do - FPageCtrl.Pages[i].Color := CLR_BG; + 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; - // Close button FBtnClose := TFlatButton.Create(Self); FBtnClose.Parent := Self; - FBtnClose.Caption := 'CLOSE'; - FBtnClose.SetBounds(Width - 90, Height - 38, 82, BTN_H); + FBtnClose.Caption := 'Close'; + FBtnClose.SetBounds(Width - 106, Height - 40, 92, 28); FBtnClose.Anchors := [akRight, akBottom]; FBtnClose.OnClick := BtnCloseClick; FBtnClose.ClrNorm := CLR_PANEL; @@ -349,278 +422,447 @@ begin 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.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; - +procedure TSettingsForm.NavClick(Sender: TObject); 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'); + 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.BuildDisplayTab; -var - i: Integer; +procedure TSettingsForm.SelectPage(APage: TWinControl; ANav: TFlatButton); begin - FDispPageCtrl := TPageControl.Create(Self); - FDispPageCtrl.Parent := FTabDisplay; - FDispPageCtrl.Align := alClient; - FDispPageCtrl.Color := CLR_BG; - FDispPageCtrl.Font.Color := CLR_TEXT; + FPageAudio.Visible := APage = FPageAudio; + FPageDisplay.Visible := APage = FPageDisplay; + FPageSpectrum.Visible := APage = FPageSpectrum; + FPageWaterfall.Visible := APage = FPageWaterfall; + FPagePA.Visible := APage = FPagePA; + FPageAdvanced.Visible := APage = FPageAdvanced; - 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; + 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; // --------------------------------------------------------------------------- -// LoadValues — заполняет элементы без fire событий +// 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( @@ -636,39 +878,33 @@ var begin FLoading := True; try - // FFT Size - Idx := 0; // default 4096 + Idx := 5; 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 + FCmbWindow.ItemIndex := 2; - // 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 + 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); - // Waterfall Detector if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then FCmbWfDet.ItemIndex := WfDet else - FCmbWfDet.ItemIndex := 2; // Average + FCmbWfDet.ItemIndex := 0; - // Waterfall Avg Mode - Idx := 1; // default Recursive + 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; @@ -677,17 +913,15 @@ begin 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 + 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; - // RX Audio Output if AudioOutDevName = '' then - FCmbRXDev.ItemIndex := 0 // 'Default' + FCmbRXDev.ItemIndex := 0 else begin Idx := FCmbRXDev.Items.IndexOf(AudioOutDevName); @@ -695,9 +929,8 @@ begin else FCmbRXDev.ItemIndex := 0; end; - // TX Audio Input if AudioInDevName = '' then - FCmbTXDev.ItemIndex := 0 // 'None' + FCmbTXDev.ItemIndex := 0 else begin Idx := FCmbTXDev.Items.IndexOf(AudioInDevName); @@ -722,7 +955,6 @@ var begin Names := TStringList.Create; try - // ── RX Output devices ───────────────────────────────────────────────── if AudioOut <> nil then begin OldRXSel := FCmbRXDev.Text; @@ -738,13 +970,14 @@ begin 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; - // ── TX Input devices ────────────────────────────────────────────────── if AudioIn <> nil then begin OldTXSel := FCmbTXDev.Text; @@ -760,6 +993,8 @@ begin 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 @@ -937,15 +1172,10 @@ begin if not Assigned(FOnAudioDevChange) then Exit; Idx := FCmbRXDev.ItemIndex; if Idx <= 0 then - begin - // "Default" - FOnAudioDevChange(-1, ''); - end + FOnAudioDevChange(-1, '') 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) @@ -964,10 +1194,7 @@ begin if not Assigned(FOnAudioInDevChange) then Exit; Idx := FCmbTXDev.ItemIndex; if Idx <= 0 then - begin - // 'None' - FOnAudioInDevChange(-1, ''); - end + FOnAudioInDevChange(-1, '') else begin DevName := FCmbTXDev.Items[Idx]; @@ -1014,7 +1241,7 @@ var begin FLoading := True; try - Idx := 3; // 20 fps default + Idx := 8; for i := 0 to High(FPS_VALUES) do if FPS_VALUES[i] = FPS then begin Idx := i; Break; end; FCmbFPS.ItemIndex := Idx; @@ -1023,121 +1250,6 @@ begin 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; diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 61b7b4b..9386f14 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -551,10 +551,10 @@ begin FANFEnabled := False; FRXAccPos := 0; // Analyzer defaults - FFFTSize := 4096; + FFFTSize := 131072; FWindowType := 2; // Hann - FDisplayFPS := 20; - FEffectiveDisplayFPS := 20.0; + FDisplayFPS := 60; + FEffectiveDisplayFPS := 60.0; FDisplayPixelCount := SPECTRUM_PIXELS; FSpecDetector := 0; // Peak FSpecAvgMode := 3; // Log Recursive