From 7c8fd298ae1bc6eb128f0ebfe1baac5e3fadef16 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Tue, 9 Jun 2026 14:43:04 +0300 Subject: [PATCH] Phase 5 (D1): move RX audio routing into the controller OnAudioReady moves from MainForm into TRadioController (radio-speaker push + local AudioOut). The web steal becomes a route hook: controller fires OnAudioConsume; TWebAdapter.AudioConsume mixes to mono and pushes to the web client, returning True to skip local AudioOut. Behavior-preserving; removes the last audio-path coupling to MainForm. Co-Authored-By: Claude Opus 4.8 --- MainForm.pas | 26 ++++---------------------- RadioController.pas | 22 ++++++++++++++++++++++ WebAdapter.pas | 22 ++++++++++++++++++++++ 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index 821eb44..c5a38eb 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -336,7 +336,6 @@ type procedure DoStopAndDisconnect; // DSP/Audio callbacks (вызываются из рабочих потоков) - procedure OnAudioReady(const Left, Right: array of Single; Count: Integer); procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer); procedure OnWaterfallReady(const Pixels: array of Single; Count: Integer); @@ -982,10 +981,12 @@ begin // DSP Engine.Open (загрузка libwdsp, ~1-2 сек) вызывается при нажатии START. FController.FWDSPReady := False; - FController.FDSPEngine.OnAudio := OnAudioReady; + FController.FDSPEngine.OnAudio := FController.OnAudioReady; FController.FDSPEngine.OnSpectrum := OnSpectrumReady; FController.FDSPEngine.OnWaterfall := OnWaterfallReady; FController.FDSPEngine.OnTXIQ := FController.OnTXIQ; + // Web-клиент перехватывает RX-аудио (моно-микс) у контроллера, если подключён. + FController.OnAudioConsume := FWebAdapter.AudioConsume; // Sound-card mic путь: WDSP TX-thread дёргает этот колбэк перед каждым // тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается. FController.FDSPEngine.OnPullMicSamples := FController.PullSoundCardMic; @@ -3206,7 +3207,7 @@ begin end; FController.FWDSPReady := False; FController.FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512); - FController.FDSPEngine.OnAudio := OnAudioReady; + FController.FDSPEngine.OnAudio := FController.OnAudioReady; FController.FDSPEngine.OnSpectrum := OnSpectrumReady; FController.FDSPEngine.OnWaterfall := OnWaterfallReady; FController.FDSPEngine.OnTXIQ := FController.OnTXIQ; @@ -5179,25 +5180,6 @@ end; // DSP/Audio callbacks — вызываются из рабочих потоков // =========================================================================== -procedure TMainForm.OnAudioReady(const Left, Right: array of Single; - Count: Integer); -var - MonoBuf: array[0..1023] of Single; - i: Integer; -begin - // Дублируем RX-аудио во встроенный динамик трансивера (DDC Audio, port 1028). - if FController.FSendAudioToRadio then - FController.FNetwork.SendSpeakerAudio(Left, Right, Count); - if Assigned(FWebServer) and FWebServer.WebClientActive then - begin - for i := 0 to Count - 1 do - MonoBuf[i] := (Left[i] + Right[i]) * 0.5; - FWebServer.PushAudio(@MonoBuf[0], Count); - Exit; - end; - FController.FAudioOut.Write(Left, Right, Count); -end; - procedure TMainForm.OnSpectrumReady(const Pixels: array of Single; Count: Integer); var diff --git a/RadioController.pas b/RadioController.pas index 84ff79b..6eeaab0 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -79,6 +79,12 @@ type // Маршалинг произвольного метода в поток контроллера (GUI: TThread.Synchronize). TUIInvokeEvent = procedure(M: TThreadMethod) of object; + // Перехват RX-аудио потребителем (web-адаптер): вернуть True, если аудио + // отправлено клиенту и НЕ должно идти в локальный AudioOut. Вызывается из + // DSP-потока (как сам OnAudioReady) — реализация должна быть thread-safe. + TAudioConsumeEvent = function(const Left, Right: array of Single; + Count: Integer): Boolean of object; + // Снимок состояния для синхронизации нового фронтенда (Arduino/Web на connect). TRadioSnapshot = record VfoA, VfoB, CenterFreq, SpanHz: Double; @@ -110,6 +116,7 @@ type FOnInvoke: TUIInvokeEvent; FOnStateChanged: TRadioStateEvent; FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA + FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент) procedure Changed(Field: TRadioField); procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг) procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode @@ -265,6 +272,7 @@ type // ---- Колбэки движков (вызываются из рабочих потоков сети/DSP) ---- // Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine // вызывающим (GUI или демон) после CreateEngines. + procedure OnAudioReady(const Left, Right: array of Single; Count: Integer); procedure OnMicPacket(const Data: TMicDataPacket); function PullSoundCardMic(MaxN: Integer): Integer; procedure OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket); @@ -429,6 +437,7 @@ type // рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned), // чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless. property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune; + property OnAudioConsume: TAudioConsumeEvent read FOnAudioConsume write FOnAudioConsume; end; implementation @@ -732,6 +741,19 @@ begin end; end; +procedure TRadioController.OnAudioReady(const Left, Right: array of Single; + Count: Integer); +// DSP-поток: маршрут RX-аудио. Встроенный динамик трансивера (DDC Audio, +// port 1028) — независимый канал. Затем web-адаптер может перехватить аудио +// (активный клиент); если перехвачено — в локальный AudioOut не пишем. +begin + if FSendAudioToRadio then + FNetwork.SendSpeakerAudio(Left, Right, Count); + if Assigned(FOnAudioConsume) and FOnAudioConsume(Left, Right, Count) then + Exit; + FAudioOut.Write(Left, Right, Count); +end; + procedure TRadioController.OnMicPacket(const Data: TMicDataPacket); // HW mic-стрим уходит в WDSP только если выбран источник Radio; иначе сэмплы // игнорируются (mic берётся со звуковой карты). diff --git a/WebAdapter.pas b/WebAdapter.pas index 540d64d..6f8b365 100644 --- a/WebAdapter.pas +++ b/WebAdapter.pas @@ -102,6 +102,12 @@ type // подача в движок thread-safe (как OnMicPacket/OnDDCIQ контроллера). procedure OnMic(Samples: PSingle; Count: Integer); + // RX-аудио-перехват: навешивается на FController.OnAudioConsume. Если есть + // активный web-клиент — сводим в моно и шлём ему, возвращаем True (локальный + // AudioOut пропускается контроллером). Вызывается из DSP-потока. + function AudioConsume(const Left, Right: array of Single; + Count: Integer): Boolean; + // Исходящее зеркало: периодический снимок состояния + спектр/водопад в web. // Зовётся хозяином из GUI-таймера (буферы пикселей — у хозяина, остальное из // FController). Сервер сам гейтит по FRunning и шлёт активным клиентам. @@ -360,6 +366,22 @@ begin FController.FDSPEngine.PushTXMicSamplesD(Buf, n); end; +function TWebAdapter.AudioConsume(const Left, Right: array of Single; + Count: Integer): Boolean; +var + MonoBuf: array[0..1023] of Single; + i, n: Integer; +begin + Result := False; + if not (Assigned(FServer) and FServer.WebClientActive) then Exit; + n := Count; + if n > Length(MonoBuf) then n := Length(MonoBuf); + for i := 0 to n - 1 do + MonoBuf[i] := (Left[i] + Right[i]) * 0.5; + FServer.PushAudio(@MonoBuf[0], n); + Result := True; +end; + // ── Исходящее зеркало состояния ────────────────────────────────────────────── procedure TWebAdapter.PushState(const ASpec, AWf: array of Single);