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 <noreply@anthropic.com>
This commit is contained in:
Uladzimir Karpenka
2026-06-09 14:43:04 +03:00
co-authored by Claude Opus 4.8
parent 682025e99b
commit 7c8fd298ae
3 changed files with 48 additions and 22 deletions
+4 -22
View File
@@ -336,7 +336,6 @@ type
procedure DoStopAndDisconnect; procedure DoStopAndDisconnect;
// DSP/Audio callbacks (вызываются из рабочих потоков) // DSP/Audio callbacks (вызываются из рабочих потоков)
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer); procedure OnSpectrumReady(const Pixels: array of Single; Count: Integer);
procedure OnWaterfallReady(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. // DSP Engine.Open (загрузка libwdsp, ~1-2 сек) вызывается при нажатии START.
FController.FWDSPReady := False; FController.FWDSPReady := False;
FController.FDSPEngine.OnAudio := OnAudioReady; FController.FDSPEngine.OnAudio := FController.OnAudioReady;
FController.FDSPEngine.OnSpectrum := OnSpectrumReady; FController.FDSPEngine.OnSpectrum := OnSpectrumReady;
FController.FDSPEngine.OnWaterfall := OnWaterfallReady; FController.FDSPEngine.OnWaterfall := OnWaterfallReady;
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ; FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
// Web-клиент перехватывает RX-аудио (моно-микс) у контроллера, если подключён.
FController.OnAudioConsume := FWebAdapter.AudioConsume;
// Sound-card mic путь: WDSP TX-thread дёргает этот колбэк перед каждым // Sound-card mic путь: WDSP TX-thread дёргает этот колбэк перед каждым
// тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается. // тиком, если выбран источник SoundCard. Для Radio колбэк не вызывается.
FController.FDSPEngine.OnPullMicSamples := FController.PullSoundCardMic; FController.FDSPEngine.OnPullMicSamples := FController.PullSoundCardMic;
@@ -3206,7 +3207,7 @@ begin
end; end;
FController.FWDSPReady := False; FController.FWDSPReady := False;
FController.FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512); FController.FDSPEngine := TWDSPEngine.Create(ASampleRate, 48000, 512);
FController.FDSPEngine.OnAudio := OnAudioReady; FController.FDSPEngine.OnAudio := FController.OnAudioReady;
FController.FDSPEngine.OnSpectrum := OnSpectrumReady; FController.FDSPEngine.OnSpectrum := OnSpectrumReady;
FController.FDSPEngine.OnWaterfall := OnWaterfallReady; FController.FDSPEngine.OnWaterfall := OnWaterfallReady;
FController.FDSPEngine.OnTXIQ := FController.OnTXIQ; FController.FDSPEngine.OnTXIQ := FController.OnTXIQ;
@@ -5179,25 +5180,6 @@ end;
// DSP/Audio callbacks — вызываются из рабочих потоков // 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; procedure TMainForm.OnSpectrumReady(const Pixels: array of Single;
Count: Integer); Count: Integer);
var var
+22
View File
@@ -79,6 +79,12 @@ type
// Маршалинг произвольного метода в поток контроллера (GUI: TThread.Synchronize). // Маршалинг произвольного метода в поток контроллера (GUI: TThread.Synchronize).
TUIInvokeEvent = procedure(M: TThreadMethod) of object; 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). // Снимок состояния для синхронизации нового фронтенда (Arduino/Web на connect).
TRadioSnapshot = record TRadioSnapshot = record
VfoA, VfoB, CenterFreq, SpanHz: Double; VfoA, VfoB, CenterFreq, SpanHz: Double;
@@ -110,6 +116,7 @@ type
FOnInvoke: TUIInvokeEvent; FOnInvoke: TUIInvokeEvent;
FOnStateChanged: TRadioStateEvent; FOnStateChanged: TRadioStateEvent;
FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA FOnAfterTune: TThreadMethod; // пост-тюн хук: канал-оркестрация после SetVfoA
FOnAudioConsume: TAudioConsumeEvent; // web-перехват RX-аудио (если активен клиент)
procedure Changed(Field: TRadioField); procedure Changed(Field: TRadioField);
procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг) procedure ApplyAGCToEngine; // применяет FAGCMode к WDSP (единый маппинг)
procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode procedure ApplyModeDefaults; // дефолтный фильтр/девиация под текущий FMode
@@ -265,6 +272,7 @@ type
// ---- Колбэки движков (вызываются из рабочих потоков сети/DSP) ---- // ---- Колбэки движков (вызываются из рабочих потоков сети/DSP) ----
// Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine // Чистая обработка данных без UI; навешиваются на FNetwork/FDSPEngine
// вызывающим (GUI или демон) после CreateEngines. // вызывающим (GUI или демон) после CreateEngines.
procedure OnAudioReady(const Left, Right: array of Single; Count: Integer);
procedure OnMicPacket(const Data: TMicDataPacket); procedure OnMicPacket(const Data: TMicDataPacket);
function PullSoundCardMic(MaxN: Integer): Integer; function PullSoundCardMic(MaxN: Integer): Integer;
procedure OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket); procedure OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket);
@@ -429,6 +437,7 @@ type
// рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned), // рендера) — сюда вешается канальная оркестрация (ChannelController.OnVfoTuned),
// чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless. // чтобы любой путь тюнинга (desktop/web/CAT/энкодер) был канал-aware headless.
property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune; property OnAfterTune: TThreadMethod read FOnAfterTune write FOnAfterTune;
property OnAudioConsume: TAudioConsumeEvent read FOnAudioConsume write FOnAudioConsume;
end; end;
implementation implementation
@@ -732,6 +741,19 @@ begin
end; end;
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); procedure TRadioController.OnMicPacket(const Data: TMicDataPacket);
// HW mic-стрим уходит в WDSP только если выбран источник Radio; иначе сэмплы // HW mic-стрим уходит в WDSP только если выбран источник Radio; иначе сэмплы
// игнорируются (mic берётся со звуковой карты). // игнорируются (mic берётся со звуковой карты).
+22
View File
@@ -102,6 +102,12 @@ type
// подача в движок thread-safe (как OnMicPacket/OnDDCIQ контроллера). // подача в движок thread-safe (как OnMicPacket/OnDDCIQ контроллера).
procedure OnMic(Samples: PSingle; Count: Integer); 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. // Исходящее зеркало: периодический снимок состояния + спектр/водопад в web.
// Зовётся хозяином из GUI-таймера (буферы пикселей — у хозяина, остальное из // Зовётся хозяином из GUI-таймера (буферы пикселей — у хозяина, остальное из
// FController). Сервер сам гейтит по FRunning и шлёт активным клиентам. // FController). Сервер сам гейтит по FRunning и шлёт активным клиентам.
@@ -360,6 +366,22 @@ begin
FController.FDSPEngine.PushTXMicSamplesD(Buf, n); FController.FDSPEngine.PushTXMicSamplesD(Buf, n);
end; 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); procedure TWebAdapter.PushState(const ASpec, AWf: array of Single);