Files
SonicForgeStudio/app/static/js/services/unifiedMidiRouter.js
T
admin ee30ca097e T4: UnifiedMidiRouter - 1 diem dispatch MIDI moi nguon
UnifiedMidiEvent (trackId, channel, command, pitch clamp 0-127, velocity
normalize float->int 1-127, sourceType, timestampNs) + UnifiedMidiRouter:
engineRegistry Map<trackId, engine> (route theo track), dispatchMidiEvent,
activeVoiceTracker key trackId_channel_pitch (dem note-on/off dung - het
stuck notes, note-off cuoi moi tat voice), panicAllNotesOff. Chua noi
vao app (T5-T7). Test node: pitch clamp, velocity normalize, tracker,
panic clear - xanh.
2026-08-11 15:38:29 +07:00

129 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SonicForge Unified MIDI Router
// Mọi nguồn MIDI (keybed preview, timeline scheduler, hardware MIDI) → chuẩn
// hóa UnifiedMidiEvent → UnifiedMidiRouter → engine theo trackId. Một điểm
// dispatch duy nhất: activeVoiceTracker đếm note-on/off đúng (hết stuck
// notes), panicAllNotesOff() quét toàn bộ voice khi đổi instrument/engine
// giữa chừng. Chưa nối vào app (T5T7 sẽ đăng ký engine + dispatch).
window.SonicUnifiedMidiRouter = window.SonicUnifiedMidiRouter || {};
(function () {
var COMMAND_NOTE_ON = 0x90;
var COMMAND_NOTE_OFF = 0x80;
var COMMAND_CC = 0xB0;
// Pitch: ép int 0-127 (spec "pitch clamp 0-127").
function clampPitch(pitch) {
pitch = Math.round(Number(pitch) || 0);
if (pitch < 0) return 0;
if (pitch > 127) return 127;
return pitch;
}
// Velocity: float 0-1 (vd 100/127) hoặc int → ép int 1-127 (note-on);
// note-off velocity = 0. Spec "velocity normalize float→int".
function normalizeVelocity(velocity, isNoteOn) {
var v = Number(velocity);
if (!isFinite(v)) v = 0;
if (v > 0 && v <= 1) v = v * 127;
v = Math.round(v);
if (v < 0) v = 0;
if (v > 127) v = 127;
if (isNoteOn && v <= 0) v = 1;
return v;
}
function UnifiedMidiRouter() {
this.engineRegistry = new Map(); // trackId → TrackInstrument
// activeVoiceTracker: key `${trackId}_${channel}_${pitch}` → { count, engine }.
// (Spec dùng `${channel}_${pitch}`; thêm trackId vì engine theo track —
// 2 track cùng channel+pitch phải đếm riêng, không lệch voice.)
this._voices = new Map();
}
// Đăng ký engine cho track (null = xóa). Đổi instrument → registerEngine
// track mới; panic trước đó để không sót voice cũ.
UnifiedMidiRouter.prototype.registerEngine = function (trackId, engine) {
if (trackId === undefined || trackId === null) return;
if (engine) this.engineRegistry.set(trackId, engine);
else this.engineRegistry.delete(trackId);
};
// Dispatch 1 event từ mọi nguồn. Không ném — voice tracker luôn đếm đúng.
// Trả true nếu event là note (đã route/count), false nếu không xử lý.
UnifiedMidiRouter.prototype.dispatchMidiEvent = function (evt) {
if (!evt) return false;
var trackId = evt.trackId;
var engine = (trackId !== undefined && trackId !== null) ? this.engineRegistry.get(trackId) : null;
var channel = Math.round(Number(evt.channel) || 0);
if (channel < 0) channel = 0;
if (channel > 15) channel = 15;
var pitch = clampPitch(evt.pitch);
var cmd = Number(evt.command);
var key = trackId + '_' + channel + '_' + pitch;
var isNoteOn = (cmd === COMMAND_NOTE_ON);
var isNoteOff = (cmd === COMMAND_NOTE_OFF) || (cmd === COMMAND_NOTE_ON && !Number(evt.velocity));
if (isNoteOn) {
var vel = normalizeVelocity(evt.velocity, true);
var cur = this._voices.get(key);
if (!cur) {
cur = { count: 0, engine: engine };
this._voices.set(key, cur);
}
cur.engine = engine;
cur.count += 1;
// Chỉ trigger âm ở note-on đầu tiên; các note-on trùng key chỉ tăng
// count (sustain lặp) — note-off cuối cùng mới tắt voice.
if (cur.count === 1 && engine && engine.playNote) {
try { engine.playNote(pitch, vel, evt.durationMs || 500); } catch (e) {}
}
return true;
}
if (isNoteOff) {
var cur2 = this._voices.get(key);
if (cur2 && cur2.count > 0) {
cur2.count -= 1;
var eng2 = cur2.engine;
if (cur2.count <= 0) {
this._voices.delete(key);
if (eng2 && eng2.noteOff) {
try { eng2.noteOff(pitch); } catch (e) {}
}
}
}
return true;
}
return false; // CC / unknown
};
// All Notes Off: quét voice tracker, gửi note-off cho engine sở hữu từng
// voice, xóa tracker. Gọi khi dừng playback / đổi instrument giữa chừng.
UnifiedMidiRouter.prototype.panicAllNotesOff = function () {
var self = this;
this._voices.forEach(function (v, key) {
if (v.engine && v.engine.noteOff) {
var parts = key.split('_');
var pitch = parseInt(parts[parts.length - 1], 10);
try { v.engine.noteOff(pitch); } catch (e) {}
}
});
this._voices.clear();
};
// Số voice active cho 1 (track, channel, pitch) — test/debug.
UnifiedMidiRouter.prototype.activeVoiceCount = function (trackId, channel, pitch) {
var cur = this._voices.get(trackId + '_' + channel + '_' + pitch);
return cur ? cur.count : 0;
};
window.SonicUnifiedMidiRouter = {
UnifiedMidiRouter: UnifiedMidiRouter,
COMMAND_NOTE_ON: COMMAND_NOTE_ON,
COMMAND_NOTE_OFF: COMMAND_NOTE_OFF,
COMMAND_CC: COMMAND_CC
};
})();