Files
SonicForgeStudio/app/static/js/sf-fx-realtime.js
T

242 lines
12 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 64 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): V23 — KHÔNG lặp block cuối (lặp tạo
// jump pha ở điểm vào hold + mỗi wrap → click), KHÔNG zero-fill: fade-out
// tuyến tính từ mẫu output cuối (HOLD_FADE block ~21ms) tới im lặng; resume
// NGAY khi có block mới (không đợi đủ FILL): đã im lặng → fade-in từ 0, đang
// giữa fade → crossfade 64 mẫu từ mẫu hiện tại (hết jump). Cap-drop (queue
// tràn sau stall) crossfade 64 mẫu (hết click) — giống bridgeAudioWorklet.
// Cap 64 block (~341ms) chặn overflow khi bridge nhanh hơn realtime.
// ponytail: thay fade-out bằng adaptive stretch khi cần chất lượng cao hơn.
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 @48k: buffer khởi đầu — hấp thụ jitter main-thread (GUI decode, rAF, VST panel). PDC 7936 = (7+24)*256 (fxRtApplyPdc).
this.CAP = 64; // cap queue chặn overflow (~341ms)
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
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.flushPending = false; // V24: đang chờ engine flush (resume sau underrun) — drop block stale
this.resumeFadeIn = false; // V24: interruption đã xử lí (clear+flush) — resume sẽ fade-in từ 0
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.skipFade = 0; // >0: crossfade 64 mẫu đang chạy sau jump (cap-drop/resume) — hết click
this.skipFromL = 0.0; // mẫu output tại điểm jump — điểm bắt đầu crossfade
this.skipFromR = 0.0;
this.lastOutL = 0.0; // mẫu output cuối — nguồn crossfade + điểm bắt đầu fade-out
this.lastOutR = 0.0;
this.fadeFromL = 0.0; // mẫu cố định khi fade-out underrun (fade tuyến tính tới 0)
this.fadeFromR = 0.0;
this.port.onmessage = (e) => {
const d = e.data;
if (d && d.type === 'set_param') { this.port.postMessage(d); return; }
if (d && d.type === 'flush_done') { this.flushPending = false; return; }
if (!d || d.type !== 'out' || !d.l || !d.r) return;
// V24: đang chờ flush (resume sau underrun/stall) → drop block stale.
// Backlog engine gửi khi worklet im là audio CŨ (chơi lại → lag cộng dồn
// ~1s) — chỉ nhận block MỚI từ sau flush_done.
if (this.flushPending) return;
this.outQueue.push({ l: d.l, r: d.r });
while (this.outQueue.length > this.CAP) {
this.outQueue.shift();
this.statCapDrop++;
// V23: drop cứng = jump mẫu → click. Crossfade 64 mẫu từ mẫu output
// cuối sang block mới; chỉ đặt 1 lần cho đợt drop liên tiếp. outPos=0:
// block mới (front) phải phát từ đầu, không tiếp tục vị trí block cũ.
if (this.skipFade <= 0) {
this.skipFade = 64;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
this.outPos = 0;
}
};
// Telemetry (chẩn đoán crackle trên session thật): đếm sự kiện + báo main
// thread mỗi ~2s. Nếu user bật master FX nghe crackle mà stats cho thấy
// underruns>0 → delivery bị stall (main-thread busy) → nguồn crackle thật.
this.statUnderrun = 0; this.statHold = 0; this.statFade = 0;
this.statSilence = 0; this.statCapDrop = 0; this.statFadeIn = 0;
this.statCalls = 0;
}
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 → fade-out 21ms từ mẫu cuối rồi
// im lặng (V23); resume NGAY khi có block mới (không đợi re-buffer FILL),
// đã im lặng → fade-in, giữa fade → crossfade 64 mẫu (hết jump).
for (let i = 0; i < n; i++) {
this.statCalls++;
if (this.statCalls >= 1500) { // ~2s @48k/128
this.statCalls = 0;
this.port.postMessage({ type: 'stats',
underruns: this.statUnderrun, holds: this.statHold, fades: this.statFade,
silences: this.statSilence, capDrops: this.statCapDrop, fadeIns: this.statFadeIn,
queueLen: this.outQueue.length });
}
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--;
}
if (this.skipFade > 0) {
const t = 1 - this.skipFade / 64;
outL[i] = (this.skipFromL * (1 - t) + blk.l[this.outPos] * t) * this.outGain;
if (outR) outR[i] = (this.skipFromR * (1 - t) + blk.r[this.outPos] * t) * this.outGain;
this.skipFade--;
} else {
outL[i] = blk.l[this.outPos] * this.outGain;
if (outR) outR[i] = blk.r[this.outPos] * this.outGain;
}
this.lastOutL = outL[i];
this.lastOutR = outR ? outR[i] : outL[i];
this.outPos++;
if (this.outPos >= this.BLOCK) {
this.outQueue.shift();
this.outPos = 0;
}
continue;
}
// Underrun giữa stream — V23: fade-out NGAY (không lặp block cuối:
// lặp tạo jump pha ở điểm vào + mỗi wrap → click). Fade tuyến tính từ
// mẫu output cuối tới 0 (21ms) — vào liền mạch (fadeFrom = lastOut),
// hết click cắt cứng. Resume → crossfade/fade-in (xem dưới).
this.playing = false;
this.statUnderrun++;
if (!this.silenced) { // đã im lặng (fade xong) → giữ im lặng
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
this.statHold++;
}
}
// Không phát: resume khi đủ block (lần đầu chờ FILL; sau đó chỉ cần 1
// block — hold che khoảng chờ). V25: sau flush (resumeFadeIn=true) chờ
// re-buffer đủ FILL mới resume — chặn loop underrun→flush→resume-1-block
// →underrun (wobble): 1 block phát hết ~5ms → cạn → flush lại.
if (this.outQueue.length >= (this.hasStarted && !this.resumeFadeIn ? 1 : this.FILL)) {
// V24: interruption (underrun sau stall) — backlog trong queue là audio
// LẠC HẬU (chơi lại → lag cộng dồn ~1s). KHÔNG chơi backlog: clear
// queue + báo main thread flush (engine xóa input/output ring, main
// drop output stale in-flight) → chờ flush_done → resume chỉ với block
// MỚI. flushPending → onmessage drop block stale trong lúc chờ (hết
// race ack). Fade-out đang chạy (nếu có) tiếp tục tới im lặng — hết
// click. resumeFadeIn guard chống re-trigger flush khi ack tới mà fade
// chưa xong. resume → fade-in từ 0 (đã im) hoặc crossfade từ mẫu hiện
// tại (giữa fade) — hết jump.
const wasInterrupted = !this.resumeFadeIn && (this.holdFadeLeft > 0 || this.silenced) && this.hasStarted;
if (wasInterrupted) {
this.outQueue.length = 0;
this.outPos = 0;
this.flushPending = true;
this.resumeFadeIn = true;
this.port.postMessage({ type: 'flush' });
i--;
continue;
}
this.playing = true;
this.outPos = 0;
this.holdFadeLeft = 0;
if (this.resumeFadeIn) {
this.resumeFadeIn = false;
if (this.silenced) {
// Đã im lặng (fade xong / sau flush) → fade-in từ 0 (hết jump ~0→1).
this.outGain = 0;
this.fadeLeft = this.FADE;
this.statFadeIn++;
} else {
// Giữa fade-out → crossfade ngắn từ mẫu hiện tại (không jump về 0).
this.skipFade = 64;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
}
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.holdFadeLeft > 0) {
// Fade-out 1→0 (underrun): fade tuyến tính từ mẫu cuối, im lặng êm.
const g = this.outGain * (this.holdFadeLeft / (this.HOLD_FADE * this.BLOCK));
outL[i] = this.fadeFromL * g;
if (outR) outR[i] = this.fadeFromR * g;
this.holdFadeLeft--;
if (this.holdFadeLeft <= 0) { this.silenced = true; this.statFade++; this.statSilence++; }
this.lastOutL = outL[i];
this.lastOutR = outR ? outR[i] : outL[i];
continue;
}
outL[i] = 0;
if (outR) outR[i] = 0;
this.lastOutL = 0;
this.lastOutR = 0;
}
return true;
}
}
registerProcessor('sf-fx-realtime', SfFxRealtimeProcessor);