yue2-studio / src /lib /abc /model.ts
RalphThings's picture
Upload folder using huggingface_hub
6a6bd27 verified
Raw History Blame Contribute Delete
3.51 kB
/**
* The note model behind the piano roll.
*
* Time is counted in *units*, where one unit is the tune's L: default note
* length. That is the same currency ABC itself writes lengths in, so a note
* written `A2` under `L:1/8` is two units long and needs no conversion in
* either direction. Using seconds or beats here would mean rounding on every
* round trip, and rounding is how an edited score slowly drifts out of time.
*/
export interface AbcNote {
id: string;
/** Index into AbcScore.voices. */
voice: number;
/** Onset in units from the start of the piece. */
start: number;
/** Length in units. Always at least 1. */
duration: number;
/** MIDI note number. Rests are held separately, so this is always a pitch. */
midi: number;
/** True when this note is tied into the one that follows it. */
tied: boolean;
}
/**
* A quoted string in the music: a chord symbol like "G#m", or an annotation
* like "^rit." with a placement prefix. YuE2 emits chord symbols routinely, so
* these are carried through rather than refused.
*/
export interface AbcMark {
id: string;
voice: number;
/** Onset in units, matching AbcNote.start. */
start: number;
/** The text as written, without the quotes. */
text: string;
/** True for a placement-prefixed annotation rather than a chord symbol. */
annotation: boolean;
}
export interface AbcVoice {
/** The V: identifier as written, e.g. "Vocal". */
id: string;
/** Anything after the identifier on the V: line, kept so it survives a rewrite. */
rest: string;
}
export interface AbcScore {
/** Header lines above the first voice, verbatim and in order. */
header: string[];
voices: AbcVoice[];
notes: AbcNote[];
marks: AbcMark[];
/** Meter numerator and denominator from M:, defaulting to 4/4. */
meter: [number, number];
/** Unit note length from L:, defaulting to 1/8. */
unit: [number, number];
/** Key signature from K:, used by the grid to shade the scale. */
key: string;
/** True when a repeat was written out so the grid could show it. */
repeatsExpanded: boolean;
/** Beats per minute from Q:, defaulting to 120 quarter notes. */
tempo: number;
}
/** Seconds one unit lasts at this score's tempo. */
export function unitSeconds(score: Pick<AbcScore, "unit" | "tempo">): number {
const [un, ud] = score.unit;
// A unit is un/ud of a whole note; a whole note is four quarters.
const quarters = (un / ud) * 4;
return quarters * (60 / Math.max(20, score.tempo));
}
/** Why a score could not be opened in the grid. */
export interface AbcParseFailure {
ok: false;
reason: string;
/** The construct that stopped us, so the message can name it. */
offending: string | null;
}
export interface AbcParseSuccess {
ok: true;
score: AbcScore;
}
export type AbcParseResult = AbcParseSuccess | AbcParseFailure;
/** How many units fill one bar, given the meter and unit length. */
export function unitsPerBar(score: Pick<AbcScore, "meter" | "unit">): number {
const [mn, md] = score.meter;
const [un, ud] = score.unit;
const bars = (mn / md) / (un / ud);
// A meter that does not divide evenly into the unit length is legal ABC but
// makes a ragged grid, so it is rounded rather than left fractional.
return Math.max(1, Math.round(bars));
}
export function noteName(midi: number): string {
const names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
return `${names[((midi % 12) + 12) % 12]}${Math.floor(midi / 12) - 1}`;
}