char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
66d16f5
main
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 [] }> = { 15ionian :{ label :"major" , steps :[ 0 , 2 , 4 , 5 , 7 , 9 , 11 ] }, 16dorian :{ label :"dorian" , steps :[ 0 , 2 , 3 , 5 , 7 , 9 , 10 ] }, 17phrygian :{ label :"phrygian" , steps :[ 0 , 1 , 3 , 5 , 7 , 8 , 10 ] }, 18lydian :{ label :"lydian" , steps :[ 0 , 2 , 4 , 6 , 7 , 9 , 11 ] }, 19mixolydian :{ label :"mixolydian" , steps :[ 0 , 2 , 4 , 5 , 7 , 9 , 10 ] }, 20aeolian :{ label :"minor" , steps :[ 0 , 2 , 3 , 5 , 7 , 8 , 10 ] }, 21locrian :{ label :"locrian" , steps :[ 0 , 1 , 3 , 5 , 6 , 8 , 10 ] }, 22harmonicMinor :{ label :"harmonic minor" , steps :[ 0 , 2 , 3 , 5 , 7 , 8 , 11 ] }, 23melodicMinor :{ 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 48const penalty = ( m :ModeId ) => ( m === "harmonicMinor" || m === "melodicMinor" ?2 :0 ); 49const distance = ( m :ModeId ) => 50MODES [ m ]. steps . filter (( s , i ) => s !== MODES [ home ]. steps [ i ]). length + penalty ( m ); 51const nearest = ( modes :ModeId []) => modes . sort (( a , b ) => distance ( a ) - distance ( b )); 52const parallel = ( m :ModeId ) => m === "ionian" || m === "aeolian" ; 53const others = MIXTURE_ORDER . filter ( m => m !== home ); 54return [ 55home , 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 { 66const steps = MODES [ key . mode ]. steps ; 67const fromTonic = pitch - pcOf ( key . tonic ); 68const within = mod ( fromTonic , 12 ); 69const degree = steps . findLastIndex ( s => s <= within ); 70const target = degree + degrees ; 71const octaves = Math . floor ( fromTonic / 12 ) + Math . floor ( target / 7 ); 72return pcOf ( key . tonic ) + 12 * octaves + steps [ mod ( target , 7 )] + ( within - steps [ degree ]); 73}