V24: fix master-chain crackle, 1s lag, solo bypass

- worklet: FILL 48->24 (PDC 7936 dung), flush protocol khi resume sau interruption
  (clear queue + fade-in/crossfade chi voi block moi), drop block stale
- engine fx_realtime: _flush_input_locked NO-DROP (ring day -> giu pending, cap 2048),
  flush_all() dong bo in/out khi worklet resume
- app.jsx: WS flush_done + flushing guard, fallback timer 500ms
- plugins.py: xu ly {cmd:flush} -> flush_done
- index.html: bump v param
This commit is contained in:
2026-08-26 20:15:13 +07:00
parent 28a06134f9
commit f38e500326
6 changed files with 148 additions and 31 deletions
+31 -11
View File
@@ -130,7 +130,8 @@ class FxRealtimeSession:
self.latencies = {} # {slot_idx: samples}
# Diagnostics (FXRT crackle hunt): drop/publish counters.
self.stats = {'publish': 0, 'drop_batch': 0, 'drop_blocks': 0,
'stale_flush': 0, 'read_blocks': 0, 'read_empty': 0}
'stale_flush': 0, 'read_blocks': 0, 'read_empty': 0,
'flush': 0}
self._spawn()
# ── process ─────────────────────────────────────────────────────────────
@@ -264,26 +265,34 @@ class FxRealtimeSession:
def _flush_input_locked(self):
"""Publish pending vào input ring. Ghi data trước, in_write += n sau →
bridge đọc in_write 1 lần → thấy nguyên batch (take=4)."""
bridge đọc in_write 1 lần → thấy nguyên batch (take=4). V24 NO-DROP:
ring đầy → GIỮ pending (pump flush_input_stale 2ms retry) — không drop
→ hết content gap + hết race pointer (drop cũ cập in_read vượt in_write
→ avail > in_slots → ghi đè slot bridge đang đọc → torn frame). Cap
2048 block (~11s) chống tích lũy vô hạn khi client dồn liên tục."""
if not self._in_pending:
return
h = self.h
if len(self._in_pending) > 2048:
drop = len(self._in_pending) - 2048
del self._in_pending[:drop] # drop block CŨ NHẤT (front)
self.stats['drop_blocks'] += drop
frames = self._in_pending
self._in_pending = []
self._in_pending_t0 = None
avail = h.in_slots - (h.in_write - h.in_read)
self.stats['publish'] += len(frames)
if avail <= 0:
h.in_read += len(frames) # ring full → drop cả batch
self.stats['drop_batch'] += len(frames)
# ring đầy → giữ nguyên pending; pump retry khi bridge drain
return
n = min(len(frames), avail)
drop = len(frames) - n
if drop > 0:
h.in_read += drop # drop frame cũ nhất
self.stats['drop_blocks'] += drop
publish = frames[:n]
if n < len(frames):
del frames[:n] # publish n block CŨ NHẤT, giữ phần thừa làm pending
self._in_pending_t0 = time.time() if frames else None
else:
self._in_pending = []
self._in_pending_t0 = None
for i in range(n):
L, R = frames[i + drop]
L, R = publish[i]
slot = (h.in_write + i) & (h.in_slots - 1)
bl = IN_L_OFF // 4 + slot * self.block_size
br = IN_R_OFF // 4 + slot * self.block_size
@@ -299,6 +308,17 @@ class FxRealtimeSession:
self.stats['stale_flush'] += 1
self._flush_input_locked()
def flush_all(self):
"""V24: reset toàn bộ pipeline khi client resume sau stall — xóa
input/output ring + pending → bridge xử lí audio MỚI, không backlog cũ
(lag cộng dồn ~1s sau stall). Gọi khi nhận {cmd:'flush'} qua WS."""
with self._lock:
self._in_pending = []
self._in_pending_t0 = None
self.h.in_read = self.h.in_write
self.h.out_read = self.h.out_write
self.stats['flush'] += 1
def read_output(self):
"""Drain output ring → list bytes (mỗi block 2048 bytes interleaved)."""
out = []