export type Pc = number; /** a spelled note name: letter index (C=0 … B=6) plus accidentals (+sharp, -flat) */ export type Spelled = { letter: number; acc: number }; export const mod = (n: number, m: number): number => ((n % m) + m) % m; const LETTERS = "CDEFGAB"; export const MAJOR_STEPS: readonly number[] = [0, 2, 4, 5, 7, 9, 11]; export const pcOf = (n: Spelled): Pc => mod(MAJOR_STEPS[n.letter] + n.acc, 12); export const accidentalValue = (s: string): number => [...s].reduce((sum, c) => sum + ("#♯".includes(c) ? 1 : -1), 0); export function parseNote(src: string): { note: Spelled; rest: string } | undefined { const m = /^([A-Ga-g])([#♯b♭]*)/.exec(src); if (!m) return undefined; const note = { letter: LETTERS.indexOf(m[1].toUpperCase()), acc: accidentalValue(m[2]) }; return { note, rest: src.slice(m[0].length) }; } export const formatNote = (n: Spelled, ascii = false): string => LETTERS[n.letter] + (n.acc > 0 ? (ascii ? "#" : "♯") : ascii ? "b" : "♭").repeat(Math.abs(n.acc)); /** spell the note `semis` above `tonic` using the letter `degree` steps above the tonic's */ export function spellDegree(tonic: Spelled, degree: number, semis: number): Spelled { const letter = mod(tonic.letter + degree, 7); return { letter, acc: mod(pcOf(tonic) + semis - MAJOR_STEPS[letter] + 6, 12) - 6 }; } // ♭2 ♭3 ♯4 ♭6 ♭7: the usual spellings of chromatic degrees in modal mixture const DEGREE_OF_SEMIS = [0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6]; export function spellPc(pc: Pc, tonic: Spelled): Spelled { const semis = mod(pc - pcOf(tonic), 12); const spelled = spellDegree(tonic, DEGREE_OF_SEMIS[semis], semis); if (Math.abs(spelled.acc) < 2) return spelled; // remote keys produce double accidentals; prefer a plainer enharmonic const flats = tonic.acc < 0; for (const acc of [0, flats ? -1 : 1, flats ? 1 : -1]) { const letter = MAJOR_STEPS.findIndex(s => mod(s + acc, 12) === mod(pc, 12)); if (letter >= 0) return { letter, acc }; } return spelled; } /** a double sharp or flat as its plain neighbour: B𝄫 → A, F𝄪 → G */ export function withoutDoubles(n: Spelled): Spelled { if (Math.abs(n.acc) < 2) return n; const letter = mod(n.letter + Math.sign(n.acc), 7); return { letter, acc: mod(pcOf(n) - MAJOR_STEPS[letter] + 6, 12) - 6 }; } /** of a tonic's enharmonic spellings, the one whose scale needs the fewest accidentals: D♭ minor * (eight flats) is better written C♯ minor (four sharps) */ export function plainestTonic(tonic: Spelled, steps: readonly number[]): Spelled { const accidentals = (t: Spelled) => steps.reduce((sum, s, degree) => sum + Math.abs(spellDegree(t, degree, s).acc), 0); return [-1, 0, 1] .map(acc => ({ letter: MAJOR_STEPS.findIndex(s => mod(s + acc, 12) === pcOf(tonic)), acc })) .filter(t => t.letter !== -1) .reduce((best, t) => (accidentals(t) < accidentals(best) ? t : best), tonic); } export function midiName(midi: number, tonic: Spelled): string { const s = spellPc(mod(midi, 12), tonic); return formatNote(s) + (Math.floor((midi - s.acc) / 12) - 1); } export const midiToFreq = (midi: number): number => 440 * 2 ** ((midi - 69) / 12);