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
This commit is contained in:
locpham
2026-08-12 22:32:10 +07:00
parent aac0f03e20
commit 36fb546d58
7 changed files with 94 additions and 194 deletions
+29 -9
View File
@@ -2,9 +2,12 @@
// Audio sink: consumes 'bridge-audio' PCM frames (native bridge) and plays them
// through a ScriptProcessorNode into the WebAudio graph (track FX / master bus).
(function () {
var RING_DEPTH = 8; // 8 blocks * 256 samples ~= 46ms anti-underrun
var SP_BUFFER = 4096; // ScriptProcessor chunk (16 bridge blocks)
var _queue = [];
// 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;
@@ -27,10 +30,26 @@
_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 (!_queue.length) return;
var f = _queue.shift();
L.set(f.l); R.set(f.r);
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;
@@ -39,11 +58,12 @@
function onAudio(l, r) {
if (!_initialized) init();
_queue.push({ l: new Float32Array(l), r: new Float32Array(r) });
if (_queue.length > RING_DEPTH) _queue.shift(); // drop oldest on overrun
_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() { _queue = []; }
function flush() { _chunks = []; }
function getOutputNode() { return _gainNode; }