char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
3c7bc6e
main
1/** a note from a midi file, timed in beats */ 2export type MidiNote = { start :number ; dur :number ; pitch :number ; vel :number }; 3export type MidiPart = { name :string ; channel :number ; notes :MidiNote [] }; 4 5const DRUM_CHANNEL = 9 ; 6const MIN_DUR = 1 / 16 ; 7 8/** 9* read the notes out of a standard midi file, one part per track and channel. drums are left 10* out: they'd only come out as a pitched mess on a piano 11*/ 12export function parseMidi ( bytes :Uint8Array ) :MidiPart [] { 13const view = new DataView ( bytes . buffer , bytes . byteOffset , bytes . byteLength ); 14const tag = ( at :number ) => String . fromCharCode ( ...bytes . subarray ( at , at + 4 )); 15if ( bytes . length < 14 || tag ( 0 ) !== "MThd" ) throw new Error ( "not a MIDI file" ); 16 17const division = view . getUint16 ( 12 ); 18if ( division & 0x8000 ) throw new Error ( "SMPTE-timed MIDI files aren't supported" ); 19 20const parts :MidiPart [] = []; 21let trackIndex = 0 ; 22for ( let pos = 8 + view . getUint32 ( 4 ); pos + 8 <= bytes . length ;) { 23const end = Math . min ( bytes . length , pos + 8 + view . getUint32 ( pos + 4 )); 24if ( tag ( pos ) === "MTrk" ) 25parts . push ( ...parseTrack ( bytes . subarray ( pos + 8 , end ), division , trackIndex ++ )); 26pos = end ; 27} 28return parts ; 29} 30 31function parseTrack ( data :Uint8Array , ppq :number , index :number ) :MidiPart [] { 32let pos = 0 ; 33let tick = 0 ; 34let status = 0 ; 35let name = "" ; 36const held = new Map < number , { tick :number ; vel :number }[]>(); 37const byChannel = new Map < number , MidiNote []>(); 38 39const varInt = () => { 40let value = 0 ; 41let b ; 42do { 43b = data [ pos ++ ]; 44value = ( value << 7 ) | ( b & 0x7f ); 45} while ( b & 0x80 && pos < data . length ); 46return value ; 47}; 48const release = ( channel :number , pitch :number , at :number ) => { 49// repeated note-ons of one pitch are paired first-in, first-out 50const on = held . get ( channel * 128 + pitch )?. shift (); 51if ( ! on ) return ; 52const notes = byChannel . get ( channel ) ?? []; 53byChannel . set ( channel , notes ); 54notes . push ({ 55start :on . tick / ppq , 56dur :Math . max ( MIN_DUR , ( at - on . tick ) / ppq ), 57 pitch, 58vel :on . vel / 127 , 59}); 60}; 61 62while ( pos < data . length ) { 63tick += varInt (); 64let event = data [ pos ]; 65if ( event & 0x80 ) pos ++ ; 66else if ( status ) 67event = status ; // running status: the data byte is still unread 68else throw new Error ( "malformed MIDI track" ); 69 70if ( event === 0xff ) { 71const type = data [ pos ++ ]; 72const length = varInt (); 73if ( type === 0x03 && ! name ) 74name = new TextDecoder (). decode ( data . subarray ( pos , pos + length )); 75if ( type === 0x2f ) break ; 76pos += length ; 77status = 0 ; 78} else if ( event === 0xf0 || event === 0xf7 ) { 79const length = varInt (); 80pos += length ; 81status = 0 ; 82} else { 83status = event ; 84const kind = event & 0xf0 ; 85const channel = event & 0x0f ; 86const pitch = data [ pos ++ ]; 87const vel = kind === 0xc0 || kind === 0xd0 ?0 :data [ pos ++ ]; 88if ( kind === 0x90 && vel > 0 ) { 89const key = channel * 128 + pitch ; 90held . set ( key , [ ...( held . get ( key ) ?? []), { tick, vel}]); 91} else if ( kind === 0x80 || kind === 0x90 ) release ( channel , pitch , tick ); 92} 93} 94 95for ( const [ key , ons ] of held ) 96ons . forEach (() => release ( Math . floor ( key / 128 ), key % 128 , tick )); 97 98const channels = [ ...byChannel . keys ()]. filter ( c => c !== DRUM_CHANNEL ). sort (( a , b ) => a - b ); 99return channels . map ( channel => ({ 100name : 101( name . trim () || `track ${ index + 1 } ` ) + ( channels . length > 1 ?` ch ${ channel + 1 } ` :"" ), 102 channel, 103notes :byChannel . get ( channel ) ! . sort (( a , b ) => a . start - b . start || a . pitch - b . pitch ), 104})); 105}