Phase 5 (batch 22): move engine lifecycle into the controller

TRadioController now owns its engines: CreateEngines(ASampleRate) builds
FNetwork/FDSPEngine/FAudioOut/FAudioIn (and the constructor now owns
FSettings), FreeEngines closes and frees them, and Destroy calls
FreeEngines + frees FSettings. FormCreate replaces the four inline
T...Create calls with a single FController.CreateEngines (after the
startup sample rate is loaded, so FDSPEngine is still born at the right
rate) and keeps wiring the MainForm callbacks. FormDestroy keeps the
graceful Run=0 stop + Disconnect but drops the per-engine Close/Free and
FSettings.Free, which now happen in the controller's destructor. This is
a behaviour-preserving ownership move toward headless/daemon operation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Uladzimir Karpenka
2026-06-05 17:20:17 +03:00
co-authored by Claude Opus 4.8
parent 33c433ce5e
commit 8be98b72e1
2 changed files with 49 additions and 26 deletions
+10 -25
View File
@@ -989,8 +989,7 @@ begin
TSettingsManager.DefaultBand(bi_, FController.FBandCache[bi_]); TSettingsManager.DefaultBand(bi_, FController.FBandCache[bi_]);
FController.FCurrentBand := 5; FController.FCurrentBand := 5;
// Загружаем JSON настройки // Загружаем JSON настройки (FSettings создаётся в TRadioController.Create)
FController.FSettings := TSettingsManager.Create;
FController.FSettings.Load; FController.FSettings.Load;
// Загружаем список каналов // Загружаем список каналов
@@ -1152,7 +1151,10 @@ begin
FSpecView.PAMaxPower := FController.FPAMaxPower; FSpecView.PAMaxPower := FController.FPAMaxPower;
FWidebandView := TWidebandView.Create; FWidebandView := TWidebandView.Create;
FController.FNetwork := THPSDRNetwork.Create; // Движки создаёт контроллер (владелец lifecycle — нужно для демона); здесь
// лишь навешиваем колбэки на методы MainForm. FSampleRate уже загружен из
// startup-preview выше, поэтому FDSPEngine рождается с нужной частотой.
FController.CreateEngines(FController.FSampleRate);
FController.FNetwork.OnDeviceFound := OnDeviceFound; FController.FNetwork.OnDeviceFound := OnDeviceFound;
FController.FNetwork.OnHPStatus := OnHPStatusCB; FController.FNetwork.OnHPStatus := OnHPStatusCB;
FController.FNetwork.OnDDCIQ := OnDDCIQCB; FController.FNetwork.OnDDCIQ := OnDDCIQCB;
@@ -1170,13 +1172,8 @@ begin
UpdateVfoDisplay; UpdateVfoDisplay;
UpdateFilterButtons; UpdateFilterButtons;
// DSP Engine — создаём объект, Open вызовется при нажатии START // DSP Engine.Open (загрузка libwdsp, ~1-2 сек) вызывается при нажатии START.
// (FController.FDSPEngine.Open загружает libwdsp и занимает ~1-2 сек)
FController.FWDSPReady := False; FController.FWDSPReady := False;
// BufSize=512: FBufSize=2048 @ 192kHz, FAudioBufSize=512 @ 48kHz.
// Уменьшение с 1024 вдвое сокращает время построения downsampler в OpenChannel RXA
// (~1300ms → ~650ms). Латентность: 512/48000 ≈ 10.7ms — допустимо для SDR.
FController.FDSPEngine := TWDSPEngine.Create(FController.FSampleRate, 48000, 512);
FController.FDSPEngine.OnAudio := OnAudioReady; FController.FDSPEngine.OnAudio := OnAudioReady;
FController.FDSPEngine.OnSpectrum := OnSpectrumReady; FController.FDSPEngine.OnSpectrum := OnSpectrumReady;
FController.FDSPEngine.OnWaterfall := OnWaterfallReady; FController.FDSPEngine.OnWaterfall := OnWaterfallReady;
@@ -1185,12 +1182,6 @@ begin
// тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается. // тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается.
FController.FDSPEngine.OnPullMicSamples := PullSoundCardMic; FController.FDSPEngine.OnPullMicSamples := PullSoundCardMic;
// Audio output/input — создаём объекты сейчас, открываем после показа формы
// (Pa_Initialize на Linux пишет в stderr до перехвата сигналов FPC)
FController.FAudioOut := TAudioOutput.Create(48000);
FController.FAudioOut.OutputBufferSize := FController.FSettings.LoadAudioBufferSize;
FController.FAudioIn := TAudioInput.Create(48000);
FMeterTimer := TTimer.Create(Self); FMeterTimer := TTimer.Create(Self);
FMeterTimer.Interval := 100; FMeterTimer.Interval := 100;
FMeterTimer.OnTimer := MeterTimerTick; FMeterTimer.OnTimer := MeterTimerTick;
@@ -1221,16 +1212,11 @@ procedure TMainForm.FormDestroy(Sender: TObject);
begin begin
FMeterTimer.Enabled := False; FMeterTimer.Enabled := False;
FSpectrumTimer.Enabled := False; FSpectrumTimer.Enabled := False;
// Грациозный стоп радио (Run=0) до освобождения; сами движки закрывает и
// освобождает TRadioController.Destroy (FreeEngines) ниже.
if FController.FNetwork.Running then if FController.FNetwork.Running then
FController.FNetwork.SetRunAndFreq(False, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), 0); FController.FNetwork.SetRunAndFreq(False, XvtrTranslate(FController.FCenterFreq), XvtrTranslate(FController.FCenterFreq), 0);
FController.FNetwork.Disconnect; FController.FNetwork.Disconnect;
FController.FNetwork.Free;
FController.FAudioOut.Close;
FController.FAudioOut.Free;
FController.FAudioIn.Close;
FController.FAudioIn.Free;
FController.FDSPEngine.Close;
FController.FDSPEngine.Free;
FreeAndNil(FSpecView); FreeAndNil(FSpecView);
FreeAndNil(FWidebandView); FreeAndNil(FWidebandView);
@@ -1245,15 +1231,14 @@ begin
// Размер окна сохраняем всегда (не зависит от подключения) // Размер окна сохраняем всегда (не зависит от подключения)
FController.FSettings.SaveStartupPreview(FController.FVfoA, FController.FVfoB, FController.FSampleRate); FController.FSettings.SaveStartupPreview(FController.FVfoA, FController.FVfoB, FController.FSampleRate);
FController.FSettings.Save; FController.FSettings.Save;
FController.FSettings.Free;
FreeAndNil(FController.FChannelStore); FreeAndNil(FController.FChannelStore);
FWebServer.Stop; FWebServer.Stop;
FWebServer.Free; FWebServer.Free;
if Assigned(FCATTcp) then begin FCATTcp.Stop; FreeAndNil(FCATTcp); end; if Assigned(FCATTcp) then begin FCATTcp.Stop; FreeAndNil(FCATTcp); end;
if Assigned(FCATSerial) then begin FCATSerial.StopAll; FreeAndNil(FCATSerial); end; if Assigned(FCATSerial) then begin FCATSerial.StopAll; FreeAndNil(FCATSerial); end;
FreeAndNil(FCATEngine); FreeAndNil(FCATEngine);
// Движки (поля FController) уже освобождены выше. // Ядро освобождаем последним — его Destroy закрывает и освобождает движки
// Освобождаем само ядро последним. // (FreeEngines) и FSettings.
FreeAndNil(FController); FreeAndNil(FController);
end; end;
+39 -1
View File
@@ -203,6 +203,13 @@ type
constructor Create; constructor Create;
destructor Destroy; override; destructor Destroy; override;
// Lifecycle движков: создаёт FNetwork/FDSPEngine/FAudioOut/FAudioIn с
// заданным sample rate (вызывающий уже определил его из настроек). Колбэки
// движков навешивает вызывающий (GUI: методы MainForm; демон: свои). Парный
// FreeEngines закрывает и освобождает их; вызывается из Destroy.
procedure CreateEngines(ASampleRate: Integer);
procedure FreeEngines;
// Маршалинг команды в поток контроллера (GUI: TThread.Synchronize). // Маршалинг команды в поток контроллера (GUI: TThread.Synchronize).
procedure Invoke(M: TThreadMethod); procedure Invoke(M: TThreadMethod);
@@ -343,6 +350,7 @@ implementation
constructor TRadioController.Create; constructor TRadioController.Create;
begin begin
inherited Create; inherited Create;
FSettings := TSettingsManager.Create; // владелец настроек (GUI и демон)
// Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны). // Дефолты (дублируют TMainForm.FormCreate; в GUI перезапишутся, в демоне нужны).
FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0; FVfoA := 14200000; FVfoB := 7100000; FActiveVfo := 0;
FMode := 1; FFilter := 5; FFilterBW := 2700; FMode := 1; FFilter := 5; FFilterBW := 2700;
@@ -357,9 +365,39 @@ begin
FillChar(FDevMAC, SizeOf(FDevMAC), 0); FillChar(FDevMAC, SizeOf(FDevMAC), 0);
end; end;
procedure TRadioController.CreateEngines(ASampleRate: Integer);
// Создаёт движки в полях контроллера. Колбэки (OnAudio/OnSpectrum/OnHPStatus…)
// навешивает вызывающий после возврата — они указывают на UI/демон.
begin
FNetwork := THPSDRNetwork.Create;
// BufSize=512: латентность 512/48000 ≈ 10.7мс — допустимо для SDR; меньший
// буфер вдвое сокращает построение downsampler в OpenChannel RXA.
FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
// Audio out/in — объекты создаём сейчас, Open вызывается позже (после показа
// формы / при подключении устройства).
FAudioOut := TAudioOutput.Create(48000);
FAudioOut.OutputBufferSize := FSettings.LoadAudioBufferSize;
FAudioIn := TAudioInput.Create(48000);
end;
procedure TRadioController.FreeEngines;
begin
if Assigned(FNetwork) then
begin
if FNetwork.Connected then FNetwork.Disconnect;
FreeAndNil(FNetwork);
end;
if Assigned(FAudioOut) then begin FAudioOut.Close; FreeAndNil(FAudioOut); end;
if Assigned(FAudioIn) then begin FAudioIn.Close; FreeAndNil(FAudioIn); end;
if Assigned(FDSPEngine) then begin FDSPEngine.Close; FreeAndNil(FDSPEngine); end;
end;
destructor TRadioController.Destroy; destructor TRadioController.Destroy;
begin begin
// Фаза 1: движки освобождает MainForm. Здесь ничего не освобождаем. FreeEngines;
FreeAndNil(FSettings);
inherited Destroy; inherited Destroy;
end; end;