fix(fx-rt): het crackle master FX - engine gui block ASAP + worklet FILL buffer 8

_pump_output bo pace realtime (truoc: moi block send cach nhau block_sec lam
client queue ~1 -> jitter bridge/WS/GC -> queue can -> zero-fill crackle).
Gio gui het burst ASAP; worklet sf-fx-realtime them FILL=8: chi phat khi
outQueue >= 8 block, underrun -> re-buffer (im lang cho du FILL) thay vi
zero-fill lien tuc. Cap 32 giu nguyen. PDC: FX_RT_ROUNDTRIP_SAMPLES 1792->3840
(RTT ~7 block + FILL 8 block = 15 block @48k) de dry-bypass path khong lech
pha voi mastered. Cache-buster v=202608212401 (precompiled + worklet).
Rebuild PyInstaller + copy 3 noi deploy. Verify: probe crackle3 25s Ozone
rebuffer_events=0 (zero_fill 12 block = startup fill), picker UI PASS (59 VST).
This commit is contained in:
2026-08-23 12:50:11 +07:00
parent 9bb9a234b0
commit 6edfe1db32
5 changed files with 56 additions and 44 deletions
+8 -11
View File
@@ -1095,17 +1095,14 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
if not blks:
await asyncio.sleep(0.002)
continue
# Pace theo deadline (không sleep cố định sau mỗi send): thời
# gian send_bytes (WS → browser) CỘNG sleep cố định làm chu kì
# mỗi block > block_sec → engine drain chậm hơn realtime
# (đo: outRead 131/s thay vì 187.5/s → worklet zero-fill crackle).
# Gửi mỗi block cách nhau đúng block_sec kể từ mốc t0.
_t0 = _loop.time()
for _i, blk in enumerate(blks):
await websocket.send_bytes(blk)
_dt = _t0 + (_i + 1) * block_sec - _loop.time()
if _dt > 0:
await asyncio.sleep(_dt)
# 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:
await websocket.send_bytes(_blk)
except Exception:
pass
pump_task = asyncio.create_task(_pump_output())
+7 -6
View File
@@ -1711,7 +1711,7 @@ async function fxRtStart() {
fxRt.sessionId = st.session_id;
fxRtGuiPushShm();
try {
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212400`);
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212401`);
fxRt.node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
const u = new URL(API_BASE_URL);
const wsProto = u.protocol === 'https:' ? 'wss://' : 'ws://';
@@ -1819,10 +1819,11 @@ function fxRtStop(silent) {
// Master chain (worklet → bridge VST3) trễ ~2 block round-trip + latency
// plugin. Track bypass mastering đi dry path (dryInput→dryOutput→output) KHÔNG
// qua worklet → lệch pha với mastered. fxRtApplyPdc trễ dry path đúng lượng
// để align. FX_RT_ROUNDTRIP_SAMPLES là ước lượng (~2 block 256 @44.1k) —
// ponytail: đo RTT thật (tag seq trên block) khi cần sample-accurate.
// Batching 4 block làm RTT ~7 block (~40ms @48k) → 1792.
const FX_RT_ROUNDTRIP_SAMPLES = 1792;
// để align. FX_RT_ROUNDTRIP_SAMPLES là ước lượng — ponytail: đo RTT thật
// (tag seq trên block) khi cần sample-accurate.
// RTT = batching 4 block (~7 block @48k ≈ 1792) + FILL buffer 8 block (2048)
// — worklet chỉ phát khi queue ≥ FILL → latency tĩnh thêm FILL → 15 block.
const FX_RT_ROUNDTRIP_SAMPLES = 3840;
function fxRtApplyPdc() {
const ctx = getAudioContext();
const bus = masterBus;
@@ -23679,7 +23680,7 @@ const App = () => {
fxRtTracks[key] = entry;
fxRtGuiPushShm();
try {
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212400`);
await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608212401`);
const node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] });
entry.node = node;
const u = new URL(API_BASE_URL);
File diff suppressed because one or more lines are too long
+22 -9
View File
@@ -11,26 +11,34 @@
// (amortize overhead: 14-19ms/4 block ≈ 3.5-4.75ms/block) → đủ 187.5/s.
// Latency thêm ~4 block (~21ms) — worklet outQueue 32 block hấp thụ.
//
// Latency ~2 block; backlog > 32 block (~190ms) → drop block cũ nhất. Cap cao
// hấp thụ jitter main thread (rAF renderFrame nặng khi mở MASTERING PANEL) mà
// không drop. ponytail: thay drop bằng adaptive stretch khi cần chất lượng cao.
// FILL (hết crackle do jitter): không phát ngay khi có block đầu — chờ queue
// ≥ FILL (8 block ~43ms @48k) rồi mới phát. Engine gửi ASAP (không pace
// realtime) → client tích lũy buffer an toàn; mọi jitter (bridge process VST
// biến thiên, GC, WS, main thread rAF) bị hấp thụ bởi FILL thay vì queue ≈1
// → cạn → zero-fill. Underrun (queue cạn khi đang phát) → re-buffer: im lặng
// chờ đủ FILL rồi phát tiếp (1 lần gián đoạn thay vì zero-fill liên tục).
// Cap 32 block (~170ms) chặn overflow khi bridge nhanh hơn realtime.
// ponytail: thay drop/rebuffer bằng adaptive stretch khi cần chất lượng cao.
class SfFxRealtimeProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.BLOCK = 256; // FXRT_BLOCK
this.BURST = 4; // số block gom 1 frame (bridge batch)
this.FILL = 8; // block buffer tối thiểu trước khi phát
this.CAP = 32; // cap queue chặn overflow
this.bufLen = this.BLOCK * this.BURST;
this.inL = new Float32Array(this.bufLen);
this.inR = new Float32Array(this.bufLen);
this.inCount = 0;
this.outQueue = []; // [{l: Float32Array, r: Float32Array}]
this.outPos = 0;
this.playing = false; // false → im lặng (đang chờ đủ FILL)
this.port.onmessage = (e) => {
const d = e.data;
if (d && d.type === 'set_param') { this.port.postMessage(d); return; }
if (!d || d.type !== 'out' || !d.l || !d.r) return;
this.outQueue.push({ l: d.l, r: d.r });
while (this.outQueue.length > 32) this.outQueue.shift();
while (this.outQueue.length > this.CAP) this.outQueue.shift();
};
}
process(inputs, outputs) {
@@ -63,12 +71,17 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
}
}
// Phát output từ queue; rỗng → zero.
// Phát output: im lặng tới khi queue ≥ FILL; queue cạn khi đang phát
// (underrun) → im lặng chờ đủ FILL (re-buffer) thay vì zero-fill liên tục.
for (let i = 0; i < n; i++) {
if (this.outQueue.length === 0) {
outL[i] = 0;
if (outR) outR[i] = 0;
continue;
if (this.playing && this.outQueue.length === 0) this.playing = false;
if (!this.playing) {
if (this.outQueue.length >= this.FILL) this.playing = true;
else {
outL[i] = 0;
if (outR) outR[i] = 0;
continue;
}
}
const blk = this.outQueue[0];
outL[i] = blk.l[this.outPos];
+1 -1
View File
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
<script src="/static/js/app.precompiled.js?v=202608212400" defer></script>
<script src="/static/js/app.precompiled.js?v=202608212401" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {