char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
a427fe3
main
1import { type Chord , chordFrom } from "./chord.ts" ; 2import { type Key , MODES } from "./key.ts" ; 3import { mod , pcOf , spellDegree } from "./pitch.ts" ; 4 5// all in root position: where the bass goes is left to voicing 6 7const has = ( c :Chord , ...tones :number []) => tones . every ( t => c . tones . includes ( t )); 8const without = ( c :Chord , ...tones :number []) => c . tones . filter ( t => ! tones . includes ( t )); 9const inKey = ( key :Key , pc :number ) => 10MODES [ key . mode ]. steps . includes ( mod ( pc - pcOf ( key . tonic ), 12 )); 11 12const isMajor = ( c :Chord ) => has ( c , 4 ) && ! has ( c , 3 ); 13const isMinor = ( c :Chord ) => has ( c , 3 ) && ! has ( c , 4 ); 14export const isDominant = ( c :Chord ) :boolean => has ( c , 4 , 10 ) && ! has ( c , 3 ); 15 16/** C ↔ Cm, keeping everything but the third */ 17export function parallel ( c :Chord ) :Chord | undefined { 18if ( has ( c , 6 ) && ! has ( c , 7 )) return undefined ; // diminished: no major counterpart 19if ( isMajor ( c )) return chordFrom ( c . root , [ ...without ( c , 4 ), 3 ]); 20if ( isMinor ( c )) return chordFrom ( c . root , [ ...without ( c , 3 ), 4 ]); 21return undefined ; 22} 23 24/** C ↔ Am, Cmaj7 ↔ Am7: the chord a minor third away sharing most of its notes */ 25export function relative ( c :Chord ) :Chord | undefined { 26if ( ! has ( c , 7 )) return undefined ; 27if ( isMajor ( c ) && ! has ( c , 10 )) { 28return chordFrom ( spellDegree ( c . root , 5 , 9 ), has ( c , 11 ) ?[ 0 , 3 , 7 , 10 ] :[ 0 , 3 , 7 ]); 29} 30if ( isMinor ( c ) && ! has ( c , 11 )) { 31return chordFrom ( spellDegree ( c . root , 2 , 3 ), has ( c , 10 ) ?[ 0 , 4 , 7 , 11 ] :[ 0 , 4 , 7 ]); 32} 33return undefined ; 34} 35 36/** G7 → D♭7: the dominant a tritone away, with the same tritone inside it */ 37export function tritoneSub ( c :Chord ) :Chord | undefined { 38return isDominant ( c ) ?chordFrom ( spellDegree ( c . root , 4 , 6 ), c . tones ) :undefined ; 39} 40 41/** add a 7th, picking whichever fits the key (C → Cmaj7, G → G7 in C), or take it away */ 42export function toggleSeventh ( c :Chord , key :Key ) :Chord | undefined { 43const dim = has ( c , 3 , 6 ) && ! has ( c , 7 ); 44if ( has ( c , 10 ) || has ( c , 11 ) || ( dim && has ( c , 9 ))) 45return chordFrom ( c . root , without ( c , 9 , 10 , 11 )); 46const root = pcOf ( c . root ); 47const [ preferred , fallback ] = dim ?[ 10 , 9 ] :isMajor ( c ) ?[ 11 , 10 ] :[ 10 , 11 ]; 48const seventh = inKey ( key , root + preferred ) 49 ?preferred 50 :inKey ( key , root + fallback ) 51 ?fallback 52 :dim 53 ?9 54 :10 ; 55return chordFrom ( c . root , [ ...c . tones , seventh ]); 56} 57 58/** replace the third with a 2nd or 4th; asked again, put the key's third back */ 59export function suspend ( c :Chord , key :Key , tone :2 | 5 ) :Chord | undefined { 60if ( has ( c , tone ) && ! has ( c , 3 ) && ! has ( c , 4 )) { 61const third = inKey ( key , pcOf ( c . root ) + 4 ) ?4 :3 ; 62return chordFrom ( c . root , [ ...without ( c , tone ), third ]); 63} 64return chordFrom ( c . root , [ ...without ( c , 3 , 4 , 2 , 5 ), tone ]); 65}