Files
SonicForgeStudio/app/static/js/services/bridgeAudioWorklet.js
T

121 lines
4.6 KiB
JavaScript

// 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 48 block (~256ms @48k) — hap thu jitter pump
// (V45.28: 32->48 de dung wobble VST2 loop/tremolo khi nhan chord — chord
// nhieu voice -> render loop C++ stall dai hon ~170ms -> underrun lap lai
// (fade ve 0) nghe nhu wobble; 8->16 (V45) dung tremolo crackle, 16->32
// (V45.27) dung crackle, 32 van chua du cho stall chord). 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 tich luy — moi block drop cong
// 256 mau fade, cap 1024 (~21ms) — crossfade dai de tranh blip/wow khi
// chord nhieu voice bi drop loat block (64 mau = 1.3ms qua ngan).
// - 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 = 48;
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
// (tich luy: moi block drop cong 256 mau fade, cap 1024 — crossfade
// dai hon de tranh blip/wow khi drop loat block o chord nhieu voice)
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);