Skip to content

Instantly share code, notes, and snippets.

@literallylara
Last active April 27, 2022 19:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save literallylara/740b34ee222b846343e085a8be6338ad to your computer and use it in GitHub Desktop.
Save literallylara/740b34ee222b846343e085a8be6338ad to your computer and use it in GitHub Desktop.
/**
* 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