char-slop/chordpad.vibed.software

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

Charlotte Somimprove interchange detection66d16f5

main
2.7 KiB73 linesraw
1import { mod, pcOf, type Spelled } from "./pitch.ts";
2
3export type ModeId =
4  | "ionian"
5  | "dorian"
6  | "phrygian"
7  | "lydian"
8  | "mixolydian"
9  | "aeolian"
10  | "locrian"
11  | "harmonicMinor"
12  | "melodicMinor";
13
14export const MODES: Record<ModeId, { label: string; steps: readonly number[] }> = {
15  ionian: { label: "major", steps: [0, 2, 4, 5, 7, 9, 11] },
16  dorian: { label: "dorian", steps: [0, 2, 3, 5, 7, 9, 10] },
17  phrygian: { label: "phrygian", steps: [0, 1, 3, 5, 7, 8, 10] },
18  lydian: { label: "lydian", steps: [0, 2, 4, 6, 7, 9, 11] },
19  mixolydian: { label: "mixolydian", steps: [0, 2, 4, 5, 7, 9, 10] },
20  aeolian: { label: "minor", steps: [0, 2, 3, 5, 7, 8, 10] },
21  locrian: { label: "locrian", steps: [0, 1, 3, 5, 6, 8, 10] },
22  harmonicMinor: { label: "harmonic minor", steps: [0, 2, 3, 5, 7, 8, 11] },
23  melodicMinor: { label: "melodic minor", steps: [0, 2, 3, 5, 7, 9, 11] },
24};
25
26export type Key = { tonic: Spelled; mode: ModeId };
27
28// how commonly each mode is borrowed from, for breaking ties
29export const MIXTURE_ORDER: readonly ModeId[] = [
30  "aeolian",
31  "mixolydian",
32  "dorian",
33  "harmonicMinor",
34  "lydian",
35  "phrygian",
36  "melodicMinor",
37  "locrian",
38  "ionian",
39];
40
41/**
42 * the home mode, then where chords are usually borrowed from: the parallel major and minor, then
43 * the other modes. nearest first within each, since a mode that alters fewer degrees is the
44 * likelier reading
45 */
46export function modesFrom(home: ModeId): ModeId[] {
47  // the minor-scale variants are less idiomatic sources than the diatonic modes
48  const penalty = (m: ModeId) => (m === "harmonicMinor" || m === "melodicMinor" ? 2 : 0);
49  const distance = (m: ModeId) =>
50    MODES[m].steps.filter((s, i) => s !== MODES[home].steps[i]).length + penalty(m);
51  const nearest = (modes: ModeId[]) => modes.sort((a, b) => distance(a) - distance(b));
52  const parallel = (m: ModeId) => m === "ionian" || m === "aeolian";
53  const others = MIXTURE_ORDER.filter(m => m !== home);
54  return [
55    home,
56    ...nearest(others.filter(parallel)),
57    ...nearest(others.filter(m => !parallel(m))),
58  ];
59}
60
61/**
62 * move a pitch by scale degrees of the key. a note outside the scale keeps its distance above
63 * the degree below it, so C♯ in C major moves up to D♯: a chromatic passing note stays one
64 */
65export function stepInScale(pitch: number, key: Key, degrees: number): number {
66  const steps = MODES[key.mode].steps;
67  const fromTonic = pitch - pcOf(key.tonic);
68  const within = mod(fromTonic, 12);
69  const degree = steps.findLastIndex(s => s <= within);
70  const target = degree + degrees;
71  const octaves = Math.floor(fromTonic / 12) + Math.floor(target / 7);
72  return pcOf(key.tonic) + 12 * octaves + steps[mod(target, 7)] + (within - steps[degree]);
73}