Files
SonicForgeStudio/app/static/js/services/bridgeAudioNode.js
T
locpham 36fb546d58 fix: SF2 sound (audio sink FIFO underrun) + VST3 native GUI open + GUI load race
- bridgeAudioNode: ScriptProcessor 4096 drained only 1 of 16 blocks -> ~94% silence; now drains all queued blocks per callback, RING_DEPTH 8->24
- app.jsx: openNativeGUI had NO caller; wire after VST3 loadInstrument OK in track Synth dropdown + Plugin Manager Load Bridge
- main.cpp: OPEN_GUI polls up to 10s for LOAD completion (worker thread) before attach - fixes race 'no instrument loaded'
- index.html: bump bridgeAudioNode/app.precompiled cache versions
2026-08-12 22:32:10 +07:00

93 lines
3.1 KiB
JavaScript

// app/static/js/services/bridgeAudioNode.js
// Audio sink: consumes 'bridge-audio' PCM frames (native bridge) and plays them
// through a ScriptProcessorNode into the WebAudio graph (track FX / master bus).
(function () {
// Bridge renders one 256-sample block per ~5.8ms; ScriptProcessor fires once
// per 4096 samples (~16 blocks @44.1k). The ring MUST cover one full callback
// period or we underrun (silence gaps) — 24 blocks ~= 139ms headroom.
var RING_DEPTH = 24;
var SP_BUFFER = 4096;
var _chunks = []; // [{l: Float32Array(256), r: Float32Array(256)}]
var _spn = null;
var _gainNode = null;
var _ctx = null;
var _initialized = false;
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 init(audioCtx) {
if (_initialized) return;
_ctx = audioCtx || _getCtx();
_gainNode = _ctx.createGain();
_gainNode.gain.value = 1.0;
_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;
// Drain as many queued blocks as fit in this callback (16 max) — filling
// only one block per callback previously played 1/16 of the audio.
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 {
// Partial block (only possible if block size != 256): keep remainder.
blk.l = blk.l.subarray(n);
blk.r = blk.r.subarray(n);
}
}
};
_spn.connect(_gainNode);
_initialized = true;
console.log('[BridgeAudioNode] initialized (ring depth ' + RING_DEPTH + ')');
}
function onAudio(l, r) {
if (!_initialized) init();
_chunks.push({ l: new Float32Array(l), r: new Float32Array(r) });
// Drop oldest on overrun — keeps latency bounded (~139ms max).
while (_chunks.length > RING_DEPTH) _chunks.shift();
}
function flush() { _chunks = []; }
function getOutputNode() { return _gainNode; }
function disconnect() {
flush();
if (_spn && _gainNode) {
try { _spn.disconnect(); } catch (e) {}
try { _gainNode.disconnect(); } catch (e) {}
}
// Reset state so a later init() can rebuild (AudioRoutingEngine re-connect).
_initialized = false;
_spn = null;
_gainNode = null;
_ctx = null;
}
window.BridgeAudioNode = {
init: init,
onAudio: onAudio,
flush: flush,
getOutputNode: getOutputNode,
connect: function (dest) { if (_gainNode) _gainNode.connect(dest); },
disconnect: disconnect,
isReady: function () { return _initialized; }
};
})();