461 lines
26 KiB
JavaScript
461 lines
26 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 (40 block ~213ms @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 = 40; // V48: 40 block ~213ms @48k — M18: deficit engine ~3.3% (session Ozone loopAvg 22.06ms > 21.33) can dan cushion V44 24 (~90ms) — stall 68-186ms > 90ms -> underrun dinh ky; len 40 -> cushion ~170ms che stall <170ms, keo dai thoi gian toi underrun. PDC 12032 = (7+40)*256 (fxRtApplyPdc).
|
|
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 = 64; // V48: 48->64 (~341ms) — M18: FILL len 40, CAP phai > FILL + in-flight/burst (~8 block) de khong cap-drop ngay sau resume; latency khoi dau/PDC van do FILL quyet dinh.
|
|
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.stopped = false; // Bug1 echo: sau flush_for_stop dung het output — chi resume khi main gui clear_queue moi
|
|
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 = 2048; // V46.2: 256->2048 (~43ms @48k) — che hole engine restart (~10-16ms) + jump noi dung end→start tai seam (256 qua ngan = giät nhẹ)
|
|
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;
|
|
// M25: nhận MessageChannel port từ main (worker FX realtime) — output
|
|
// đi THẲNG worker→worklet (bỏ hop main thread, hết underrun lớp u1/u10);
|
|
// input burst 'in' cũng gửi thẳng ngược chiều trên cùng port. Track FX
|
|
// path (không có directPort) vẫn dùng node.port như cũ — không đụng.
|
|
if (d && d.type === '__direct') {
|
|
const _dp = (e.ports && e.ports[0]) || (d.port) || null;
|
|
if (_dp) {
|
|
this.directPort = _dp;
|
|
this.directPort.onmessage = (e2) => {
|
|
const d2 = e2.data;
|
|
if (!d2) return;
|
|
if (d2.type === 'out' && d2.l && d2.r) this._enqueueOut(d2.l, d2.r);
|
|
else if (d2.type === 'flush_done') this.flushPending = false;
|
|
};
|
|
}
|
|
return;
|
|
}
|
|
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.stopped = true; // Bug1: drop moi 'out' sau stop — engine van render input dang fade + plugin tail; neu khong drop, queue day RESUME_FILL -> worklet tu resume phat tail = echo sau stop.
|
|
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
|
|
this.stopped = false; // Bug1: play/seek/wrap moi — cho phep nhan out tro lai
|
|
// 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;
|
|
const _preQ = this.outQueue.length; // V46.1 instrument seam
|
|
if (_preQ > KEEP) {
|
|
const head = _preQ - 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;
|
|
}
|
|
}
|
|
// V46.2: countdown = DUNG so block tail (sau splice) — truoc day cong
|
|
// ceil(latency/BLOCK)~137 (estimate 365ms) => crossfade ban tre ~380ms
|
|
// (giua bar1) = vo hieu, seam that di qua khong crossfade. Pipeline lag
|
|
// THUC ~25ms (fx_out_first +8ms sau clear) chu khong phai VST latency.
|
|
this.wrapFadeBlocks = this.outQueue.length;
|
|
// V46.1 instrument seam: pre=queue truoc splice, keep=sau splice, lat=latency est, wfb=countdown toi seam
|
|
this.port.postMessage({ type: 'dbg', tag: 'fx_loop_clear', t: Math.round(currentTime * 1000), pre: _preQ, keep: this.outQueue.length, lat: this.latencySamples || 0, wfb: this.wrapFadeBlocks, playing: this.playing ? 1 : 0 });
|
|
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;
|
|
// M19 Bug A: wake PWR ON sau warm-bypass — worklet rời graph không process
|
|
// nên hasStarted còn true từ lần play trước → không bao giờ gửi 'started'
|
|
// lại → main không crossfade đúng lúc. Reset ở clear mode 'play' → resume
|
|
// sau FILL gửi 'started' lần nữa (giống cold start). Clear 'play' khác khi
|
|
// đã active: main fadedIn=true rồi → 'started' thêm bị bỏ qua, vô hại.
|
|
if (d.mode === 'play') this.hasStarted = false;
|
|
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;
|
|
this._enqueueOut(d.l, d.r);
|
|
};
|
|
// 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;
|
|
}
|
|
// M25: enqueue 1 block output — dùng chung cho node.port (track FX) và
|
|
// directPort (master FX worker→worklet thẳng). Logic giữ nguyên V24/V23.
|
|
_enqueueOut(l, r) {
|
|
if (this.stopped || this.flushPending) return;
|
|
this.outQueue.push({ l: l, r: 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;
|
|
}
|
|
}
|
|
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];
|
|
}
|
|
if (this.directPort) {
|
|
// M25: master FX — input burst đi THẲNG worklet→worker (bỏ hop
|
|
// main; main stall không làm gián đoạn nguồn input → engine không
|
|
// ngừng sản xuất output).
|
|
this.directPort.postMessage({ type: 'in', data: inter.buffer }, [inter.buffer]);
|
|
} else {
|
|
// Track FX / chưa có directPort — legacy qua main thread.
|
|
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.
|
|
// V46: non-wrap underrun-stall (producer stall giua play, transport
|
|
// khong doi) — sau flush da bo backlog stale, cushion 8 block (~43ms,
|
|
// proven V45.4 seek/wrap) la du; FILL 24 (~128ms) lam moi underrun
|
|
// them ~60-130ms silence (recovery ~150-400ms -> ~90-130ms).
|
|
const _wrap = this.wrapMode;
|
|
this.outQueue.length = 0;
|
|
this.outPos = 0;
|
|
this.resumeFadeIn = true;
|
|
// V46.3: wrap-context underrun — resume chi can 4 block (~10.7ms) khong
|
|
// phai 8 (~21ms): queue pass moi la audio LIEN TUC (bar1 da bat dau),
|
|
// khong con backlog stale de phong — cho 8 block them ~10ms gap seam
|
|
// (log 08:49 wrap2: q=4 tai wrap -> tail 10.7ms can, bar1 toi +26ms,
|
|
// resume 8-block -> gap ~35ms = giat lag). Non-wrap giu 8 (V46).
|
|
this.resumeFill = _wrap ? 4 : 8;
|
|
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);
|