// sf-fx-realtime.js — AudioWorkletProcessor nối WebAudio → SHM FX engine. // process() gom 128-sample quantum thành block 256 stereo float32 (khớp // FXRT_BLOCK trong FxRealtimeIPC.h) → postMessage cho main thread gửi WS; // nhận block đã xử lý từ main thread → queue → phát ra output. Queue rỗng → // zero fill (WS chưa kịp round-trip). // // BURST (fix lag/giật/crackle): gom 4 block (1024 mẫu) vào 1 frame rồi post. // Bridge VST3 (fx_vst_bridge) chạy chậm hơn realtime khi take=1 (256 mẫu: // ~5.5ms/call > budget 5.33ms → 179/s < 187.5/s → engine drop input → // crackle). Gửi burst 4 block → bridge take=4 → 1 call process 1024 mẫu // (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ụ. // // FILL (hết crackle do jitter): không phát ngay khi có block đầu — chờ queue // ≥ FILL (24 block ~128ms @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 → ngắt quãng. // UNDERRUN (queue cạn khi đang phát): KHÔNG zero-fill/re-buffer im lặng — lặp // block cuối tối đa HOLD block (~11ms) giữ âm liên tục (hết click/ngắt), rồi // FADE-OUT HOLD_FADE block (~21ms) gain 1→0 trước khi im lặng nếu stall kéo // dài (hết click cắt cứng — engine không thể gửi fade tới kịp khi main-thread // stall chặn cả 2 hướng); resume NGAY khi có block mới (không đợi đủ FILL), // nếu đang giữa fade-out → fade-in nhanh (hết jump ~0→1) + không re-fade sau // lần đầu (tránh tụt volume). // Cap 48 block (~256ms) chặn overflow khi bridge nhanh hơn realtime. // ponytail: thay hold 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 = 24; // 24 block ~128ms: buffer khởi đầu — hấp thụ jitter main-thread (GUI decode, rAF, solo restart) this.CAP = 48; // cap queue chặn overflow (~256ms) this.HOLD = 2; // 2 block ~11ms: lặp block cuối khi underrun (hết click) trước khi im lặng this.HOLD_FADE = 4; // 4 block ~21ms: fade 1→0 sau HOLD hết (hết click cắt cứng) this.lastBlock = null; // block cuối đã phát xong (hold khi underrun) this.holdPos = 0; this.holdLeft = 0; this.holdFadeLeft = 0; // mẫu còn lại của fade-out sau HOLD this.silenced = false; // đã fade về 0 → im lặng; resume phải fade-in, không hold lại 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.hasStarted = false; // đã phát block đầu chưa — FILL chỉ chờ ở lần khởi đầu this.FADE = 512; // fade-in 512 mẫu (~11ms @48k) khi bắt đầu phát/resume — hết click this.outGain = 0; // gain phát hiện tại (0→1 trong fade) this.fadeLeft = 0; // số mẫu còn lại của fade 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 > this.CAP) this.outQueue.shift(); }; } process(inputs, outputs) { const input = inputs[0]; const output = outputs[0]; const n = output[0] ? output[0].length : 128; const outL = output[0]; const outR = output[1]; // 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]) { const inL = input[0]; const inR = input[1] || input[0]; for (let i = 0; i < n; i++) { if (this.inCount < this.bufLen) { this.inL[this.inCount] = inL[i]; this.inR[this.inCount] = inR[i]; } this.inCount++; if (this.inCount >= this.bufLen) { const inter = new Float32Array(this.bufLen * 2); for (let j = 0; j < this.bufLen; j++) { inter[j * 2] = this.inL[j]; inter[j * 2 + 1] = this.inR[j]; } this.port.postMessage({ type: 'in', data: inter.buffer }, [inter.buffer]); this.inCount = 0; } } } // Phát output: chờ queue ≥ FILL trước khi phát lần đầu (hấp thụ jitter // engine/bridge/WS); underrun GIỮA stream → lặp block cuối tối đa HOLD // block (không zero-fill → hết click/ngắt) rồi mới im lặng; resume NGAY // khi có block mới (không đợi re-buffer FILL) + không re-fade sau lần đầu. for (let i = 0; i < n; i++) { if (this.playing) { const blk = this.outQueue[0]; if (blk) { if (this.fadeLeft > 0) { this.outGain = Math.min(1, this.outGain + 1 / this.FADE); this.fadeLeft--; } outL[i] = blk.l[this.outPos] * this.outGain; if (outR) outR[i] = blk.r[this.outPos] * this.outGain; this.outPos++; if (this.outPos >= this.BLOCK) { this.lastBlock = blk; this.outQueue.shift(); this.outPos = 0; } continue; } // Underrun giữa stream — chuyển hold (không cắt zero ngay → hết click). this.playing = false; if (!this.silenced) { // đã im lặng (fade xong) → giữ im lặng, không hold lại block cũ this.holdPos = 0; this.holdLeft = this.HOLD * this.BLOCK; this.holdFadeLeft = this.HOLD_FADE * this.BLOCK; } } // Không phát: resume khi đủ block (lần đầu chờ FILL; sau đó chỉ cần 1 // block — hold che khoảng chờ). if (this.outQueue.length >= (this.hasStarted ? 1 : this.FILL)) { this.playing = true; this.outPos = 0; const wasFading = this.holdFadeLeft > 0 && this.holdLeft <= 0; this.holdLeft = 0; this.holdFadeLeft = 0; if ((wasFading || this.silenced) && this.hasStarted) { // Resume giữa fade-out / sau im lặng: gain đang <1 (hoặc 0) → // fade-in nhanh tránh jump ~0→1 (click sau long stall). this.outGain = 0; this.fadeLeft = this.FADE; } this.silenced = false; if (!this.hasStarted) { this.hasStarted = true; this.outGain = 0; this.fadeLeft = this.FADE; // Báo main thread 1 lần để crossfade dry→fx đúng thời điểm. this.port.postMessage({ type: 'started' }); } i--; continue; } if (this.holdLeft > 0 && this.lastBlock) { outL[i] = this.lastBlock.l[this.holdPos] * this.outGain; if (outR) outR[i] = this.lastBlock.r[this.holdPos] * this.outGain; this.holdPos++; if (this.holdPos >= this.BLOCK) this.holdPos = 0; this.holdLeft--; continue; } if (this.holdFadeLeft > 0 && this.lastBlock) { // Fade-out 1→0 sau HOLD: im lặng êm, hết click cắt cứng. const g = this.outGain * (this.holdFadeLeft / (this.HOLD_FADE * this.BLOCK)); outL[i] = this.lastBlock.l[this.holdPos] * g; if (outR) outR[i] = this.lastBlock.r[this.holdPos] * g; this.holdPos++; if (this.holdPos >= this.BLOCK) this.holdPos = 0; this.holdFadeLeft--; if (this.holdFadeLeft <= 0) this.silenced = true; // fade xong → im lặng thật continue; } outL[i] = 0; if (outR) outR[i] = 0; } return true; } } registerProcessor('sf-fx-realtime', SfFxRealtimeProcessor);