midi-file-io
Version:
Reads, parses and writes MIDI files. Fork of NHQ's midi-file-parser.
246 lines (245 loc) • 11.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseMidiBuffer = parseMidiBuffer;
exports.parseMidiFile = parseMidiFile;
const fs_1 = require("fs");
const read_1 = require("./stream/read");
const types_1 = require("./types");
function parseMidiBuffer(data) {
let lastEventTypeByte;
function readChunk(stream) {
const id = stream.read(4);
const length = stream.readInt32();
return {
id,
length,
data: stream.read(length)
};
}
function readEvent(stream) {
// @ts-ignore
const event = {
deltaTime: stream.readVarInt(),
// note: we don't know these yet but are defaulting to get around ts errors
subtype: types_1.MidiIoEventSubtype.Unknown
};
let eventTypeByte = stream.readInt8();
if ((eventTypeByte & 0xf0) == 0xf0) {
/* system / meta event */
if (eventTypeByte == 0xff) {
/* meta event */
event.type = types_1.MidiIoEventType.Meta;
const subtypeByte = stream.readInt8(), length = stream.readVarInt();
switch (subtypeByte) {
case 0x00:
event.subtype = types_1.MidiIoEventSubtype.SequenceNumber;
if (length != 2) {
throw new Error(`expected length for sequenceNumber event is 2 but found ${length}`);
}
event.number = stream.readInt16();
return event;
case 0x01:
event.subtype = types_1.MidiIoEventSubtype.Text;
event.text = stream.read(length);
return event;
case 0x02:
event.subtype = types_1.MidiIoEventSubtype.CopyrightNotice;
event.text = stream.read(length);
return event;
case 0x03:
event.subtype = types_1.MidiIoEventSubtype.TrackName;
event.text = stream.read(length);
return event;
case 0x04:
event.subtype = types_1.MidiIoEventSubtype.InstrumentName;
event.text = stream.read(length);
return event;
case 0x05:
event.subtype = types_1.MidiIoEventSubtype.Lyrics;
event.text = stream.read(length);
return event;
case 0x06:
event.subtype = types_1.MidiIoEventSubtype.Marker;
event.text = stream.read(length);
return event;
case 0x07:
event.subtype = types_1.MidiIoEventSubtype.CuePoint;
event.text = stream.read(length);
return event;
case 0x20:
event.subtype = types_1.MidiIoEventSubtype.MidiChannelPrefix;
if (length != 1) {
throw new Error(`expected length for midiChannelPrefix event is 1 but found ${length}`);
}
event.channel = stream.readInt8();
return event;
case 0x2f:
event.subtype = types_1.MidiIoEventSubtype.EndOfTrack;
if (length != 0) {
throw new Error(`expected length for endOfTrack event is 0 but found ${length}`);
}
return event;
case 0x51:
event.subtype = types_1.MidiIoEventSubtype.SetTempo;
if (length != 3) {
throw new Error(`expected length for setTempo event is 3 but found ${length}`);
}
event.microsecondsPerBeat = ((stream.readInt8() << 16)
+ (stream.readInt8() << 8)
+ stream.readInt8());
return event;
case 0x54:
event.subtype = types_1.MidiIoEventSubtype.SmpteOffset;
if (length != 5) {
throw new Error(`expected length for smpteOffset event is 5 but found ${length}`);
}
let hourByte = stream.readInt8();
event.frameRate = [24, 25, 29, 30][hourByte >> 5];
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 = types_1.MidiIoEventSubtype.TimeSignature;
if (length != 4) {
throw new Error(`expected length for timeSignature event is 4 but found ${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 = types_1.MidiIoEventSubtype.KeySignature;
if (length != 2) {
throw new Error(`expected length for keySignature event is 2 but found ${length}`);
}
event.key = stream.readInt8(true);
event.scale = stream.readInt8();
return event;
case 0x7f:
event.subtype = types_1.MidiIoEventSubtype.SequencerSpecific;
event.data = stream.read(length);
return event;
default:
// console.log("Unrecognised meta event subtype: " + subtypeByte);
event.subtype = types_1.MidiIoEventSubtype.Unknown;
event.data = stream.read(length);
return event;
}
}
else if (eventTypeByte == 0xf0) {
const length = stream.readVarInt();
event.type = types_1.MidiIoEventType.SysEx;
event.data = stream.read(length);
return event;
}
else if (eventTypeByte == 0xf7) {
const length = stream.readVarInt();
event.type = types_1.MidiIoEventType.DividedSysEx;
event.data = stream.read(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 = types_1.MidiIoEventType.Channel;
switch (eventType) {
case 0x08:
event.subtype = types_1.MidiIoEventSubtype.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 = types_1.MidiIoEventSubtype.NoteOff;
}
else {
event.subtype = types_1.MidiIoEventSubtype.NoteOn;
}
return event;
case 0x0a:
event.subtype = types_1.MidiIoEventSubtype.NoteAftertouch;
event.noteNumber = param1;
event.amount = stream.readInt8();
return event;
case 0x0b:
event.subtype = types_1.MidiIoEventSubtype.Controller;
event.controllerType = param1;
event.value = stream.readInt8();
return event;
case 0x0c:
event.subtype = types_1.MidiIoEventSubtype.ProgramChange;
event.programNumber = param1;
return event;
case 0x0d:
event.subtype = types_1.MidiIoEventSubtype.ChannelAftertouch;
event.amount = param1;
return event;
case 0x0e:
event.subtype = types_1.MidiIoEventSubtype.PitchBend;
event.value = param1 + (stream.readInt8() << 7);
return event;
default:
throw new Error(`unrecognised MIDI event type "${eventType}"`);
}
}
}
function readHeader(stream) {
const headerChunk = readChunk(stream);
if (headerChunk.id != "MThd" || headerChunk.length != 6) {
throw new Error("MIDI header not found");
}
const headerStream = new read_1.ReadStream(headerChunk.data), header = {
formatType: headerStream.readInt16(),
trackCount: headerStream.readInt16(),
ticksPerQuarter: headerStream.readInt16()
};
if (header.ticksPerQuarter & 0x8000) {
throw new Error("expressing time division in SMTPE frames is not supported yet");
}
return header;
}
function readTrack(stream) {
const trackChunk = readChunk(stream);
if (trackChunk.id != "MTrk") {
throw new Error(`unexpected chunk: expected MTrk but found "${trackChunk.id}"`);
}
const track = [], trackStream = new read_1.ReadStream(trackChunk.data);
while (!trackStream.eof()) {
track.push(readEvent(trackStream));
}
return track;
}
const stream = new read_1.ReadStream(data), header = readHeader(stream), tracks = [];
for (let i = 0; i < header.trackCount; i++) {
tracks.push(readTrack(stream));
}
return {
header,
tracks
};
}
function parseMidiFile(path) {
const buffer = (0, fs_1.readFileSync)(path, { encoding: "binary" });
return parseMidiBuffer(buffer);
}