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, | |
| }; | |
| }; | |