mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Web TX mic: gain slider, jitter buffer, Opus PLC, user-gesture fixes
WebPageHtml.pas:
- Add MIC gain slider (0..300%) with localStorage persistence; inserts
Web Audio GainNode between MediaStreamSource and Opus encoder.
- Disable autoGainControl in getUserMedia so the manual gain actually works.
- Pre-create+resume AudioContext synchronously inside the MOX click handler
(before any await) so user-gesture context isn't lost during getUserMedia
on macOS/Android.
- Call startTXMic() from the click handler too — the call from applyState
runs in a WS-message context which mobile browsers reject.
- Re-resume txMicACtx on every MOX→TX (Android can suspend it between
cycles) and reuse the existing context instead of recreating per session.
- stopTXMic now a no-op: the previous flag reset created a race that spun
up duplicate pipelines on rapid RX→TX toggles.
WebServer.pas:
- Pre-roll cushion: hold first 40ms of decoded mic samples before flushing
to FTXMicRing, raising steady-state ring depth from ~9ms to ~50ms so
WiFi jitter no longer drains the buffer into silence ("robotic" voice).
- Wall-clock-based Opus PLC: synthesize up to 5 frames via
opus_decode_float(NULL) when packet gap exceeds expected 20ms cadence.
- A gap > 200ms is treated as a new TX session: pre-roll resets, PLC
is skipped (no useful decoder state).
- Local (non-web) mic paths are untouched — they bypass WebOnMic entirely.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+78
-1
@@ -80,6 +80,17 @@ const
|
||||
OPUS_FRAME_SAMP = OPUS_SAMPLE_RATE * OPUS_FRAME_MS div 1000; // 960 samples
|
||||
OPUS_BITRATE = 32000;
|
||||
OPUS_CHANNELS = 1;
|
||||
// ── TX-mic jitter handling (web → server) ──
|
||||
// Pre-roll: при старте новой TX-сессии задерживаем 40мс аудио, чтобы дать
|
||||
// FTXMicRing запас перед началом потребления — иначе при джиттере WiFi
|
||||
// ring мгновенно пустеет и WDSP TX-thread заливает тишину = "робот".
|
||||
MIC_PREROLL_MS = 40;
|
||||
MIC_PREROLL_SAMP = OPUS_SAMPLE_RATE * MIC_PREROLL_MS div 1000; // 1920
|
||||
// Опоздание > MIC_IDLE_MS считаем концом TX-сессии: сбрасываем pre-roll
|
||||
// (на следующий пакет — снова накапливаем cushion).
|
||||
MIC_IDLE_MS = 200;
|
||||
// Максимум подряд синтезированных PLC-кадров (Opus PLC деградирует после ~3–5 кадров).
|
||||
MIC_MAX_PLC = 5;
|
||||
MAX_WS_CLIENTS = 4;
|
||||
WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||
|
||||
@@ -168,6 +179,12 @@ type
|
||||
FOpusDecDecode: TOpus_decode_float;
|
||||
FOpusDecDestroy: TOpus_decoder_destroy;
|
||||
FOnWebMic: TWebMicCB;
|
||||
// ── Jitter buffer для TX-mic от веб-клиента ──
|
||||
// FMicLastTick = 0 → нет активной TX-сессии (на следующий пакет — pre-roll сброс)
|
||||
// FMicPreRollPos < MIC_PREROLL_SAMP → ещё копим pre-roll
|
||||
FMicLastTick: QWord;
|
||||
FMicPreRollPos: Integer;
|
||||
FMicPreRoll: array[0..MIC_PREROLL_SAMP-1] of Single;
|
||||
|
||||
// ── TCP ──
|
||||
FListenSock: TSocket;
|
||||
@@ -262,6 +279,7 @@ type
|
||||
// ── Внутренние методы ──
|
||||
function LoadOpus: Boolean;
|
||||
procedure UnloadOpus;
|
||||
procedure PushMicSamples(P: PSingle; N: Integer);
|
||||
function InitListen: Boolean;
|
||||
procedure AcceptLoop;
|
||||
procedure PushLoop;
|
||||
@@ -525,6 +543,39 @@ begin
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure TWebServer.PushMicSamples(P: PSingle; N: Integer);
|
||||
// Прокладка между Opus-декодером и FOnWebMic с pre-roll cushion.
|
||||
// Первые MIC_PREROLL_SAMP сэмплов TX-сессии копим в FMicPreRoll и сливаем
|
||||
// одним блоком — это даёт FTXMicRing запас глубины ~40мс на джиттер.
|
||||
// Дальше всё пушим напрямую.
|
||||
var
|
||||
Want, Remainder: Integer;
|
||||
P2: PSingle;
|
||||
begin
|
||||
if not Assigned(FOnWebMic) or (N <= 0) then Exit;
|
||||
if FMicPreRollPos < MIC_PREROLL_SAMP then
|
||||
begin
|
||||
Want := MIC_PREROLL_SAMP - FMicPreRollPos;
|
||||
if N <= Want then
|
||||
begin
|
||||
Move(P^, FMicPreRoll[FMicPreRollPos], N * SizeOf(Single));
|
||||
Inc(FMicPreRollPos, N);
|
||||
end
|
||||
else
|
||||
begin
|
||||
Move(P^, FMicPreRoll[FMicPreRollPos], Want * SizeOf(Single));
|
||||
FMicPreRollPos := MIC_PREROLL_SAMP;
|
||||
FOnWebMic(@FMicPreRoll[0], MIC_PREROLL_SAMP);
|
||||
Remainder := N - Want;
|
||||
P2 := P;
|
||||
Inc(P2, Want);
|
||||
FOnWebMic(P2, Remainder);
|
||||
end;
|
||||
end
|
||||
else
|
||||
FOnWebMic(P, N);
|
||||
end;
|
||||
|
||||
procedure TWebServer.UnloadOpus;
|
||||
begin
|
||||
if FOpusReady and Assigned(FOpusDestroy) and (FOpusEnc <> nil) then
|
||||
@@ -793,6 +844,10 @@ var
|
||||
RawLen: Integer;
|
||||
PcmBuf: array[0..5759] of Single; // 120ms max @ 48kHz (для RX TX-mic)
|
||||
Decoded: Integer;
|
||||
NowTick: QWord;
|
||||
MissedFrames: Integer;
|
||||
PlcDecoded: Integer;
|
||||
k: Integer;
|
||||
begin
|
||||
WsHandled := False;
|
||||
RawLen := 0;
|
||||
@@ -948,10 +1003,32 @@ begin
|
||||
Assigned(FOpusDec) and Assigned(FOpusDecDecode) and
|
||||
Assigned(FOnWebMic) then
|
||||
begin
|
||||
NowTick := GetTickCount64;
|
||||
if (FMicLastTick = 0) or ((NowTick - FMicLastTick) > MIC_IDLE_MS) then
|
||||
// Новая TX-сессия (первый пакет или длинный простой):
|
||||
// сбрасываем pre-roll, PLC не применяем — нет состояния для предсказания.
|
||||
FMicPreRollPos := 0
|
||||
else
|
||||
begin
|
||||
// PLC для опоздавших/потерянных кадров по wall-clock.
|
||||
// expected_packets ≈ elapsed_ms / 20ms; out of them один — текущий,
|
||||
// остальные считаем "потерянными" и синтезируем Opus PLC.
|
||||
MissedFrames := Integer((NowTick - FMicLastTick) div OPUS_FRAME_MS);
|
||||
if MissedFrames > 0 then Dec(MissedFrames);
|
||||
if MissedFrames > MIC_MAX_PLC then MissedFrames := MIC_MAX_PLC;
|
||||
for k := 1 to MissedFrames do
|
||||
begin
|
||||
// opus_decode_float(dec, NULL, 0, pcm, frame_size, 0) → PLC frame
|
||||
PlcDecoded := FOpusDecDecode(FOpusDec, nil, 0,
|
||||
@PcmBuf[0], OPUS_FRAME_SAMP, 0);
|
||||
if PlcDecoded > 0 then PushMicSamples(@PcmBuf[0], PlcDecoded);
|
||||
end;
|
||||
end;
|
||||
FMicLastTick := NowTick;
|
||||
Decoded := FOpusDecDecode(FOpusDec, @Payload[1], PayLen - 1,
|
||||
@PcmBuf[0], Length(PcmBuf), 0);
|
||||
if Decoded > 0 then
|
||||
FOnWebMic(@PcmBuf[0], Decoded);
|
||||
PushMicSamples(@PcmBuf[0], Decoded);
|
||||
end;
|
||||
end;
|
||||
$08: // Close
|
||||
|
||||
Reference in New Issue
Block a user