UNPKG

qambi

Version:

MIDI sequencer, loads MIDI files, can record and playback MIDI, uses WebMIDI and WebAudio

292 lines (273 loc) 8.79 kB
/* Extracts all midi events from a binary midi file, uses midi_stream.js based on: https://github.com/gasman/jasmid */ 'use strict'; import MIDIStream from './midi_stream'; let lastEventTypeByte; let originalTrackName; function readChunk(stream){ let id = stream.read(4, true); let length = stream.readInt32(); //console.log(length); return{ id, length, data: stream.read(length, false) }; } function readEvent(stream){ var event = {}; var length; event.deltaTime = stream.readVarInt(); let eventTypeByte = stream.readInt8(); //console.log(eventTypeByte, eventTypeByte & 0x80, 146 & 0x0f); if((eventTypeByte & 0xf0) == 0xf0){ /* system / meta event */ if(eventTypeByte == 0xff){ /* meta event */ event.type = 'meta'; let subtypeByte = stream.readInt8(); length = stream.readVarInt(); switch(subtypeByte){ case 0x00: event.subtype = 'sequenceNumber'; if(length !== 2){ throw 'Expected length for sequenceNumber event is 2, got ' + length; } event.number = stream.readInt16(); return event; case 0x01: event.subtype = 'text'; event.text = stream.read(length); return event; case 0x02: event.subtype = 'copyrightNotice'; event.text = stream.read(length); return event; case 0x03: event.subtype = 'trackName'; event.text = stream.read(length); originalTrackName = event.text; return event; case 0x04: event.subtype = 'instrumentName'; event.text = stream.read(length); return event; case 0x05: event.subtype = 'lyrics'; event.text = stream.read(length); return event; case 0x06: event.subtype = 'marker'; event.text = stream.read(length); return event; case 0x07: event.subtype = 'cuePoint'; event.text = stream.read(length); return event; case 0x20: event.subtype = 'midiChannelPrefix'; if(length !== 1){ throw 'Expected length for midiChannelPrefix event is 1, got ' + length; } event.channel = stream.readInt8(); return event; case 0x2f: event.subtype = 'endOfTrack'; if(length !== 0){ throw 'Expected length for endOfTrack event is 0, got ' + length; } return event; case 0x51: event.subtype = 'setTempo'; if(length !== 3){ throw 'Expected length for setTempo event is 3, got ' + length; } event.microsecondsPerBeat = ( (stream.readInt8() << 16) + (stream.readInt8() << 8) + stream.readInt8() ); return event; case 0x54: event.subtype = 'smpteOffset'; if(length !== 5){ throw 'Expected length for smpteOffset event is 5, got ' + length; } let hourByte = stream.readInt8(); event.frameRate = { 0x00: 24, 0x20: 25, 0x40: 29, 0x60: 30 }[hourByte & 0x60]; event.hour = hourByte & 0x1f; event.min = stream.readInt8(); event.sec = stream.readInt8(); event.frame = stream.readInt8(); event.subframe = stream.readInt8(); return event; case 0x58: event.subtype = 'timeSignature'; if(length !== 4){ throw 'Expected length for timeSignature event is 4, got ' + length; } event.numerator = stream.readInt8(); event.denominator = Math.pow(2, stream.readInt8()); event.metronome = stream.readInt8(); event.thirtyseconds = stream.readInt8(); return event; case 0x59: event.subtype = 'keySignature'; if(length !== 2){ throw 'Expected length for keySignature event is 2, got ' + length; } event.key = stream.readInt8(true); event.scale = stream.readInt8(); return event; case 0x7f: event.subtype = 'sequencerSpecific'; event.data = stream.read(length); return event; default: //if(sequencer.debug >= 2){ // console.warn('Unrecognised meta event subtype: ' + subtypeByte); //} event.subtype = 'unknown'; event.data = stream.read(length); return event; } event.data = stream.read(length); return event; } else if(eventTypeByte == 0xf0){ event.type = 'sysEx'; length = stream.readVarInt(); event.data = stream.read(length); return event; } else if(eventTypeByte == 0xf7){ event.type = 'dividedSysEx'; length = stream.readVarInt(); event.data = stream.read(length); return event; } else { throw 'Unrecognised MIDI event type byte: ' + eventTypeByte; } }else{ /* channel event */ let param1; if((eventTypeByte & 0x80) === 0){ /* running status - reuse lastEventTypeByte as the event type. eventTypeByte is actually the first parameter */ //console.log('running status'); param1 = eventTypeByte; eventTypeByte = lastEventTypeByte; }else{ param1 = stream.readInt8(); //console.log('last', eventTypeByte); lastEventTypeByte = eventTypeByte; } let eventType = eventTypeByte >> 4; event.channel = eventTypeByte & 0x0f; event.type = 'channel'; switch (eventType){ case 0x08: event.subtype = 'noteOff'; event.noteNumber = param1; event.velocity = stream.readInt8(); return event; case 0x09: event.noteNumber = param1; event.velocity = stream.readInt8(); if(event.velocity === 0){ event.subtype = 'noteOff'; }else{ event.subtype = 'noteOn'; //console.log('noteOn'); } return event; case 0x0a: event.subtype = 'noteAftertouch'; event.noteNumber = param1; event.amount = stream.readInt8(); return event; case 0x0b: event.subtype = 'controller'; event.controllerType = param1; event.value = stream.readInt8(); return event; case 0x0c: event.subtype = 'programChange'; event.programNumber = param1; return event; case 0x0d: event.subtype = 'channelAftertouch'; event.amount = param1; //if(trackName === 'SH-S1-44-C09 L=SML IN=3'){ // console.log('channel pressure', trackName, param1); //} return event; case 0x0e: event.subtype = 'pitchBend'; event.value = param1 + (stream.readInt8() << 7); return event; default: /* throw 'Unrecognised MIDI event type: ' + eventType; console.log('Unrecognised MIDI event type: ' + eventType); */ event.value = stream.readInt8(); event.subtype = 'unknown'; //console.log(event); /* event.noteNumber = param1; event.velocity = stream.readInt8(); event.subtype = 'noteOn'; console.log('weirdo', trackName, param1, event.velocity); */ return event; } } } export function parseMIDIFile(buffer){ if(buffer instanceof Uint8Array === false && buffer instanceof ArrayBuffer === false){ console.error('buffer should be an instance of Uint8Array of ArrayBuffer') return } if(buffer instanceof ArrayBuffer){ buffer = new Uint8Array(buffer) } let tracks = new Map(); let stream = new MIDIStream(buffer); let headerChunk = readChunk(stream); if(headerChunk.id !== 'MThd' || headerChunk.length !== 6){ throw 'Bad .mid file - header not found'; } let headerStream = new MIDIStream(headerChunk.data); let formatType = headerStream.readInt16(); let trackCount = headerStream.readInt16(); let timeDivision = headerStream.readInt16(); if(timeDivision & 0x8000){ throw 'Expressing time division in SMTPE frames is not supported yet'; } let header = { ticksPerBeat: timeDivision }; for(let i = 0; i < trackCount; i++){ originalTrackName = false; let track = []; let trackChunk = readChunk(stream); if(trackChunk.id !== 'MTrk'){ throw 'Unexpected chunk - expected MTrk, got ' + trackChunk.id; } let trackStream = new MIDIStream(trackChunk.data); while(!trackStream.eof()){ let event = readEvent(trackStream); track.push(event); } const trackName = originalTrackName || 'track_' + i; tracks.set(trackName, track); } return { header, tracks }; }