char-slop/chordpad.vibed.software

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

Charlotte Somexperiment: chord connectionsa427fe3

main
2.7 KiB65 linesraw
1import { type Chord, chordFrom } from "./chord.ts";
2import { type Key, MODES } from "./key.ts";
3import { mod, pcOf, spellDegree } from "./pitch.ts";
4
5// all in root position: where the bass goes is left to voicing
6
7const has = (c: Chord, ...tones: number[]) => tones.every(t => c.tones.includes(t));
8const without = (c: Chord, ...tones: number[]) => c.tones.filter(t => !tones.includes(t));
9const inKey = (key: Key, pc: number) =>
10  MODES[key.mode].steps.includes(mod(pc - pcOf(key.tonic), 12));
11
12const isMajor = (c: Chord) => has(c, 4) && !has(c, 3);
13const isMinor = (c: Chord) => has(c, 3) && !has(c, 4);
14export const isDominant = (c: Chord): boolean => has(c, 4, 10) && !has(c, 3);
15
16/** C ↔ Cm, keeping everything but the third */
17export function parallel(c: Chord): Chord | undefined {
18  if (has(c, 6) && !has(c, 7)) return undefined; // diminished: no major counterpart
19  if (isMajor(c)) return chordFrom(c.root, [...without(c, 4), 3]);
20  if (isMinor(c)) return chordFrom(c.root, [...without(c, 3), 4]);
21  return undefined;
22}
23
24/** C ↔ Am, Cmaj7 ↔ Am7: the chord a minor third away sharing most of its notes */
25export function relative(c: Chord): Chord | undefined {
26  if (!has(c, 7)) return undefined;
27  if (isMajor(c) && !has(c, 10)) {
28    return chordFrom(spellDegree(c.root, 5, 9), has(c, 11) ? [0, 3, 7, 10] : [0, 3, 7]);
29  }
30  if (isMinor(c) && !has(c, 11)) {
31    return chordFrom(spellDegree(c.root, 2, 3), has(c, 10) ? [0, 4, 7, 11] : [0, 4, 7]);
32  }
33  return undefined;
34}
35
36/** G7 → D♭7: the dominant a tritone away, with the same tritone inside it */
37export function tritoneSub(c: Chord): Chord | undefined {
38  return isDominant(c) ? chordFrom(spellDegree(c.root, 4, 6), c.tones) : undefined;
39}
40
41/** add a 7th, picking whichever fits the key (C → Cmaj7, G → G7 in C), or take it away */
42export function toggleSeventh(c: Chord, key: Key): Chord | undefined {
43  const dim = has(c, 3, 6) && !has(c, 7);
44  if (has(c, 10) || has(c, 11) || (dim && has(c, 9)))
45    return chordFrom(c.root, without(c, 9, 10, 11));
46  const root = pcOf(c.root);
47  const [preferred, fallback] = dim ? [10, 9] : isMajor(c) ? [11, 10] : [10, 11];
48  const seventh = inKey(key, root + preferred)
49    ? preferred
50    : inKey(key, root + fallback)
51      ? fallback
52      : dim
53        ? 9
54        : 10;
55  return chordFrom(c.root, [...c.tones, seventh]);
56}
57
58/** replace the third with a 2nd or 4th; asked again, put the key's third back */
59export function suspend(c: Chord, key: Key, tone: 2 | 5): Chord | undefined {
60  if (has(c, tone) && !has(c, 3) && !has(c, 4)) {
61    const third = inKey(key, pcOf(c.root) + 4) ? 4 : 3;
62    return chordFrom(c.root, [...without(c, tone), third]);
63  }
64  return chordFrom(c.root, [...without(c, 3, 4, 2, 5), tone]);
65}