// app/static/js/services/unifiedMidiRouter.js // Single MIDI entry point for Web MIDI keyboard + timeline playback. // bridge connected -> NativeBridgeService.dispatchMidiEvent (Rust SHM -> C++ bridge) // bridge down -> onFallback callback (app.jsx wires SonicSF/Carla path). (function () { var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15]; // skip 9 (percussion) var router = { bridgeConnected: false, onFallback: null, // function(cmd, channel, pitch, velocity) — set by app.jsx forceWasm: false, // debug: D10 "Ép dùng WASM" _channels: {}, // trackId -> { ch, percussion } _nextMelodicIdx: 0, setBridgeConnected: function (flag) { this.bridgeConnected = !!flag; }, setForceWasm: function (flag) { this.forceWasm = !!flag; }, isBridgeActive: function () { return this.bridgeConnected && !this.forceWasm && !!window.NativeBridgeService; }, /** trackId -> MIDI channel; percussion (bank 128) -> ch 9. */ allocateChannel: function (trackId, isPercussion) { if (this._channels[trackId]) return this._channels[trackId].ch; var ch; if (isPercussion) { ch = 9; } else { ch = MELODIC_CHANNELS[this._nextMelodicIdx % MELODIC_CHANNELS.length]; this._nextMelodicIdx++; } this._channels[trackId] = { ch: ch, percussion: !!isPercussion }; return ch; }, resetChannels: function () { this._channels = {}; this._nextMelodicIdx = 0; }, /** * cmd: 'NOTE_ON' | 'NOTE_OFF' | 'CC' | 'PROGRAM' | 'PITCH_BEND' * channel: MIDI channel (or trackId -> allocateChannel first) * velocity: 0..1 (normalized); sampleOffset: samples within current block. * data2/data3 (A12): CC value / program / PB LSB|MSB — passed to bridge only. */ pushEvent: function (opts) { var cmd = opts.cmd, ch = opts.channel, pitch = opts.pitch; var vel = (opts.velocity === undefined ? 1.0 : opts.velocity); var sampleOffset = opts.sampleOffset || 0; if (typeof ch === 'string') ch = this.allocateChannel(ch, opts.percussion); if (ch === undefined || ch === null) ch = 0; if (this.isBridgeActive()) { window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3); return; } if (this.onFallback) this.onFallback(cmd, ch, pitch, vel); }, /** Stop/panic -> bridge transport panic (flush all notes) + fallback local stopAll. */ panic: function () { if (this.isBridgeActive() && window.NativeBridgeService.transport) { window.NativeBridgeService.transport('panic'); } if (this.onFallback) this.onFallback('PANIC', 0, 0, 0); } }; window.SonicMidiRouter = router; })();