fix(fxrt): underrun xu ly o worklet (hold+fade), bo fade engine, fix wet bus
- sf-fx-realtime.js: underrun -> hold block cuoi (HOLD) roi fade-out 4 block ~21ms gain 1->0 truoc khi im lang (het click cat cung khi main-thread stall chan ca 2 huong - engine khong the gui fade toi kip); resume giua fade -> fade-in nhanh - plugins.py: bo fade-out/fade-in engine (den tre vo dung + fade-in sau stall tao dip dinh ky = vibrato/wow&flutter), giu to_thread(time.sleep) poll 2ms - app.jsx/precompiled/index.html: fix master wet bus (no dry lai) + latency const FX_RT_ROUNDTRIP_SAMPLES + bump worklet cache-buster v=202608241800 - fxrt_ws_probe.py: consumer stall (mo hinh dung app that), WorkletSim holdfade mode Probe consumer-stall (app that): light 5/5 hard click 0.245-0.285 -> hard=0; heavy: recv=sent (het fade thua), hard=0
This commit is contained in:
+7
-63
@@ -1097,38 +1097,16 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
|
||||
# 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
|
||||
# zero-fill crackle. Worklet cap 48 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)
|
||||
# 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])
|
||||
# UNDERRUN do stall (main-thread browser block GUI/rAF/React) xử lí Ở
|
||||
# CLIENT: worklet lặp block cuối (HOLD) rồi fade-out ~21ms
|
||||
# (sf-fx-realtime.js). Engine KHÔNG fade — fade gửi từ engine không thể
|
||||
# tới worklet kịp khi main-thread stall chặn cả 2 hướng (fade nằm trong
|
||||
# browser queue, tới sau khi worklet đã cạn buffer → click vẫn còn), và
|
||||
# fade-in sau stall tạo dip âm lượng định kỳ = vibrato/wow&flutter.
|
||||
_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:
|
||||
try:
|
||||
sess.flush_input_stale() # input lẻ kẹt trong pending → publish
|
||||
@@ -1145,47 +1123,13 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
|
||||
pass
|
||||
blks = sess.read_output()
|
||||
if not blks:
|
||||
_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
|
||||
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())
|
||||
|
||||
Reference in New Issue
Block a user