Files
SonicForgeStudio/app/static/js/services/unifiedMidiRouter.js
T

97 lines
4.5 KiB
JavaScript

// 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/native path).
(function () {
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14]; // skip 9 (percussion); 15 reserved for Media Explorer preview
var _lastTimelineDbgTs = 0;
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) {
// Media Explorer preview dùng channel 15 cố định — tránh đè channel
// melodic track (0-8,10-14) / percussion (9) đang giữ instrument.
if (trackId === 'media_explorer') return 15;
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) {
// V28: midi bất kỳ (keyboard/pad/timeline) → restore bridge gain đang
// giữ ~0.0001 sau stop (ding fix — V27 restore 800ms không đủ tail dài).
try { if (window.__restorePendingFadeSession) window.__restorePendingFadeSession(); } catch (e) {}
// V31: live note (ARM keyboard preview) — clear master FX outQueue backlog
// (idle block ~341ms) truoc khi note moi render; timeline note (live=false)
// KHONG clear — tranh drop audio dang phat.
if (opts.live && opts.cmd === 'NOTE_ON') { try { if (window.__clearMasterFxQueue) window.__clearMasterFxQueue(); } catch (e) {} }
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;
try {
// V36: midi_push log = 1 Tauri invoke sync (dbg_log -> disk I/O tren main
// thread Rust) moi event. Timeline phat ~380 event/s -> main thread nghet
// -> stall 110-170ms -> crackle. Chi log live (ARM preview), timeline
// gioi han 1/200ms.
var _nowMs = (window.performance && performance.now) ? performance.now() : Date.now();
if (window.__dbgLog && (opts.live || _nowMs - _lastTimelineDbgTs > 200)) {
if (!opts.live) _lastTimelineDbgTs = _nowMs;
window.__dbgLog('midi_push', 'cmd=' + cmd + ' ch=' + ch + ' p=' + (pitch === undefined ? 'n/a' : pitch) + ' live=' + (opts.live ? 1 : 0) + ' vel=' + Math.round((vel || 0) * 100) + (window.__dbgBridgeGain ? ' ' + window.__dbgBridgeGain() : '') + ' q=' + (window.__fxRtQueueLen ? window.__fxRtQueueLen() : 'n/a'));
}
} catch (e) {}
if (this.isBridgeActive()) {
window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3, opts.live);
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;
})();