char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
3c7bc6e
main
1import { assertEquals } from "@std/assert" ; 2import { type HookpadChord , type HookpadSong , readSection } from "./hookpad.ts" ; 3import { theoryTabPath , theoryTabProject } from "./theorytab.ts" ; 4 5const chord = ( fields :Partial < HookpadChord >) :HookpadChord => ({ 6beat :1 , 7duration :4 , 8root :1 , 9type :5 , 10inversion :0 , 11applied :0 , 12adds :[], 13omits :[], 14alterations :[], 15suspensions :[], 16borrowed :null , 17pedal :null , 18substitutions :[], 19isRest :false , 20 ...fields , 21}); 22 23const song = ( fields :Partial < HookpadSong >) :HookpadSong => ({ 24keys :[{ beat :1 , scale :"major" , tonic :"C" }], 25meters :[{ beat :1 , numBeats :4 , beatUnit :1 }], 26tempos :[{ beat :1 , bpm :100 }], 27chords :[], 28notes :[], 29endBeat :5 , 30 ...fields , 31}); 32 33const symbols = ( chords :Partial < HookpadChord >[], key = { scale :"major" , tonic :"C" }) => 34readSection ( song ({ keys :[{ beat :1 , ...key }], chords :chords . map ( chord ) })). chords . map ( 35c => c . symbol , 36); 37 38Deno . test ( "hookpad chords as symbols" , () => { 39assertEquals ( 40symbols ([ 41{ root :1 }, 42{ root :6 }, 43{ root :5 , type :7 }, 44{ root :5 , type :7 , inversion :1 }, 45{ root :1 , inversion :2 }, 46{ root :2 , type :7 , inversion :3 }, 47{ root :7 , type :7 }, 48{ root :4 , type :9 }, 49{ root :5 , suspensions :[ 4 ] }, 50{ root :1 , adds :[ 9 ] }, 51{ root :1 , omits :[ 3 ] }, 52{ root :5 , type :7 , alterations :[ "b9" ] }, 53]), 54[ "C" , "Am" , "G7" , "G7/B" , "C/G" , "Dm7/C" , "Bm7b5" , "Fmaj9" , "Gsus4" , "Cadd9" , "C5" , "G7b9" ], 55); 56}); 57 58Deno . test ( "borrowed and secondary chords" , () => { 59assertEquals ( 60symbols ([ 61{ root :7 , borrowed :"minor" }, 62{ root :4 , borrowed :"minor" }, 63{ root :6 , borrowed :"minor" , type :7 }, 64{ root :2 , borrowed :"phrygian" }, 65// stored as the target degree, with the secondary function in `applied` 66{ root :6 , applied :5 }, 67{ root :5 , applied :5 , type :7 }, 68{ root :2 , applied :7 , type :7 }, 69// a custom scale, in semitones above the tonic 70{ root :3 , borrowed :[ 0 , 2 , 3 , 5 , 7 , 8 , 10 ] }, 71]), 72[ "Bb" , "Fm" , "Abmaj7" , "Db" , "E" , "D7" , "C#dim7" , "Eb" ], 73); 74}); 75 76Deno . test ( "minor keys count degrees in the minor scale" , () => { 77assertEquals ( 78symbols ([{ root :1 }, { root :3 }, { root :5 }, { root :5 , borrowed :"harmonicMinor" }], { 79scale :"minor" , 80tonic :"A" , 81}), 82[ "Am" , "C" , "Em" , "E" ], 83); 84}); 85 86Deno . test ( "theoretical keys and double accidentals get their everyday spellings" , () => { 87assertEquals ( symbols ([{ root :1 }, { root :5 }], { scale :"major" , tonic :"D#" }), [ 88"Eb" , 89"Bb" , 90]); 91assertEquals ( symbols ([{ root :1 }, { root :6 }], { scale :"minor" , tonic :"Db" }), [ 92"C#m" , 93"A" , 94]); 95// V6/vi in B major: D♯ with F𝄪 in the bass 96const secondary = symbols ([{ root :6 , applied :5 , inversion :1 }], { 97scale :"major" , 98tonic :"B" , 99}); 100assertEquals ( secondary , [ "D#/G" ]); 101const key = readSection ( song ({ keys :[{ beat :1 , scale :"minor" , tonic :"Gb" }] })). key ; 102assertEquals ( key , { tonic :"F#" , scale :"minor" }); 103}); 104 105Deno . test ( "melody pitches: octave 0 starts at the tonic above middle C" , () => { 106const read = readSection ( 107song ({ 108keys :[{ beat :1 , scale :"major" , tonic :"A" }], 109notes :[ 110{ beat :1 , duration :1 , sd :"1" , octave :0 , isRest :false }, 111{ beat :2 , duration :1 , sd :"5" , octave :- 1 , isRest :false }, 112{ beat :3 , duration :0.5 , sd :"b3" , octave :0 , isRest :false }, 113{ beat :3.5 , duration :0.5 , sd :"1" , octave :0 , isRest :true }, 114{ beat :4 , duration :1 , sd :"#4" , octave :1 , isRest :false }, 115], 116}), 117); 118assertEquals ( read . notes , [ 119{ start :0 , dur :1 , pitch :69 }, 120{ start :1 , dur :1 , pitch :64 }, 121{ start :2 , dur :0.5 , pitch :72 }, 122{ start :3 , dur :1 , pitch :87 }, 123]); 124}); 125 126Deno . test ( "a key change applies from its beat on" , () => { 127const read = readSection ( 128song ({ 129keys :[ 130{ beat :1 , scale :"major" , tonic :"C" }, 131{ beat :5 , scale :"major" , tonic :"D" }, 132], 133chords :[ chord ({ root :1 , beat :1 }), chord ({ root :1 , beat :5 })], 134notes :[{ beat :5 , duration :1 , sd :"1" , octave :0 , isRest :false }], 135endBeat :9 , 136}), 137); 138assertEquals ( 139read . chords . map ( c => [ c . start , c . symbol ]), 140[ 141[ 0 , "C" ], 142[ 4 , "D" ], 143], 144); 145assertEquals ( read . notes [ 0 ]. pitch , 62 ); 146}); 147 148Deno . test ( "a theorytab becomes a project with its sections end to end, on bar lines" , () => { 149const verse = song ({ 150keys :[{ beat :1 , scale :"minor" , tonic :"D#" }], 151tempos :[{ beat :1 , bpm :92.4 }], 152chords :[ chord ({ root :1 , duration :4 }), chord ({ root :6 , beat :5 , duration :2 })], 153endBeat :7 , // a bar and a half: the chorus should still start on bar 3 154}); 155const chorus = song ({ 156keys :[{ beat :1 , scale :"minor" , tonic :"D#" }], 157chords :[ chord ({ root :4 })], 158notes :[{ beat :1 , duration :2 , sd :"5" , octave :0 , isRest :false }], 159}); 160const p = theoryTabProject ({ 161title :"Some Song – Someone" , 162sections :[ 163{ name :"verse" , song :verse }, 164{ name :"empty" , song :song ({}) }, 165{ name :"chorus" , song :chorus }, 166], 167}); 168assertEquals ( 169[ p . name , p . bpm , p . key ], 170[ "Some Song – Someone" , 92 , { tonic :"D#" , mode :"aeolian" }], 171); 172const [ chords , melody ] = p . tracks ; 173assertEquals ( chords . kind === "chords" && chords . chords . map ( c => [ c . start , c . dur , c . symbol ]), [ 174[ 0 , 4 , "D#m" ], 175[ 4 , 2 , "B" ], 176[ 8 , 4 , "G#m" ], 177]); 178assertEquals ( melody . kind === "notes" && melody . notes . map ( n => [ n . start , n . pitch ]), [[ 8 , 70 ]]); 179}); 180 181Deno . test ( "theorytab links" , () => { 182const path = "/theorytab/view/the-beatles/let-it-be" ; 183assertEquals ( 184theoryTabPath ( "https://www.hooktheory.com/theorytab/view/the-beatles/let-it-be" ), 185path , 186); 187assertEquals ( 188theoryTabPath ( " hooktheory.com/theorytab/view/the-beatles/let-it-be" ), 189undefined , 190); 191assertEquals ( 192theoryTabPath ( "https://hooktheory.com/theorytab/view/the-beatles/let-it-be/chorus?x#y" ), 193path , 194); 195assertEquals ( 196theoryTabPath ( "https://example.com/theorytab/view/the-beatles/let-it-be" ), 197undefined , 198); 199assertEquals ( 200theoryTabPath ( "https://www.hooktheory.com/theorytab/artist/the-beatles" ), 201undefined , 202); 203});