118 lines
5.5 KiB
JavaScript
118 lines
5.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;
|
|
// V45.26: dem note live dang giu + lan live event gan nhat — quyet dinh co
|
|
// clear master FX outQueue khong (chi idle->active; KHONG khi dang phat lien tuc).
|
|
var _liveHeldCount = 0;
|
|
var _lastLiveEventAt = 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.
|
|
// V45.26: chi clear khi THAT SU idle -> active (khong con note dang giu
|
|
// VA im lang >500ms). Truoc day clear MOI note-on live -> rapid 1/8 lien
|
|
// tuc: moi note wipe outQueue (dang chua tail note truoc + resume fade-in)
|
|
// -> wobble/crackle (bug user). Note dang ngan / chuoi note nhanh -> giu
|
|
// queue -> tail note truoc khong bi cat.
|
|
if (opts.live) {
|
|
try {
|
|
var _nowEv = (window.performance && performance.now) ? performance.now() : Date.now();
|
|
if (opts.cmd === 'NOTE_ON') {
|
|
if (_liveHeldCount === 0 && (_nowEv - _lastLiveEventAt) > 500 && window.__clearMasterFxQueue) window.__clearMasterFxQueue();
|
|
_liveHeldCount++;
|
|
} else if (opts.cmd === 'NOTE_OFF') {
|
|
if (_liveHeldCount > 0) _liveHeldCount--;
|
|
}
|
|
_lastLiveEventAt = _nowEv;
|
|
} 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 () {
|
|
_liveHeldCount = 0;
|
|
if (this.isBridgeActive() && window.NativeBridgeService.transport) {
|
|
window.NativeBridgeService.transport('panic');
|
|
}
|
|
if (this.onFallback) this.onFallback('PANIC', 0, 0, 0);
|
|
}
|
|
};
|
|
|
|
window.SonicMidiRouter = router;
|
|
})();
|