import { assertEquals } from "@std/assert"; import { type HookpadChord, type HookpadSong, readSection } from "./hookpad.ts"; import { theoryTabPath, theoryTabProject } from "./theorytab.ts"; const chord = (fields: Partial): HookpadChord => ({ beat: 1, duration: 4, root: 1, type: 5, inversion: 0, applied: 0, adds: [], omits: [], alterations: [], suspensions: [], borrowed: null, pedal: null, substitutions: [], isRest: false, ...fields, }); const song = (fields: Partial): HookpadSong => ({ keys: [{ beat: 1, scale: "major", tonic: "C" }], meters: [{ beat: 1, numBeats: 4, beatUnit: 1 }], tempos: [{ beat: 1, bpm: 100 }], chords: [], notes: [], endBeat: 5, ...fields, }); const symbols = (chords: Partial[], key = { scale: "major", tonic: "C" }) => readSection(song({ keys: [{ beat: 1, ...key }], chords: chords.map(chord) })).chords.map( c => c.symbol, ); Deno.test("hookpad chords as symbols", () => { assertEquals( symbols([ { root: 1 }, { root: 6 }, { root: 5, type: 7 }, { root: 5, type: 7, inversion: 1 }, { root: 1, inversion: 2 }, { root: 2, type: 7, inversion: 3 }, { root: 7, type: 7 }, { root: 4, type: 9 }, { root: 5, suspensions: [4] }, { root: 1, adds: [9] }, { root: 1, omits: [3] }, { root: 5, type: 7, alterations: ["b9"] }, ]), ["C", "Am", "G7", "G7/B", "C/G", "Dm7/C", "Bm7b5", "Fmaj9", "Gsus4", "Cadd9", "C5", "G7b9"], ); }); Deno.test("borrowed and secondary chords", () => { assertEquals( symbols([ { root: 7, borrowed: "minor" }, { root: 4, borrowed: "minor" }, { root: 6, borrowed: "minor", type: 7 }, { root: 2, borrowed: "phrygian" }, // stored as the target degree, with the secondary function in `applied` { root: 6, applied: 5 }, { root: 5, applied: 5, type: 7 }, { root: 2, applied: 7, type: 7 }, // a custom scale, in semitones above the tonic { root: 3, borrowed: [0, 2, 3, 5, 7, 8, 10] }, ]), ["Bb", "Fm", "Abmaj7", "Db", "E", "D7", "C#dim7", "Eb"], ); }); Deno.test("minor keys count degrees in the minor scale", () => { assertEquals( symbols([{ root: 1 }, { root: 3 }, { root: 5 }, { root: 5, borrowed: "harmonicMinor" }], { scale: "minor", tonic: "A", }), ["Am", "C", "Em", "E"], ); }); Deno.test("theoretical keys and double accidentals get their everyday spellings", () => { assertEquals(symbols([{ root: 1 }, { root: 5 }], { scale: "major", tonic: "D#" }), [ "Eb", "Bb", ]); assertEquals(symbols([{ root: 1 }, { root: 6 }], { scale: "minor", tonic: "Db" }), [ "C#m", "A", ]); // V6/vi in B major: D♯ with F𝄪 in the bass const secondary = symbols([{ root: 6, applied: 5, inversion: 1 }], { scale: "major", tonic: "B", }); assertEquals(secondary, ["D#/G"]); const key = readSection(song({ keys: [{ beat: 1, scale: "minor", tonic: "Gb" }] })).key; assertEquals(key, { tonic: "F#", scale: "minor" }); }); Deno.test("melody pitches: octave 0 starts at the tonic above middle C", () => { const read = readSection( song({ keys: [{ beat: 1, scale: "major", tonic: "A" }], notes: [ { beat: 1, duration: 1, sd: "1", octave: 0, isRest: false }, { beat: 2, duration: 1, sd: "5", octave: -1, isRest: false }, { beat: 3, duration: 0.5, sd: "b3", octave: 0, isRest: false }, { beat: 3.5, duration: 0.5, sd: "1", octave: 0, isRest: true }, { beat: 4, duration: 1, sd: "#4", octave: 1, isRest: false }, ], }), ); assertEquals(read.notes, [ { start: 0, dur: 1, pitch: 69 }, { start: 1, dur: 1, pitch: 64 }, { start: 2, dur: 0.5, pitch: 72 }, { start: 3, dur: 1, pitch: 87 }, ]); }); Deno.test("a key change applies from its beat on", () => { const read = readSection( song({ keys: [ { beat: 1, scale: "major", tonic: "C" }, { beat: 5, scale: "major", tonic: "D" }, ], chords: [chord({ root: 1, beat: 1 }), chord({ root: 1, beat: 5 })], notes: [{ beat: 5, duration: 1, sd: "1", octave: 0, isRest: false }], endBeat: 9, }), ); assertEquals( read.chords.map(c => [c.start, c.symbol]), [ [0, "C"], [4, "D"], ], ); assertEquals(read.notes[0].pitch, 62); }); Deno.test("a theorytab becomes a project with its sections end to end, on bar lines", () => { const verse = song({ keys: [{ beat: 1, scale: "minor", tonic: "D#" }], tempos: [{ beat: 1, bpm: 92.4 }], chords: [chord({ root: 1, duration: 4 }), chord({ root: 6, beat: 5, duration: 2 })], endBeat: 7, // a bar and a half: the chorus should still start on bar 3 }); const chorus = song({ keys: [{ beat: 1, scale: "minor", tonic: "D#" }], chords: [chord({ root: 4 })], notes: [{ beat: 1, duration: 2, sd: "5", octave: 0, isRest: false }], }); const p = theoryTabProject({ title: "Some Song – Someone", sections: [ { name: "verse", song: verse }, { name: "empty", song: song({}) }, { name: "chorus", song: chorus }, ], }); assertEquals( [p.name, p.bpm, p.key], ["Some Song – Someone", 92, { tonic: "D#", mode: "aeolian" }], ); const [chords, melody] = p.tracks; assertEquals(chords.kind === "chords" && chords.chords.map(c => [c.start, c.dur, c.symbol]), [ [0, 4, "D#m"], [4, 2, "B"], [8, 4, "G#m"], ]); assertEquals(melody.kind === "notes" && melody.notes.map(n => [n.start, n.pitch]), [[8, 70]]); }); Deno.test("theorytab links", () => { const path = "/theorytab/view/the-beatles/let-it-be"; assertEquals( theoryTabPath("https://www.hooktheory.com/theorytab/view/the-beatles/let-it-be"), path, ); assertEquals( theoryTabPath(" hooktheory.com/theorytab/view/the-beatles/let-it-be"), undefined, ); assertEquals( theoryTabPath("https://hooktheory.com/theorytab/view/the-beatles/let-it-be/chorus?x#y"), path, ); assertEquals( theoryTabPath("https://example.com/theorytab/view/the-beatles/let-it-be"), undefined, ); assertEquals( theoryTabPath("https://www.hooktheory.com/theorytab/artist/the-beatles"), undefined, ); });