char-slop/chordpad.vibed.software

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

Charlotte Sominitial commit3c7bc6e

main
3.1 KiB72 linesraw
1export type Pc = number;
2/** a spelled note name: letter index (C=0 … B=6) plus accidentals (+sharp, -flat) */
3export type Spelled = { letter: number; acc: number };
4
5export const mod = (n: number, m: number): number => ((n % m) + m) % m;
6
7const LETTERS = "CDEFGAB";
8export const MAJOR_STEPS: readonly number[] = [0, 2, 4, 5, 7, 9, 11];
9
10export const pcOf = (n: Spelled): Pc => mod(MAJOR_STEPS[n.letter] + n.acc, 12);
11
12export const accidentalValue = (s: string): number =>
13  [...s].reduce((sum, c) => sum + ("#♯".includes(c) ? 1 : -1), 0);
14
15export function parseNote(src: string): { note: Spelled; rest: string } | undefined {
16  const m = /^([A-Ga-g])([#♯b♭]*)/.exec(src);
17  if (!m) return undefined;
18  const note = { letter: LETTERS.indexOf(m[1].toUpperCase()), acc: accidentalValue(m[2]) };
19  return { note, rest: src.slice(m[0].length) };
20}
21
22export const formatNote = (n: Spelled, ascii = false): string =>
23  LETTERS[n.letter] +
24  (n.acc > 0 ? (ascii ? "#" : "♯") : ascii ? "b" : "♭").repeat(Math.abs(n.acc));
25
26/** spell the note `semis` above `tonic` using the letter `degree` steps above the tonic's */
27export function spellDegree(tonic: Spelled, degree: number, semis: number): Spelled {
28  const letter = mod(tonic.letter + degree, 7);
29  return { letter, acc: mod(pcOf(tonic) + semis - MAJOR_STEPS[letter] + 6, 12) - 6 };
30}
31
32// ♭2 ♭3 ♯4 ♭6 ♭7: the usual spellings of chromatic degrees in modal mixture
33const DEGREE_OF_SEMIS = [0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6];
34
35export function spellPc(pc: Pc, tonic: Spelled): Spelled {
36  const semis = mod(pc - pcOf(tonic), 12);
37  const spelled = spellDegree(tonic, DEGREE_OF_SEMIS[semis], semis);
38  if (Math.abs(spelled.acc) < 2) return spelled;
39
40  // remote keys produce double accidentals; prefer a plainer enharmonic
41  const flats = tonic.acc < 0;
42  for (const acc of [0, flats ? -1 : 1, flats ? 1 : -1]) {
43    const letter = MAJOR_STEPS.findIndex(s => mod(s + acc, 12) === mod(pc, 12));
44    if (letter >= 0) return { letter, acc };
45  }
46  return spelled;
47}
48
49/** a double sharp or flat as its plain neighbour: B𝄫 → A, F𝄪 → G */
50export function withoutDoubles(n: Spelled): Spelled {
51  if (Math.abs(n.acc) < 2) return n;
52  const letter = mod(n.letter + Math.sign(n.acc), 7);
53  return { letter, acc: mod(pcOf(n) - MAJOR_STEPS[letter] + 6, 12) - 6 };
54}
55
56/** of a tonic's enharmonic spellings, the one whose scale needs the fewest accidentals: D♭ minor
57 * (eight flats) is better written C♯ minor (four sharps) */
58export function plainestTonic(tonic: Spelled, steps: readonly number[]): Spelled {
59  const accidentals = (t: Spelled) =>
60    steps.reduce((sum, s, degree) => sum + Math.abs(spellDegree(t, degree, s).acc), 0);
61  return [-1, 0, 1]
62    .map(acc => ({ letter: MAJOR_STEPS.findIndex(s => mod(s + acc, 12) === pcOf(tonic)), acc }))
63    .filter(t => t.letter !== -1)
64    .reduce((best, t) => (accidentals(t) < accidentals(best) ? t : best), tonic);
65}
66
67export function midiName(midi: number, tonic: Spelled): string {
68  const s = spellPc(mod(midi, 12), tonic);
69  return formatNote(s) + (Math.floor((midi - s.acc) / 12) - 1);
70}
71
72export const midiToFreq = (midi: number): number => 440 * 2 ** ((midi - 69) / 12);