3f59c2c4c2
- NativeInstrumentEngine: track GM bank per channel (CC0/CC32), use bank in programChange - app.jsx: send CC0/CC32+PROGRAM before notes via __ensureBridgeProgram, dedupe, clear dedupe after async LOAD - audioRoutingEngine/bridgeAudioNode: idempotent connect (no disconnect-flush on re-connect), fixes note cut & multi-track stuck - main.cpp: remove 10s poll in OPEN_GUI control job (blocked realtime loop, watchdog race), VstWindowProc stores channel not inst pointer, cleanup gui maps on WM_DESTROY
101 lines
3.4 KiB
JavaScript
101 lines
3.4 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;
|
|
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 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;
|
|
_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; }
|
|
};
|
|
})();
|