Last active
April 27, 2022 19:07
-
-
Save literallylara/740b34ee222b846343e085a8be6338ad to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* MIDI parser for JavaScript | |
* Reference: midi.org/specifications-old/item/the-midi-1-0-specification | |
* | |
* @author Lara Sophie Schütt (@literallylara) | |
* @license MIT | |
*/ | |
const MIDI = {} | |
let _data = null | |
let _pointer = null | |
const TYPE_NUL = 0 | |
const TYPE_ARR = 1 | |
const TYPE_INT = 2 | |
const TYPE_STR = 3 | |
const EVENTS = | |
{ | |
// <delta-time> 0xSn <data> (S = status, n = channel) | |
// running status: <delta-time> <data> ... | |
voice: | |
{ | |
0x80: ["note off" , TYPE_ARR, 2], | |
0x90: ["note on" , TYPE_ARR, 2], | |
0xA0: ["key aftertouch" , TYPE_ARR, 2], | |
0xC0: ["program" , TYPE_INT, 1], | |
0xD0: ["channel aftertouch", TYPE_ARR, 1], | |
0xE0: ["pitch bend" , TYPE_ARR, 2] | |
}, | |
// <delta-time> 0xBn <type> <data> (n = channel) | |
// running status: <delta-time> <type> <data> ... | |
mode: | |
{ | |
0x78: ["all sounds off" , TYPE_NUL, 1], | |
0x79: ["reset all controllers", TYPE_NUL, 1], | |
0x7A: ["local control" , TYPE_INT, 1], | |
0x7B: ["all notes off" , TYPE_NUL, 1], | |
0x7C: ["omni off" , TYPE_NUL, 1], | |
0x7D: ["omni on" , TYPE_NUL, 1], | |
0x7E: ["mono on" , TYPE_INT, 1], | |
0x7F: ["poly on" , TYPE_NUL, 1] | |
}, | |
// <delta-time> 0xFF <type> <length> <data> | |
meta: | |
{ | |
0x00: ["sequence number", TYPE_INT, 2], | |
0x01: ["text" , TYPE_STR, undefined], | |
0x02: ["copyright" , TYPE_STR, undefined], | |
0x03: ["sequence/track" , TYPE_STR, undefined], | |
0x04: ["instrument" , TYPE_STR, undefined], | |
0x05: ["lyric" , TYPE_STR, undefined], | |
0x06: ["marker" , TYPE_STR, undefined], | |
0x07: ["cue point" , TYPE_STR, undefined], | |
0x20: ["channel prefix" , TYPE_INT, 1], | |
0x2F: ["end of track" , TYPE_NUL, 0], | |
0x51: ["set tempo" , TYPE_INT, 3], | |
0x54: ["SMPTE offset" , TYPE_ARR, 5], | |
0x58: ["time signature" , TYPE_ARR, 4], | |
0x59: ["key signature" , TYPE_ARR, 2], | |
0x7F: ["sequencer data" , TYPE_ARR, undefined] | |
}, | |
// <delta-time> 0xFn <length> <data> | |
system: | |
{ | |
0xF0: ["sysex" , TYPE_ARR, undefined], | |
0xF7: ["sysex (escape)" , TYPE_ARR, undefined] | |
} | |
} | |
MIDI.readInput = (input, callback) => | |
{ | |
input.onchange = () => | |
{ | |
const r = new window.FileReader() | |
r.onload = e => callback(MIDI.readBuffer(r.result)) | |
r.readAsArrayBuffer(this.files[0]) | |
} | |
} | |
MIDI.readBuffer = b => | |
{ | |
_data = new Uint8Array(b) | |
_pointer = 0 | |
const out = { header: {}, tracks: [] } | |
// loop chunks | |
while (_pointer < _data.length) | |
{ | |
const chk = { events: [] } | |
const type = readStr(4) | |
const len = readInt(4) | |
const end = _pointer + len | |
// header chunk | |
if (type == "MThd") | |
{ | |
out.header.format = readInt(2) | |
out.header.tracks = readInt(2) | |
out.header.division = readInt(2) | |
continue | |
} | |
// loop events | |
while (_pointer < end) | |
{ | |
const evt = {} | |
evt.deltaTime = readVLQ() | |
evt.status = readInt(1) | |
let type = null | |
let dataType = null | |
let dataLength = null | |
// voice/mode | |
if (evt.status > 0x7F && evt.status < 0xF0) | |
{ | |
evt.channel = evt.status & 0x0F | |
evt.status = evt.status & 0xF0 | |
if (evt.status == 0xB0) | |
{ | |
evt.type = readInt(1) | |
type = EVENTS.mode[evt.type] || ["unknown control", TYPE_INT, 1] | |
} | |
else | |
{ | |
type = EVENTS.voice[evt.status] | |
} | |
dataLength = type[2] | |
} | |
// meta | |
else if (evt.status == 0xFF) | |
{ | |
evt.type = readInt(1) | |
type = EVENTS.meta[evt.type] | |
dataLength = readVLQ() | |
} | |
// system | |
else if (evt.status == 0xF0 || evt.status == 0xF7) | |
{ | |
type = EVENTS.system[evt.status] | |
dataLength = readVLQ() | |
} | |
if (!type) | |
{ | |
type = ["unknown", TYPE_ARR] | |
dataLength = readVLQ() | |
} | |
evt.info = type[0] | |
dataType = type[1] | |
if (evt.status > 0x7F && evt.status < 0xF0) | |
{ | |
do | |
{ | |
if (evt.deltaTime === null) | |
{ | |
evt.deltaTime = readVLQ() | |
if (evt.status == 0xB0) | |
{ | |
evt.type = readInt(1) | |
type = EVENTS.mode[evt.type] || ["unknown control", TYPE_INT, 1] | |
evt.info = type[0] | |
dataType = type[1] | |
} | |
} | |
evt.data = readData(dataType, dataLength) | |
chk.events.push(Object.assign({}, evt)) | |
evt.deltaTime = null | |
} while (isRunningStatus()) | |
} | |
else | |
{ | |
evt.data = readData(dataType, dataLength) | |
// filter special meta data | |
switch (evt.type) | |
{ | |
case 0x03: chk.name = evt.data; continue | |
case 0x51: out.header.tempo = evt.data; continue | |
case 0x58: out.header.time = evt.data; continue | |
} | |
chk.events.push(Object.assign({}, evt)) | |
} | |
} | |
out.tracks.push(chk) | |
} | |
return out | |
} | |
function readArr(l) | |
{ | |
return _data.slice(_pointer, _pointer += l) | |
} | |
function readInt(l) | |
{ | |
let n = 0 | |
for (let i = 0; i < l; i++) | |
{ | |
n += _data[_pointer++] << (8*(l-i-1)) | |
} | |
return n | |
} | |
function readStr(l) | |
{ | |
let s = [] | |
for (let i = 0; i < l; i++) | |
{ | |
s.push(String.fromCharCode(_data[_pointer++])) | |
} | |
return s.join("") | |
} | |
function readData(type, l) | |
{ | |
switch (type) | |
{ | |
case TYPE_INT: return readInt(l) | |
case TYPE_STR: return readStr(l) | |
case TYPE_ARR: return readArr(l) | |
} | |
} | |
function readVLQ() | |
{ | |
if (_data[_pointer] < 0x80) return _data[_pointer++] | |
const a = [] | |
let n = 0 | |
let f = 1 | |
let v = null | |
while (f) | |
{ | |
v = _data[_pointer++] | |
f = v & 0b10000000 | |
v = v & 0b01111111 | |
a.push(v) | |
} | |
for (let i = 0, l = a.length; i < l; i++) | |
{ | |
n += a[i] << (7*(l-i-1)) | |
} | |
return n | |
} | |
function isRunningStatus() | |
{ | |
const i = _pointer | |
const d = readVLQ() | |
const b = readInt(1) | |
_pointer = i | |
return b < 0x80 | |
} | |
export default MIDI |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment