diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 2cc0ca4..0e82d71 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -1135,9 +1135,18 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str): _pf.write("%d pump_start\n" % int(_time.time() * 1000)) _last_lat = None _last_proc = None + _hb_last = _time.time() try: while True: try: + # V46: heartbeat — mỗi ~100ms; gap giữa 2 pump_hb liên tiếp + # > 300ms = event loop engine bị freeze (cause toàn cục stall + # cả 2 session — bridge idle chờ input). Phân biệt với client + # stall (wl_gap/main_in_gap) và bridge stall (pump_gap). + _hb_now = _time.time() + if _pf is not None and (_hb_now - _hb_last) >= 0.1: + _hb_last = _hb_now + _pf.write("%d pump_hb" % int(_hb_now * 1000) + chr(10)) sess.flush_input_stale() # input lẻ kẹt trong pending → publish _lats = sess.get_latencies() _total = sum(_lats.values()) if _lats else 0 diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 2ba3c4c..a0fa184 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -303,6 +303,7 @@ window.__fxRtQueueLen = function () { // V28: do jank — play_start timestamp; play_rAF / play_first_audio log delta. let _dbgPlayStartMs = 0; let _dbgLastClearMs = 0; +let _inLastIn = 0; // V46: thời điểm 'in' handler cuối — đo gián đoạn main thread let _dbgFirstAudioPending = false; let _dbgFirstAudiblePending = false; // V40: real PDC — freeze playhead theo latency đo được (fx_play_first self-calib). @@ -2074,6 +2075,14 @@ async function fxRtStart() { return; } if (d.type !== 'in') return; + // V46: main_in_gap — main thread nhận 'in' cách >30ms = main thread bị + // block (rAF/React/GC) → input delivery đứt → engine đói → wobble. Kết + // hợp wl_gap (audio thread) + pump_hb (engine) phân biệt lớp gây stall. + const _inNow = performance.now(); + if (_inLastIn && (_inNow - _inLastIn) > 30) { + try { _dbgLog('main_in_gap', Math.round(_inNow - _inLastIn)); } catch (e) {} + } + _inLastIn = _inNow; // Gửi NGAY, không pace setTimeout: input pacing = đồng hồ audio worklet // (1 frame/21.33ms), không phải timer main-thread. Timer bận/trễ → input // gửi trễ → engine/bridge chỉ sản xuất output khi có input → cushion cạn @@ -2165,7 +2174,9 @@ function fxRtStop(silent) { // ≈ 1792) + FILL buffer 48 block (12288) = 55 block ≈ 14080 (~293ms @48k). Giữ // đồng bộ với sf-fx-realtime.js. FX_RT_START_EST_MS: ước lượng khởi đầu khi // chưa có phép đo (fx_play_first) — dùng cho freeze playhead thay V39 0.31s. -const FX_RT_ROUNDTRIP_SAMPLES = 14080; +// V46: FILL 48→96 → RTT = 7 + 96 = 103 block ≈ 26368 (~549ms @48k). Đồng bộ +// sf-fx-realtime.js V46 (hấp thụ stall ~500ms u2 08:16:44.8). +const FX_RT_ROUNDTRIP_SAMPLES = 26368; const FX_RT_START_EST_MS = 450; // G3: anchor processed — playhead = startOffset + (processed - anchor - latency)/sr // (da tru latency -> bam vi tri mau NGHE DUOC; pre-roll tu giu tai startOffset — diff --git a/app/static/js/sf-fx-realtime.js b/app/static/js/sf-fx-realtime.js index 2b90b7f..e9df348 100644 --- a/app/static/js/sf-fx-realtime.js +++ b/app/static/js/sf-fx-realtime.js @@ -29,10 +29,10 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { super(); this.BLOCK = 256; // FXRT_BLOCK this.BURST = 4; // số block gom 1 frame (bridge batch) - this.FILL = 48; // V45: 48 block ~256ms @48k: hấp thụ stall OS-level ~200ms (V44 21:17:37.9: engine pump 198ms + main 196ms cùng lúc → u=2 underrun → lag 20-30s). PDC 14080 = (7+48)*256 (fxRtApplyPdc). V44 24 -> cushion 128ms < stall 196ms -> underrun. V45 48 -> cushion 256ms (margin ~60ms). + this.FILL = 96; // V46: 96 block ~512ms @48k: hấp thụ stall ~500ms (u2 08:16:44.8: pump deficit 723ms, event gap 507ms → V45 cushion 256ms cạn → silence ~500ms → wobble). PDC 26368 = (7+96)*256 (fxRtApplyPdc). V45 48 -> cushion 256ms < deficit 723ms -> underrun. V46 96 -> cushion ~512ms (margin ~200ms vs event 507ms; deficit cluster vẫn có thể underrun ngắn — chấp nhận, content jump giữ policy V24). this.RESUME_FILL = 24; // V26: re-buffer khi resume SAU STOP (flush_for_stop) — 16 block ~85ms (V43: = FILL) → play kế tiếp bắt đầu nhanh; underrun-stall vẫn dùng FILL (crackle/wobble). this.resumeFill = this.FILL; // mức re-buffer khi resume sau flush — stop: RESUME_FILL, underrun-stall: FILL - this.CAP = 56; // V45: = FILL+8: cap queue chặn overflow (~300ms) + this.CAP = 104; // V46: = FILL+8: cap queue chặn overflow (~550ms) this.HOLD_FADE = 4; // 4 block ~21ms: fade 1→0 sau HOLD hết (hết click cắt cứng) this.holdFadeLeft = 0; // mẫu còn lại của fade-out khi underrun this.silenced = false; // đã fade về 0 → im lặng; resume phải fade-in, không hold lại @@ -171,6 +171,8 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { this.dbgInFirst = false; this.dbgOutFirst = false; this.dbgPlayFirst = false; + this._wlLast = 0; // V46: thời điểm process() lần trước — đo gián đoạn audio thread + this._wlGapAt = 0; // V46: rate-limit wl_gap 1/s } process(inputs, outputs) { const input = inputs[0]; @@ -179,6 +181,17 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { const outL = output[0]; const outR = output[1]; + // V46: wl_gap — audio thread bị gián đoạn (process() cách >30ms, thường do + // main thread block chia sẻ buffer/GC) → dbg log. Rate-limit 1/s. + if (this._wlLast > 0) { + const _wlGap = currentTime - this._wlLast; + if (_wlGap > 0.030 && (currentTime - this._wlGapAt) > 1.0) { + this._wlGapAt = currentTime; + this.port.postMessage({ type: 'dbg', tag: 'wl_gap', t: Math.round(currentTime * 1000), g: Math.round(_wlGap * 1000) }); + } + } + this._wlLast = currentTime; + // Góp input thành burst 4 block (1024 mẫu) → gửi main thread // (transferable buffer). Main thread tách 4 WS frame → bridge batch 4. if (input && input[0]) {