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

176 lines
6.5 KiB
JavaScript

// app/static/js/services/bridgeAudioNode.js
// Audio sink: consumes 'bridge-audio' PCM frames (native bridge) and plays them
// into the WebAudio graph. Primary path: AudioWorklet (audio thread, no main
// thread jank). Fallback: ScriptProcessorNode when worklet unavailable.
// VSTi crackle fix (FIX_VSTI_AUDIO_CRACKLE.md P1/P2): worklet ring absorbs pump
// jitter, underrun fades to 0 (no click), overrun drops oldest + crossfade.
(function () {
var RING_DEPTH = 24; // fallback ring (blocks of 256 samples)
var WORKLET_MAX = 64; // blocks buffered while worklet loads (~371ms @48k)
var SP_BUFFER = 4096;
var _chunks = []; // fallback: [{l: Float32Array(256), r: Float32Array(256)}]
var _preBuffer = []; // pending blocks while worklet module loads
var _spn = null;
var _gainNode = null;
var _workletNode = null;
var _ctx = null;
var _initialized = false;
var _frames = 0;
var _peak = 0;
var _useWorklet = false;
var _dest = null; // destination gain node da noi (chong connect trung)
function _getCtx() {
if (_ctx) return _ctx;
if (typeof getAudioContext === 'function') _ctx = getAudioContext();
else if (window.__sharedAudioCtx) _ctx = window.__sharedAudioCtx;
else _ctx = new (window.AudioContext || window.webkitAudioContext)();
return _ctx;
}
function _setupWorklet() {
if (!_ctx.audioWorklet || !_ctx.audioWorklet.addModule) {
console.warn('[BridgeAudioNode] AudioWorklet unavailable — ScriptProcessor fallback');
return false;
}
var url = '/static/js/services/bridgeAudioWorklet.js?v=202608152200';
_ctx.audioWorklet.addModule(url).then(function () {
if (!_initialized) return; // disconnect() happened while loading
if (_useWorklet) return; // already set up (init retry)
try {
_workletNode = new AudioWorkletNode(_ctx, 'bridge-audio-sink', {
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [2]
});
_workletNode.connect(_gainNode);
_useWorklet = true;
// drain pre-buffer into worklet (order preserved)
while (_preBuffer.length) {
var b = _preBuffer.shift();
_workletNode.port.postMessage({ type: 'audio', l: b.l, r: b.r });
}
console.log('[BridgeAudioNode] AudioWorklet active');
} catch (err) {
console.warn('[BridgeAudioNode] AudioWorkletNode failed — ScriptProcessor fallback', err);
_useWorklet = false;
}
}).catch(function (err) {
console.warn('[BridgeAudioNode] addModule failed — ScriptProcessor fallback', err);
});
return true; // pending — worklet may become active later
}
function init(audioCtx) {
if (_initialized) return;
_ctx = audioCtx || _getCtx();
_gainNode = _ctx.createGain();
_gainNode.gain.value = 1.0;
// ScriptProcessor created but NOT connected until we know worklet failed.
_spn = _ctx.createScriptProcessor(SP_BUFFER, 0, 2);
_spn.onaudioprocess = function (e) {
var L = e.outputBuffer.getChannelData(0);
var R = e.outputBuffer.getChannelData(1);
var outLen = L.length;
L.fill(0); R.fill(0);
if (!_chunks.length) return;
var written = 0;
while (written < outLen && _chunks.length) {
var blk = _chunks[0];
var n = Math.min(blk.l.length, outLen - written);
L.set(blk.l.subarray(0, n), written);
R.set(blk.r.subarray(0, n), written);
written += n;
if (n >= blk.l.length) {
_chunks.shift();
} else {
blk.l = blk.l.subarray(n);
blk.r = blk.r.subarray(n);
}
}
};
if (!_setupWorklet()) {
// No worklet at all — go straight to ScriptProcessor.
_spn.connect(_gainNode);
_useWorklet = false;
while (_preBuffer.length) _chunks.push(_preBuffer.shift());
while (_chunks.length > RING_DEPTH) _chunks.shift();
}
_initialized = true;
console.log('[BridgeAudioNode] initialized (ring depth ' + RING_DEPTH + ')');
}
function onAudio(l, r) {
if (!_initialized) init();
if (!window.__baeAt || Date.now() - window.__baeAt >= 1000) { window.__baeAt = Date.now(); if (window.__sfDiag) window.__sfDiag('BRIDGE_AUDIO_ENTER len=' + (l ? l.length : -1)); }
var blk = { l: new Float32Array(l), r: new Float32Array(r) };
_frames = (_frames || 0) + blk.l.length;
for (var _i = 0; _i < blk.l.length; _i++) {
var _a = Math.abs(blk.l[_i]); if (_a > (_peak || 0)) _peak = _a;
var _b = Math.abs(blk.r[_i]); if (_b > (_peak || 0)) _peak = _b;
}
if (!_lastDiag || Date.now() - _lastDiag >= 1000) {
_lastDiag = Date.now();
if (window.__sfDiag) window.__sfDiag("BRIDGE_AUDIO frames=" + _frames + " peak=" + (_peak ? (20 * Math.log10(_peak)).toFixed(1) : "-inf") + "dB path=" + (_useWorklet ? "worklet" : (_preBuffer.length ? "preBuffer" : "chunks")));
_peak = 0;
}
if (_useWorklet && _workletNode) {
_workletNode.port.postMessage({ type: 'audio', l: blk.l, r: blk.r });
return;
}
if (!_useWorklet && _ctx && _ctx.audioWorklet && _ctx.audioWorklet.addModule) {
// Worklet loading — buffer temporarily (bounded), avoid fallback churn.
_preBuffer.push(blk);
while (_preBuffer.length > WORKLET_MAX) _preBuffer.shift();
return;
}
_chunks.push(blk);
while (_chunks.length > RING_DEPTH) _chunks.shift();
}
function flush() {
_preBuffer = [];
_chunks = [];
if (_useWorklet && _workletNode) {
try { _workletNode.port.postMessage({ type: 'flush' }); } catch (e) {}
}
}
function getOutputNode() { return _gainNode; }
function disconnect() {
flush();
if (_workletNode) {
try { _workletNode.disconnect(); } catch (e) {}
try { _workletNode.port.close(); } catch (e) {}
}
if (_spn && _gainNode) {
try { _spn.disconnect(); } catch (e) {}
try { _gainNode.disconnect(); } catch (e) {}
}
_initialized = false;
_useWorklet = false;
_workletNode = null;
_spn = null;
_gainNode = null;
_ctx = null;
_dest = null;
}
window.BridgeAudioNode = {
init: init,
onAudio: onAudio,
flush: flush,
getOutputNode: getOutputNode,
connect: function (dest) {
if (!_gainNode) return;
if (_dest === dest) return; // da noi — khong noi trung (double-sum)
if (_dest) { try { _gainNode.disconnect(_dest); } catch (e) {} }
_gainNode.connect(dest);
_dest = dest;
},
disconnect: disconnect,
isReady: function () { return _initialized; }
};
})();