5ca2027966
- 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
167 lines
7.8 KiB
JavaScript
167 lines
7.8 KiB
JavaScript
// 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.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;
|
|
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.hasStarted) {
|
|
// Resume giữa fade-out: gain đang <1 → fade-in nhanh tránh jump ~0→1
|
|
this.outGain = 0;
|
|
this.fadeLeft = this.FADE;
|
|
}
|
|
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--;
|
|
continue;
|
|
}
|
|
outL[i] = 0;
|
|
if (outR) outR[i] = 0;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
registerProcessor('sf-fx-realtime', SfFxRealtimeProcessor);
|