From 7664907b3e7f858086d7afde06cad6ffc090ef8b Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Tue, 24 Mar 2026 21:21:30 +0300 Subject: [PATCH] add settingsForm --- AudioInput.pas | 501 ++++++++++++++++++++++ SettingsForm.pas | 1038 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1539 insertions(+) create mode 100644 AudioInput.pas create mode 100644 SettingsForm.pas diff --git a/AudioInput.pas b/AudioInput.pas new file mode 100644 index 0000000..900ec2a --- /dev/null +++ b/AudioInput.pas @@ -0,0 +1,501 @@ +unit AudioInput; + +{ + PortAudio input — TX microphone capture. + Modelled after piHPSDR/portaudio.c (DL1YCF), parallel to AudioOutput.pas. + + Key features: + - Mono input stream, paFloat32, 48000 Hz (fixed, как в piHPSDR) + - Pa_Initialize called once in Initialize + - Ring buffer of MY_MIC_RING_SIZE single-channel Double samples + - Callback (PaInCallback) writes mic samples into the ring buffer + - ReadSample() returns next sample, 0.0 if buffer empty + - EnumInputDevices enumerates devices with maxInputChannels > 0 +} + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +interface + +uses + Classes, SysUtils, SyncObjs, DynLibs; + +const + MY_MIC_BUFFER_SIZE = 128; // PA frames per callback + MY_MIC_RING_SIZE = 9600; // ring buffer size (mono), как в piHPSDR + +type + TPaError = LongInt; + TPaDeviceIndex = LongInt; + TPaTime = Double; + TPaStream = Pointer; + PPaStream = ^TPaStream; + + TPaMicStreamParameters = record + device: TPaDeviceIndex; + channelCount: LongInt; + sampleFormat: LongWord; + suggestedLatency: TPaTime; + hostApiSpecificStreamInfo: Pointer; + end; + PPaMicStreamParameters = ^TPaMicStreamParameters; + + TPaMicDeviceInfo = record + structVersion: Integer; + name: PAnsiChar; + hostApi: Integer; + maxInputChannels: Integer; + maxOutputChannels: Integer; + defaultLowInputLatency: TPaTime; + defaultLowOutputLatency: TPaTime; + defaultHighInputLatency: TPaTime; + defaultHighOutputLatency: TPaTime; + defaultSampleRate: Double; + end; + PPaMicDeviceInfo = ^TPaMicDeviceInfo; + + TPaMicStreamCallback = function(inputBuffer, outputBuffer: Pointer; + framesPerBuffer: LongWord; + timeInfo: Pointer; + statusFlags: LongWord; + userData: Pointer): LongInt; cdecl; + + TPaMic_Initialize = function: TPaError; cdecl; + TPaMic_Terminate = function: TPaError; cdecl; + TPaMic_GetDeviceCount = function: TPaDeviceIndex; cdecl; + TPaMic_GetDeviceInfo = function(device: TPaDeviceIndex): PPaMicDeviceInfo; cdecl; + TPaMic_OpenStream = function(stream: PPaStream; + inputParam: PPaMicStreamParameters; + outputParam: PPaMicStreamParameters; + sampleRate: Double; + framesPerBuffer: LongWord; + streamFlags: LongWord; + callback: TPaMicStreamCallback; + userData: Pointer): TPaError; cdecl; + TPaMic_StartStream = function(stream: TPaStream): TPaError; cdecl; + TPaMic_StopStream = function(stream: TPaStream): TPaError; cdecl; + TPaMic_CloseStream = function(stream: TPaStream): TPaError; cdecl; + TPaMic_GetErrorText = function(err: TPaError): PAnsiChar; cdecl; + + { TAudioInput } + TAudioInput = class + private + FLibHandle: TLibHandle; + FStream: TPaStream; + FSampleRate: Integer; + FOpen: Boolean; + FPAInited: Boolean; + FLastError: string; + FDeviceIndex: Integer; // -1 = нет устройства (не открывается без явного выбора) + + // Ring buffer (mono double), как в piHPSDR + FBuf: array[0..MY_MIC_RING_SIZE - 1] of Double; + FInPt: Integer; // write pointer (callback writes) + FOutPt: Integer; // read pointer (main thread reads via ReadSample) + FMutex: TCriticalSection; + + // PortAudio functions + FPa_Initialize: TPaMic_Initialize; + FPa_Terminate: TPaMic_Terminate; + FPa_GetDeviceCount: TPaMic_GetDeviceCount; + FPa_GetDeviceInfo: TPaMic_GetDeviceInfo; + FPa_OpenStream: TPaMic_OpenStream; + FPa_StartStream: TPaMic_StartStream; + FPa_StopStream: TPaMic_StopStream; + FPa_CloseStream: TPaMic_CloseStream; + FPa_GetErrorText: TPaMic_GetErrorText; + + function LoadLib: Boolean; + function GetLastError: string; + + public + constructor Create(SampleRate: Integer = 48000); + destructor Destroy; override; + + // Загружает PA и вызывает Pa_Initialize (без открытия стрима) + function Initialize: Boolean; + // Открывает input stream для выбранного устройства + function Open: Boolean; + procedure Close; + + // Перечисляет устройства ввода (maxInputChannels > 0). + // Names[i] — имя, Indices[i] — PA device index, Count — реальное кол-во. + function EnumInputDevices(Names: TStrings; out Indices: array of Integer; + out Count: Integer): Boolean; + + // Найти PA device index по имени (-1 если не найдено) + function FindDeviceByName(const AName: string): Integer; + + // Читает следующий моно-сэмпл из кольцевого буфера. + // Возвращает 0.0 если буфер пуст. + function ReadSample: Double; + + property IsOpen: Boolean read FOpen; + property LastError: string read GetLastError; + property DeviceIndex: Integer read FDeviceIndex write FDeviceIndex; + property SampleRate: Integer read FSampleRate write FSampleRate; + end; + +// Глобальный PA callback для input (cdecl, не метод) +function PaInCallback(inputBuffer, outputBuffer: Pointer; + framesPerBuffer: LongWord; + timeInfo: Pointer; + statusFlags: LongWord; + userData: Pointer): LongInt; cdecl; + +implementation + +const +{$IFDEF UNIX} + PA_IN_LIBS: array[0..3] of AnsiString = ( + 'libportaudio.so.2', + 'libportaudio.so', + 'libportaudio.so.2.0.0', + 'libportaudio.so.0' + ); +{$ELSE} + PA_IN_LIBS: array[0..1] of AnsiString = ( + 'portaudio_x64.dll', + 'portaudio.dll' + ); +{$ENDIF} + PA_NO_ERROR = 0; + PA_FLOAT32 = LongWord(1); + PA_NO_FLAG = LongWord(0); + PA_CONTINUE = LongInt(0); + +// --------------------------------------------------------------------------- +// Callback — читает моно float32 из inputBuffer, пишет в кольцевой буфер. +// Точно по образцу pa_in_cb из piHPSDR/portaudio.c +// --------------------------------------------------------------------------- + +function PaInCallback(inputBuffer, outputBuffer: Pointer; + framesPerBuffer: LongWord; + timeInfo: Pointer; + statusFlags: LongWord; + userData: Pointer): LongInt; cdecl; +var + Audio: TAudioInput; + In_: PSingle; + i: LongWord; + newpt: Integer; +begin + Audio := TAudioInput(userData); + In_ := PSingle(inputBuffer); + + if In_ = nil then + begin + Result := PA_CONTINUE; + Exit; + end; + + // Lock-free: callback только пишет FInPt, читает FOutPt. + // ReadSample только читает FInPt, пишет FOutPt — каждый указатель пишет один поток. + newpt := Audio.FInPt; + for i := 0 to framesPerBuffer - 1 do + begin + // Проверяем переполнение: если буфер полон — новый сэмпл отбрасывается + if (newpt + 1) mod MY_MIC_RING_SIZE <> Audio.FOutPt then + begin + Audio.FBuf[newpt] := In_^; + Inc(newpt); + if newpt >= MY_MIC_RING_SIZE then newpt := 0; + end; + Inc(In_); + end; + Audio.FInPt := newpt; + + Result := PA_CONTINUE; +end; + +// --------------------------------------------------------------------------- + +constructor TAudioInput.Create(SampleRate: Integer); +begin + inherited Create; + FSampleRate := SampleRate; + FDeviceIndex := -1; + FOpen := False; + FPAInited := False; + FStream := nil; + FLibHandle := NilHandle; + FLastError := ''; + FInPt := 0; + FOutPt := 0; + FillChar(FBuf, SizeOf(FBuf), 0); + FMutex := TCriticalSection.Create; +end; + +destructor TAudioInput.Destroy; +begin + Close; + if FPAInited and Assigned(FPa_Terminate) then + begin + FPa_Terminate(); + FPAInited := False; + end; + if FLibHandle <> NilHandle then + begin + FreeLibrary(FLibHandle); + FLibHandle := NilHandle; + end; + FMutex.Free; + inherited; +end; + +function TAudioInput.GetLastError: string; +begin + Result := FLastError; +end; + +function TAudioInput.LoadLib: Boolean; +var + i: Integer; +begin + Result := False; + if FLibHandle <> NilHandle then begin Result := True; Exit; end; + + for i := 0 to High(PA_IN_LIBS) do + begin + FLibHandle := LoadLibrary(PA_IN_LIBS[i]); + if FLibHandle <> NilHandle then Break; + end; + + if FLibHandle = NilHandle then + begin + FLastError := 'libportaudio not found'; + Exit; + end; + + FPa_Initialize := TPaMic_Initialize (GetProcAddress(FLibHandle, 'Pa_Initialize')); + FPa_Terminate := TPaMic_Terminate (GetProcAddress(FLibHandle, 'Pa_Terminate')); + FPa_GetDeviceCount := TPaMic_GetDeviceCount(GetProcAddress(FLibHandle, 'Pa_GetDeviceCount')); + FPa_GetDeviceInfo := TPaMic_GetDeviceInfo (GetProcAddress(FLibHandle, 'Pa_GetDeviceInfo')); + FPa_OpenStream := TPaMic_OpenStream (GetProcAddress(FLibHandle, 'Pa_OpenStream')); + FPa_StartStream := TPaMic_StartStream (GetProcAddress(FLibHandle, 'Pa_StartStream')); + FPa_StopStream := TPaMic_StopStream (GetProcAddress(FLibHandle, 'Pa_StopStream')); + 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 + begin + FLastError := 'libportaudio: symbols not found'; + FreeLibrary(FLibHandle); + FLibHandle := NilHandle; + Exit; + end; + + Result := True; +end; + +// --------------------------------------------------------------------------- +// Initialize +// --------------------------------------------------------------------------- + +function TAudioInput.Initialize: Boolean; +var + Err: TPaError; +begin + Result := False; + if not LoadLib then Exit; + if FPAInited then begin Result := True; Exit; end; + Err := FPa_Initialize(); + if Err <> PA_NO_ERROR then + begin + FLastError := 'Pa_Initialize: '; + if Assigned(FPa_GetErrorText) then + FLastError := FLastError + string(FPa_GetErrorText(Err)) + else + FLastError := FLastError + IntToStr(Err); + Exit; + end; + FPAInited := True; + Result := True; +end; + +// --------------------------------------------------------------------------- +// EnumInputDevices +// --------------------------------------------------------------------------- + +function TAudioInput.EnumInputDevices(Names: TStrings; + out Indices: array of Integer; out Count: Integer): Boolean; +var + I, N: Integer; + Info: PPaMicDeviceInfo; + DevName: string; +begin + Count := 0; + Result := False; + if not Initialize then Exit; + if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then Exit; + + N := FPa_GetDeviceCount(); + if N <= 0 then Exit; + + for I := 0 to N - 1 do + begin + Info := FPa_GetDeviceInfo(I); + if Info = nil then Continue; + if Info^.maxInputChannels <= 0 then Continue; + if Count >= Length(Indices) then Break; + + if Info^.name <> nil then + DevName := string(AnsiString(Info^.name)) + else + DevName := Format('Device %d', [I]); + + Names.Add(DevName); + Indices[Count] := I; + Inc(Count); + end; + Result := Count > 0; +end; + +// --------------------------------------------------------------------------- +// FindDeviceByName +// --------------------------------------------------------------------------- + +function TAudioInput.FindDeviceByName(const AName: string): Integer; +var + I, N: Integer; + Info: PPaMicDeviceInfo; + DevName: string; +begin + Result := -1; + if AName = '' then Exit; + if not Initialize then Exit; + if not Assigned(FPa_GetDeviceCount) or not Assigned(FPa_GetDeviceInfo) then Exit; + + N := FPa_GetDeviceCount(); + for I := 0 to N - 1 do + begin + Info := FPa_GetDeviceInfo(I); + if Info = nil then Continue; + if Info^.maxInputChannels <= 0 then Continue; + if Info^.name <> nil then + DevName := string(AnsiString(Info^.name)) + else + DevName := Format('Device %d', [I]); + if SameText(DevName, AName) then + begin + Result := I; + Exit; + end; + end; +end; + +// --------------------------------------------------------------------------- +// Open — открывает input stream, как audio_open_input в piHPSDR +// --------------------------------------------------------------------------- + +function TAudioInput.Open: Boolean; +var + InParam: TPaMicStreamParameters; + Err: TPaError; + Info: PPaMicDeviceInfo; +begin + Result := False; + if FOpen then begin Result := True; Exit; end; + if FDeviceIndex < 0 then + begin + FLastError := 'No input device selected'; + Exit; + end; + if not Initialize then Exit; + + FillChar(InParam, SizeOf(InParam), 0); + InParam.channelCount := 1; // Mono, как в piHPSDR + InParam.device := FDeviceIndex; + InParam.sampleFormat := PA_FLOAT32; + + // suggestedLatency = defaultLowInputLatency устройства (как в piHPSDR) + Info := FPa_GetDeviceInfo(FDeviceIndex); + if Info <> nil then + InParam.suggestedLatency := Info^.defaultLowInputLatency + else + InParam.suggestedLatency := 0.0; + + InParam.hostApiSpecificStreamInfo := nil; + + Err := FPa_OpenStream(@FStream, @InParam, nil, + FSampleRate, MY_MIC_BUFFER_SIZE, + PA_NO_FLAG, PaInCallback, Self); + if Err <> PA_NO_ERROR then + begin + FLastError := 'Pa_OpenStream (in): '; + if Assigned(FPa_GetErrorText) then + FLastError := FLastError + string(FPa_GetErrorText(Err)) + else + FLastError := FLastError + IntToStr(Err); + FStream := nil; + Exit; + end; + + // Сбрасываем буфер перед стартом + FInPt := 0; + FOutPt := 0; + FillChar(FBuf, SizeOf(FBuf), 0); + + Err := FPa_StartStream(FStream); + if Err <> PA_NO_ERROR then + begin + FLastError := 'Pa_StartStream (in): '; + if Assigned(FPa_GetErrorText) then + FLastError := FLastError + string(FPa_GetErrorText(Err)) + else + FLastError := FLastError + IntToStr(Err); + FPa_CloseStream(FStream); + FStream := nil; + Exit; + end; + + FOpen := True; + FLastError := ''; + Result := True; +end; + +// --------------------------------------------------------------------------- +// Close — как audio_close_input в piHPSDR +// --------------------------------------------------------------------------- + +procedure TAudioInput.Close; +var Err: TPaError; +begin + if not FOpen then Exit; + if FStream <> nil then + begin + Err := FPa_StopStream(FStream); + if (Err <> PA_NO_ERROR) and Assigned(FPa_GetErrorText) then + FLastError := 'Pa_StopStream (in): ' + string(FPa_GetErrorText(Err)); + + Err := FPa_CloseStream(FStream); + if (Err <> PA_NO_ERROR) and Assigned(FPa_GetErrorText) then + FLastError := 'Pa_CloseStream (in): ' + string(FPa_GetErrorText(Err)); + + FStream := nil; + end; + FOpen := False; +end; + +// --------------------------------------------------------------------------- +// ReadSample — как audio_get_next_mic_sample в piHPSDR +// --------------------------------------------------------------------------- + +function TAudioInput.ReadSample: Double; +var newpt: Integer; +begin + // Lock-free: читает FOutPt, читает FInPt + if FOutPt = FInPt then + begin + Result := 0.0; // буфер пуст — тишина + Exit; + end; + Result := FBuf[FOutPt]; + newpt := FOutPt + 1; + if newpt >= MY_MIC_RING_SIZE then newpt := 0; + FOutPt := newpt; +end; + +end. diff --git a/SettingsForm.pas b/SettingsForm.pas new file mode 100644 index 0000000..3564001 --- /dev/null +++ b/SettingsForm.pas @@ -0,0 +1,1038 @@ +unit SettingsForm; + +{ + SettingsForm.pas — диалог настроек EWSDR SDR приложения. + + Вкладки: General | Audio | Display | DSP | Transmit | PA Settings | CAT + Display имеет подвкладки: General | RX1 | RX2 | TX + + Все изменения применяются МГНОВЕННО через events: + OnDisplayChange — изменены FFT/Window/Detector/Avg параметры + OnWaterfallChange — изменены параметры водопада + OnGridChange — изменена сетка спектра + OnAudioChange — изменено аудио устройство/буфер + OnSampleRateChange — изменён audio output sample rate (только PortAudio, DDC не трогается) + + Для применения настроек: создайте форму, зарегистрируйте callbacks, + потом вызовите Show или ShowModal. +} + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +interface + +uses + Classes, SysUtils, Math, + Forms, Controls, Graphics, Dialogs, + StdCtrls, ExtCtrls, ComCtrls, Spin, + FlatButton, AudioOutput, AudioInput; + +type + TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector, + SpecAvgMode, SpecAvgCount: Integer; SpecBackmult: Double) of object; + TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode, WfAvgCount: Integer; + WfBackmult: Double) of object; + TOnGridParamChange = procedure(RefLevel, Range, GridStep: Double) of object; + TOnAudioDevChange = procedure(DevIndex: Integer; const DevName: string) of object; + TOnAudioInDevChange = procedure(DevIndex: Integer; const DevName: string) of object; + TOnVisibilityChange = procedure(ShowSpectrum, ShowWaterfall: Boolean) of object; + TOnFPSChange = procedure(FPS: Integer) of object; + + { TSettingsForm } + TSettingsForm = class(TForm) + private + // ---- Tabs ---- + FPageCtrl: TPageControl; + FTabGeneral: TTabSheet; + FTabAudio: TTabSheet; + FTabDisplay: TTabSheet; + FTabDSP: TTabSheet; + FTabTransmit: TTabSheet; + FTabPA: TTabSheet; + FTabCAT: TTabSheet; + + // ---- Audio tab controls ---- + FCmbRXDev: TComboBox; + FRXDevIndices: array[0..63] of Integer; + FRXDevCount: Integer; + FCmbTXDev: TComboBox; + FTXDevIndices: array[0..63] of Integer; + FTXDevCount: Integer; + + // ---- Display sub-tabs ---- + FDispPageCtrl: TPageControl; + FTabDispGeneral: TTabSheet; + FTabDispRX1: TTabSheet; + FTabDispRX2: TTabSheet; + FTabDispTX: TTabSheet; + + // ---- RX1 sub-tab controls ---- + // FFT group + FCmbFFTSize: TComboBox; + FCmbWindow: TComboBox; + // Spectrum group + FCmbSpecDet: TComboBox; + FCmbSpecAvg: TComboBox; + FSpinSpecCount: TSpinEdit; + FEdSpecBackmult: TEdit; + // Waterfall group + FCmbWfDet: TComboBox; + FCmbWfAvg: TComboBox; + FSpinWfCount: TSpinEdit; + FEdWfBackmult: TEdit; + // Grid group + FEdRefLevel: TEdit; + FEdRange: TEdit; + FCmbGridStep: TComboBox; + + // ---- General display tab controls ---- + FChkShowSpectrum: TCheckBox; + FChkShowWaterfall: TCheckBox; + FCmbFPS: TComboBox; + + // ---- Close button ---- + FBtnClose: TFlatButton; + + // ---- Callbacks ---- + FOnDisplayChange: TOnDisplayParamChange; + FOnWaterfallChange: TOnWaterfallParamChange; + FOnGridChange: TOnGridParamChange; + FOnAudioDevChange: TOnAudioDevChange; + FOnAudioInDevChange: TOnAudioInDevChange; + FOnVisibilityChange: TOnVisibilityChange; + FOnFPSChange: TOnFPSChange; + + // ---- Suppress change events during load ---- + FLoading: Boolean; + + procedure BuildUI; + procedure BuildAudioTab; + procedure BuildDisplayTab; + procedure BuildRX1Tab; + procedure ApplyTheme; + + // Helpers + function MakeLbl(AParent: TWinControl; const Cap: string; + ALeft, ATop, AW: Integer): TLabel; + function MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer; + OnChange: TNotifyEvent): TComboBox; + function MakeGroupBox(AParent: TWinControl; const Cap: string; + ALeft, ATop, AW, AH: Integer): TGroupBox; + + // Event handlers + procedure OnFFTSizeChange(Sender: TObject); + procedure OnWindowChange(Sender: TObject); + procedure OnSpecDetChange(Sender: TObject); + procedure OnSpecAvgModeChange(Sender: TObject); + procedure OnSpecCountChange(Sender: TObject); + procedure OnSpecBackmultChange(Sender: TObject); + procedure OnWfDetChange(Sender: TObject); + procedure OnWfAvgModeChange(Sender: TObject); + procedure OnWfCountChange(Sender: TObject); + procedure OnWfBackmultChange(Sender: TObject); + procedure OnRefLevelChange(Sender: TObject); + procedure OnRangeChange(Sender: TObject); + procedure OnGridStepChange(Sender: TObject); + procedure OnRXDevChange(Sender: TObject); + procedure OnTXDevChange(Sender: TObject); + procedure OnVisibilityChkChange(Sender: TObject); + procedure OnFPSCmbChange(Sender: TObject); + procedure BtnCloseClick(Sender: TObject); + + procedure FireDisplayChange; + procedure FireWaterfallChange; + procedure FireGridChange; + + function GetSpecBackmult: Double; + function GetWfBackmult: Double; + function GetRefLevel: Double; + function GetRange: Double; + function GetGridStep: Double; + function GetFFTSize: Integer; + + public + constructor Create(AOwner: TComponent); override; + + // Заполнить элементы управления текущими значениями (без firing events) + procedure LoadValues( + FFTSize, WinType, SpecDet, SpecAvgMode, SpecAvgCount: Integer; + SpecBackmult: Double; + WfDet, WfAvgMode, WfAvgCount: Integer; + WfBackmult: Double; + RefLevel, Range, GridStep: Double; + const AudioOutDevName: string; + const AudioInDevName: string); + + // Перечислить PA устройства ввода и вывода + procedure RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput); + // Загрузить текущие значения видимости (без fire событий) + procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean); + // Загрузить текущий FPS (без fire события) + procedure LoadFPS(FPS: Integer); + + property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange; + property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange; + property OnGridChange: TOnGridParamChange read FOnGridChange write FOnGridChange; + property OnAudioDevChange: TOnAudioDevChange read FOnAudioDevChange write FOnAudioDevChange; + property OnAudioInDevChange: TOnAudioInDevChange read FOnAudioInDevChange write FOnAudioInDevChange; + property OnVisibilityChange: TOnVisibilityChange read FOnVisibilityChange write FOnVisibilityChange; + property OnFPSChange: TOnFPSChange read FOnFPSChange write FOnFPSChange; + end; + +implementation + +const + CLR_BG = TColor($00121212); + CLR_PANEL = TColor($001A1A1A); + CLR_GROUP = TColor($00202020); + CLR_TEXT = TColor($00E0E0E0); + CLR_TEXTDIM = TColor($00888888); + CLR_BORDER = TColor($00303030); + CLR_ACCENT = TColor($0040FF80); + BTN_H = 24; + + // FFT sizes (index → value) + FFT_SIZES: array[0..5] of Integer = (1024, 2048, 4096, 8192, 16384, 32768); + FFT_NAMES: array[0..5] of string = ('1024','2048','4096','8192','16384','32768'); + + WINDOW_NAMES: array[0..6] of string = ( + 'Rectangular', '4-term Blackman-Harris', 'Hann', + 'Flat-top', 'Hamming', 'Kaiser', '7-term Blackman-Harris'); + + DETECTOR_NAMES: array[0..4] of string = ( + 'Peak', 'Rosenfell', 'Average', 'Sample', 'RMS'); + + AVGMODE_NAMES: array[0..4] of string = ( + 'Peak Hold', 'None', 'Recursive', 'Time Window', 'Log Recursive'); + // Note: AVERAGE_PEAK_HOLD=-1, NONE=0, RECURSIVE=1, TIME_WINDOW=2, LOG_RECURSIVE=3 + // Index → WDSP value mapping: 0→-1, 1→0, 2→1, 3→2, 4→3 + AVGMODE_VALUES: array[0..4] of Integer = (-1, 0, 1, 2, 3); + + GRID_STEPS: array[0..4] of Double = (5.0, 10.0, 20.0, 30.0, 40.0); + GRID_STEP_NAMES: array[0..4] of string = ('5 dB','10 dB','20 dB','30 dB','40 dB'); + + FPS_VALUES: array[0..5] of Integer = (5, 10, 15, 20, 25, 30); + FPS_NAMES: array[0..5] of string = ('5 fps','10 fps','15 fps','20 fps','25 fps','30 fps'); + +{ TSettingsForm } + +constructor TSettingsForm.Create(AOwner: TComponent); +begin + inherited CreateNew(AOwner); + Caption := 'Settings'; + Width := 700; + Height := 560; + Position := poScreenCenter; + BorderStyle := bsSizeable; + Color := CLR_BG; + Font.Name := 'Courier New'; + Font.Size := 8; + Font.Color := CLR_TEXT; + FLoading := False; + FRXDevCount := 0; + BuildUI; + ApplyTheme; +end; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function TSettingsForm.MakeLbl(AParent: TWinControl; const Cap: string; + ALeft, ATop, AW: Integer): TLabel; +begin + Result := TLabel.Create(Self); + Result.Parent := AParent; + Result.Caption := Cap; + Result.Left := ALeft; + Result.Top := ATop; + if AW > 0 then Result.Width := AW; + Result.Font.Color := CLR_TEXTDIM; + Result.Font.Name := 'Courier New'; + Result.Font.Size := 8; +end; + +function TSettingsForm.MakeCombo(AParent: TWinControl; ALeft, ATop, AW: Integer; + OnChange: TNotifyEvent): TComboBox; +begin + Result := TComboBox.Create(Self); + Result.Parent := AParent; + Result.Left := ALeft; + Result.Top := ATop; + Result.Width := AW; + Result.Height := BTN_H; + Result.Style := csDropDownList; + Result.Color := CLR_PANEL; + Result.Font.Color := CLR_TEXT; + Result.Font.Name := 'Courier New'; + Result.Font.Size := 8; + Result.OnChange := OnChange; +end; + +function TSettingsForm.MakeGroupBox(AParent: TWinControl; const Cap: string; + ALeft, ATop, AW, AH: Integer): TGroupBox; +begin + Result := TGroupBox.Create(Self); + Result.Parent := AParent; + Result.Caption := Cap; + Result.SetBounds(ALeft, ATop, AW, AH); + Result.Color := CLR_GROUP; + Result.Font.Color := CLR_ACCENT; + Result.Font.Name := 'Courier New'; + Result.Font.Size := 8; +end; + +// --------------------------------------------------------------------------- +// BuildUI +// --------------------------------------------------------------------------- + +procedure TSettingsForm.BuildUI; +var + i: Integer; +begin + FPageCtrl := TPageControl.Create(Self); + FPageCtrl.Parent := Self; + FPageCtrl.SetBounds(4, 4, Width - 12, Height - 48); + FPageCtrl.Anchors := [akLeft, akTop, akRight, akBottom]; + FPageCtrl.Color := CLR_BG; + FPageCtrl.Font.Color := CLR_TEXT; + + FTabGeneral := FPageCtrl.AddTabSheet; FTabGeneral.Caption := 'General'; + FTabAudio := FPageCtrl.AddTabSheet; FTabAudio.Caption := 'Audio'; + FTabDisplay := FPageCtrl.AddTabSheet; FTabDisplay.Caption := 'Display'; + FTabDSP := FPageCtrl.AddTabSheet; FTabDSP.Caption := 'DSP'; + FTabTransmit := FPageCtrl.AddTabSheet; FTabTransmit.Caption := 'Transmit'; + FTabPA := FPageCtrl.AddTabSheet; FTabPA.Caption := 'PA Settings'; + FTabCAT := FPageCtrl.AddTabSheet; FTabCAT.Caption := 'CAT'; + + for i := 0 to FPageCtrl.PageCount - 1 do + FPageCtrl.Pages[i].Color := CLR_BG; + + BuildAudioTab; + BuildDisplayTab; + + // Close button + FBtnClose := TFlatButton.Create(Self); + FBtnClose.Parent := Self; + FBtnClose.Caption := 'CLOSE'; + FBtnClose.SetBounds(Width - 90, Height - 38, 82, BTN_H); + FBtnClose.Anchors := [akRight, akBottom]; + FBtnClose.OnClick := BtnCloseClick; + FBtnClose.ClrNorm := CLR_PANEL; + FBtnClose.ClrBorder := TColor($00404040); + FBtnClose.ClrHot := TColor($00303030); + FBtnClose.ClrActive := TColor($00003300); + FBtnClose.ClrText := CLR_TEXT; + FBtnClose.ClrTextAct:= CLR_ACCENT; +end; + +procedure TSettingsForm.BuildAudioTab; +const + R1 = 10; // top padding inside groups + LW = 110; // label width +var + Grp: TGroupBox; + + procedure MkGrp(out G: TGroupBox; const Cap: string); + begin + G := TGroupBox.Create(Self); + G.Parent := FTabAudio; + G.Caption := Cap; + G.Color := CLR_GROUP; + G.Font.Color := CLR_ACCENT; + G.Font.Name := 'Courier New'; + G.Font.Size := 8; + G.AutoSize := True; + G.Align := alTop; + end; + + procedure MkSep; + var S: TPanel; + begin + S := TPanel.Create(Self); + S.Parent := FTabAudio; + S.BevelOuter := bvNone; + S.Color := CLR_BG; + S.Align := alTop; + S.Height := 12; + end; + +begin + // ── Top padding ─────────────────────────────────────────────────────────── + MkSep; + + // ── TX Audio Input ──────────────────────────────────────────────────────── + MkGrp(Grp, 'TX Audio Input'); + MakeLbl(Grp, 'Input Device:', 10, R1+4, LW); + FCmbTXDev := MakeCombo(Grp, LW+14, R1, 440, OnTXDevChange); + FCmbTXDev.Items.Add('None'); + + MkSep; + + // ── RX Audio Output ─────────────────────────────────────────────────────── + MkGrp(Grp, 'RX Audio Output'); + MakeLbl(Grp, 'Output Device:', 10, R1+4, LW); + FCmbRXDev := MakeCombo(Grp, LW+14, R1, 440, OnRXDevChange); + FCmbRXDev.Items.Add('Default'); +end; + +procedure TSettingsForm.BuildDisplayTab; +var + i: Integer; +begin + FDispPageCtrl := TPageControl.Create(Self); + FDispPageCtrl.Parent := FTabDisplay; + FDispPageCtrl.Align := alClient; + FDispPageCtrl.Color := CLR_BG; + FDispPageCtrl.Font.Color := CLR_TEXT; + + FTabDispGeneral := FDispPageCtrl.AddTabSheet; FTabDispGeneral.Caption := 'General'; + FTabDispRX1 := FDispPageCtrl.AddTabSheet; FTabDispRX1.Caption := 'RX1'; + FTabDispRX2 := FDispPageCtrl.AddTabSheet; FTabDispRX2.Caption := 'RX2'; + FTabDispTX := FDispPageCtrl.AddTabSheet; FTabDispTX.Caption := 'TX'; + + FTabDispGeneral.Color := CLR_BG; + FTabDispRX1.Color := CLR_BG; + FTabDispRX2.Color := CLR_BG; + FTabDispTX.Color := CLR_BG; + + // ---- General sub-tab: visibility checkboxes ---- + FChkShowSpectrum := TCheckBox.Create(Self); + FChkShowSpectrum.Parent := FTabDispGeneral; + FChkShowSpectrum.Caption := 'Show Spectrum'; + FChkShowSpectrum.SetBounds(12, 16, 200, 22); + FChkShowSpectrum.Checked := True; + FChkShowSpectrum.Font.Color := CLR_TEXT; + FChkShowSpectrum.Font.Name := 'Courier New'; + FChkShowSpectrum.Font.Size := 8; + FChkShowSpectrum.OnChange := OnVisibilityChkChange; + + FChkShowWaterfall := TCheckBox.Create(Self); + FChkShowWaterfall.Parent := FTabDispGeneral; + FChkShowWaterfall.Caption := 'Show Waterfall'; + FChkShowWaterfall.SetBounds(12, 44, 200, 22); + FChkShowWaterfall.Checked := True; + FChkShowWaterfall.Font.Color := CLR_TEXT; + FChkShowWaterfall.Font.Name := 'Courier New'; + FChkShowWaterfall.Font.Size := 8; + FChkShowWaterfall.OnChange := OnVisibilityChkChange; + + // FPS label + combo + with TLabel.Create(Self) do + begin + Parent := FTabDispGeneral; + Caption := 'Display FPS:'; + SetBounds(12, 76, 100, 22); + Font.Color := CLR_TEXTDIM; + Font.Name := 'Courier New'; + Font.Size := 8; + end; + FCmbFPS := MakeCombo(FTabDispGeneral, 116, 74, 100, OnFPSCmbChange); + for i := 0 to High(FPS_NAMES) do + FCmbFPS.Items.Add(FPS_NAMES[i]); + FCmbFPS.ItemIndex := 3; // 20 fps default + + BuildRX1Tab; +end; + +procedure TSettingsForm.BuildRX1Tab; +// Layout (alTop stack in FTabDispRX1): +// 8px top padding +// FFT group — full width +// 12px separator +// PnlMid — Spectrum (alLeft) | 10px gap | Waterfall (alClient), same height +// 12px separator +// Spectrum Grid group — full width +// hint (alBottom) +// +// Row step: STEP = BTN_H + 10 = 34px. Inner padding: R1 = 10. +const + R1 = 10; // top padding inside groups + LX = 10; // left margin inside group + LW = 88; // label column width + CX = LX + LW + 4; // combo/edit column start (= 102) + STEP = BTN_H + 10; // row step (= 34) + SEP = 12; // gap between groups + COLW = 328; // left column panel width + COLGAP = 10; // horizontal gap between columns + GH2 = 100; // GroupBox height for 2-row groups + GH3 = 136; // GroupBox height for 3-row groups +var + PnlFFT, PnlMid, PnlL, PnlSepH, PnlR: TPanel; + Grp: TGroupBox; + Lbl: TLabel; + i: Integer; + + function MkGrp(AParent: TWinControl; const Cap: string; AH: Integer): TGroupBox; + begin + Result := TGroupBox.Create(Self); + Result.Parent := AParent; + Result.Caption := Cap; + Result.Color := CLR_GROUP; + Result.Font.Color := CLR_ACCENT; + Result.Font.Name := 'Courier New'; + Result.Font.Size := 8; + Result.Align := alTop; + Result.Height := AH; + end; + + procedure MkSep(AParent: TWinControl; AH: Integer); + var S: TPanel; + begin + S := TPanel.Create(Self); + S.Parent := AParent; + S.BevelOuter := bvNone; + S.Color := CLR_BG; + S.Align := alTop; + S.Height := AH; + end; + + function MkEdit(AParent: TWinControl; AX, AY, AW: Integer; + AOnExit: TNotifyEvent): TEdit; + begin + Result := TEdit.Create(Self); + Result.Parent := AParent; + Result.SetBounds(AX, AY, AW, BTN_H); + Result.Color := CLR_PANEL; + Result.Font.Color := CLR_TEXT; + Result.Font.Name := 'Courier New'; + Result.Font.Size := 8; + Result.OnExit := AOnExit; + end; + + function MkSpin(AParent: TWinControl; AX, AY, AW: Integer; + AOnChange: TNotifyEvent): TSpinEdit; + begin + Result := TSpinEdit.Create(Self); + Result.Parent := AParent; + Result.SetBounds(AX, AY, AW, BTN_H); + Result.MinValue := 1; + Result.MaxValue := 256; + Result.Value := 2; + Result.Color := CLR_PANEL; + Result.Font.Color := CLR_TEXT; + Result.Font.Name := 'Courier New'; + Result.Font.Size := 8; + Result.OnChange := AOnChange; + end; + +begin + // Hint at bottom (added first so alBottom is honoured by later alTop panels) + Lbl := MakeLbl(FTabDispRX1, + 'Backmult: higher value = slower decay (range 0.0 .. 1.0)', LX, 0, 0); + Lbl.Align := alBottom; + Lbl.Height := 22; + Lbl.Font.Color := CLR_TEXTDIM; + + // ── Top padding ─────────────────────────────────────────────────────────── + MkSep(FTabDispRX1, 8); + + // ── FFT (full width, alTop) ─────────────────────────────────────────────── + PnlFFT := TPanel.Create(Self); + PnlFFT.Parent := FTabDispRX1; + PnlFFT.BevelOuter := bvNone; + PnlFFT.Color := CLR_BG; + PnlFFT.Align := alTop; + PnlFFT.Height := GH2; + Grp := MkGrp(PnlFFT, 'FFT', GH2); + MakeLbl(Grp, 'FFT Size:', LX, R1+4, LW); + FCmbFFTSize := MakeCombo(Grp, CX, R1, 130, OnFFTSizeChange); + for i := 0 to High(FFT_NAMES) do FCmbFFTSize.Items.Add(FFT_NAMES[i]); + MakeLbl(Grp, 'Window:', LX, R1+STEP+4, LW); + FCmbWindow := MakeCombo(Grp, CX, R1+STEP, 250, OnWindowChange); + for i := 0 to High(WINDOW_NAMES) do FCmbWindow.Items.Add(WINDOW_NAMES[i]); + + // ── Separator ───────────────────────────────────────────────────────────── + MkSep(FTabDispRX1, SEP); + + // ── Middle row: Spectrum (left) | Waterfall (right) at the same level ───── + PnlMid := TPanel.Create(Self); + PnlMid.Parent := FTabDispRX1; + PnlMid.BevelOuter := bvNone; + PnlMid.Color := CLR_BG; + PnlMid.Align := alTop; + PnlMid.Height := GH3; + + PnlL := TPanel.Create(Self); + PnlL.Parent := PnlMid; + PnlL.BevelOuter := bvNone; + PnlL.Color := CLR_BG; + PnlL.Align := alLeft; + PnlL.Width := COLW; + + PnlSepH := TPanel.Create(Self); + PnlSepH.Parent := PnlMid; + PnlSepH.BevelOuter := bvNone; + PnlSepH.Color := CLR_BG; + PnlSepH.Align := alLeft; + PnlSepH.Width := COLGAP; + + PnlR := TPanel.Create(Self); + PnlR.Parent := PnlMid; + PnlR.BevelOuter := bvNone; + PnlR.Color := CLR_BG; + PnlR.Align := alClient; + + // ── Spectrum (left column) ──────────────────────────────────────────────── + Grp := MkGrp(PnlL, 'Spectrum', GH3); + MakeLbl(Grp, 'Detector:', LX, R1+4, LW); + FCmbSpecDet := MakeCombo(Grp, CX, R1, 172, OnSpecDetChange); + for i := 0 to High(DETECTOR_NAMES) do FCmbSpecDet.Items.Add(DETECTOR_NAMES[i]); + MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW); + FCmbSpecAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnSpecAvgModeChange); + for i := 0 to High(AVGMODE_NAMES) do FCmbSpecAvg.Items.Add(AVGMODE_NAMES[i]); + MakeLbl(Grp, 'Avg Count:', LX, R1+2*STEP+4, LW); + FSpinSpecCount := MkSpin(Grp, CX, R1+2*STEP, 72, OnSpecCountChange); + MakeLbl(Grp, 'Backmult:', CX+78, R1+2*STEP+4, 64); + FEdSpecBackmult := MkEdit(Grp, CX+78+68, R1+2*STEP, 74, OnSpecBackmultChange); + FEdSpecBackmult.Text := '0.45'; + + // ── Waterfall (right column) ────────────────────────────────────────────── + Grp := MkGrp(PnlR, 'Waterfall', GH3); + MakeLbl(Grp, 'Detector:', LX, R1+4, LW); + FCmbWfDet := MakeCombo(Grp, CX, R1, 172, OnWfDetChange); + for i := 0 to High(DETECTOR_NAMES) do FCmbWfDet.Items.Add(DETECTOR_NAMES[i]); + MakeLbl(Grp, 'Avg Mode:', LX, R1+STEP+4, LW); + FCmbWfAvg := MakeCombo(Grp, CX, R1+STEP, 172, OnWfAvgModeChange); + for i := 0 to High(AVGMODE_NAMES) do FCmbWfAvg.Items.Add(AVGMODE_NAMES[i]); + MakeLbl(Grp, 'Avg Count:', LX, R1+2*STEP+4, LW); + FSpinWfCount := MkSpin(Grp, CX, R1+2*STEP, 72, OnWfCountChange); + MakeLbl(Grp, 'Backmult:', CX+78, R1+2*STEP+4, 64); + FEdWfBackmult := MkEdit(Grp, CX+78+68, R1+2*STEP, 74, OnWfBackmultChange); + FEdWfBackmult.Text := '0.45'; + + // ── Separator ───────────────────────────────────────────────────────────── + MkSep(FTabDispRX1, SEP); + + // ── Spectrum Grid (full width, alTop) ───────────────────────────────────── + Grp := MkGrp(FTabDispRX1, 'Spectrum Grid', GH2); + MakeLbl(Grp, 'Ref Level (dBm):', LX, R1+4, 116); + FEdRefLevel := MkEdit(Grp, LX+120, R1, 80, OnRefLevelChange); + FEdRefLevel.Text := '-20'; + MakeLbl(Grp, 'Range (dB):', LX, R1+STEP+4, LW); + FEdRange := MkEdit(Grp, CX, R1+STEP, 80, OnRangeChange); + FEdRange.Text := '110'; + MakeLbl(Grp, 'Grid Step:', CX+90, R1+STEP+4, 72); + FCmbGridStep := MakeCombo(Grp, CX+90+76, R1+STEP, 80, OnGridStepChange); + for i := 0 to High(GRID_STEP_NAMES) do FCmbGridStep.Items.Add(GRID_STEP_NAMES[i]); + FCmbGridStep.ItemIndex := 1; +end; + +procedure TSettingsForm.ApplyTheme; +var + i: Integer; +begin + Color := CLR_BG; + for i := 0 to FPageCtrl.PageCount - 1 do + FPageCtrl.Pages[i].Color := CLR_BG; + FDispPageCtrl.Color := CLR_BG; + for i := 0 to FDispPageCtrl.PageCount - 1 do + FDispPageCtrl.Pages[i].Color := CLR_BG; +end; + +// --------------------------------------------------------------------------- +// LoadValues — заполняет элементы без fire событий +// --------------------------------------------------------------------------- + +procedure TSettingsForm.LoadValues( + FFTSize, WinType, SpecDet, SpecAvgMode, SpecAvgCount: Integer; + SpecBackmult: Double; + WfDet, WfAvgMode, WfAvgCount: Integer; + WfBackmult: Double; + RefLevel, Range, GridStep: Double; + const AudioOutDevName: string; + const AudioInDevName: string); +var + i, Idx: Integer; +begin + FLoading := True; + try + // FFT Size + Idx := 2; // default 4096 + for i := 0 to High(FFT_SIZES) do + if FFT_SIZES[i] = FFTSize then begin Idx := i; Break; end; + FCmbFFTSize.ItemIndex := Idx; + + // Window Type + if (WinType >= 0) and (WinType < FCmbWindow.Items.Count) then + FCmbWindow.ItemIndex := WinType + else + FCmbWindow.ItemIndex := 5; // Kaiser + + // Spectrum Detector + if (SpecDet >= 0) and (SpecDet < FCmbSpecDet.Items.Count) then + FCmbSpecDet.ItemIndex := SpecDet + else + FCmbSpecDet.ItemIndex := 0; + + // Spectrum Avg Mode (WDSP value → combo index) + Idx := 2; // default Recursive + for i := 0 to High(AVGMODE_VALUES) do + if AVGMODE_VALUES[i] = SpecAvgMode then begin Idx := i; Break; end; + FCmbSpecAvg.ItemIndex := Idx; + + FSpinSpecCount.Value := EnsureRange(SpecAvgCount, 1, 256); + FEdSpecBackmult.Text := FormatFloat('0.##', SpecBackmult); + + // Waterfall Detector + if (WfDet >= 0) and (WfDet < FCmbWfDet.Items.Count) then + FCmbWfDet.ItemIndex := WfDet + else + FCmbWfDet.ItemIndex := 2; // Average + + // Waterfall Avg Mode + Idx := 4; // default Log Recursive + for i := 0 to High(AVGMODE_VALUES) do + if AVGMODE_VALUES[i] = WfAvgMode then begin Idx := i; Break; end; + FCmbWfAvg.ItemIndex := Idx; + + FSpinWfCount.Value := EnsureRange(WfAvgCount, 1, 256); + FEdWfBackmult.Text := FormatFloat('0.##', WfBackmult); + + // Grid + FEdRefLevel.Text := FormatFloat('0.#', RefLevel); + FEdRange.Text := FormatFloat('0.#', Range); + Idx := 1; // 10 dB default + for i := 0 to High(GRID_STEPS) do + if Abs(GRID_STEPS[i] - GridStep) < 0.5 then begin Idx := i; Break; end; + FCmbGridStep.ItemIndex := Idx; + + // RX Audio Output + if AudioOutDevName = '' then + FCmbRXDev.ItemIndex := 0 // 'Default' + else + begin + Idx := FCmbRXDev.Items.IndexOf(AudioOutDevName); + if Idx >= 0 then FCmbRXDev.ItemIndex := Idx + else FCmbRXDev.ItemIndex := 0; + end; + + // TX Audio Input + if AudioInDevName = '' then + FCmbTXDev.ItemIndex := 0 // 'None' + else + begin + Idx := FCmbTXDev.Items.IndexOf(AudioInDevName); + if Idx >= 0 then FCmbTXDev.ItemIndex := Idx + else FCmbTXDev.ItemIndex := 0; + end; + finally + FLoading := False; + end; +end; + +// --------------------------------------------------------------------------- +// RefreshAudioDevices +// --------------------------------------------------------------------------- + +procedure TSettingsForm.RefreshAudioDevices(AudioOut: TAudioOutput; AudioIn: TAudioInput); +var + Names: TStringList; + Cnt, i: Integer; + OldRXSel: string; + OldTXSel: string; +begin + Names := TStringList.Create; + try + // ── RX Output devices ───────────────────────────────────────────────── + if AudioOut <> nil then + begin + OldRXSel := FCmbRXDev.Text; + FCmbRXDev.Items.Clear; + FCmbRXDev.Items.Add('Default'); + FRXDevCount := 0; + + Cnt := 0; + Names.Clear; + if AudioOut.EnumOutputDevices(Names, FRXDevIndices, Cnt) then + begin + FRXDevCount := Cnt; + for i := 0 to Cnt - 1 do + FCmbRXDev.Items.Add(Names[i]); + end; + + i := FCmbRXDev.Items.IndexOf(OldRXSel); + if i >= 0 then FCmbRXDev.ItemIndex := i + else FCmbRXDev.ItemIndex := 0; + end; + + // ── TX Input devices ────────────────────────────────────────────────── + if AudioIn <> nil then + begin + OldTXSel := FCmbTXDev.Text; + FCmbTXDev.Items.Clear; + FCmbTXDev.Items.Add('None'); + FTXDevCount := 0; + + Cnt := 0; + Names.Clear; + if AudioIn.EnumInputDevices(Names, FTXDevIndices, Cnt) then + begin + FTXDevCount := Cnt; + for i := 0 to Cnt - 1 do + FCmbTXDev.Items.Add(Names[i]); + end; + + i := FCmbTXDev.Items.IndexOf(OldTXSel); + if i >= 0 then FCmbTXDev.ItemIndex := i + else FCmbTXDev.ItemIndex := 0; + end; + finally + Names.Free; + end; +end; + +// --------------------------------------------------------------------------- +// Value accessors +// --------------------------------------------------------------------------- + +function TSettingsForm.GetFFTSize: Integer; +var Idx: Integer; +begin + Idx := FCmbFFTSize.ItemIndex; + if (Idx >= 0) and (Idx <= High(FFT_SIZES)) then + Result := FFT_SIZES[Idx] + else + Result := 4096; +end; + +function TSettingsForm.GetSpecBackmult: Double; +var V: Double; Err: Integer; +begin + Val(FEdSpecBackmult.Text, V, Err); + if Err <> 0 then V := 0.45; + Result := EnsureRange(V, 0.0, 1.0); +end; + +function TSettingsForm.GetWfBackmult: Double; +var V: Double; Err: Integer; +begin + Val(FEdWfBackmult.Text, V, Err); + if Err <> 0 then V := 0.45; + Result := EnsureRange(V, 0.0, 1.0); +end; + +function TSettingsForm.GetRefLevel: Double; +var V: Double; Err: Integer; +begin + Val(FEdRefLevel.Text, V, Err); + if Err <> 0 then V := -20.0; + Result := EnsureRange(V, -200.0, 0.0); +end; + +function TSettingsForm.GetRange: Double; +var V: Double; Err: Integer; +begin + Val(FEdRange.Text, V, Err); + if Err <> 0 then V := 110.0; + Result := EnsureRange(V, 10.0, 200.0); +end; + +function TSettingsForm.GetGridStep: Double; +var Idx: Integer; +begin + Idx := FCmbGridStep.ItemIndex; + if (Idx >= 0) and (Idx <= High(GRID_STEPS)) then + Result := GRID_STEPS[Idx] + else + Result := 10.0; +end; + +// --------------------------------------------------------------------------- +// Fire helpers +// --------------------------------------------------------------------------- + +procedure TSettingsForm.FireDisplayChange; +var + AvgIdx: Integer; + AvgVal: Integer; +begin + if FLoading then Exit; + if not Assigned(FOnDisplayChange) then Exit; + AvgIdx := FCmbSpecAvg.ItemIndex; + if (AvgIdx >= 0) and (AvgIdx <= High(AVGMODE_VALUES)) then + AvgVal := AVGMODE_VALUES[AvgIdx] + else + AvgVal := 3; + FOnDisplayChange( + GetFFTSize, + FCmbWindow.ItemIndex, + FCmbSpecDet.ItemIndex, + AvgVal, + FSpinSpecCount.Value, + GetSpecBackmult); +end; + +procedure TSettingsForm.FireWaterfallChange; +var + AvgIdx: Integer; + AvgVal: Integer; +begin + if FLoading then Exit; + if not Assigned(FOnWaterfallChange) then Exit; + AvgIdx := FCmbWfAvg.ItemIndex; + if (AvgIdx >= 0) and (AvgIdx <= High(AVGMODE_VALUES)) then + AvgVal := AVGMODE_VALUES[AvgIdx] + else + AvgVal := 3; + FOnWaterfallChange( + FCmbWfDet.ItemIndex, + AvgVal, + FSpinWfCount.Value, + GetWfBackmult); +end; + +procedure TSettingsForm.FireGridChange; +begin + if FLoading then Exit; + if not Assigned(FOnGridChange) then Exit; + FOnGridChange(GetRefLevel, GetRange, GetGridStep); +end; + +// --------------------------------------------------------------------------- +// Event handlers +// --------------------------------------------------------------------------- + +procedure TSettingsForm.OnFFTSizeChange(Sender: TObject); +begin FireDisplayChange; end; + +procedure TSettingsForm.OnWindowChange(Sender: TObject); +begin FireDisplayChange; end; + +procedure TSettingsForm.OnSpecDetChange(Sender: TObject); +begin FireDisplayChange; end; + +procedure TSettingsForm.OnSpecAvgModeChange(Sender: TObject); +begin FireDisplayChange; end; + +procedure TSettingsForm.OnSpecCountChange(Sender: TObject); +begin FireDisplayChange; end; + +procedure TSettingsForm.OnSpecBackmultChange(Sender: TObject); +begin FireDisplayChange; end; + +procedure TSettingsForm.OnWfDetChange(Sender: TObject); +begin FireWaterfallChange; end; + +procedure TSettingsForm.OnWfAvgModeChange(Sender: TObject); +begin FireWaterfallChange; end; + +procedure TSettingsForm.OnWfCountChange(Sender: TObject); +begin FireWaterfallChange; end; + +procedure TSettingsForm.OnWfBackmultChange(Sender: TObject); +begin FireWaterfallChange; end; + +procedure TSettingsForm.OnRefLevelChange(Sender: TObject); +begin FireGridChange; end; + +procedure TSettingsForm.OnRangeChange(Sender: TObject); +begin FireGridChange; end; + +procedure TSettingsForm.OnGridStepChange(Sender: TObject); +begin FireGridChange; end; + +procedure TSettingsForm.OnRXDevChange(Sender: TObject); +var + Idx: Integer; + DevIdx: Integer; + DevName: string; +begin + if FLoading then Exit; + if not Assigned(FOnAudioDevChange) then Exit; + Idx := FCmbRXDev.ItemIndex; + if Idx <= 0 then + begin + // "Default" + FOnAudioDevChange(-1, ''); + end + else + begin + DevName := FCmbRXDev.Items[Idx]; + // Map combo index to PA device index + // Combo: 0=Default, 1..N = real devices + DevIdx := Idx - 1; + if (DevIdx >= 0) and (DevIdx < FRXDevCount) then + FOnAudioDevChange(FRXDevIndices[DevIdx], DevName) + else + FOnAudioDevChange(-1, ''); + end; +end; + +procedure TSettingsForm.OnTXDevChange(Sender: TObject); +var + Idx: Integer; + DevIdx: Integer; + DevName: string; +begin + if FLoading then Exit; + if not Assigned(FOnAudioInDevChange) then Exit; + Idx := FCmbTXDev.ItemIndex; + if Idx <= 0 then + begin + // 'None' + FOnAudioInDevChange(-1, ''); + end + else + begin + DevName := FCmbTXDev.Items[Idx]; + DevIdx := Idx - 1; + if (DevIdx >= 0) and (DevIdx < FTXDevCount) then + FOnAudioInDevChange(FTXDevIndices[DevIdx], DevName) + else + FOnAudioInDevChange(-1, ''); + end; +end; + +procedure TSettingsForm.LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean); +begin + FLoading := True; + try + FChkShowSpectrum.Checked := ShowSpectrum; + FChkShowWaterfall.Checked := ShowWaterfall; + finally + FLoading := False; + end; +end; + +procedure TSettingsForm.OnVisibilityChkChange(Sender: TObject); +begin + if FLoading then Exit; + if not Assigned(FOnVisibilityChange) then Exit; + FOnVisibilityChange(FChkShowSpectrum.Checked, FChkShowWaterfall.Checked); +end; + +procedure TSettingsForm.OnFPSCmbChange(Sender: TObject); +var + Idx: Integer; +begin + if FLoading then Exit; + if not Assigned(FOnFPSChange) then Exit; + Idx := FCmbFPS.ItemIndex; + if (Idx >= 0) and (Idx <= High(FPS_VALUES)) then + FOnFPSChange(FPS_VALUES[Idx]); +end; + +procedure TSettingsForm.LoadFPS(FPS: Integer); +var + i, Idx: Integer; +begin + FLoading := True; + try + Idx := 3; // 20 fps default + for i := 0 to High(FPS_VALUES) do + if FPS_VALUES[i] = FPS then begin Idx := i; Break; end; + FCmbFPS.ItemIndex := Idx; + finally + FLoading := False; + end; +end; + +procedure TSettingsForm.BtnCloseClick(Sender: TObject); +begin + Close; +end; + +end.