import { type Chord, chordFrom } from "./chord.ts"; import { mod, pcOf, spellDegree, withoutDoubles } from "./pitch.ts"; /** a way into a chord: what to play just before it, named for what it is relative to it */ export type Connection = { name: string; chords: Chord[] }; /** * the chords to fill a gap with, from `prev` into `target`, after the bossa nova autocomposer of * nogloff's autoharp: dominants, ii–Vs and their tritone substitutes, and the more colourful * approaches (♭VI–V, the backdoor, planing, Coltrane's thirds). best first: those that follow on * most smoothly from `prev`, where there is one. the approach chords match the target's colour, * 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 */ export function connections(prev: Chord | undefined, target: Chord): Connection[] { const t = target.tones; const minor = t.includes(3) && !t.includes(4); const ninths = t.includes(2) && (t.includes(10) || t.includes(11)); // a chord on a degree of the target's major scale, `semis` above it: no double flats, though // ♭II of A♭ is B♭♭ by the book const on = (degree: number, semis: number, tones: number[] = t) => chordFrom(withoutDoubles(spellDegree(target.root, degree, semis)), tones)!; const dom = ninths ? [0, 2, 4, 7, 10] : [0, 4, 7, 10]; const m7 = ninths ? [0, 2, 3, 7, 10] : [0, 3, 7, 10]; const maj7 = ninths ? [0, 2, 4, 7, 11] : [0, 4, 7, 11]; const V = on(4, 7, minor ? [0, 1, 4, 7, 10] : dom); const subV = on(1, 1, dom); const ii = on(1, 2, minor ? [0, 3, 6, 10] : m7); const routes: Connection[] = [ { name: "V", chords: [V] }, { name: "ii–V", chords: [ii, V] }, { name: "subV", chords: [subV] }, { name: "ii–subV", chords: [ii, subV] }, { name: "vii°7", chords: [on(6, 11, [0, 3, 6, 9])] }, { name: "♭VI–V", chords: [on(5, 8, maj7), V] }, ...(minor ? [] : [ { name: "iv–♭VII", chords: [on(3, 5, m7), on(6, 10, dom)] }, { name: "iii–VI–ii–V", chords: [on(2, 4, m7), on(5, 9, dom), ii, V] }, ]), // the target's own shape, sliding into it from a semitone away { name: "planed ♭II", chords: [on(1, 1)] }, { name: "planed VII", chords: [on(6, 11)] }, ]; const from = prev && pcOf(prev.root); // coltrane's changes: in by major thirds, from a major third away const third = from !== undefined && [4, 8].includes(mod(pcOf(target.root) - from, 12)); if (third && !minor) { routes.push({ name: "coltrane", chords: [on(6, 11, dom), on(2, 4, maj7), V] }); } if (!prev) return routes; const same = (a: Chord, b: Chord) => pcOf(a.root) === pcOf(b.root) && a.tones.join() === b.tones.join(); // how smoothly a chord follows `prev`: best down a fifth, then by step, then the rest const cost = (c: Chord) => ({ 5: 0, 1: 1, 11: 1, 2: 2, 10: 2, 0: 2, 6: 4 })[mod(pcOf(c.root) - from!, 12)] ?? 3; const seen = new Set(); return ( routes // no playing the chord that's already there again .map(r => (same(r.chords[0], prev) ? { ...r, chords: r.chords.slice(1) } : r)) .filter(r => { const id = r.chords.map(c => `${pcOf(c.root)}:${c.tones}`).join(" "); if (!r.chords.length || seen.has(id)) return false; seen.add(id); return true; }) .map((r, i) => ({ r, i, cost: cost(r.chords[0]) })) .sort((a, b) => a.cost - b.cost || a.i - b.i) .map(({ r }) => r) ); }