midi-file-io
Version:
Reads, parses and writes MIDI files. Fork of NHQ's midi-file-parser.
224 lines (223 loc) • 9.5 kB
JavaScript
;
/**
* User: curtis
* Date: 2019-01-20
* Time: 15:02
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeMidiToBuffer = writeMidiToBuffer;
exports.writeMidiToFile = writeMidiToFile;
const fs_1 = require("fs");
const path_1 = require("path");
const write_1 = require("./stream/write");
const types_1 = require("./types");
/**
* Maps midi code to the actual number of frames
*/
const frameCodeMap = {
24: 0,
25: 1,
29: 2,
30: 3
};
function writeMidiToBuffer(midiData) {
function writeChunk(stream, id, data) {
stream.write(id);
stream.writeInt32(data.length);
stream.write(data);
}
function writeHeader(stream) {
const data = new write_1.WriteStream();
data.writeInt16(midiData.header.formatType);
data.writeInt16(midiData.tracks.length);
data.writeInt16(midiData.header.ticksPerQuarter);
writeChunk(stream, "MThd", data);
}
/**
* todo: implement running status if you find this and can't stop yourself
*/
function writeTrack(stream, track) {
const trackStream = new write_1.WriteStream();
function writeEvent(event) {
function writeChannelData() {
switch (event.subtype) {
case types_1.MidiIoEventSubtype.NoteOff: {
trackStream.writeInt8(0x80 | event.channel);
trackStream.writeInt8(event.noteNumber);
trackStream.writeInt8(event.velocity);
break;
}
case types_1.MidiIoEventSubtype.NoteOn: {
trackStream.writeInt8(0x90 | event.channel);
trackStream.writeInt8(event.noteNumber);
trackStream.writeInt8(event.velocity);
break;
}
case types_1.MidiIoEventSubtype.NoteAftertouch: {
trackStream.writeInt8(0x0a | event.channel);
trackStream.writeInt8(event.noteNumber);
trackStream.writeInt8(event.amount);
break;
}
case types_1.MidiIoEventSubtype.Controller: {
trackStream.writeInt8(0x0b | event.channel);
trackStream.writeInt8(event.controllerType);
trackStream.writeInt8(event.value);
break;
}
case types_1.MidiIoEventSubtype.ProgramChange: {
trackStream.writeInt8(0x0c | event.channel);
trackStream.writeInt8(event.programNumber);
break;
}
case types_1.MidiIoEventSubtype.ChannelAftertouch: {
trackStream.writeInt8(0x0d | event.channel);
trackStream.writeInt8(event.amount);
break;
}
case types_1.MidiIoEventSubtype.PitchBend: {
trackStream.writeInt8(0x0e | event.channel);
trackStream.writeInt8(event.value & 0x7f);
trackStream.writeInt8(event.value >> 7);
break;
}
default: {
throw new Error(`unrecognised MIDI event : ${event.subtype}`);
}
}
}
function writeMetaData() {
function _writeText(subType) {
const text = event.text || "";
trackStream.writeInt8(subType);
trackStream.writeVarInt(text.length);
trackStream.write(text);
}
// write metadata's status byte
trackStream.writeInt8(0xff);
switch (event.subtype) {
case types_1.MidiIoEventSubtype.SequenceNumber: {
trackStream.writeInt8(0x00);
trackStream.writeVarInt(2);
trackStream.writeInt16(event.number);
break;
}
case types_1.MidiIoEventSubtype.Text: {
_writeText(0x01);
break;
}
case types_1.MidiIoEventSubtype.CopyrightNotice: {
_writeText(0x02);
break;
}
case types_1.MidiIoEventSubtype.TrackName: {
_writeText(0x03);
break;
}
case types_1.MidiIoEventSubtype.InstrumentName: {
_writeText(0x04);
break;
}
case types_1.MidiIoEventSubtype.Lyrics: {
_writeText(0x05);
break;
}
case types_1.MidiIoEventSubtype.Marker: {
_writeText(0x06);
break;
}
case types_1.MidiIoEventSubtype.CuePoint: {
_writeText(0x07);
break;
}
case types_1.MidiIoEventSubtype.MidiChannelPrefix: {
trackStream.writeInt8(0x20);
trackStream.writeVarInt(1);
trackStream.writeInt8(event.channel);
break;
}
case types_1.MidiIoEventSubtype.EndOfTrack: {
trackStream.writeInt8(0x2f);
trackStream.writeVarInt(0);
break;
}
case types_1.MidiIoEventSubtype.SetTempo: {
trackStream.writeInt8(0x51);
trackStream.writeVarInt(3);
trackStream.writeInt8(event.microsecondsPerBeat >> 16 & 0xff);
trackStream.writeInt8(event.microsecondsPerBeat >> 8 & 0xff);
trackStream.writeInt8(event.microsecondsPerBeat & 0xff);
break;
}
case types_1.MidiIoEventSubtype.SmpteOffset: {
const frameCode = frameCodeMap[event.frameRate];
trackStream.writeInt8(0x54);
trackStream.writeVarInt(5);
trackStream.writeInt8(event.hour | frameCode << 5);
trackStream.writeInt8(event.min);
trackStream.writeInt8(event.sec);
trackStream.writeInt8(event.frame);
trackStream.writeInt8(event.subframe);
break;
}
case types_1.MidiIoEventSubtype.TimeSignature: {
trackStream.writeInt8(0x58);
trackStream.writeVarInt(4);
trackStream.writeInt8(event.numerator);
trackStream.writeInt8(Math.log2(event.denominator));
trackStream.writeInt8(event.metronome);
trackStream.writeInt8(event.thirtyseconds);
break;
}
case types_1.MidiIoEventSubtype.KeySignature: {
trackStream.writeInt8(0x59);
trackStream.writeVarInt(2);
trackStream.writeInt8(event.key);
trackStream.writeInt8(event.scale);
break;
}
case types_1.MidiIoEventSubtype.SequencerSpecific: {
trackStream.writeInt8(0x7f);
trackStream.writeVarInt(event.data.length);
trackStream.write(event.data);
break;
}
default: {
throw new Error(`unknown channel event: ${event.subtype}`);
}
}
}
function writeSysexData() {
throw new Error("sysex not supported yet");
}
// write the event's delta offset
trackStream.writeVarInt(event.deltaTime);
if (event.type === types_1.MidiIoEventType.Channel) {
writeChannelData();
}
else if (event.type === types_1.MidiIoEventType.Meta) {
writeMetaData();
}
else if (event.type === types_1.MidiIoEventType.SysEx) {
writeSysexData();
}
}
track.forEach(writeEvent.bind(null));
writeChunk(stream, "MTrk", trackStream);
}
const stream = new write_1.WriteStream();
writeHeader(stream);
midiData.tracks.forEach(writeTrack.bind(null, stream));
return stream.buffer;
}
/**
* @throws {Error}
*/
function writeMidiToFile(midiData, filePath) {
const buffer = writeMidiToBuffer(midiData);
if (/.mid$/i.test(filePath) === false) {
filePath += ".mid";
}
(0, fs_1.mkdirSync)((0, path_1.parse)(filePath).dir, { recursive: true });
(0, fs_1.writeFileSync)(filePath, buffer, { encoding: "binary" });
}