char-slop/chordpad.vibed.software

git clone https://git.t4t.associates/char-slop/chordpad.vibed.software

Charlotte Sominitial commit3c7bc6e

main
6.1 KiB203 linesraw
1import { assertEquals } from "@std/assert";
2import { type HookpadChord, type HookpadSong, readSection } from "./hookpad.ts";
3import { theoryTabPath, theoryTabProject } from "./theorytab.ts";
4
5const chord = (fields: Partial<HookpadChord>): HookpadChord => ({
6  beat: 1,
7  duration: 4,
8  root: 1,
9  type: 5,
10  inversion: 0,
11  applied: 0,
12  adds: [],
13  omits: [],
14  alterations: [],
15  suspensions: [],
16  borrowed: null,
17  pedal: null,
18  substitutions: [],
19  isRest: false,
20  ...fields,
21});
22
23const song = (fields: Partial<HookpadSong>): HookpadSong => ({
24  keys: [{ beat: 1, scale: "major", tonic: "C" }],
25  meters: [{ beat: 1, numBeats: 4, beatUnit: 1 }],
26  tempos: [{ beat: 1, bpm: 100 }],
27  chords: [],
28  notes: [],
29  endBeat: 5,
30  ...fields,
31});
32
33const symbols = (chords: Partial<HookpadChord>[], key = { scale: "major", tonic: "C" }) =>
34  readSection(song({ keys: [{ beat: 1, ...key }], chords: chords.map(chord) })).chords.map(
35    c => c.symbol,
36  );
37
38Deno.test("hookpad chords as symbols", () => {
39  assertEquals(
40    symbols([
41      { root: 1 },
42      { root: 6 },
43      { root: 5, type: 7 },
44      { root: 5, type: 7, inversion: 1 },
45      { root: 1, inversion: 2 },
46      { root: 2, type: 7, inversion: 3 },
47      { root: 7, type: 7 },
48      { root: 4, type: 9 },
49      { root: 5, suspensions: [4] },
50      { root: 1, adds: [9] },
51      { root: 1, omits: [3] },
52      { root: 5, type: 7, alterations: ["b9"] },
53    ]),
54    ["C", "Am", "G7", "G7/B", "C/G", "Dm7/C", "Bm7b5", "Fmaj9", "Gsus4", "Cadd9", "C5", "G7b9"],
55  );
56});
57
58Deno.test("borrowed and secondary chords", () => {
59  assertEquals(
60    symbols([
61      { root: 7, borrowed: "minor" },
62      { root: 4, borrowed: "minor" },
63      { root: 6, borrowed: "minor", type: 7 },
64      { root: 2, borrowed: "phrygian" },
65      // stored as the target degree, with the secondary function in `applied`
66      { root: 6, applied: 5 },
67      { root: 5, applied: 5, type: 7 },
68      { root: 2, applied: 7, type: 7 },
69      // a custom scale, in semitones above the tonic
70      { root: 3, borrowed: [0, 2, 3, 5, 7, 8, 10] },
71    ]),
72    ["Bb", "Fm", "Abmaj7", "Db", "E", "D7", "C#dim7", "Eb"],
73  );
74});
75
76Deno.test("minor keys count degrees in the minor scale", () => {
77  assertEquals(
78    symbols([{ root: 1 }, { root: 3 }, { root: 5 }, { root: 5, borrowed: "harmonicMinor" }], {
79      scale: "minor",
80      tonic: "A",
81    }),
82    ["Am", "C", "Em", "E"],
83  );
84});
85
86Deno.test("theoretical keys and double accidentals get their everyday spellings", () => {
87  assertEquals(symbols([{ root: 1 }, { root: 5 }], { scale: "major", tonic: "D#" }), [
88    "Eb",
89    "Bb",
90  ]);
91  assertEquals(symbols([{ root: 1 }, { root: 6 }], { scale: "minor", tonic: "Db" }), [
92    "C#m",
93    "A",
94  ]);
95  // V6/vi in B major: D♯ with F𝄪 in the bass
96  const secondary = symbols([{ root: 6, applied: 5, inversion: 1 }], {
97    scale: "major",
98    tonic: "B",
99  });
100  assertEquals(secondary, ["D#/G"]);
101  const key = readSection(song({ keys: [{ beat: 1, scale: "minor", tonic: "Gb" }] })).key;
102  assertEquals(key, { tonic: "F#", scale: "minor" });
103});
104
105Deno.test("melody pitches: octave 0 starts at the tonic above middle C", () => {
106  const read = readSection(
107    song({
108      keys: [{ beat: 1, scale: "major", tonic: "A" }],
109      notes: [
110        { beat: 1, duration: 1, sd: "1", octave: 0, isRest: false },
111        { beat: 2, duration: 1, sd: "5", octave: -1, isRest: false },
112        { beat: 3, duration: 0.5, sd: "b3", octave: 0, isRest: false },
113        { beat: 3.5, duration: 0.5, sd: "1", octave: 0, isRest: true },
114        { beat: 4, duration: 1, sd: "#4", octave: 1, isRest: false },
115      ],
116    }),
117  );
118  assertEquals(read.notes, [
119    { start: 0, dur: 1, pitch: 69 },
120    { start: 1, dur: 1, pitch: 64 },
121    { start: 2, dur: 0.5, pitch: 72 },
122    { start: 3, dur: 1, pitch: 87 },
123  ]);
124});
125
126Deno.test("a key change applies from its beat on", () => {
127  const read = readSection(
128    song({
129      keys: [
130        { beat: 1, scale: "major", tonic: "C" },
131        { beat: 5, scale: "major", tonic: "D" },
132      ],
133      chords: [chord({ root: 1, beat: 1 }), chord({ root: 1, beat: 5 })],
134      notes: [{ beat: 5, duration: 1, sd: "1", octave: 0, isRest: false }],
135      endBeat: 9,
136    }),
137  );
138  assertEquals(
139    read.chords.map(c => [c.start, c.symbol]),
140    [
141      [0, "C"],
142      [4, "D"],
143    ],
144  );
145  assertEquals(read.notes[0].pitch, 62);
146});
147
148Deno.test("a theorytab becomes a project with its sections end to end, on bar lines", () => {
149  const verse = song({
150    keys: [{ beat: 1, scale: "minor", tonic: "D#" }],
151    tempos: [{ beat: 1, bpm: 92.4 }],
152    chords: [chord({ root: 1, duration: 4 }), chord({ root: 6, beat: 5, duration: 2 })],
153    endBeat: 7, // a bar and a half: the chorus should still start on bar 3
154  });
155  const chorus = song({
156    keys: [{ beat: 1, scale: "minor", tonic: "D#" }],
157    chords: [chord({ root: 4 })],
158    notes: [{ beat: 1, duration: 2, sd: "5", octave: 0, isRest: false }],
159  });
160  const p = theoryTabProject({
161    title: "Some Song – Someone",
162    sections: [
163      { name: "verse", song: verse },
164      { name: "empty", song: song({}) },
165      { name: "chorus", song: chorus },
166    ],
167  });
168  assertEquals(
169    [p.name, p.bpm, p.key],
170    ["Some Song – Someone", 92, { tonic: "D#", mode: "aeolian" }],
171  );
172  const [chords, melody] = p.tracks;
173  assertEquals(chords.kind === "chords" && chords.chords.map(c => [c.start, c.dur, c.symbol]), [
174    [0, 4, "D#m"],
175    [4, 2, "B"],
176    [8, 4, "G#m"],
177  ]);
178  assertEquals(melody.kind === "notes" && melody.notes.map(n => [n.start, n.pitch]), [[8, 70]]);
179});
180
181Deno.test("theorytab links", () => {
182  const path = "/theorytab/view/the-beatles/let-it-be";
183  assertEquals(
184    theoryTabPath("https://www.hooktheory.com/theorytab/view/the-beatles/let-it-be"),
185    path,
186  );
187  assertEquals(
188    theoryTabPath("  hooktheory.com/theorytab/view/the-beatles/let-it-be"),
189    undefined,
190  );
191  assertEquals(
192    theoryTabPath("https://hooktheory.com/theorytab/view/the-beatles/let-it-be/chorus?x#y"),
193    path,
194  );
195  assertEquals(
196    theoryTabPath("https://example.com/theorytab/view/the-beatles/let-it-be"),
197    undefined,
198  );
199  assertEquals(
200    theoryTabPath("https://www.hooktheory.com/theorytab/artist/the-beatles"),
201    undefined,
202  );
203});