mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
Fix TX mic DUC starvation: add 512-sample staging buffer
Opus decodes 960 samples per frame; the TX DSP thread consumes 512-sample blocks. 960 mod 512 = 448, so the ring always kept a sub-threshold tail, causing TTXDSPThread to skip ticks and starve the DUC hardware every ~20 ms. FlushMicToCallback accumulates samples in FMicStageBuf and calls FOnWebMic only when a full 512-sample block is ready. The remainder stays in the buffer until the next Opus frame arrives. This ensures FTXMicRing always receives data in multiples of 512, eliminating the 0 < Avail < 512 dead-zone entirely. FMicStageLen is reset together with FMicPreRollPos on new TX session to prevent stale samples from a previous session mixing in. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+57
-7
@@ -187,6 +187,16 @@ type
|
||||
FMicPreRollPos: Integer;
|
||||
FMicPreRoll: array[0..MIC_PREROLL_SAMP-1] of Single;
|
||||
|
||||
// ── Staging buffer: выравнивание по границе DSP-блока ──────────────────
|
||||
// Opus декодирует по 960 сэмплов, а TX DSP потребляет по 512.
|
||||
// 960 mod 512 = 448 — в ring всегда остаётся хвост меньше порога,
|
||||
// из-за чего TTXDSPThread пропускает тики и DUC hardware голодает.
|
||||
// FlushMicToCallback накапливает сэмплы здесь и вызывает FOnWebMic
|
||||
// ровно тогда, когда накопился полный блок 512 сэмплов.
|
||||
// Гарантия: FTXMicRing всегда получает данные кратными 512.
|
||||
FMicStageBuf: array[0..511] of Single; // буфер одного DSP-блока
|
||||
FMicStageLen: Integer; // сколько сэмплов накоплено
|
||||
|
||||
// ── TCP ──
|
||||
FListenSock: TSocket;
|
||||
FClients: array[0..MAX_WS_CLIENTS-1] of TWsClient;
|
||||
@@ -281,6 +291,7 @@ type
|
||||
function LoadOpus: Boolean;
|
||||
procedure UnloadOpus;
|
||||
procedure PushMicSamples(P: PSingle; N: Integer);
|
||||
procedure FlushMicToCallback(P: PSingle; N: Integer);
|
||||
function InitListen: Boolean;
|
||||
procedure AcceptLoop;
|
||||
procedure PushLoop;
|
||||
@@ -544,11 +555,44 @@ begin
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure TWebServer.FlushMicToCallback(P: PSingle; N: Integer);
|
||||
// Аккумулирует сэмплы в FMicStageBuf и вызывает FOnWebMic ровно когда
|
||||
// накопился полный DSP-блок (512 сэмплов). Неполный остаток хранится
|
||||
// в буфере до следующего вызова.
|
||||
//
|
||||
// Почему 512: TX DSP поток читает из FTXMicRing блоками по FAudioBufSize=512.
|
||||
// Если ring содержит 0 < Avail < 512, поток пропускает тик → DUC underflow.
|
||||
// Staging гарантирует что ring всегда получает данные кратными 512.
|
||||
var
|
||||
Src: PSingle;
|
||||
Fill: Integer;
|
||||
begin
|
||||
if not Assigned(FOnWebMic) or (N <= 0) then Exit;
|
||||
Src := P;
|
||||
while N > 0 do
|
||||
begin
|
||||
// Докладываем в stage сколько нужно до полного блока (или сколько есть)
|
||||
Fill := Min(N, 512 - FMicStageLen);
|
||||
Move(Src^, FMicStageBuf[FMicStageLen], Fill * SizeOf(Single));
|
||||
Inc(FMicStageLen, Fill);
|
||||
Inc(Src, Fill);
|
||||
Dec(N, Fill);
|
||||
|
||||
// Полный блок готов — передаём в DSP-цепь и сбрасываем stage
|
||||
if FMicStageLen = 512 then
|
||||
begin
|
||||
FOnWebMic(@FMicStageBuf[0], 512);
|
||||
FMicStageLen := 0;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TWebServer.PushMicSamples(P: PSingle; N: Integer);
|
||||
// Прокладка между Opus-декодером и FOnWebMic с pre-roll cushion.
|
||||
// Прокладка между Opus-декодером и FlushMicToCallback с pre-roll cushion.
|
||||
// Первые MIC_PREROLL_SAMP сэмплов TX-сессии копим в FMicPreRoll и сливаем
|
||||
// одним блоком — это даёт FTXMicRing запас глубины ~40мс на джиттер.
|
||||
// Дальше всё пушим напрямую.
|
||||
// После pre-roll все сэмплы уходят через FlushMicToCallback, которая
|
||||
// выравнивает поток по границе 512 сэмплов.
|
||||
var
|
||||
Want, Remainder: Integer;
|
||||
P2: PSingle;
|
||||
@@ -559,22 +603,24 @@ begin
|
||||
Want := MIC_PREROLL_SAMP - FMicPreRollPos;
|
||||
if N <= Want then
|
||||
begin
|
||||
// Pre-roll ещё не набран: просто копим
|
||||
Move(P^, FMicPreRoll[FMicPreRollPos], N * SizeOf(Single));
|
||||
Inc(FMicPreRollPos, N);
|
||||
end
|
||||
else
|
||||
begin
|
||||
// Pre-roll заполнен: сливаем накопленный буфер + остаток пакета
|
||||
Move(P^, FMicPreRoll[FMicPreRollPos], Want * SizeOf(Single));
|
||||
FMicPreRollPos := MIC_PREROLL_SAMP;
|
||||
FOnWebMic(@FMicPreRoll[0], MIC_PREROLL_SAMP);
|
||||
FlushMicToCallback(@FMicPreRoll[0], MIC_PREROLL_SAMP);
|
||||
Remainder := N - Want;
|
||||
P2 := P;
|
||||
Inc(P2, Want);
|
||||
FOnWebMic(P2, Remainder);
|
||||
FlushMicToCallback(P2, Remainder);
|
||||
end;
|
||||
end
|
||||
else
|
||||
FOnWebMic(P, N);
|
||||
FlushMicToCallback(P, N);
|
||||
end;
|
||||
|
||||
procedure TWebServer.UnloadOpus;
|
||||
@@ -1006,9 +1052,13 @@ begin
|
||||
begin
|
||||
NowTick := GetTickCount64;
|
||||
if (FMicLastTick = 0) or ((NowTick - FMicLastTick) > MIC_IDLE_MS) then
|
||||
begin
|
||||
// Новая TX-сессия (первый пакет или длинный простой):
|
||||
// сбрасываем pre-roll, PLC не применяем — нет состояния для предсказания.
|
||||
FMicPreRollPos := 0
|
||||
// сбрасываем pre-roll и staging, PLC не применяем —
|
||||
// нет состояния для предсказания.
|
||||
FMicPreRollPos := 0;
|
||||
FMicStageLen := 0;
|
||||
end
|
||||
else
|
||||
begin
|
||||
// PLC для опоздавших/потерянных кадров по wall-clock.
|
||||
|
||||
Reference in New Issue
Block a user