char-slop/chordpad.vibed.software

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

Charlotte Sominitial commit3c7bc6e

main
3.5 KiB108 linesraw
1import { assert, assertEquals } from "@std/assert";
2import { isKeyRelative } from "../theory/chord.ts";
3import { rekey, unpinRedundant, voiceTrack } from "./harmony.ts";
4import {
5  type ChordTrack,
6  newChordTrack,
7  newProject,
8  placeChord,
9  type Project,
10  projectKey,
11} from "./model.ts";
12
13const SYMBOLS = ["I", "vi", "IV/A", "V7/V", "bVI", "iiø7", "4", "57", "Cmaj7", "b7", "ii"];
14
15function sketch(): Project {
16  const p = newProject();
17  const track = newChordTrack();
18  SYMBOLS.forEach((symbol, i) =>
19    placeChord(track, { id: `${i}`, start: i * 4, dur: 4, symbol }),
20  );
21  track.chords[10].notes = [38, 57, 62, 65]; // ii, hand-voiced
22  p.tracks = [track];
23  return p;
24}
25const chords = (p: Project) => p.tracks[0] as ChordTrack;
26const sounding = (p: Project) => voiceTrack(chords(p), projectKey(p)).map(v => v.notes);
27const symbols = (p: Project) => chords(p).chords.map(c => c.symbol);
28
29Deno.test("changing key or mode changes no voicing", () => {
30  const p = sketch();
31  const before = sounding(p);
32  for (const key of [
33    { tonic: "A", mode: "aeolian" },
34    { tonic: "Eb", mode: "dorian" },
35    { tonic: "F#", mode: "lydian" },
36    { tonic: "C", mode: "ionian" },
37  ] as const) {
38    rekey(p, key);
39    assertEquals(sounding(p), before, `${key.tonic} ${key.mode}: ${symbols(p)}`);
40  }
41  // everything written relative to the key still is, though a scale degree the key has no chord
42  // for (F major in E♭ dorian) has to become a roman numeral
43  for (const [i, symbol] of symbols(p).entries()) {
44    if (SYMBOLS[i] !== "Cmaj7") assert(isKeyRelative(symbol!), symbol);
45  }
46});
47
48Deno.test("chords written relative to the key stay that way, renamed for the new one", () => {
49  const p = sketch();
50  rekey(p, { tonic: "A", mode: "aeolian" });
51  assertEquals(symbols(p), [
52    "III",
53    "i",
54    "VI/A",
55    "IV7",
56    "#VII",
57    "ivø7",
58    "6",
59    "77",
60    "Cmaj7",
61    "bII",
62    "iv",
63  ]);
64  for (const [i, symbol] of symbols(p).entries()) {
65    if (SYMBOLS[i] !== "Cmaj7") assert(isKeyRelative(symbol!), symbol);
66  }
67});
68
69function threeChords(): Project {
70  const p = newProject();
71  const track = newChordTrack();
72  for (const [i, symbol] of ["C", "Am", "F"].entries()) {
73    placeChord(track, { id: symbol, start: i * 4, dur: 4, symbol });
74  }
75  p.tracks = [track];
76  return p;
77}
78const pinned = (p: Project) => chords(p).chords.map(c => c.notes !== undefined);
79
80Deno.test("hand-set notes that are just the auto-voicing get unpinned", () => {
81  const p = threeChords();
82  const auto = sounding(p);
83  // as if Am were inverted up, then back down
84  chords(p).chords[1].notes = [...auto[1]];
85  const settled = unpinRedundant(p);
86  assertEquals(pinned(settled), [false, false, false]);
87  assertEquals(sounding(settled), auto);
88});
89
90Deno.test("a real hand-voicing stays pinned, and so does what follows it", () => {
91  const p = threeChords();
92  const [bass, ...upper] = sounding(p)[1];
93  chords(p).chords[1].notes = [bass, ...upper.map(n => n + 12)];
94  const settled = unpinRedundant(p);
95  assertEquals(pinned(settled), [false, true, false]);
96  assertEquals(settled, p); // nothing to change: the very same project back
97});
98
99Deno.test("notes without a symbol, voiced as their chord would be, become that chord", () => {
100  const p = threeChords();
101  const auto = sounding(p);
102  const am = chords(p).chords[1];
103  delete am.symbol;
104  am.notes = [...auto[1]];
105  const settled = unpinRedundant(p);
106  assertEquals(chords(settled).chords[1], { id: "Am", start: 4, dur: 4, symbol: "Am" });
107  assertEquals(sounding(settled), auto);
108});