UNPKG

midi-file

Version:
344 lines (309 loc) 9.65 kB
// data can be any array-like object. It just needs to support .length, .slice, and an element getter [] function parseMidi(data) { var p = new Parser(data) var headerChunk = p.readChunk() if (headerChunk.id != 'MThd') throw "Bad MIDI file. Expected 'MHdr', got: '" + headerChunk.id + "'" var header = parseHeader(headerChunk.data) var tracks = [] for (var i=0; !p.eof() && i < header.numTracks; i++) { var trackChunk = p.readChunk() if (trackChunk.id != 'MTrk') throw "Bad MIDI file. Expected 'MTrk', got: '" + trackChunk.id + "'" var track = parseTrack(trackChunk.data) tracks.push(track) } return { header: header, tracks: tracks } } function parseHeader(data) { var p = new Parser(data) var format = p.readUInt16() var numTracks = p.readUInt16() var result = { format: format, numTracks: numTracks } var timeDivision = p.readUInt16() if (timeDivision & 0x8000) { result.framesPerSecond = 0x100 - (timeDivision >> 8) result.ticksPerFrame = timeDivision & 0xFF } else { result.ticksPerBeat = timeDivision } return result } function parseTrack(data) { var p = new Parser(data) var events = [] while (!p.eof()) { var event = readEvent() events.push(event) } return events var lastEventTypeByte = null function readEvent() { var event = {} event.deltaTime = p.readVarInt() var eventTypeByte = p.readUInt8() if ((eventTypeByte & 0xf0) === 0xf0) { // system / meta event if (eventTypeByte === 0xff) { // meta event event.meta = true var metatypeByte = p.readUInt8() var length = p.readVarInt() switch (metatypeByte) { case 0x00: event.type = 'sequenceNumber' if (length !== 2) throw "Expected length for sequenceNumber event is 2, got " + length event.number = p.readUInt16() return event case 0x01: event.type = 'text' event.text = p.readString(length) return event case 0x02: event.type = 'copyrightNotice' event.text = p.readString(length) return event case 0x03: event.type = 'trackName' event.text = p.readString(length) return event case 0x04: event.type = 'instrumentName' event.text = p.readString(length) return event case 0x05: event.type = 'lyrics' event.text = p.readString(length) return event case 0x06: event.type = 'marker' event.text = p.readString(length) return event case 0x07: event.type = 'cuePoint' event.text = p.readString(length) return event case 0x20: event.type = 'channelPrefix' if (length != 1) throw "Expected length for channelPrefix event is 1, got " + length event.channel = p.readUInt8() return event case 0x21: event.type = 'portPrefix' if (length != 1) throw "Expected length for portPrefix event is 1, got " + length event.port = p.readUInt8() return event case 0x2f: event.type = 'endOfTrack' if (length != 0) throw "Expected length for endOfTrack event is 0, got " + length return event case 0x51: event.type = 'setTempo'; if (length != 3) throw "Expected length for setTempo event is 3, got " + length event.microsecondsPerBeat = p.readUInt24() return event case 0x54: event.type = 'smpteOffset'; if (length != 5) throw "Expected length for smpteOffset event is 5, got " + length var hourByte = p.readUInt8() var FRAME_RATES = { 0x00: 24, 0x20: 25, 0x40: 29, 0x60: 30 } event.frameRate = FRAME_RATES[hourByte & 0x60] event.hour = hourByte & 0x1f event.min = p.readUInt8() event.sec = p.readUInt8() event.frame = p.readUInt8() event.subFrame = p.readUInt8() return event case 0x58: event.type = 'timeSignature' if (length != 2 && length != 4) throw "Expected length for timeSignature event is 4 or 2, got " + length event.numerator = p.readUInt8() event.denominator = (1 << p.readUInt8()) if (length === 4) { event.metronome = p.readUInt8() event.thirtyseconds = p.readUInt8() } else { event.metronome = 0x24 event.thirtyseconds = 0x08 } return event case 0x59: event.type = 'keySignature' if (length != 2) throw "Expected length for keySignature event is 2, got " + length event.key = p.readInt8() event.scale = p.readUInt8() return event case 0x7f: event.type = 'sequencerSpecific' event.data = p.readBytes(length) return event default: event.type = 'unknownMeta' event.data = p.readBytes(length) event.metatypeByte = metatypeByte return event } } else if (eventTypeByte == 0xf0) { event.type = 'sysEx' var length = p.readVarInt() event.data = p.readBytes(length) return event } else if (eventTypeByte == 0xf7) { event.type = 'endSysEx' var length = p.readVarInt() event.data = p.readBytes(length) return event } else { throw "Unrecognised MIDI event type byte: " + eventTypeByte } } else { // channel event var param1 if ((eventTypeByte & 0x80) === 0) { // running status - reuse lastEventTypeByte as the event type. // eventTypeByte is actually the first parameter if (lastEventTypeByte === null) throw "Running status byte encountered before status byte" param1 = eventTypeByte eventTypeByte = lastEventTypeByte event.running = true } else { param1 = p.readUInt8() lastEventTypeByte = eventTypeByte } var eventType = eventTypeByte >> 4 event.channel = eventTypeByte & 0x0f switch (eventType) { case 0x08: event.type = 'noteOff' event.noteNumber = param1 event.velocity = p.readUInt8() return event case 0x09: var velocity = p.readUInt8() event.type = velocity === 0 ? 'noteOff' : 'noteOn' event.noteNumber = param1 event.velocity = velocity if (velocity === 0) event.byte9 = true return event case 0x0a: event.type = 'noteAftertouch' event.noteNumber = param1 event.amount = p.readUInt8() return event case 0x0b: event.type = 'controller' event.controllerType = param1 event.value = p.readUInt8() return event case 0x0c: event.type = 'programChange' event.programNumber = param1 return event case 0x0d: event.type = 'channelAftertouch' event.amount = param1 return event case 0x0e: event.type = 'pitchBend' event.value = (param1 + (p.readUInt8() << 7)) - 0x2000 return event default: throw "Unrecognised MIDI event type: " + eventType } } } } function Parser(data) { this.buffer = data this.bufferLen = this.buffer.length this.pos = 0 } Parser.prototype.eof = function() { return this.pos >= this.bufferLen } Parser.prototype.readUInt8 = function() { var result = this.buffer[this.pos] this.pos += 1 return result } Parser.prototype.readInt8 = function() { var u = this.readUInt8() if (u & 0x80) return u - 0x100 else return u } Parser.prototype.readUInt16 = function() { var b0 = this.readUInt8(), b1 = this.readUInt8() return (b0 << 8) + b1 } Parser.prototype.readInt16 = function() { var u = this.readUInt16() if (u & 0x8000) return u - 0x10000 else return u } Parser.prototype.readUInt24 = function() { var b0 = this.readUInt8(), b1 = this.readUInt8(), b2 = this.readUInt8() return (b0 << 16) + (b1 << 8) + b2 } Parser.prototype.readInt24 = function() { var u = this.readUInt24() if (u & 0x800000) return u - 0x1000000 else return u } Parser.prototype.readUInt32 = function() { var b0 = this.readUInt8(), b1 = this.readUInt8(), b2 = this.readUInt8(), b3 = this.readUInt8() return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3 } Parser.prototype.readBytes = function(len) { var bytes = this.buffer.slice(this.pos, this.pos + len) this.pos += len return bytes } Parser.prototype.readString = function(len) { var bytes = this.readBytes(len) return String.fromCharCode.apply(null, bytes) } Parser.prototype.readVarInt = function() { var result = 0 while (!this.eof()) { var b = this.readUInt8() if (b & 0x80) { result += (b & 0x7f) result <<= 7 } else { // b is last byte return result + b } } // premature eof return result } Parser.prototype.readChunk = function() { var id = this.readString(4) var length = this.readUInt32() var data = this.readBytes(length) return { id: id, length: length, data: data } } module.exports = parseMidi