const { test } = require('node:test'), assert = require('node:assert/strict'); const P = require('./piano-core'); test('compact notes sort, round timing and infer hands below middle C', () => { const notes = P.normalizeNotes([ { pitch: 60, start: 1.23456, end: 1.8, velocity: 85 }, { pitch: 48, start: 0, end: 0.5, velocity: 150 }, { pitch: 55, start: 2, end: 3, velocity: 40, hand: 'right' }, ]); assert.equal(notes[0].hand, 'left'); assert.equal(notes[0].velocity, 127); assert.equal(notes[1].start, 1.235); assert.equal(notes[1].hand, 'right'); assert.equal(notes[2].hand, 'right'); }); test('invalid notes are rejected and tiny notes retain positive duration', () => { assert.throws(() => P.normalizeNotes([{ pitch: 200, start: 0, end: 1, velocity: 90 }])); assert.throws(() => P.normalizeNotes([{ pitch: 60, start: 2, end: 1, velocity: 90 }])); const [note] = P.normalizeNotes([{ pitch: 60, start: 0.0001, end: 0.0002, velocity: 0 }]); assert.ok(note.end > note.start); }); test('Basic Pitch output converts amplitude to MIDI velocity and active notes use exclusive ends', () => { const notes = P.fromBasicPitch([ { pitchMidi: 60, startTimeSeconds: 1, durationSeconds: 0.5, amplitude: 0.5 }, ]); assert.equal(notes[0].velocity, 64); assert.equal(P.activeNotes(notes, 1).length, 1); assert.equal(P.activeNotes(notes, 1.5).length, 0); assert.deepEqual(P.practiceRange(95, 100), { start: 95, end: 100 }); assert.equal(P.practiceRange(100, 100), null); }); test('browser engine prefers WebGPU and reliably falls back to CPU', async () => { const { selectBackend } = await import('./piano-engine.mjs'); const calls = []; const tf = { setBackend: async (name) => { calls.push(name); return true; }, }; assert.equal(await selectBackend(tf, true, async () => {}), 'webgpu'); assert.deepEqual(calls, ['webgpu']); const fallback = { setBackend: async (name) => name === 'cpu' }; assert.equal( await selectBackend(fallback, true, async () => { throw new Error('Unavailable GPU'); }), 'cpu', ); });