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;
|
FMicPreRollPos: Integer;
|
||||||
FMicPreRoll: array[0..MIC_PREROLL_SAMP-1] of Single;
|
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 ──
|
// ── TCP ──
|
||||||
FListenSock: TSocket;
|
FListenSock: TSocket;
|
||||||
FClients: array[0..MAX_WS_CLIENTS-1] of TWsClient;
|
FClients: array[0..MAX_WS_CLIENTS-1] of TWsClient;
|
||||||
@@ -281,6 +291,7 @@ type
|
|||||||
function LoadOpus: Boolean;
|
function LoadOpus: Boolean;
|
||||||
procedure UnloadOpus;
|
procedure UnloadOpus;
|
||||||
procedure PushMicSamples(P: PSingle; N: Integer);
|
procedure PushMicSamples(P: PSingle; N: Integer);
|
||||||
|
procedure FlushMicToCallback(P: PSingle; N: Integer);
|
||||||
function InitListen: Boolean;
|
function InitListen: Boolean;
|
||||||
procedure AcceptLoop;
|
procedure AcceptLoop;
|
||||||
procedure PushLoop;
|
procedure PushLoop;
|
||||||
@@ -544,11 +555,44 @@ begin
|
|||||||
Result := True;
|
Result := True;
|
||||||
end;
|
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);
|
procedure TWebServer.PushMicSamples(P: PSingle; N: Integer);
|
||||||
// Прокладка между Opus-декодером и FOnWebMic с pre-roll cushion.
|
// Прокладка между Opus-декодером и FlushMicToCallback с pre-roll cushion.
|
||||||
// Первые MIC_PREROLL_SAMP сэмплов TX-сессии копим в FMicPreRoll и сливаем
|
// Первые MIC_PREROLL_SAMP сэмплов TX-сессии копим в FMicPreRoll и сливаем
|
||||||
// одним блоком — это даёт FTXMicRing запас глубины ~40мс на джиттер.
|
// одним блоком — это даёт FTXMicRing запас глубины ~40мс на джиттер.
|
||||||
// Дальше всё пушим напрямую.
|
// После pre-roll все сэмплы уходят через FlushMicToCallback, которая
|
||||||
|
// выравнивает поток по границе 512 сэмплов.
|
||||||
var
|
var
|
||||||
Want, Remainder: Integer;
|
Want, Remainder: Integer;
|
||||||
P2: PSingle;
|
P2: PSingle;
|
||||||
@@ -559,22 +603,24 @@ begin
|
|||||||
Want := MIC_PREROLL_SAMP - FMicPreRollPos;
|
Want := MIC_PREROLL_SAMP - FMicPreRollPos;
|
||||||
if N <= Want then
|
if N <= Want then
|
||||||
begin
|
begin
|
||||||
|
// Pre-roll ещё не набран: просто копим
|
||||||
Move(P^, FMicPreRoll[FMicPreRollPos], N * SizeOf(Single));
|
Move(P^, FMicPreRoll[FMicPreRollPos], N * SizeOf(Single));
|
||||||
Inc(FMicPreRollPos, N);
|
Inc(FMicPreRollPos, N);
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
|
// Pre-roll заполнен: сливаем накопленный буфер + остаток пакета
|
||||||
Move(P^, FMicPreRoll[FMicPreRollPos], Want * SizeOf(Single));
|
Move(P^, FMicPreRoll[FMicPreRollPos], Want * SizeOf(Single));
|
||||||
FMicPreRollPos := MIC_PREROLL_SAMP;
|
FMicPreRollPos := MIC_PREROLL_SAMP;
|
||||||
FOnWebMic(@FMicPreRoll[0], MIC_PREROLL_SAMP);
|
FlushMicToCallback(@FMicPreRoll[0], MIC_PREROLL_SAMP);
|
||||||
Remainder := N - Want;
|
Remainder := N - Want;
|
||||||
P2 := P;
|
P2 := P;
|
||||||
Inc(P2, Want);
|
Inc(P2, Want);
|
||||||
FOnWebMic(P2, Remainder);
|
FlushMicToCallback(P2, Remainder);
|
||||||
end;
|
end;
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
FOnWebMic(P, N);
|
FlushMicToCallback(P, N);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TWebServer.UnloadOpus;
|
procedure TWebServer.UnloadOpus;
|
||||||
@@ -1006,9 +1052,13 @@ begin
|
|||||||
begin
|
begin
|
||||||
NowTick := GetTickCount64;
|
NowTick := GetTickCount64;
|
||||||
if (FMicLastTick = 0) or ((NowTick - FMicLastTick) > MIC_IDLE_MS) then
|
if (FMicLastTick = 0) or ((NowTick - FMicLastTick) > MIC_IDLE_MS) then
|
||||||
|
begin
|
||||||
// Новая TX-сессия (первый пакет или длинный простой):
|
// Новая TX-сессия (первый пакет или длинный простой):
|
||||||
// сбрасываем pre-roll, PLC не применяем — нет состояния для предсказания.
|
// сбрасываем pre-roll и staging, PLC не применяем —
|
||||||
FMicPreRollPos := 0
|
// нет состояния для предсказания.
|
||||||
|
FMicPreRollPos := 0;
|
||||||
|
FMicStageLen := 0;
|
||||||
|
end
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
// PLC для опоздавших/потерянных кадров по wall-clock.
|
// PLC для опоздавших/потерянных кадров по wall-clock.
|
||||||
|
|||||||
Reference in New Issue
Block a user