char-slop/chordpad.vibed.software

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

Charlotte Somchord panel style tweakad1ae60

main
3.4 KiB75 linesraw
1import { type Chord, chordFrom } from "./chord.ts";
2import { mod, pcOf, spellDegree, withoutDoubles } from "./pitch.ts";
3
4/** a way into a chord: what to play just before it, named for what it is relative to it */
5export type Connection = { name: string; chords: Chord[] };
6
7/**
8 * the chords to fill a gap with, from `prev` into `target`, after the bossa nova autocomposer of
9 * nogloff's autoharp: dominants, ii–Vs and their tritone substitutes, and the more colourful
10 * approaches (♭VI–V, the backdoor, planing, Coltrane's thirds). best first: those that follow on
11 * most smoothly from `prev`, where there is one. the approach chords match the target's colour,
12 * so a maj9 is led into from m9 and 9, a maj7 from m7 and 7, and a minor chord from m7♭5 and 7♭9
13 */
14export function connections(prev: Chord | undefined, target: Chord): Connection[] {
15  const t = target.tones;
16  const minor = t.includes(3) && !t.includes(4);
17  const ninths = t.includes(2) && (t.includes(10) || t.includes(11));
18  // a chord on a degree of the target's major scale, `semis` above it: no double flats, though
19  // ♭II of A♭ is B♭♭ by the book
20  const on = (degree: number, semis: number, tones: number[] = t) =>
21    chordFrom(withoutDoubles(spellDegree(target.root, degree, semis)), tones)!;
22  const dom = ninths ? [0, 2, 4, 7, 10] : [0, 4, 7, 10];
23  const m7 = ninths ? [0, 2, 3, 7, 10] : [0, 3, 7, 10];
24  const maj7 = ninths ? [0, 2, 4, 7, 11] : [0, 4, 7, 11];
25
26  const V = on(4, 7, minor ? [0, 1, 4, 7, 10] : dom);
27  const subV = on(1, 1, dom);
28  const ii = on(1, 2, minor ? [0, 3, 6, 10] : m7);
29  const routes: Connection[] = [
30    { name: "V", chords: [V] },
31    { name: "ii–V", chords: [ii, V] },
32    { name: "subV", chords: [subV] },
33    { name: "ii–subV", chords: [ii, subV] },
34    { name: "vii°7", chords: [on(6, 11, [0, 3, 6, 9])] },
35    { name: "♭VI–V", chords: [on(5, 8, maj7), V] },
36    ...(minor
37      ? []
38      : [
39          { name: "iv–♭VII", chords: [on(3, 5, m7), on(6, 10, dom)] },
40          { name: "iii–VI–ii–V", chords: [on(2, 4, m7), on(5, 9, dom), ii, V] },
41        ]),
42    // the target's own shape, sliding into it from a semitone away
43    { name: "planed ♭II", chords: [on(1, 1)] },
44    { name: "planed VII", chords: [on(6, 11)] },
45  ];
46
47  const from = prev && pcOf(prev.root);
48  // coltrane's changes: in by major thirds, from a major third away
49  const third = from !== undefined && [4, 8].includes(mod(pcOf(target.root) - from, 12));
50  if (third && !minor) {
51    routes.push({ name: "coltrane", chords: [on(6, 11, dom), on(2, 4, maj7), V] });
52  }
53  if (!prev) return routes;
54
55  const same = (a: Chord, b: Chord) =>
56    pcOf(a.root) === pcOf(b.root) && a.tones.join() === b.tones.join();
57  // how smoothly a chord follows `prev`: best down a fifth, then by step, then the rest
58  const cost = (c: Chord) =>
59    ({ 5: 0, 1: 1, 11: 1, 2: 2, 10: 2, 0: 2, 6: 4 })[mod(pcOf(c.root) - from!, 12)] ?? 3;
60  const seen = new Set<string>();
61  return (
62    routes
63      // no playing the chord that's already there again
64      .map(r => (same(r.chords[0], prev) ? { ...r, chords: r.chords.slice(1) } : r))
65      .filter(r => {
66        const id = r.chords.map(c => `${pcOf(c.root)}:${c.tones}`).join(" ");
67        if (!r.chords.length || seen.has(id)) return false;
68        seen.add(id);
69        return true;
70      })
71      .map((r, i) => ({ r, i, cost: cost(r.chords[0]) }))
72      .sort((a, b) => a.cost - b.cost || a.i - b.i)
73      .map(({ r }) => r)
74  );
75}