Spaces:
Sleeping
Sleeping
/* | |
class to parse the .mid file format | |
(depends on stream.js) | |
*/ | |
const Stream = require("./stream.js"); | |
module.exports = function MidiFile (data) { | |
function readChunk (stream) { | |
const id = stream.readString(4); | |
const length = stream.readInt32(); | |
return { | |
id, | |
length, | |
data: stream.read(length), | |
}; | |
} | |
let lastEventTypeByte; | |
function readEvent (stream) { | |
const event = {}; | |
event.deltaTime = stream.readVarInt(); | |
let eventTypeByte = stream.readInt8(); | |
if ((eventTypeByte & 0xf0) === 0xf0) { | |
// system / meta event | |
if (eventTypeByte === 0xff) { | |
// meta event | |
event.type = "meta"; | |
const subtypeByte = stream.readInt8(); | |
const length = stream.readVarInt(); | |
switch (subtypeByte) { | |
case 0x00: | |
event.subtype = "sequenceNumber"; | |
if (length !== 2) | |
throw new Error("Expected length for sequenceNumber event is 2, got " + length); | |
event.number = stream.readInt16(); | |
return event; | |
case 0x01: | |
event.subtype = "text"; | |
event.text = stream.readString(length); | |
return event; | |
case 0x02: | |
event.subtype = "copyrightNotice"; | |
event.text = stream.readString(length); | |
return event; | |
case 0x03: | |
event.subtype = "trackName"; | |
event.text = stream.readString(length); | |
return event; | |
case 0x04: | |
event.subtype = "instrumentName"; | |
event.text = stream.readString(length); | |
return event; | |
case 0x05: | |
event.subtype = "lyrics"; | |
event.text = stream.readString(length); | |
return event; | |
case 0x06: | |
event.subtype = "marker"; | |
event.text = stream.readString(length); | |
return event; | |
case 0x07: | |
event.subtype = "cuePoint"; | |
event.text = stream.readString(length); | |
return event; | |
case 0x20: | |
event.subtype = "midiChannelPrefix"; | |
if (length !== 1) | |
throw new Error("Expected length for midiChannelPrefix event is 1, got " + length); | |
event.channel = stream.readInt8(); | |
return event; | |
case 0x2f: | |
event.subtype = "endOfTrack"; | |
if (length !== 0) | |
throw new Error("Expected length for endOfTrack event is 0, got " + length); | |
return event; | |
case 0x51: | |
event.subtype = "setTempo"; | |
if (length !== 3) | |
throw new Error("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 new Error("Expected length for smpteOffset event is 5, got " + length); | |
const 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 new Error("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 new Error("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.readString(length); | |
return event; | |
default: | |
// console.log("Unrecognised meta event subtype: " + subtypeByte); | |
event.subtype = "unknown"; | |
event.data = stream.readString(length); | |
return event; | |
} | |
//event.data = stream.readString(length); | |
//return event; | |
} | |
else if (eventTypeByte === 0xf0) { | |
event.type = "sysEx"; | |
const length = stream.readVarInt(); | |
event.data = stream.readString(length); | |
return event; | |
} | |
else if (eventTypeByte === 0xf7) { | |
event.type = "dividedSysEx"; | |
const length = stream.readVarInt(); | |
event.data = stream.readString(length); | |
return event; | |
} | |
else | |
throw new Error("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 | |
*/ | |
param1 = eventTypeByte; | |
eventTypeByte = lastEventTypeByte; | |
} | |
else { | |
param1 = stream.readInt8(); | |
lastEventTypeByte = eventTypeByte; | |
} | |
const 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"; | |
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; | |
return event; | |
case 0x0e: | |
event.subtype = "pitchBend"; | |
event.value = param1 + (stream.readInt8() << 7); | |
return event; | |
default: | |
throw new Error("Unrecognised MIDI event type: " + eventType); | |
/* | |
console.log("Unrecognised MIDI event type: " + eventType); | |
stream.readInt8(); | |
event.subtype = 'unknown'; | |
return event; | |
*/ | |
} | |
} | |
} | |
let source = data; | |
if (typeof data === "string") | |
source = data.split("").map(c => c.charCodeAt(0)); | |
const stream = new Stream(source); | |
const headerChunk = readChunk(stream); | |
if (headerChunk.id !== "MThd" || headerChunk.length !== 6) | |
throw new Error("Bad .mid file - header not found"); | |
const headerStream = new Stream(headerChunk.data); | |
const formatType = headerStream.readInt16(); | |
const trackCount = headerStream.readInt16(); | |
const timeDivision = headerStream.readInt16(); | |
let ticksPerBeat; | |
if (timeDivision & 0x8000) | |
throw new Error("Expressing time division in SMTPE frames is not supported yet"); | |
else | |
ticksPerBeat = timeDivision; | |
const header = { | |
formatType, | |
trackCount, | |
ticksPerBeat, | |
}; | |
const tracks = []; | |
for (let i = 0; i < header.trackCount; i++) { | |
tracks[i] = []; | |
const trackChunk = readChunk(stream); | |
if (trackChunk.id !== "MTrk") | |
throw new Error("Unexpected chunk - expected MTrk, got " + trackChunk.id); | |
const trackStream = new Stream(trackChunk.data); | |
while (!trackStream.eof()) { | |
const event = readEvent(trackStream); | |
tracks[i].push(event); | |
} | |
} | |
return { | |
header, | |
tracks, | |
}; | |
}; | |