char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
3c7bc6e
main
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 { 16const m = / ^([A-Ga-g])([#♯b♭]*) / . exec ( src ); 17if ( ! m ) return undefined ; 18const note = { letter :LETTERS . indexOf ( m [ 1 ]. toUpperCase ()), acc :accidentalValue ( m [ 2 ]) }; 19return { note, rest :src . slice ( m [ 0 ]. length ) }; 20} 21 22export const formatNote = ( n :Spelled , ascii = false ) :string => 23LETTERS [ 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 { 28const letter = mod ( tonic . letter + degree , 7 ); 29return { 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 { 36const semis = mod ( pc - pcOf ( tonic ), 12 ); 37const spelled = spellDegree ( tonic , DEGREE_OF_SEMIS [ semis ], semis ); 38if ( Math . abs ( spelled . acc ) < 2 ) return spelled ; 39 40// remote keys produce double accidentals; prefer a plainer enharmonic 41const flats = tonic . acc < 0 ; 42for ( const acc of [ 0 , flats ?- 1 :1 , flats ?1 :- 1 ]) { 43const letter = MAJOR_STEPS . findIndex ( s => mod ( s + acc , 12 ) === mod ( pc , 12 )); 44if ( letter >= 0 ) return { letter, acc}; 45} 46return spelled ; 47} 48 49/** a double sharp or flat as its plain neighbour: B𝄫 → A, F𝄪 → G */ 50export function withoutDoubles ( n :Spelled ) :Spelled { 51if ( Math . abs ( n . acc ) < 2 ) return n ; 52const letter = mod ( n . letter + Math . sign ( n . acc ), 7 ); 53return { 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 { 59const accidentals = ( t :Spelled ) => 60steps . reduce (( sum , s , degree ) => sum + Math . abs ( spellDegree ( t , degree , s ). acc ), 0 ); 61return [ - 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 { 68const s = spellPc ( mod ( midi , 12 ), tonic ); 69return formatNote ( s ) + ( Math . floor (( midi - s . acc ) / 12 ) - 1 ); 70} 71 72export const midiToFreq = ( midi :number ) :number => 440 * 2 ** (( midi - 69 ) / 12 );