fix(engine): giam jitter bridge timer va fade-out khi transport stall

- RealtimeFxLoop: timeBeginPeriod(1) + sleep_until thay Sleep(1) -> loopMax 32.9->24.8ms
- plugins.py _pump_output: stall-fade 8 block gain 1->0 khi gap > 5.5*block_sec (~29ms),
  burst send, fade-in 0->1 khi resume; poll dung asyncio.to_thread(time.sleep) vi asyncio.sleep
  bi cap 15.6ms tren Win11 (IOCP) du timeBeginPeriod
- fx_realtime.py: stats dem publish/drop/stale/read cho diagnostics
- rebuild bridge binary
This commit is contained in:
2026-08-24 10:40:21 +07:00
parent 708e7de32b
commit 21130858d6
5 changed files with 135 additions and 14 deletions
+70 -13
View File
@@ -1093,14 +1093,40 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
# nếu chỉ drain trong vòng receive_bytes, output thừa kẹt lại trong SHM ring
# (client ngừng gửi → server block ở receive → âm tắc).
async def _pump_output():
# Pace mỗi block theo tốc độ âm thật (256/sr ~ 5.8ms): bridge sản xuất
# output theo burst khi nhận input dồn; bắn toàn bộ cùng lúc → worklet
# overflow drop block cũ nhất → âm ngắt quãng.
# Gửi ASAP (không pace theo realtime): bridge sản xuất burst theo batch
# 4 → gửi hết ngay → CLIENT tích lũy buffer (worklet FILL) hấp thụ
# jitter bridge/WS. Pace đúng realtime trước đây làm client queue ≈1 →
# bất kỳ jitter (bridge process VST biến thiên, GC, WS) → queue cạn →
# zero-fill crackle. Worklet cap 32 block chặn overflow khi bridge
# nhanh hơn.
# StALL-FADE (fix crackle nặng khi load VST FX): transport stall (browser
# main-thread block GUI/rAF/React) làm bridge ngừng sản xuất output →
# worklet hết buffer (FILL=24 ~128ms) → HOLD 2 block → CẮT cứng xuống
# silence → click. Khi nhận ra output ngừng > ngưỡng → gửi fade-out:
# lặp block real cuối với gain 1→0 qua 8 block (~42ms, pace realtime).
# Worklet hết buffer nhưng nội dung đã về ~0 → hold/silence không click.
# Output trở lại → fade-in 8 block đầu 0→1 (hết click resume).
import struct as _struct
block_sec = (sess.block_size or 256) / max(int(sess.sample_rate or 44100), 1)
# Gap 8 (PDC realtime): drain latency chain tu SHM ring (bridge bao qua
# REPORT_LATENCY per-slot) -> push text frame khi tong doi. Client dung
# de bu delay dry-bypass path (app.jsx fxRtApplyPdc).
# Ngưỡng stall: > batch period (4*block_sec) + jitter; 5.5*block_sec ≈ 29.3ms
# (batch gap thường ~21.3ms + jitter bridge ~2ms sau fix timer 1ms + wakeup
# pump hiếm ~8ms). 6x (32ms) bỏ sót case queue worklet thấp lúc recovery;
# 5x false-fade khi gap 29ms không phải stall thật. 5.5x dung hòa.
_stall_s = 5.5 * block_sec
_fade_blocks = 8
_fade_fmt = '<%df' % ((sess.block_size or 256) * 2)
def _gain(blk, g):
if g >= 1.0: return blk
if g <= 0.0: return b'\x00' * len(blk)
vals = _struct.unpack(_fade_fmt, blk)
return _struct.pack(_fade_fmt, *[v * g for v in vals])
_last_lat = None
_last_blk = None
_last_out = 0.0
_had_out = False
_fading = False
_fade_i = 0
_fade_in = 0
try:
_loop = asyncio.get_event_loop()
while True:
@@ -1119,16 +1145,47 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
pass
blks = sess.read_output()
if not blks:
await asyncio.sleep(0.002)
_now = _loop.time()
_gap = _now - _last_out if _had_out else 0.0
if (_had_out and not _fading and _fade_in <= 0 and
_last_blk is not None and _fade_i == 0 and
_gap > _stall_s):
_fading = True
_fade_i = 0
if _fading and _last_blk is not None and _fade_i < _fade_blocks:
# Gửi BURST toàn bộ fade-out (không pace realtime): worklet
# queue (FILL 24) hấp thụ 8 block; fade frames xếp sau buffer
# real → worklet phát dốc 1→0 khi hết buffer → silence ~0.
# Burst cũng phủ case queue worklet THẤP lúc recovery (fade
# đến trước khi queue cạn, thay vì chậm 42ms như pace thật).
while _fade_i < _fade_blocks:
# gain (8-i-1)/7: block cuối đúng 0.0 → hold/silence ~0
await websocket.send_bytes(_gain(
_last_blk,
(_fade_blocks - 1 - _fade_i) / (_fade_blocks - 1)))
_fade_i += 1
_last_out = _loop.time()
_fading = False # đã về ~0 → worklet tự silence
continue
# to_thread(time.sleep): asyncio.sleep trên Windows bị cap
# 15.6ms (GQCS/IOCP, Win11 cap nền) dù timeBeginPeriod;
# time.sleep dùng high-res waitable timer -> poll đúng 2ms.
await asyncio.to_thread(_time.sleep, 0.002)
continue
# Gửi ASAP (không pace theo realtime): bridge sản xuất burst
# theo batch 4 → gửi hết ngay → CLIENT tích lũy buffer
# (worklet FILL) hấp thụ jitter bridge/WS. Pace đúng realtime
# trước đây làm client queue ≈1 → bất kỳ jitter (bridge process
# VST biến thiên, GC, WS) → queue cạn → zero-fill crackle.
# Worklet cap 32 block chặn overflow khi bridge nhanh hơn.
for _blk in blks:
if _fading or _fade_i >= _fade_blocks:
# resume (giữa fade hoặc sau fade xong) → reset + fade-in
_fading = False
_fade_i = 0
_fade_in = _fade_blocks
if _fade_in > 0:
# gain (8-i-1)/7 dốc 0→1: block cuối full
_blk = _gain(_blk, 1.0 - (_fade_in - 1) / _fade_blocks)
_fade_in -= 1
await websocket.send_bytes(_blk)
_last_blk = _blk
_last_out = _loop.time()
_had_out = True
except Exception:
pass
pump_task = asyncio.create_task(_pump_output())