music21j-port
Version:
A toolkit for computer-aided musicology, Javascript version
264 lines (257 loc) • 7.46 kB
JavaScript
/*
class to parse the .mid file format
(depends on stream.js)
*/
import { Stream } from './stream.js';
export class MidiFileClass {
constructor(data) {
this.data = data;
this.stream = new Stream(data);
this.lastEventTypeByte = undefined;
}
readChunk(stream) {
const id = stream.read(4);
const length = stream.readInt32();
return {
'id': id,
'length': length,
'data': stream.read(length)
};
}
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 "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);
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;
}
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 "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:
// console.log("Unrecognised meta event subtype: " + subtypeByte);
event.subtype = 'unknown';
event.data = stream.read(length);
return event;
}
} else if (eventTypeByte === 0xf0) {
event.type = 'sysEx';
const length = stream.readVarInt();
event.data = stream.read(length);
return event;
} else if (eventTypeByte === 0xf7) {
event.type = 'dividedSysEx';
const 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
*/
param1 = eventTypeByte;
eventTypeByte = this.lastEventTypeByte;
} else {
param1 = stream.readInt8();
this.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 "Unrecognised MIDI event type: " + eventType
/*
console.log("Unrecognised MIDI event type: " + eventType);
stream.readInt8();
event.subtype = 'unknown';
return event;
*/
}
}
}
parseAndReturn() {
const stream = this.stream;
const headerChunk = this.readChunk(stream);
if (headerChunk.id !== 'MThd' || headerChunk.length !== 6) {
throw "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 "Expressing time division in SMTPE frames is not supported yet"
} else {
ticksPerBeat = timeDivision;
}
const header = {
'formatType': formatType,
'trackCount': trackCount,
'ticksPerBeat': ticksPerBeat,
};
const tracks = [];
for (let i = 0; i < header.trackCount; i++) {
tracks[i] = [];
const trackChunk = this.readChunk(stream);
if (trackChunk.id !== 'MTrk') {
throw "Unexpected chunk - expected MTrk, got "+ trackChunk.id;
}
const trackStream = new Stream(trackChunk.data);
while (!trackStream.eof()) {
const event = this.readEvent(trackStream);
tracks[i].push(event);
//console.log(event);
}
}
return {
'header': header,
'tracks': tracks,
};
}
}
export function MidiFile(data) {
const midiFileObject = new MidiFileClass(data);
return midiFileObject.parseAndReturn();
}