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.
This commit is contained in:
2026-08-11 15:38:29 +07:00
parent fa2f96dbe3
commit ee30ca097e
2 changed files with 185 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
// T4 test: node tests/unified_midi_router.test.js
// Router chưa nối app — test độc lập qua window stub.
'use strict';
global.window = {};
require('../app/static/js/services/unifiedMidiRouter.js');
const R = global.window.SonicUnifiedMidiRouter;
const assert = require('assert');
const calls = [];
function makeEngine() {
return {
playNote(pitch, vel, dur) { calls.push(['on', pitch, vel, dur]); },
noteOff(pitch) { calls.push(['off', pitch]); }
};
}
// 1) pitch clamp
const r = new R.UnifiedMidiRouter();
const eng = makeEngine();
r.registerEngine('t1', eng);
r.dispatchMidiEvent({ trackId: 't1', channel: 0, command: R.COMMAND_NOTE_ON, pitch: 200, velocity: 1, sourceType: 'TEST' });
assert.deepStrictEqual(calls[0], ['on', 127, 127, 500], 'pitch clamp 200->127');
r.dispatchMidiEvent({ trackId: 't1', channel: 0, command: R.COMMAND_NOTE_OFF, pitch: -3 });
assert.strictEqual(calls.length, 1, 'note-off khong co voice -> khong goi engine (pitch clamp khong crash)');
// 2) velocity normalize float->int (100/127 -> 100, 0.5 -> 64)
r.dispatchMidiEvent({ trackId: 't1', channel: 1, command: R.COMMAND_NOTE_ON, pitch: 60, velocity: 100 / 127 });
assert.deepStrictEqual(calls[1], ['on', 60, 100, 500], 'velocity 100/127 -> 100');
r.dispatchMidiEvent({ trackId: 't1', channel: 1, command: R.COMMAND_NOTE_ON, pitch: 61, velocity: 0.5 });
assert.deepStrictEqual(calls[2], ['on', 61, 64, 500], 'velocity 0.5 -> 64');
// 3) tracker đếm đúng: 2 note-on trùng key -> 1 playNote; off cuối mới tắt
r.dispatchMidiEvent({ trackId: 't1', channel: 2, command: R.COMMAND_NOTE_ON, pitch: 64, velocity: 100 });
r.dispatchMidiEvent({ trackId: 't1', channel: 2, command: R.COMMAND_NOTE_ON, pitch: 64, velocity: 100 });
assert.strictEqual(r.activeVoiceCount('t1', 2, 64), 2, '2 note-on count=2');
const onCalls = calls.filter(c => c[0] === 'on' && c[1] === 64).length;
assert.strictEqual(onCalls, 1, 'trung key chi play 1 lan');
r.dispatchMidiEvent({ trackId: 't1', channel: 2, command: R.COMMAND_NOTE_OFF, pitch: 64 });
assert.strictEqual(r.activeVoiceCount('t1', 2, 64), 1, '1 off -> count=1, chua noteOff');
r.dispatchMidiEvent({ trackId: 't1', channel: 2, command: R.COMMAND_NOTE_OFF, pitch: 64 });
assert.strictEqual(r.activeVoiceCount('t1', 2, 64), 0, 'off cuoi -> count=0');
assert.deepStrictEqual(calls[calls.length - 1], ['off', 64], 'off cuoi moi noteOff');
// 4) 2 track cùng channel+pitch đếm riêng (không lệch voice)
const eng2 = makeEngine();
r.registerEngine('t2', eng2);
r.dispatchMidiEvent({ trackId: 't2', channel: 2, command: R.COMMAND_NOTE_ON, pitch: 64, velocity: 90 });
assert.strictEqual(r.activeVoiceCount('t2', 2, 64), 1);
assert.strictEqual(r.activeVoiceCount('t1', 2, 64), 0);
// 5) panic clear: note đang giữ -> noteOff + tracker rỗng
r.dispatchMidiEvent({ trackId: 't1', channel: 3, command: R.COMMAND_NOTE_ON, pitch: 72, velocity: 80 });
r.panicAllNotesOff();
assert.strictEqual(r.activeVoiceCount('t1', 3, 72), 0, 'panic xoa tracker');
assert.deepStrictEqual(calls[calls.length - 1], ['off', 72], 'panic gui noteOff');
console.log('unified_midi_router: ALL TESTS PASSED');