// app/static/js/services/bridgeAudioWorklet.js // VSTi crackle fix: audio sink chay tren audio thread (khong jank main thread). // - Ring block 256 mau, toi da 32 block (~170ms @48k) — hap thu jitter pump // (V45.27: 16->32 de dung crackle VST2 loop/tremolo — render loop C++ bi // stall khi instrument xu li loop/tremolo nang CPU; 8->16 truoc do da dung // tremolo crackle, 16 van chua du cho stall >85ms). Flush o play start // (app.jsx) xoa ring nen do tre khong tang: ring thuong can o preview. // - Underrun: fade ve 0 thay vi click (hard silence). // - Resume sau silence: fade vao tu 0. // - Overrun: drop block cu nhat + crossfade 64 mau tai diem drop (het click). // - Stats: dem overrun/underrun, gui ve main thread moi ~0.5s — kiem tra // con crackle nao la do ring can (underrun) hay ring tran (overrun). class BridgeAudioSinkProcessor extends AudioWorkletProcessor { constructor() { super(); this._ring = []; this._MAX = 32; this._gain = 1.0; this._lastOutL = 0.0; this._lastOutR = 0.0; this._skipFade = 0; // >0: con lai cua crossfade dang chay sau overrun drop this._fadeLen = 0; // tong chieu dai crossfade hien tai (cap 1024) this._skipFromL = 0.0; this._skipFromR = 0.0; this._wasUnderrun = false; this._statAccum = 0; this._stats = { overruns: 0, underruns: 0 }; this.port.onmessage = (e) => { const m = e.data; if (!m) return; if (m.type === 'audio') { this._ring.push({ l: m.l, r: m.r }); while (this._ring.length > this._MAX) { // overrun: bo block cu nhat; lan consume toi se crossfade ranh gioi // V45.32: crossfade tich luy — moi block drop cong 256 mau fade, cap // 1024 (~21ms) thay vi co dinh 64 mau (1.3ms qua ngan -> blip/wow khi // drop loat block o chord nhieu voice = crackle VST2 tremolo/loop) this._ring.shift(); this._skipFade = Math.min(this._skipFade + 256, 1024); this._fadeLen = this._skipFade; this._stats.overruns++; } } else if (m.type === 'flush') { this._ring.length = 0; this._gain = 1.0; this._skipFade = 0; this._fadeLen = 0; } }; } process(inputs, outputs) { const out = outputs[0]; if (!out || out.length < 2) return true; const L = out[0]; const R = out[1]; const n = L.length; let w = 0; while (w < n) { if (this._ring.length) { if (this._wasUnderrun) this._wasUnderrun = false; const blk = this._ring[0]; const avail = blk.l.length; const take = Math.min(avail, n - w); if (this._skipFade > 0) { const done = this._fadeLen - this._skipFade; for (let i = 0; i < take; i++) { const t = (done + i) / this._fadeLen; L[w + i] = (this._lastOutL * (1 - t) + blk.l[i] * t) * this._gain; R[w + i] = (this._lastOutR * (1 - t) + blk.r[i] * t) * this._gain; } this._skipFade = Math.max(0, this._skipFade - take); } else { for (let i = 0; i < take; i++) { L[w + i] = blk.l[i] * this._gain; R[w + i] = blk.r[i] * this._gain; } } this._lastOutL = L[w + take - 1]; this._lastOutR = R[w + take - 1]; if (this._gain < 1.0) this._gain = Math.min(1.0, this._gain + take / 256.0); w += take; if (take >= avail) { this._ring.shift(); } else { blk.l = blk.l.subarray(take); blk.r = blk.r.subarray(take); } } else { // underrun: fade ve 0 (khong click) — dem 1 lan moi lan roi vao trang thai trong if (!this._wasUnderrun) { this._wasUnderrun = true; this._stats.underruns++; } for (let i = w; i < n; i++) { this._gain = Math.max(0, this._gain - 1.0 / 256.0); L[i] = this._lastOutL * this._gain; R[i] = this._lastOutR * this._gain; } this._lastOutL *= this._gain; this._lastOutR *= this._gain; w = n; } } // Stats: gui ve main thread moi ~0.5s (24000 mau @48k) — log overrun/underrun this._statAccum += n; if (this._statAccum >= 24000) { this._statAccum = 0; try { this.port.postMessage({ type: 'stats', overruns: this._stats.overruns, underruns: this._stats.underruns, ring: this._ring.length, max: this._MAX }); } catch (e) {} } return true; } } registerProcessor('bridge-audio-sink', BridgeAudioSinkProcessor);