char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
c0bb435
main
1import { type Chord , chordPcs , degreeChord , ROMANS } from "./chord.ts" ; 2import { type Key , MODES , type ModeId , modesFrom } from "./key.ts" ; 3import { mod , pcOf , plainestTonic , spellDegree } from "./pitch.ts" ; 4 5const ROMAN_SUFFIX :Record < string , string > = { 6m :"" , 7dim :"°" , 8aug :"+" , 9m7 :"7" , 10m7b5 :"ø7" , 11dim7 :"°7" , 12mMaj7 :"maj7" , 13m6 :"6" , 14madd9 :"add9" , 15m9 :"9" , 16m11 :"11" , 17m13 :"13" , 18}; 19 20// which degree each interval above the tonic counts as: its usual one (the ♭2 ♭3 ♯4 ♭6 ♭7 of 21// modal mixture), then the other a chord's spelling might ask for. the accidentals are for the 22// major scale; a numeral's own accidental comes from the key's mode 23// prettier-ignore 24const DEGREES :[ degree :number , acc :number ][][] = [ 25[[ 0 , 0 ]], [[ 1 , - 1 ], [ 0 , 1 ]], [[ 1 , 0 ]], [[ 2 , - 1 ], [ 1 , 1 ]], [[ 2 , 0 ]], [[ 3 , 0 ]], 26[[ 3 , 1 ], [ 4 , - 1 ]], [[ 4 , 0 ]], [[ 5 , - 1 ], [ 4 , 1 ]], [[ 5 , 0 ]], [[ 6 , - 1 ], [ 5 , 1 ]], [[ 6 , 0 ]], 27]; 28 29/** 30* roman numeral counted in the key's mode, as Hooktheory does, in a form `parseChord` reads back: 31* in A minor, C is III, G♯ is ♯VII. it goes by pitch, so G♯m in A♭ minor is i. the spelling only 32* has a say where both names are in real use: the tritone (F♯ is ♯IV in C, G♭ is ♭V), and 33* diminished chords, which are named for where they lead (C♯°7 is ♯i°7, on its way to ii) 34*/ 35export function romanOf ( chord :Chord , key :Key ) :string { 36const interval = mod ( pcOf ( chord . root ) - pcOf ( key . tonic ), 12 ); 37const names = DEGREES [ interval ]; 38const diminished = 39chord . tones . includes ( 3 ) && chord . tones . includes ( 6 ) && ! chord . tones . includes ( 7 ); 40const lettered = mod ( chord . root . letter - key . tonic . letter , 7 ); 41const spelled = ( interval === 6 || diminished ) && names . find (([ d ]) => d === lettered ); 42const [ degree ] = spelled || names [ 0 ]; 43const acc = mod ( interval - MODES [ key . mode ]. steps [ degree ] + 6 , 12 ) - 6 ; 44const lower = chord . tones . includes ( 3 ) && ! chord . tones . includes ( 4 ); 45const numeral = lower ?ROMANS [ degree ]. toLowerCase () :ROMANS [ degree ]; 46const suffix = 47ROMAN_SUFFIX [ chord . suffix ] ?? ( lower ?chord . suffix . replace ( / ^m(?!aj) / , "" ) :chord . suffix ); 48return ( acc > 0 ?"#" :"b" ). repeat ( Math . abs ( acc )) + numeral + suffix ; 49} 50 51export const prettyRoman = ( roman :string ) :string => 52roman 53. replace ( / ^[b#]+ / , a => a . replaceAll ( "b" , "♭" ). replaceAll ( "#" , "♯" )) 54. replace ( / b(?=\d) / g , "♭" ) 55. replace ( / #(?=\d) / g , "♯" ); 56 57/** 58* where a chord's notes come from, relative to the key: the key itself, the other keys on its 59* axis (`vertex`), or the parallel mode it'd be borrowed from. and, apart from that, what it does: 60* whether it's a dominant leading into a chord of the key (`secondary`, named for what it leads 61* to). E7 on its way to Am in C does both: its G♯ is the sharp vertex's, and it's a V7/vi 62*/ 63export type Analysis = { 64roman :string ; 65diatonic :boolean ; 66vertex ?:Vertex ; 67borrowedFrom ?:ModeId ; 68secondary ?:string ; 69}; 70 71/** `next`, the chord that follows, tells a dominant that resolves from one that only looks like one */ 72export function analyze ( chord :Chord , key :Key , next ?:Chord ) :Analysis { 73const roman = romanOf ( chord , key ); 74const pcs = chordPcs ( chord ); 75const inKey = ( k :Key ) => 76pcs . every ( p => MODES [ k . mode ]. steps . includes ( mod ( p - pcOf ( k . tonic ), 12 ))); 77if ( inKey ( key )) return { roman, diatonic :true }; 78 79// a vertex that's only the parallel major or minor goes by that name, as plainer 80const vertex = vertices ( key ). find ( v => ! v . parallel && inKey ( v . key ))?. vertex ; 81const borrowedFrom = modesFrom ( key . mode ). find ( mode => inKey ({ tonic :key . tonic , mode})); 82const dominant = dominantFunction ( chord , key , next ); 83return { 84 roman, 85diatonic :false , 86 ...( vertex && { vertex}), 87 ...( borrowedFrom && { borrowedFrom}), 88 ...( dominant ?. name && { secondary :dominant . name }), 89}; 90} 91 92/** 93* where a chord's notes come from, the one source worth pointing out, as relative axis 94* interchange has it: the key; a parallel mode, as modal interchange is the plainer explanation 95* (vii7 in C is lydian's, though vii9 can only be the sharp vertex's ii9); the sharp or flat 96* vertex; locrian, which is heard as a vertex where one fits (♭iii in minor is the flat vertex's 97* i); and only then the inverse vertex, the furthest from the key 98*/ 99export function origin ( a :Analysis ) :"home" | Vertex | ModeId | undefined { 100if ( a . diatonic ) return "home" ; 101if ( a . borrowedFrom && a . borrowedFrom !== "locrian" ) return a . borrowedFrom ; 102if ( a . vertex === "sharp" || a . vertex === "flat" ) return a . vertex ; 103return a . borrowedFrom ?? a . vertex ; 104} 105 106/** 107* a chord as a dominant leading into one of the key's chords: a fifth above it, or (as its 108* tritone substitute) a semitone above. it only does when it's followed there: a seventh makes a 109* chord look like a dominant, but where it goes makes it one. D7 in C minor that slides down to 110* D♭7 is lydian's II7, not a V7/v; B♭7 in C is as often a ♭VII7 back home as on its way to Am. 111* and a major V of the key itself is borrowed, from the parallel major or harmonic minor: there's 112* no other chord it's leading the key into 113*/ 114function dominantFunction ( chord :Chord , key :Key , next ?:Chord ) :{ name :string } | undefined { 115const t = chord . tones ; 116// a major third (with a ♯9 or not), or a suspended fourth instead of any third 117const third = t . includes ( 4 ) || ( t . includes ( 5 ) && ! t . includes ( 3 )); 118const seventh = third && t . includes ( 10 ) && ! t . includes ( 11 ); 119if ( ! seventh && t . join () !== "0,4,7" ) return undefined ; 120 121const root = mod ( pcOf ( chord . root ) - pcOf ( key . tonic ), 12 ); 122const resolves = ( to :number ) => !! next && mod ( pcOf ( next . root ) - pcOf ( key . tonic ), 12 ) === to ; 123for ( const [ above , sub ] of [ 124[ 7 , false ], 125[ 1 , true ], 126] as const ) { 127if ( sub && ! seventh ) continue ; 128const to = mod ( root - above , 12 ); 129const degree = MODES [ key . mode ]. steps . indexOf ( to ); 130if ( degree < 0 ) continue ; 131const target = degreeChord ( key , key . mode , degree , false ); 132if ( target . suffix === "dim" ) continue ; 133const leads = resolves ( to ) && ( sub || degree !== 0 ); 134if ( ! leads ) continue ; 135const name = ` ${ sub ? "subV" : "V" } ${ seventh ? "7" : "" } ` ; 136return { name :degree ?` ${ name } / ${ romanOf ( target , key )} ` :name }; 137} 138return undefined ; 139} 140 141export type PaletteEntry = { chord :Chord ; symbol :string ; diatonic :boolean }; 142 143/** 144* how a palette row's scale relates to the key. the vertices are the key's own scale moved a 145* minor third up (flat) or down (sharp), or a tritone (inverse): the other keys on its axis 146*/ 147export type Relation = "home" | "sharp" | "flat" | "inverse" | "parallel" ; 148export type PaletteRow = { 149key :Key ; 150relation :Relation ; 151entries :PaletteEntry []; 152/** for a parallel mode, the vertex it's the same scale as */ 153vertex ?:Vertex ; 154}; 155 156export type Vertex = "sharp" | "flat" | "inverse" ; 157 158/** 159* each vertex's tonic as a degree above the key's, and the semitones to it. named for where they 160* are on the circle of fifths: a minor third up is three steps flatwards (C to E♭), and down is 161* three steps sharpwards (C to A) 162*/ 163const VERTICES :[ vertex :Vertex , degree :number , semis :number ][] = [ 164[ "flat" , 2 , 3 ], 165[ "sharp" , 5 , 9 ], 166// a diminished fifth rather than an augmented fourth, when both are as plain 167[ "inverse" , 4 , 6 ], 168]; 169 170/** 171* the other keys on the key's axis: its own scale a minor third up, down, and a tritone away. 172* in major and minor keys, one of them is no more than the parallel minor or major (C major's 173* flat vertex, E♭ major, has the notes of C minor), which is said to be its `parallel` 174*/ 175export function vertices ( home :Key ) :{ vertex :Vertex ; key :Key ; parallel ?:ModeId }[] { 176const steps = MODES [ home . mode ]. steps ; 177const pcs = ( k :Key ) => MODES [ k . mode ]. steps . map ( s => mod ( pcOf ( k . tonic ) + s , 12 )). sort (); 178return VERTICES . map (([ vertex , degree , semis ]) => { 179const key = { 180tonic :plainestTonic ( spellDegree ( home . tonic , degree , semis ), steps ), 181mode :home . mode , 182}; 183const parallel = ([ "ionian" , "aeolian" ] as const ). find ( 184mode => mode !== home . mode && pcs ({ tonic :home . tonic , mode}). join () === pcs ( key ). join (), 185); 186return { vertex, key, ...( parallel && { parallel}) }; 187}); 188} 189 190/** the key's chords, then its vertices', then its parallel modes' (nearest first) */ 191export function palette ( home :Key , sevenths :boolean ) :PaletteRow [] { 192const entries = ( key :Key ) => 193[ 0 , 1 , 2 , 3 , 4 , 5 , 6 ]. map ( degree => { 194const chord = degreeChord ( key , key . mode , degree , sevenths ); 195return { chord, symbol :romanOf ( chord , home ), diatonic :analyze ( chord , home ). diatonic }; 196}); 197 198const axis = vertices ( home ); 199return [ 200{ key :home , relation :"home" , entries :entries ( home ) }, 201// the vertex that's the parallel major or minor, as that, keeping its place on the axis 202 ...axis . map (({ vertex, key, parallel}) :PaletteRow => { 203if ( ! parallel ) return { key, relation :vertex , entries :entries ( key ) }; 204const same = { tonic :home . tonic , mode :parallel }; 205return { key :same , relation :"parallel" , vertex, entries :entries ( same ) }; 206}), 207 ...modesFrom ( home . mode ) 208. slice ( 1 ) 209. filter ( mode => ! axis . some ( v => v . parallel === mode )) 210. map ( mode => ({ 211key :{ tonic :home . tonic , mode}, 212relation :"parallel" as const , 213entries :entries ({ tonic :home . tonic , mode}), 214})), 215]; 216}