char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
3c7bc6e
main
1import { assertEquals } from "@std/assert" ; 2import { voiceTrack } from "../project/harmony.ts" ; 3import { 4newChordTrack , 5newNoteTrack , 6newProject , 7placeChord , 8projectKey , 9} from "../project/model.ts" ; 10import { projectMidi } from "./export.ts" ; 11import { parseMidi } from "./smf.ts" ; 12import { writeMidi } from "./write.ts" ; 13 14const notesOf = ( bytes :Uint8Array ) => 15parseMidi ( bytes ). map ( p => ({ 16name :p . name , 17channel :p . channel , 18notes :p . notes . map ( n => [ n . start , n . dur , n . pitch , Math . round ( n . vel * 127 )]), 19})); 20 21Deno . test ( "written notes read back as they went in" , () => { 22const bytes = writeMidi ({ 23bpm :120 , 24beatsPerBar :4 , 25tracks :[ 26{ 27name :"lead" , 28channel :0 , 29program :4 , 30notes :[ 31{ start :0 , dur :1 , pitch :60 , vel :1 }, 32{ start :1 , dur :1 / 3 , pitch :62 , vel :0.5 }, 33{ start :4 / 3 , dur :1 / 3 , pitch :64 , vel :0.5 }, 34{ start :0.25 , dur :0.25 , pitch :72 , vel :0.75 }, 35], 36}, 37{ 38name :"bass ♭" , 39channel :1 , 40program :4 , 41notes :[{ start :2 , dur :2 , pitch :36 , vel :0.6 }], 42}, 43], 44}); 45const round = ( xs :number [][]) => xs . map ( x => x . map ( v => Math . round ( v * 1000 ) / 1000 )); 46const parts = notesOf ( bytes ). map ( p => ({ ...p , notes :round ( p . notes ) })); 47assertEquals ( parts , [ 48{ 49name :"lead" , 50channel :0 , 51notes :[ 52[ 0 , 1 , 60 , 127 ], 53[ 0.25 , 0.25 , 72 , 95 ], 54[ 1 , 0.333 , 62 , 64 ], 55[ 1.333 , 0.333 , 64 , 64 ], 56], 57}, 58{ name :"bass ♭" , channel :1 , notes :[[ 2 , 2 , 36 , 76 ]] }, 59]); 60}); 61 62Deno . test ( "repeated pitches stay separate notes" , () => { 63const bytes = writeMidi ({ 64bpm :100 , 65beatsPerBar :4 , 66tracks :[ 67{ 68name :"t" , 69channel :0 , 70program :0 , 71notes :[ 72{ start :0 , dur :1 , pitch :60 , vel :0.8 }, 73{ start :1 , dur :1 , pitch :60 , vel :0.8 }, // starts as the first ends 74{ start :2 , dur :2 , pitch :60 , vel :0.8 }, 75{ start :3 , dur :1 , pitch :60 , vel :0.8 }, // starts while the one before still sounds 76], 77}, 78], 79}); 80assertEquals ( 81parseMidi ( bytes )[ 0 ]. notes . map ( n => [ n . start , n . dur ]), 82[ 83[ 0 , 1 ], 84[ 1 , 1 ], 85[ 2 , 1 ], 86[ 3 , 1 ], 87], 88); 89}); 90 91Deno . test ( "a project exports its chords as voiced, and its notes, a track each" , () => { 92const p = newProject (); 93const chords = newChordTrack ( "harmony" ); 94for ( const [ start , symbol ] of [ 95[ 0 , "C" ], 96[ 4 , "Am7" ], 97[ 8 , "bVI" ], 98] as const ) { 99placeChord ( chords , { id :symbol , start, dur :4 , symbol}); 100} 101chords . chords [ 1 ]. notes = [ 45 , 60 , 64 , 67 ]; // hand-voiced 102const melody = newNoteTrack ( "tune" ); 103melody . notes . push ({ id :"n" , start :1.5 , dur :0.5 , pitch :76 , vel :0.8 }); 104p . tracks = [ chords , melody ]; 105 106const [ harmony , tune ] = parseMidi ( projectMidi ( p )); 107const voiced = voiceTrack ( chords , projectKey ( p )); 108const at = ( beat :number ) => harmony . notes . filter ( n => n . start === beat ). map ( n => n . pitch ); 109assertEquals ([ harmony . name , tune . name ], [ "harmony" , "tune" ]); 110assertEquals ( 111[ 0 , 4 , 8 ]. map ( at ), 112voiced . map ( v => [ ...v . notes ]. sort (( a , b ) => a - b )), 113); 114assertEquals ( at ( 4 ), [ 45 , 60 , 64 , 67 ]); 115assertEquals ( 116tune . notes . map ( n => [ n . start , n . dur , n . pitch ]), 117[[ 1.5 , 0.5 , 76 ]], 118); 119assertEquals ([ harmony . channel , tune . channel ], [ 0 , 1 ]); 120});