char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
ad1ae60
main
1import { type Chord , chordFrom } from "./chord.ts" ; 2import { mod , pcOf , spellDegree , withoutDoubles } from "./pitch.ts" ; 3 4/** a way into a chord: what to play just before it, named for what it is relative to it */ 5export type Connection = { name :string ; chords :Chord [] }; 6 7/** 8* the chords to fill a gap with, from `prev` into `target`, after the bossa nova autocomposer of 9* nogloff's autoharp: dominants, ii–Vs and their tritone substitutes, and the more colourful 10* approaches (♭VI–V, the backdoor, planing, Coltrane's thirds). best first: those that follow on 11* most smoothly from `prev`, where there is one. the approach chords match the target's colour, 12* so a maj9 is led into from m9 and 9, a maj7 from m7 and 7, and a minor chord from m7♭5 and 7♭9 13*/ 14export function connections ( prev :Chord | undefined , target :Chord ) :Connection [] { 15const t = target . tones ; 16const minor = t . includes ( 3 ) && ! t . includes ( 4 ); 17const ninths = t . includes ( 2 ) && ( t . includes ( 10 ) || t . includes ( 11 )); 18// a chord on a degree of the target's major scale, `semis` above it: no double flats, though 19// ♭II of A♭ is B♭♭ by the book 20const on = ( degree :number , semis :number , tones :number [] = t ) => 21chordFrom ( withoutDoubles ( spellDegree ( target . root , degree , semis )), tones ) ! ; 22const dom = ninths ?[ 0 , 2 , 4 , 7 , 10 ] :[ 0 , 4 , 7 , 10 ]; 23const m7 = ninths ?[ 0 , 2 , 3 , 7 , 10 ] :[ 0 , 3 , 7 , 10 ]; 24const maj7 = ninths ?[ 0 , 2 , 4 , 7 , 11 ] :[ 0 , 4 , 7 , 11 ]; 25 26const V = on ( 4 , 7 , minor ?[ 0 , 1 , 4 , 7 , 10 ] :dom ); 27const subV = on ( 1 , 1 , dom ); 28const ii = on ( 1 , 2 , minor ?[ 0 , 3 , 6 , 10 ] :m7 ); 29const routes :Connection [] = [ 30{ name :"V" , chords :[ V ] }, 31{ name :"ii–V" , chords :[ ii , V ] }, 32{ name :"subV" , chords :[ subV ] }, 33{ name :"ii–subV" , chords :[ ii , subV ] }, 34{ name :"vii°7" , chords :[ on ( 6 , 11 , [ 0 , 3 , 6 , 9 ])] }, 35{ name :"♭VI–V" , chords :[ on ( 5 , 8 , maj7 ), V ] }, 36 ...( minor 37 ?[] 38 :[ 39{ name :"iv–♭VII" , chords :[ on ( 3 , 5 , m7 ), on ( 6 , 10 , dom )] }, 40{ name :"iii–VI–ii–V" , chords :[ on ( 2 , 4 , m7 ), on ( 5 , 9 , dom ), ii , V ] }, 41]), 42// the target's own shape, sliding into it from a semitone away 43{ name :"planed ♭II" , chords :[ on ( 1 , 1 )] }, 44{ name :"planed VII" , chords :[ on ( 6 , 11 )] }, 45]; 46 47const from = prev && pcOf ( prev . root ); 48// coltrane's changes: in by major thirds, from a major third away 49const third = from !== undefined && [ 4 , 8 ]. includes ( mod ( pcOf ( target . root ) - from , 12 )); 50if ( third && ! minor ) { 51routes . push ({ name :"coltrane" , chords :[ on ( 6 , 11 , dom ), on ( 2 , 4 , maj7 ), V ] }); 52} 53if ( ! prev ) return routes ; 54 55const same = ( a :Chord , b :Chord ) => 56pcOf ( a . root ) === pcOf ( b . root ) && a . tones . join () === b . tones . join (); 57// how smoothly a chord follows `prev`: best down a fifth, then by step, then the rest 58const cost = ( c :Chord ) => 59({ 5 :0 , 1 :1 , 11 :1 , 2 :2 , 10 :2 , 0 :2 , 6 :4 })[ mod ( pcOf ( c . root ) - from ! , 12 )] ?? 3 ; 60const seen = new Set < string >(); 61return ( 62routes 63// no playing the chord that's already there again 64. map ( r => ( same ( r . chords [ 0 ], prev ) ?{ ...r , chords :r . chords . slice ( 1 ) } :r )) 65. filter ( r => { 66const id = r . chords . map ( c => ` ${ pcOf ( c . root )} : ${ c . tones } ` ). join ( " " ); 67if ( ! r . chords . length || seen . has ( id )) return false ; 68seen . add ( id ); 69return true ; 70}) 71. map (( r , i ) => ({ r, i, cost :cost ( r . chords [ 0 ]) })) 72. sort (( a , b ) => a . cost - b . cost || a . i - b . i ) 73. map (({ r}) => r ) 74); 75}