// tools/test_midi_polyphony.js — runnable check cho Phase A (dedup/legato/polyphony). // Đọc app.jsx, extract 2 hàm thuần, eval, assert spec §II.1 + §II.5. const fs = require('fs'); const path = require('path'); const src = fs.readFileSync(path.join(__dirname, '..', 'app', 'static', 'js', 'app.jsx'), 'utf8'); const start = src.indexOf('const _cleanNativeNotes = (notes) => {'); const end = src.indexOf('const _nativeTrackKey', start); if (start < 0 || end < 0) { console.error('FAIL: khong tim thay 2 ham'); process.exit(1); } const code = src.slice(start, end) + ';globalThis._cleanNativeNotes=_cleanNativeNotes;' + 'globalThis._scaleNativePolyphony=_scaleNativePolyphony;'; // eslint-disable-next-line no-eval (0, eval)(code); // indirect eval: const khai báo ở global scope let fails = 0; const assert = (cond, msg) => { if (!cond) { fails++; console.error('FAIL: ' + msg); } else { console.log('ok: ' + msg); } }; const near = (a, b, eps) => Math.abs(a - b) < (eps || 1e-9); // 1. 8-note chord cùng lúc → tổng peak ≈ 0.3*8/sqrt(8) ≤ 1.0 const chord = []; for (let i = 0; i < 8; i++) chord.push({ pitch: 60 + i, start_beat: 0, duration_beats: 2, velocity: 0.3 }); const sc = _scaleNativePolyphony(_cleanNativeNotes(chord)); const sum = sc.reduce((a, n) => a + n.velocity, 0); assert(sc.length === 8, 'chord giu 8 note'); assert(near(sum, 8 * 0.3 / Math.sqrt(8)), 'tong peak ' + sum.toFixed(4) + ' ~= ' + (8 * 0.3 / Math.sqrt(8)).toFixed(4)); assert(sum <= 1.0, 'tong peak <= 1.0'); // 2. Solo → giữ nguyên velocity const solo = _scaleNativePolyphony([{ pitch: 60, start_beat: 0, duration_beats: 1, velocity: 0.8 }]); assert(near(solo[0].velocity, 0.8), 'solo velocity giu nguyen 0.8'); // 3. Dedup: cùng pitch + start_beat → gộp, velocity max const dup = _cleanNativeNotes([ { pitch: 60, start_beat: 1, duration_beats: 2, velocity: 0.5 }, { pitch: 60, start_beat: 1, duration_beats: 3, velocity: 0.7 }, ]); assert(dup.length === 1, 'dedup gop 2 note'); assert(near(dup[0].velocity, 0.7), 'dedup giu velocity cao 0.7'); assert(near(dup[0].duration_beats, 3), 'dedup giu duration dai nhat'); // 4. Legato: note trước cắt 0.01 beats trước note sau cùng pitch const leg = _cleanNativeNotes([ { pitch: 60, start_beat: 0, duration_beats: 4, velocity: 0.8 }, { pitch: 60, start_beat: 2, duration_beats: 1, velocity: 0.8 }, ]); assert(leg.length === 2, 'legato giu 2 note'); assert(near(leg[0].duration_beats, 2 - 0.01, 1e-6), 'note dau cat con ' + leg[0].duration_beats); assert(near(leg[1].duration_beats, 1), 'note sau giu nguyen'); // 5. Nối tiếp khác pitch → không cắt const seq = _cleanNativeNotes([ { pitch: 60, start_beat: 0, duration_beats: 4, velocity: 0.8 }, { pitch: 64, start_beat: 2, duration_beats: 1, velocity: 0.8 }, ]); assert(near(seq[0].duration_beats, 4), 'khac pitch khong bi cat'); console.log(fails ? 'FAILED: ' + fails : 'ALL PASS'); process.exit(fails ? 1 : 0);