181 lines
6.5 KiB
JavaScript
181 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 _useWorklet = false;
|
|
var _workletStats = null; // stats tu worklet (overrun/underrun) — getDiag
|
|
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=202608311820';
|
|
_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);
|
|
_workletNode.port.onmessage = function (ev) {
|
|
if (ev.data && ev.data.type === 'stats') _workletStats = ev.data;
|
|
};
|
|
_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');
|
|
try { if (window.__dbgLog) window.__dbgLog('worklet', 'active pre=' + _preBuffer.length); } catch (e) {}
|
|
} catch (err) {
|
|
console.warn('[BridgeAudioNode] AudioWorkletNode failed — ScriptProcessor fallback', err);
|
|
_useWorklet = false;
|
|
try { if (window.__dbgLog) window.__dbgLog('worklet', 'NODE_FAIL'); } catch (e) {}
|
|
}
|
|
}).catch(function (err) {
|
|
console.warn('[BridgeAudioNode] addModule failed — ScriptProcessor fallback', err);
|
|
try { if (window.__dbgLog) window.__dbgLog('worklet', 'MODULE_FAIL'); } catch (e) {}
|
|
});
|
|
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();
|
|
var blk = { l: new Float32Array(l), r: new Float32Array(r) };
|
|
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; },
|
|
getDiag: function () {
|
|
return {
|
|
initialized: _initialized,
|
|
useWorklet: _useWorklet,
|
|
hasWorkletNode: !!_workletNode,
|
|
preBufferLen: _preBuffer.length,
|
|
chunksLen: _chunks.length,
|
|
gain: _gainNode ? _gainNode.gain.value : -1,
|
|
ws: _workletStats
|
|
};
|
|
}
|
|
};
|
|
})();
|