char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
3c7bc6e
main
1const ALTERED :Record < number , [ name :string , degree :number ]> = { 21 :[ "b9" , 9 ], 33 :[ "#9" , 9 ], 46 :[ "#11" , 11 ], 58 :[ "b13" , 13 ], 6}; 7 8/** 9* name a set of intervals above a root, as a chord suffix that `parseChord` reads back, along 10* with a cost: how unusual that reading is. names may leave out notes they imply (the fifth, 11* and the 9th under an 11th or 13th) but never include ones that aren't there 12*/ 13export function describeTones ( 14intervals :Iterable < number >, 15) :{ suffix :string ; cost :number } | undefined { 16const tones = new Set ([ ...intervals ]. map ( t => (( t % 12 ) + 12 ) % 12 )); 17if ( ! tones . delete ( 0 )) return undefined ; 18if ( tones . size < 2 ) { 19return tones . size === 1 && tones . has ( 7 ) ?{ suffix :"5" , cost :0.6 } :undefined ; 20} 21const take = ( t :number ) => tones . delete ( t ); 22 23const third = take ( 4 ) ?"major" :take ( 3 ) ?"minor" :undefined ; 24const seventh = take ( 10 ) ?10 :take ( 11 ) ?11 :undefined ; 25let fifth = take ( 7 ) ?7 :undefined ; 26// without a fifth, a tritone in an extended chord is its ♯11, not a flattened fifth 27const extended = seventh !== undefined && ( tones . has ( 2 ) || tones . has ( 9 )); 28if ( fifth === undefined && third === "minor" && take ( 6 )) fifth = 6 ; 29else if ( fifth === undefined && third === "major" && take ( 8 )) fifth = 8 ; 30else if ( fifth === undefined && third === "major" && ! extended && take ( 6 )) fifth = 6 ; 31const dim7 = ! seventh && third === "minor" && fifth === 6 && take ( 9 ); 32const sus = third ?undefined :take ( 5 ) ?"sus4" :take ( 2 ) ?"sus2" :undefined ; 33const six = ! seventh && ! dim7 && take ( 9 ); 34 35const natural = new Set < number >(); 36const altered :[ name :string , degree :number ][] = []; 37for ( const t of tones ) { 38if ( t === 2 || t === 5 || t === 9 ) natural . add ( t === 2 ?9 :t === 5 ?11 :13 ); 39else if ( 40t in ALTERED && 41( t !== 3 || third === "major" ) && 42( t < 6 || ( fifth !== 6 && fifth !== 8 )) 43) { 44altered . push ( ALTERED [ t ]); 45} else return undefined ; 46} 47 48let cost = 0 ; 49let suffix = "" ; 50const alterations :string [] = []; 51 52if ( dim7 ) [ suffix , cost ] = [ "dim7" , 0.6 ]; 53else if ( third === "minor" && fifth === 6 && ! seventh ) [ suffix , cost ] = [ "dim" , 0.3 ]; 54else if ( third === "major" && fifth === 8 && ! seventh ) [ suffix , cost ] = [ "aug" , 0.5 ]; 55else { 56suffix = third === "minor" ?"m" :"" ; 57if ( seventh ) { 58// the highest natural extension names the chord, implying the 9th beneath it. an 59// alteration replaces its natural when read back, so an altered degree can't be implied 60const clashes = ( degree :number ) => altered . some (([, d ]) => d === degree ); 61const top = [ 13 , 11 , 9 ]. find ( d => natural . has ( d ) && ! clashes ( d )) ?? 7 ; 62suffix += ( seventh === 11 ?( third === "minor" ?"Maj" :"maj" ) :"" ) + top ; 63cost += ( seventh === 11 ?0.2 :0.1 ) + ( top === 9 ?0.3 :top > 9 ?0.5 :0 ); 64natural . delete ( top ); 65if ( top > 9 && ! clashes ( 9 )) natural . delete ( 9 ); 66} 67if ( fifth === 6 ) alterations . push ( "b5" ); 68if ( fifth === 8 ) alterations . push ( "#5" ); 69cost += fifth === 6 ?( third === "minor" ?0.3 :0.8 ) :fifth === 8 ?0.5 :0 ; 70} 71// a missing fifth is routine in a seventh chord, and suspicious in a triad 72if ( fifth === undefined ) cost += seventh ?0.2 :1.2 ; 73 74if ( six ) { 75suffix += natural . delete ( 9 ) ?"6/9" :"6" ; 76cost += suffix . endsWith ( "/9" ) ?0.7 :0.3 ; 77} 78 79if ( sus ) { 80alterations . push ( sus ); 81cost += 0.4 ; 82} else if ( ! third ) { 83alterations . push ( "no3" ); 84cost += 1 ; 85} 86 87// on a seventh chord alterations are written bare ("7♭9"), on anything else they're added 88const bare = seventh !== undefined || dim7 ; 89for ( const [ name ] of altered ) alterations . push ( bare ?name :"add" + name ); 90cost += altered . length * ( bare ?0.6 :1.5 ); 91// naturals come last, so an alteration of the same degree can't cancel them when read back 92for ( const degree of natural ) alterations . push ( "add" + degree ); 93cost += natural . size * 0.4 ; 94 95// a bare "b5" would read as part of the root's name: "Cb5" 96const tail = alterations . join ( "" ); 97return { suffix :suffix || ! /^[b#] / . test ( tail ) ?suffix + tail :`( ${ tail } )` , cost}; 98}