import { mod, pcOf, type Spelled } from "./pitch.ts"; export type ModeId = | "ionian" | "dorian" | "phrygian" | "lydian" | "mixolydian" | "aeolian" | "locrian" | "harmonicMinor" | "melodicMinor"; export const MODES: Record = { ionian: { label: "major", steps: [0, 2, 4, 5, 7, 9, 11] }, dorian: { label: "dorian", steps: [0, 2, 3, 5, 7, 9, 10] }, phrygian: { label: "phrygian", steps: [0, 1, 3, 5, 7, 8, 10] }, lydian: { label: "lydian", steps: [0, 2, 4, 6, 7, 9, 11] }, mixolydian: { label: "mixolydian", steps: [0, 2, 4, 5, 7, 9, 10] }, aeolian: { label: "minor", steps: [0, 2, 3, 5, 7, 8, 10] }, locrian: { label: "locrian", steps: [0, 1, 3, 5, 6, 8, 10] }, harmonicMinor: { label: "harmonic minor", steps: [0, 2, 3, 5, 7, 8, 11] }, melodicMinor: { label: "melodic minor", steps: [0, 2, 3, 5, 7, 9, 11] }, }; export type Key = { tonic: Spelled; mode: ModeId }; // how commonly each mode is borrowed from, for breaking ties export const MIXTURE_ORDER: readonly ModeId[] = [ "aeolian", "mixolydian", "dorian", "harmonicMinor", "lydian", "phrygian", "melodicMinor", "locrian", "ionian", ]; /** * the home mode, then where chords are usually borrowed from: the parallel major and minor, then * the other modes. nearest first within each, since a mode that alters fewer degrees is the * likelier reading */ export function modesFrom(home: ModeId): ModeId[] { // the minor-scale variants are less idiomatic sources than the diatonic modes const penalty = (m: ModeId) => (m === "harmonicMinor" || m === "melodicMinor" ? 2 : 0); const distance = (m: ModeId) => MODES[m].steps.filter((s, i) => s !== MODES[home].steps[i]).length + penalty(m); const nearest = (modes: ModeId[]) => modes.sort((a, b) => distance(a) - distance(b)); const parallel = (m: ModeId) => m === "ionian" || m === "aeolian"; const others = MIXTURE_ORDER.filter(m => m !== home); return [ home, ...nearest(others.filter(parallel)), ...nearest(others.filter(m => !parallel(m))), ]; } /** * move a pitch by scale degrees of the key. a note outside the scale keeps its distance above * the degree below it, so C♯ in C major moves up to D♯: a chromatic passing note stays one */ export function stepInScale(pitch: number, key: Key, degrees: number): number { const steps = MODES[key.mode].steps; const fromTonic = pitch - pcOf(key.tonic); const within = mod(fromTonic, 12); const degree = steps.findLastIndex(s => s <= within); const target = degree + degrees; const octaves = Math.floor(fromTonic / 12) + Math.floor(target / 7); return pcOf(key.tonic) + 12 * octaves + steps[mod(target, 7)] + (within - steps[degree]); }