File size: 3,510 Bytes
5c0b162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a6bd27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5c0b162
 
 
 
 
 
 
 
 
 
 
 
6a6bd27
5c0b162
 
 
 
 
 
 
 
6a6bd27
 
 
 
 
 
 
 
 
 
5c0b162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/**
 * 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}`;
}