Files
ytplayer/frontend/piano-core.js

60 lines
2.2 KiB
JavaScript

/* Compact note sequences and practice ranges, shared by browser and server. */
(function (root) {
'use strict';
const MAX_NOTES = 30000;
function normalizeNotes(input) {
if (!Array.isArray(input) || input.length > MAX_NOTES)
throw new Error('Invalid or oversized note sequence');
return input
.map((note) => {
const pitch = Number(note.pitch),
start = Number(note.start),
end = Number(note.end),
velocity = Number(note.velocity);
if (
!Number.isInteger(pitch) ||
pitch < 21 ||
pitch > 108 ||
!Number.isFinite(start) ||
start < 0 ||
!Number.isFinite(end) ||
end <= start ||
end > 3600 ||
!Number.isFinite(velocity)
)
throw new Error('Invalid piano note');
return {
pitch,
start: Math.round(start * 1000) / 1000,
end: Math.max(Math.round(end * 1000) / 1000, Math.round(start * 1000) / 1000 + 0.001),
velocity: Math.max(1, Math.min(127, Math.round(velocity))),
hand: ['left', 'right'].includes(note.hand) ? note.hand : pitch < 60 ? 'left' : 'right',
};
})
.sort((a, b) => a.start - b.start || a.pitch - b.pitch);
}
function fromBasicPitch(notes) {
return normalizeNotes(
notes
.filter((n) => n.pitchMidi >= 21 && n.pitchMidi <= 108)
.map((n) => ({
pitch: n.pitchMidi,
start: n.startTimeSeconds,
end: n.startTimeSeconds + n.durationSeconds,
velocity: n.amplitude * 127,
})),
);
}
const activeNotes = (notes, time) => notes.filter((n) => n.start <= time && n.end > time);
function practiceRange(time, duration) {
const start = Math.max(0, Number(time) || 0),
end = Math.min(start + 8, Number(duration) || start + 8);
return end > start
? { start: Math.round(start * 1000) / 1000, end: Math.round(end * 1000) / 1000 }
: null;
}
const api = { normalizeNotes, fromBasicPitch, activeNotes, practiceRange, MAX_NOTES };
if (typeof module !== 'undefined') module.exports = api;
else root.PianoCore = api;
})(typeof window !== 'undefined' ? window : globalThis);