char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
3c7bc6e
main
1import { assertEquals , assertThrows } from "@std/assert" ; 2import { parseMidi } from "./smf.ts" ; 3 4const u16 = ( n :number ) => [ n >> 8 , n & 0xff ]; 5const u32 = ( n :number ) => [ n >>> 24 , ( n >> 16 ) & 0xff , ( n >> 8 ) & 0xff , n & 0xff ]; 6const ascii = ( s :string ) => [ ...s ]. map ( c => c . charCodeAt ( 0 )); 7function varInt ( n :number ) { 8const out = [ n & 0x7f ]; 9while (( n >>= 7 )) out . unshift (( n & 0x7f ) | 0x80 ); 10return out ; 11} 12 13const header = ( format :number , tracks :number , ppq :number ) => [ 14 ...ascii ( "MThd" ), 15 ...u32 ( 6 ), 16 ...u16 ( format ), 17 ...u16 ( tracks ), 18 ...u16 ( ppq ), 19]; 20const track = ( ...events :number [][]) => { 21const body = [ ...events . flat (), 0 , 0xff , 0x2f , 0 ]; 22return [ ...ascii ( "MTrk" ), ...u32 ( body . length ), ...body ]; 23}; 24const trackName = ( name :string ) => [ 0 , 0xff , 0x03 , ...varInt ( name . length ), ...ascii ( name )]; 25const midi = ( ...parts :number [][]) => new Uint8Array ( parts . flat ()); 26 27Deno . test ( "notes come out in beats, with running status and zero-velocity note-offs" , () => { 28const parts = parseMidi ( 29midi ( 30header ( 0 , 1 , 480 ), 31track ( 32[ 0 , 0x90 , 60 , 127 ], 33[ ...varInt ( 480 ), 60 , 0 ], // running status note-on, velocity 0: a note-off 34[ 0 , 64 , 64 ], 35[ ...varInt ( 240 ), 0x80 , 64 , 0 ], 36), 37), 38); 39assertEquals ( parts , [ 40{ 41name :"track 1" , 42channel :0 , 43notes :[ 44{ start :0 , dur :1 , pitch :60 , vel :1 }, 45{ start :1 , dur :0.5 , pitch :64 , vel :64 / 127 }, 46], 47}, 48]); 49}); 50 51Deno . test ( "each track with notes becomes a part, named, and drums are left out" , () => { 52const parts = parseMidi ( 53midi ( 54header ( 1 , 3 , 96 ), 55track ( trackName ( "tempo" ), [ 0 , 0xff , 0x51 , 3 , 0x07 , 0xa1 , 0x20 ]), 56track ( trackName ( "lead" ), [ 0 , 0x91 , 72 , 100 ], [ 96 , 0x81 , 72 , 0 ]), 57track ( trackName ( "drums" ), [ 0 , 0x99 , 36 , 100 ], [ 96 , 0x89 , 36 , 0 ]), 58), 59); 60assertEquals ( 61parts . map ( p => [ p . name , p . channel , p . notes . length ]), 62[[ "lead" , 1 , 1 ]], 63); 64}); 65 66Deno . test ( "a track spanning several channels splits into one part per channel" , () => { 67const parts = parseMidi ( 68midi ( 69header ( 0 , 1 , 96 ), 70track ([ 0 , 0x90 , 60 , 90 ], [ 0 , 0x92 , 48 , 90 ], [ 96 , 0x80 , 60 , 0 ], [ 0 , 0x82 , 48 , 0 ]), 71), 72); 73assertEquals ( 74parts . map ( p => [ p . name , p . notes [ 0 ]. pitch ]), 75[ 76[ "track 1 ch1" , 60 ], 77[ "track 1 ch3" , 48 ], 78], 79); 80}); 81 82Deno . test ( "overlapping repeats pair up in order, and held notes end with the track" , () => { 83const [ part ] = parseMidi ( 84midi ( 85header ( 0 , 1 , 100 ), 86track ( 87[ 0 , 0x90 , 60 , 100 ], 88[ 50 , 0x90 , 60 , 100 ], 89[ 50 , 0x80 , 60 , 0 ], 90[ 0 , 0xf0 , 2 , 0x7e , 0xf7 ], // sysex between events cancels running status 91[ 0 , 0x90 , 67 , 100 ], 92[ 100 , 0x80 , 60 , 0 ], 93), 94), 95); 96assertEquals ( 97part . notes . map ( n => [ n . pitch , n . start , n . dur ]), 98[ 99[ 60 , 0 , 1 ], 100[ 60 , 0.5 , 1.5 ], 101[ 67 , 1 , 1 ], 102], 103); 104}); 105 106Deno . test ( "rejects files that aren't midi" , () => { 107assertThrows ( 108() => parseMidi ( new TextEncoder (). encode ( "RIFF....WAVEfmt " )), 109Error , 110"not a MIDI file" , 111); 112assertThrows (() => parseMidi ( midi ( header ( 0 , 1 , 0xe728 ), track ())), Error , "SMPTE" ); 113});