char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
3c7bc6e
main
1import { assert , assertEquals } from "@std/assert" ; 2import { isKeyRelative } from "../theory/chord.ts" ; 3import { rekey , unpinRedundant , voiceTrack } from "./harmony.ts" ; 4import { 5type ChordTrack , 6newChordTrack , 7newProject , 8placeChord , 9type Project , 10projectKey , 11} from "./model.ts" ; 12 13const SYMBOLS = [ "I" , "vi" , "IV/A" , "V7/V" , "bVI" , "iiø7" , "4" , "57" , "Cmaj7" , "b7" , "ii" ]; 14 15function sketch () :Project { 16const p = newProject (); 17const track = newChordTrack (); 18SYMBOLS . forEach (( symbol , i ) => 19placeChord ( track , { id :` ${ i } ` , start :i * 4 , dur :4 , symbol}), 20); 21track . chords [ 10 ]. notes = [ 38 , 57 , 62 , 65 ]; // ii, hand-voiced 22p . tracks = [ track ]; 23return p ; 24} 25const chords = ( p :Project ) => p . tracks [ 0 ] as ChordTrack ; 26const sounding = ( p :Project ) => voiceTrack ( chords ( p ), projectKey ( p )). map ( v => v . notes ); 27const symbols = ( p :Project ) => chords ( p ). chords . map ( c => c . symbol ); 28 29Deno . test ( "changing key or mode changes no voicing" , () => { 30const p = sketch (); 31const before = sounding ( p ); 32for ( const key of [ 33{ tonic :"A" , mode :"aeolian" }, 34{ tonic :"Eb" , mode :"dorian" }, 35{ tonic :"F#" , mode :"lydian" }, 36{ tonic :"C" , mode :"ionian" }, 37] as const ) { 38rekey ( p , key ); 39assertEquals ( sounding ( p ), before , ` ${ key . tonic } ${ key . mode } : ${ symbols ( p )} ` ); 40} 41// everything written relative to the key still is, though a scale degree the key has no chord 42// for (F major in E♭ dorian) has to become a roman numeral 43for ( const [ i , symbol ] of symbols ( p ). entries ()) { 44if ( SYMBOLS [ i ] !== "Cmaj7" ) assert ( isKeyRelative ( symbol ! ), symbol ); 45} 46}); 47 48Deno . test ( "chords written relative to the key stay that way, renamed for the new one" , () => { 49const p = sketch (); 50rekey ( p , { tonic :"A" , mode :"aeolian" }); 51assertEquals ( symbols ( p ), [ 52"III" , 53"i" , 54"VI/A" , 55"IV7" , 56"#VII" , 57"ivø7" , 58"6" , 59"77" , 60"Cmaj7" , 61"bII" , 62"iv" , 63]); 64for ( const [ i , symbol ] of symbols ( p ). entries ()) { 65if ( SYMBOLS [ i ] !== "Cmaj7" ) assert ( isKeyRelative ( symbol ! ), symbol ); 66} 67}); 68 69function threeChords () :Project { 70const p = newProject (); 71const track = newChordTrack (); 72for ( const [ i , symbol ] of [ "C" , "Am" , "F" ]. entries ()) { 73placeChord ( track , { id :symbol , start :i * 4 , dur :4 , symbol}); 74} 75p . tracks = [ track ]; 76return p ; 77} 78const pinned = ( p :Project ) => chords ( p ). chords . map ( c => c . notes !== undefined ); 79 80Deno . test ( "hand-set notes that are just the auto-voicing get unpinned" , () => { 81const p = threeChords (); 82const auto = sounding ( p ); 83// as if Am were inverted up, then back down 84chords ( p ). chords [ 1 ]. notes = [ ...auto [ 1 ]]; 85const settled = unpinRedundant ( p ); 86assertEquals ( pinned ( settled ), [ false , false , false ]); 87assertEquals ( sounding ( settled ), auto ); 88}); 89 90Deno . test ( "a real hand-voicing stays pinned, and so does what follows it" , () => { 91const p = threeChords (); 92const [ bass , ...upper ] = sounding ( p )[ 1 ]; 93chords ( p ). chords [ 1 ]. notes = [ bass , ...upper . map ( n => n + 12 )]; 94const settled = unpinRedundant ( p ); 95assertEquals ( pinned ( settled ), [ false , true , false ]); 96assertEquals ( settled , p ); // nothing to change: the very same project back 97}); 98 99Deno . test ( "notes without a symbol, voiced as their chord would be, become that chord" , () => { 100const p = threeChords (); 101const auto = sounding ( p ); 102const am = chords ( p ). chords [ 1 ]; 103delete am . symbol ; 104am . notes = [ ...auto [ 1 ]]; 105const settled = unpinRedundant ( p ); 106assertEquals ( chords ( settled ). chords [ 1 ], { id :"Am" , start :4 , dur :4 , symbol :"Am" }); 107assertEquals ( sounding ( settled ), auto ); 108});