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

409 lines
22 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 (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 → 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; // V44: 24 block ~128ms @48k: buffer khởi đầu — hấp thụ jitter main-thread + stall deschedule bridge 68-138ms. PDC 7936 = (7+24)*256 (fxRtApplyPdc). V43 16 -> cushion ~9 (q 3-9) chua du: stall 68-82ms van gay underrun. V44 len 24 -> cushion ~17 (~90ms).
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 = 32; // V40: cap queue chặn overflow (~170ms)
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 đang chạy sau jump (cap-drop/resume/wrap) — hết click
this.skipFadeTotal = 64; // V42: độ dài crossfade hiện tại (64 mẫu thường; WRAP_FADE 256 tại seam wrap)
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.wrapFadeBlocks = -1; // V42: số block tail pass cũ còn lại sau clear_queue('loop') — đếm ngược để crossfade đúng seam tail→head
this.latencySamples = 0; // G3: latency chain thật (engine qua WS) — align wrap fade theo processed tail
this.wrapMode = false; // V45.3: dang trong wrap (clear_queue loop) — resume underrun bo flush + re-buffer nho
this.WRAP_FADE = 256; // V42: crossfade 256 mẫu (~5.3ms @48k) tại seam wrap — che click jump nội dung end→start (FX path)
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 === 'latency') { this.latencySamples = (d.samples || 0); return; }
if (d && d.type === 'flush_for_stop') {
// V26: user stop — main thread báo xóa audio tail trong queue (audio đã
// xử lí còn phát ~0.3-0.5s sau stop = tiếng "ding"). Fade-out 21ms từ
// mẫu cuối (hết click) → im lặng; báo main flush engine ring (audio
// stale trong WS/SHM không chơi tiếp). Resume lần sau: RESUME_FILL
// (8 block ~43ms, V40: = FILL) — play kế tiếp không chờ trễ.
this.outQueue.length = 0;
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1; // V42: stop — hủy wrap crossfade chờ
this.flushPending = true;
this.resumeFadeIn = true;
this.resumeFill = this.RESUME_FILL;
this.silenced = false;
this.wrapMode = false;
// V30: fade-out tu mau output CUOI (lastOut) — truoc day dung fadeFrom
// cu (mac dinh 0) -> jump mau -> click/crackle ngay khi stop.
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
this.port.postMessage({ type: 'flush' });
return;
}
if (d && d.type === 'clear_queue') {
this.clearMode = d.mode; // V45.19: nho mode de tag fx_seek_first tai dbgPlayFirst
// V34: backlog guard o DAU handler (V33 dat sau mutation -> outQueue.length=0
// chay truoc -> guard luon return, clear van drop queue + fade + re-buffer moi
// lan = V31). Queue nho (< FILL 8 block) -> bo qua hoan toan (khong mutation,
// khong re-buffer) -> het ARM crackle (queue ARM chi 0-8 block, engine ~realtime).
// Queue day (idle lau, engine chay truoc ~32 block) -> clear dung (fix lag 170ms).
// V35: mode:'play' (start/section/pianoroll) LUON clear - queue < FILL luc
// play-start con stale block idle/ARM (~16) -> bo qua => phat audio cu roi
// can queue ~500ms (cold start). mode:'live' (ARM) giu guard het crackle.
// V37 ARM probe: reset dbg flags BEFORE guard so live notes (small queue) also log fx_hop
this.dbgClearAt = currentTime * 1000;
this.dbgInFirst = true;
this.dbgOutFirst = true;
this.dbgPlayFirst = true;
// V39: loop wrap — KHONG wipe: giu queue chay lien tuc (tail pass cu
// pre-roll pass moi qua VST lookahead) -> het wobble dau bai. Chi reset
// dbg flags. mode 'play' van wipe (bao ve audio cu khi seek/cold start).
// V42: dem so block tail pass cu con lai trong queue -> wrap crossfade
// trigger dung luc block dau pass moi len front (seam tail->head).
if (d.mode === 'loop') {
// G3: align processed-tail — tail pass cũ còn in-flight trong bridge
// (FX latency thật từ engine) chưa về queue; đếm đủ block tới seam
// (block đầu pass mới lên front) → crossfade đúng chỗ jump end→start.
// V45.2 (option B): cắt bớt tail cũ giữ KEEP block thay vì giữ nguyên
// FILL 24 (~128ms) → downbeat pass mới không trễ ~110ms. skipFade
// crossfade từ lastOut che jump cắt front; wrapFadeBlocks đếm lại từ
// KEEP để seam crossfade vẫn đúng lúc.
// V45.3: KEEP 4 -> 16 (~85ms): KEEP 4 (~21ms) engine (stall bridge
// 68-138ms sau transport play) khong refill kip -> queue can -> underrun
// -> flush + re-buffer FILL 24 = gap ~110-235ms moi wrap (WORSE hon
// truoc V45.2: downbeat tre 106-128ms nhung khong gap). KEEP 16 che
// stall dien hinh 68-82ms lien tuc (khong gap, downbeat ~85ms); stall
// dai hon roi vao nhanh resume wrap (bo flush + resumeFill 8).
const KEEP = 16;
this.wrapMode = true;
if (this.outQueue.length > KEEP) {
const head = this.outQueue.length - KEEP;
this.outQueue.splice(0, head);
this.outPos = 0;
if (this.playing) {
this.skipFade = this.WRAP_FADE;
this.skipFadeTotal = this.WRAP_FADE;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
}
this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK);
return;
}
if (d.mode === 'seek') {
// V45.18: seek khi play master FX active — wipe queue cu (audio cu
// choi tiep 0-50ms sau click) + resume nhanh RESUME_FILL 8 (~43ms)
// thay vi FILL 24 (~128ms) — het "am tre 1 beat" (hit dau roi vao
// vung cam 128ms). Giong resume wrap V45.4 (proven khong gap).
this.outQueue.length = 0;
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1;
this.wrapMode = false;
this.resumeFadeIn = true;
this.resumeFill = 8;
this.silenced = false;
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
this.port.postMessage({ type: 'dbg', tag: 'fx_seek_clear', t: Math.round(currentTime * 1000), q: 0 });
return;
}
if (d.mode !== 'play' && this.outQueue.length < this.FILL) return;
// V31: play/ARM live note — main thread bao xoa backlog block idle
// (engine render lien tuc khi STOP -> outQueue day ~32 block ~170ms
// audio cu; play moi phai cho phia sau -> lag 0.3-0.5s). KHONG post
// 'flush' (khong chaym engine WS — note-on play vua day xuong se bi
// drop). Resume sau 8 block (~43ms) + fade-out/fade-in tu mau cuoi.
this.outQueue.length = 0;
this.outPos = 0;
this.playing = false;
this.wrapFadeBlocks = -1; // V42: wipe — hủy wrap crossfade chờ (không có tail cũ)
this.wrapMode = false; // V45.3: play/seek — het context wrap
this.resumeFadeIn = true;
// V34: mode phan biet — play re-buffer FILL 8 (~43ms, cushion du hap thu
// engine ramp/jitter dau play); ARM live note giu RESUME_FILL 8 (~43ms) — preview
// nhanh, khong cho FILL 8.
this.resumeFill = (d.mode === 'play') ? this.FILL : this.RESUME_FILL;
this.silenced = false;
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
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 });
if (this.dbgOutFirst) {
this.dbgOutFirst = false;
this.port.postMessage({ type: 'dbg', tag: 'fx_out_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
}
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.skipFadeTotal = 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;
// V32: do latency master FX path — danh dau clear_queue, bao main thread hop dau
// sau clear (in_first / out_first / play_first) → dbg.log delta vs play_start.
this.dbgClearAt = 0;
this.dbgInFirst = false;
this.dbgOutFirst = false;
this.dbgPlayFirst = false;
}
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;
if (this.dbgInFirst) {
this.dbgInFirst = false;
this.port.postMessage({ type: 'dbg', tag: 'fx_in_first', t: Math.round(currentTime * 1000) });
}
}
}
}
// 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 / this.skipFadeTotal;
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;
// V42: seam tail→head — sau khi shift hết N block tail pass cũ, block
// đầu pass mới lên front → crossfade WRAP_FADE từ mẫu cuối (che click
// jump nội dung end→start, FX path). Trigger 1 lần mỗi wrap.
if (this.wrapFadeBlocks > 0) {
this.wrapFadeBlocks--;
if (this.wrapFadeBlocks === 0) {
this.skipFade = this.WRAP_FADE;
this.skipFadeTotal = this.WRAP_FADE;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
this.port.postMessage({ type: 'dbg', tag: 'fx_wrap_xfade', t: Math.round(currentTime * 1000), q: this.outQueue.length });
}
}
}
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.wrapFadeBlocks = -1; // V42: underrun — hủy wrap crossfade chờ (resume qua fade-in/crossfade thường)
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 ? this.resumeFill : 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) {
// V45.3: wrap (loop restart) — engine da transport('play') tu main
// (D17): block moi = audio pass moi, KHONG phai backlog stale (boi
// worklet flush gui 'flush' xuong engine ring = lam mat tac dung D17
// skip-flush o stopAllPlayback -> gap giua seam). Bo flush (round-trip)
// + re-buffer nho hon FILL 24 (~128ms) -> resume sau stall som hon
// (~43ms) -> gap wrap giam tu ~110-235ms xuong con vai chuc ms.
const _wrap = this.wrapMode;
this.outQueue.length = 0;
this.outPos = 0;
this.resumeFadeIn = true;
this.resumeFill = _wrap ? 8 : this.FILL;
this.flushPending = _wrap ? false : true;
if (!_wrap) this.port.postMessage({ type: 'flush' });
i--;
continue;
}
this.playing = true;
this.outPos = 0;
this.holdFadeLeft = 0;
this.wrapMode = false; // V45.3: resume xong — ve che do thuong (underrun sau nay flush nhu cu)
if (this.dbgPlayFirst) {
this.dbgPlayFirst = false;
// V45.19 Bug 1b: seek -> tag fx_seek_first (main thread snap khong can
// guard 50ms — seek resume nhanh ~43ms < 50ms). Wrap/play -> fx_play_first.
this.port.postMessage({ type: 'dbg', tag: (this.clearMode === 'seek' ? 'fx_seek_first' : 'fx_play_first'), t: Math.round(currentTime * 1000), q: this.outQueue.length, m: this.clearMode || '' });
}
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.skipFadeTotal = 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);