// 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; } }; })();