midi-writer-js
Version:
A library providing an API for generating MIDI files.
1,325 lines (1,297 loc) • 51 kB
JavaScript
'use strict';
/**
* MIDI file format constants.
* @return {Constants}
*/
var Constants = {
VERSION: '3.1.1',
HEADER_CHUNK_TYPE: [0x4d, 0x54, 0x68, 0x64],
HEADER_CHUNK_LENGTH: [0x00, 0x00, 0x00, 0x06],
HEADER_CHUNK_FORMAT0: [0x00, 0x00],
HEADER_CHUNK_FORMAT1: [0x00, 0x01],
HEADER_CHUNK_DIVISION: [0x00, 0x80],
TRACK_CHUNK_TYPE: [0x4d, 0x54, 0x72, 0x6b],
META_EVENT_ID: 0xFF,
META_SMTPE_OFFSET: 0x54
};
// src/utils.ts
var fillStr = (s, n) => Array(Math.abs(n) + 1).join(s);
// src/named.ts
function isNamed(src) {
return src !== null && typeof src === "object" && typeof src.name === "string" ? true : false;
}
// src/pitch.ts
function isPitch(pitch) {
return pitch !== null && typeof pitch === "object" && typeof pitch.step === "number" && typeof pitch.alt === "number" ? true : false;
}
var FIFTHS = [0, 2, 4, -1, 1, 3, 5];
var STEPS_TO_OCTS = FIFTHS.map(
(fifths) => Math.floor(fifths * 7 / 12)
);
function encode(pitch) {
const { step, alt, oct, dir = 1 } = pitch;
const f = FIFTHS[step] + 7 * alt;
if (oct === void 0) {
return [dir * f];
}
const o = oct - STEPS_TO_OCTS[step] - 4 * alt;
return [dir * f, dir * o];
}
// src/note.ts
var NoNote = { empty: true, name: "", pc: "", acc: "" };
var cache = /* @__PURE__ */ new Map();
var stepToLetter = (step) => "CDEFGAB".charAt(step);
var altToAcc = (alt) => alt < 0 ? fillStr("b", -alt) : fillStr("#", alt);
var accToAlt = (acc) => acc[0] === "b" ? -acc.length : acc.length;
function note(src) {
const stringSrc = JSON.stringify(src);
const cached = cache.get(stringSrc);
if (cached) {
return cached;
}
const value = typeof src === "string" ? parse(src) : isPitch(src) ? note(pitchName(src)) : isNamed(src) ? note(src.name) : NoNote;
cache.set(stringSrc, value);
return value;
}
var REGEX = /^([a-gA-G]?)(#{1,}|b{1,}|x{1,}|)(-?\d*)\s*(.*)$/;
function tokenizeNote(str) {
const m = REGEX.exec(str);
return [m[1].toUpperCase(), m[2].replace(/x/g, "##"), m[3], m[4]];
}
var mod = (n, m) => (n % m + m) % m;
var SEMI = [0, 2, 4, 5, 7, 9, 11];
function parse(noteName) {
const tokens = tokenizeNote(noteName);
if (tokens[0] === "" || tokens[3] !== "") {
return NoNote;
}
const letter = tokens[0];
const acc = tokens[1];
const octStr = tokens[2];
const step = (letter.charCodeAt(0) + 3) % 7;
const alt = accToAlt(acc);
const oct = octStr.length ? +octStr : void 0;
const coord = encode({ step, alt, oct });
const name = letter + acc + octStr;
const pc = letter + acc;
const chroma = (SEMI[step] + alt + 120) % 12;
const height = oct === void 0 ? mod(SEMI[step] + alt, 12) - 12 * 99 : SEMI[step] + alt + 12 * (oct + 1);
const midi = height >= 0 && height <= 127 ? height : null;
const freq = oct === void 0 ? null : Math.pow(2, (height - 69) / 12) * 440;
return {
empty: false,
acc,
alt,
chroma,
coord,
freq,
height,
letter,
midi,
name,
oct,
pc,
step
};
}
function pitchName(props) {
const { step, alt, oct } = props;
const letter = stepToLetter(step);
if (!letter) {
return "";
}
const pc = letter + altToAcc(alt);
return oct || oct === 0 ? pc + oct : pc;
}
// index.ts
function isMidi(arg) {
return +arg >= 0 && +arg <= 127;
}
function toMidi(note$1) {
if (isMidi(note$1)) {
return +note$1;
}
const n = note(note$1);
return n.empty ? null : n.midi;
}
/**
* Static utility functions used throughout the library.
*/
var Utils = /** @class */ (function () {
function Utils() {
}
/**
* Gets MidiWriterJS version number.
* @return {string}
*/
Utils.version = function () {
return Constants.VERSION;
};
/**
* Convert a string to an array of bytes
* @param {string} string
* @return {array}
*/
Utils.stringToBytes = function (string) {
return string.split('').map(function (char) { return char.charCodeAt(0); });
};
/**
* Checks if argument is a valid number.
* @param {*} n - Value to check
* @return {boolean}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Utils.isNumeric = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
/**
* Returns the correct MIDI number for the specified pitch.
* Uses Tonal Midi - https://github.com/danigb/tonal/tree/master/packages/midi
* @param {(string|number)} pitch - 'C#4' or midi note code
* @param {string} middleC
* @return {number}
*/
Utils.getPitch = function (pitch, middleC) {
if (middleC === void 0) { middleC = 'C4'; }
return 60 - toMidi(middleC) + toMidi(pitch);
};
/**
* Translates number of ticks to MIDI timestamp format, returning an array of
* hex strings with the time values. Midi has a very particular time to express time,
* take a good look at the spec before ever touching this function.
* Thanks to https://github.com/sergi/jsmidi
*
* @param {number} ticks - Number of ticks to be translated
* @return {array} - Bytes that form the MIDI time value
*/
Utils.numberToVariableLength = function (ticks) {
ticks = Math.round(ticks);
var buffer = ticks & 0x7F;
// eslint-disable-next-line no-cond-assign
while (ticks = ticks >> 7) {
buffer <<= 8;
buffer |= ((ticks & 0x7F) | 0x80);
}
var bList = [];
// eslint-disable-next-line no-constant-condition
while (true) {
bList.push(buffer & 0xff);
if (buffer & 0x80)
buffer >>= 8;
else {
break;
}
}
return bList;
};
/**
* Counts number of bytes in string
* @param {string} s
* @return {number}
*/
Utils.stringByteCount = function (s) {
return encodeURI(s).split(/%..|./).length - 1;
};
/**
* Get an int from an array of bytes.
* @param {array} bytes
* @return {number}
*/
Utils.numberFromBytes = function (bytes) {
var hex = '';
var stringResult;
bytes.forEach(function (byte) {
stringResult = byte.toString(16);
// ensure string is 2 chars
if (stringResult.length == 1)
stringResult = "0" + stringResult;
hex += stringResult;
});
return parseInt(hex, 16);
};
/**
* Takes a number and splits it up into an array of bytes. Can be padded by passing a number to bytesNeeded
* @param {number} number
* @param {number} bytesNeeded
* @return {array} - Array of bytes
*/
Utils.numberToBytes = function (number, bytesNeeded) {
bytesNeeded = bytesNeeded || 1;
var hexString = number.toString(16);
if (hexString.length & 1) { // Make sure hex string is even number of chars
hexString = '0' + hexString;
}
// Split hex string into an array of two char elements
var hexArray = hexString.match(/.{2}/g);
// Now parse them out as integers
var intArray = hexArray.map(function (item) { return parseInt(item, 16); });
// Prepend empty bytes if we don't have enough
if (intArray.length < bytesNeeded) {
while (bytesNeeded - intArray.length > 0) {
intArray.unshift(0);
}
}
return intArray;
};
/**
* Converts value to array if needed.
* @param {any} value
* @return {array}
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Utils.toArray = function (value) {
if (Array.isArray(value))
return value;
return [value];
};
/**
* Converts velocity to value 0-127
* @param {number} velocity - Velocity value 1-100
* @return {number}
*/
Utils.convertVelocity = function (velocity) {
// Max passed value limited to 100
velocity = velocity > 100 ? 100 : velocity;
return Math.round(velocity / 100 * 127);
};
/**
* Gets the total number of ticks of a specified duration.
* Note: type=='note' defaults to quarter note, type==='rest' defaults to 0
* @param {(string|array)} duration
* @return {number}
*/
Utils.getTickDuration = function (duration) {
if (Array.isArray(duration)) {
// Recursively execute this method for each item in the array and return the sum of tick durations.
return duration.map(function (value) {
return Utils.getTickDuration(value);
}).reduce(function (a, b) {
return a + b;
}, 0);
}
duration = duration.toString();
if (duration.toLowerCase().charAt(0) === 't') {
// If duration starts with 't' then the number that follows is an explicit tick count
var ticks = parseInt(duration.substring(1));
if (isNaN(ticks) || ticks < 0) {
throw new Error(duration + ' is not a valid duration.');
}
return ticks;
}
// Need to apply duration here. Quarter note == Constants.HEADER_CHUNK_DIVISION
var quarterTicks = Utils.numberFromBytes(Constants.HEADER_CHUNK_DIVISION);
var tickDuration = quarterTicks * Utils.getDurationMultiplier(duration);
return Utils.getRoundedIfClose(tickDuration);
};
/**
* Due to rounding errors in JavaScript engines,
* it's safe to round when we're very close to the actual tick number
*
* @static
* @param {number} tick
* @return {number}
*/
Utils.getRoundedIfClose = function (tick) {
var roundedTick = Math.round(tick);
return Math.abs(roundedTick - tick) < 0.000001 ? roundedTick : tick;
};
/**
* Due to low precision of MIDI,
* we need to keep track of rounding errors in deltas.
* This function will calculate the rounding error for a given duration.
*
* @static
* @param {number} tick
* @return {number}
*/
Utils.getPrecisionLoss = function (tick) {
var roundedTick = Math.round(tick);
return roundedTick - tick;
};
/**
* Gets what to multiple ticks/quarter note by to get the specified duration.
* Note: type=='note' defaults to quarter note, type==='rest' defaults to 0
* @param {string} duration
* @return {number}
*/
Utils.getDurationMultiplier = function (duration) {
// Need to apply duration here.
// Quarter note == Constants.HEADER_CHUNK_DIVISION ticks.
if (duration === '0')
return 0;
var match = duration.match(/^(?<dotted>d+)?(?<base>\d+)(?:t(?<tuplet>\d*))?/);
if (match) {
var base = Number(match.groups.base);
// 1 or any power of two:
var isValidBase = base === 1 || ((base & (base - 1)) === 0);
if (isValidBase) {
// how much faster or slower is this note compared to a quarter?
var ratio = base / 4;
var durationInQuarters = 1 / ratio;
var _a = match.groups, dotted = _a.dotted, tuplet = _a.tuplet;
if (dotted) {
var thisManyDots = dotted.length;
var divisor = Math.pow(2, thisManyDots);
durationInQuarters = durationInQuarters + (durationInQuarters * ((divisor - 1) / divisor));
}
if (typeof tuplet === 'string') {
var fitInto = durationInQuarters * 2;
// default to triplet:
var thisManyNotes = Number(tuplet || '3');
durationInQuarters = fitInto / thisManyNotes;
}
return durationInQuarters;
}
}
throw new Error(duration + ' is not a valid duration.');
};
return Utils;
}());
/**
* Holds all data for a "controller change" MIDI event
* @param {object} fields {controllerNumber: integer, controllerValue: integer, delta: integer}
* @return {ControllerChangeEvent}
*/
var ControllerChangeEvent = /** @class */ (function () {
function ControllerChangeEvent(fields) {
this.channel = fields.channel - 1 || 0;
this.controllerValue = fields.controllerValue;
this.controllerNumber = fields.controllerNumber;
this.delta = fields.delta || 0x00;
this.name = 'ControllerChangeEvent';
this.status = 0xB0;
this.data = Utils.numberToVariableLength(fields.delta).concat(this.status | this.channel, this.controllerNumber, this.controllerValue);
}
return ControllerChangeEvent;
}());
/**
* Object representation of a tempo meta event.
* @param {object} fields {text: string, delta: integer}
* @return {CopyrightEvent}
*/
var CopyrightEvent = /** @class */ (function () {
function CopyrightEvent(fields) {
this.delta = fields.delta || 0x00;
this.name = 'CopyrightEvent';
this.text = fields.text;
this.type = 0x02;
var textBytes = Utils.stringToBytes(this.text);
// Start with zero time delta
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
textBytes);
}
return CopyrightEvent;
}());
/**
* Object representation of a cue point meta event.
* @param {object} fields {text: string, delta: integer}
* @return {CuePointEvent}
*/
var CuePointEvent = /** @class */ (function () {
function CuePointEvent(fields) {
this.delta = fields.delta || 0x00;
this.name = 'CuePointEvent';
this.text = fields.text;
this.type = 0x07;
var textBytes = Utils.stringToBytes(this.text);
// Start with zero time delta
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
textBytes);
}
return CuePointEvent;
}());
/**
* Object representation of a end track meta event.
* @param {object} fields {delta: integer}
* @return {EndTrackEvent}
*/
var EndTrackEvent = /** @class */ (function () {
function EndTrackEvent(fields) {
this.delta = (fields === null || fields === void 0 ? void 0 : fields.delta) || 0x00;
this.name = 'EndTrackEvent';
this.type = [0x2F, 0x00];
// Start with zero time delta
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type);
}
return EndTrackEvent;
}());
/**
* Object representation of an instrument name meta event.
* @param {object} fields {text: string, delta: integer}
* @return {InstrumentNameEvent}
*/
var InstrumentNameEvent = /** @class */ (function () {
function InstrumentNameEvent(fields) {
this.delta = fields.delta || 0x00;
this.name = 'InstrumentNameEvent';
this.text = fields.text;
this.type = 0x04;
var textBytes = Utils.stringToBytes(this.text);
// Start with zero time delta
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
textBytes);
}
return InstrumentNameEvent;
}());
/**
* Object representation of a key signature meta event.
* @return {KeySignatureEvent}
*/
var KeySignatureEvent = /** @class */ (function () {
function KeySignatureEvent(sf, mi) {
this.name = 'KeySignatureEvent';
this.type = 0x59;
var mode = mi || 0;
sf = sf || 0;
// Function called with string notation
if (typeof mi === 'undefined') {
var fifths = [
['Cb', 'Gb', 'Db', 'Ab', 'Eb', 'Bb', 'F', 'C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#'],
['ab', 'eb', 'bb', 'f', 'c', 'g', 'd', 'a', 'e', 'b', 'f#', 'c#', 'g#', 'd#', 'a#']
];
var _sflen = sf.length;
var note = sf || 'C';
if (sf[0] === sf[0].toLowerCase())
mode = 1;
if (_sflen > 1) {
switch (sf.charAt(_sflen - 1)) {
case 'm':
mode = 1;
note = sf.charAt(0).toLowerCase();
note = note.concat(sf.substring(1, _sflen - 1));
break;
case '-':
mode = 1;
note = sf.charAt(0).toLowerCase();
note = note.concat(sf.substring(1, _sflen - 1));
break;
case 'M':
mode = 0;
note = sf.charAt(0).toUpperCase();
note = note.concat(sf.substring(1, _sflen - 1));
break;
case '+':
mode = 0;
note = sf.charAt(0).toUpperCase();
note = note.concat(sf.substring(1, _sflen - 1));
break;
}
}
var fifthindex = fifths[mode].indexOf(note);
sf = fifthindex === -1 ? 0 : fifthindex - 7;
}
// Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, this.type, [0x02], // Size
Utils.numberToBytes(sf, 1), // Number of sharp or flats ( < 0 flat; > 0 sharp)
Utils.numberToBytes(mode, 1));
}
return KeySignatureEvent;
}());
/**
* Object representation of a lyric meta event.
* @param {object} fields {text: string, delta: integer}
* @return {LyricEvent}
*/
var LyricEvent = /** @class */ (function () {
function LyricEvent(fields) {
this.delta = fields.delta || 0x00;
this.name = 'LyricEvent';
this.text = fields.text;
this.type = 0x05;
var textBytes = Utils.stringToBytes(this.text);
// Start with zero time delta
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
textBytes);
}
return LyricEvent;
}());
/**
* Object representation of a marker meta event.
* @param {object} fields {text: string, delta: integer}
* @return {MarkerEvent}
*/
var MarkerEvent = /** @class */ (function () {
function MarkerEvent(fields) {
this.delta = fields.delta || 0x00;
this.name = 'MarkerEvent';
this.text = fields.text;
this.type = 0x06;
var textBytes = Utils.stringToBytes(this.text);
// Start with zero time delta
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
textBytes);
}
return MarkerEvent;
}());
/**
* Holds all data for a "note on" MIDI event
* @param {object} fields {data: []}
* @return {NoteOnEvent}
*/
var NoteOnEvent = /** @class */ (function () {
function NoteOnEvent(fields) {
this.name = 'NoteOnEvent';
this.channel = fields.channel || 1;
this.pitch = fields.pitch;
this.wait = fields.wait || 0;
this.velocity = fields.velocity || 50;
this.tick = fields.tick || null;
this.delta = null;
this.data = fields.data;
this.status = 0x90;
}
/**
* Builds int array for this event.
* @param {Track} track - parent track
* @return {NoteOnEvent}
*/
NoteOnEvent.prototype.buildData = function (track, precisionDelta, options) {
if (options === void 0) { options = {}; }
this.data = [];
// Explicitly defined startTick event
if (this.tick) {
this.tick = Utils.getRoundedIfClose(this.tick);
// If this is the first event in the track then use event's starting tick as delta.
if (track.tickPointer == 0) {
this.delta = this.tick;
}
}
else {
this.delta = Utils.getTickDuration(this.wait);
this.tick = Utils.getRoundedIfClose(track.tickPointer + this.delta);
}
this.deltaWithPrecisionCorrection = Utils.getRoundedIfClose(this.delta - precisionDelta);
this.data = Utils.numberToVariableLength(this.deltaWithPrecisionCorrection)
.concat(this.status | this.channel - 1, Utils.getPitch(this.pitch, options.middleC), Utils.convertVelocity(this.velocity));
return this;
};
return NoteOnEvent;
}());
/**
* Holds all data for a "note off" MIDI event
* @param {object} fields {data: []}
* @return {NoteOffEvent}
*/
var NoteOffEvent = /** @class */ (function () {
function NoteOffEvent(fields) {
this.name = 'NoteOffEvent';
this.channel = fields.channel || 1;
this.pitch = fields.pitch;
this.velocity = fields.velocity || 50;
this.tick = fields.tick || null;
this.data = fields.data;
this.delta = fields.delta || Utils.getTickDuration(fields.duration);
this.status = 0x80;
}
/**
* Builds int array for this event.
* @param {Track} track - parent track
* @return {NoteOffEvent}
*/
NoteOffEvent.prototype.buildData = function (track, precisionDelta, options) {
if (options === void 0) { options = {}; }
if (this.tick === null) {
this.tick = Utils.getRoundedIfClose(this.delta + track.tickPointer);
}
this.deltaWithPrecisionCorrection = Utils.getRoundedIfClose(this.delta - precisionDelta);
this.data = Utils.numberToVariableLength(this.deltaWithPrecisionCorrection)
.concat(this.status | this.channel - 1, Utils.getPitch(this.pitch, options.middleC), Utils.convertVelocity(this.velocity));
return this;
};
return NoteOffEvent;
}());
/**
* Wrapper for noteOnEvent/noteOffEvent objects that builds both events.
* @param {object} fields - {pitch: '[C4]', duration: '4', wait: '4', velocity: 1-100}
* @return {NoteEvent}
*/
var NoteEvent = /** @class */ (function () {
function NoteEvent(fields) {
this.data = [];
this.name = 'NoteEvent';
this.pitch = Utils.toArray(fields.pitch);
this.channel = fields.channel || 1;
this.duration = fields.duration || '4';
this.grace = fields.grace;
this.repeat = fields.repeat || 1;
this.sequential = fields.sequential || false;
this.tick = fields.startTick || fields.tick || null;
this.velocity = fields.velocity || 50;
this.wait = fields.wait || 0;
this.tickDuration = Utils.getTickDuration(this.duration);
this.restDuration = Utils.getTickDuration(this.wait);
this.events = []; // Hold actual NoteOn/NoteOff events
}
/**
* Builds int array for this event.
* @return {NoteEvent}
*/
NoteEvent.prototype.buildData = function () {
var _this = this;
// Reset data array
this.data = [];
// Apply grace note(s) and subtract ticks (currently 1 tick per grace note) from tickDuration so net value is the same
if (this.grace) {
var graceDuration_1 = 1;
this.grace = Utils.toArray(this.grace);
this.grace.forEach(function () {
var noteEvent = new NoteEvent({ pitch: _this.grace, duration: 'T' + graceDuration_1 });
_this.data = _this.data.concat(noteEvent.data);
});
}
// fields.pitch could be an array of pitches.
// If so create note events for each and apply the same duration.
// By default this is a chord if it's an array of notes that requires one NoteOnEvent.
// If this.sequential === true then it's a sequential string of notes that requires separate NoteOnEvents.
if (!this.sequential) {
// Handle repeat
for (var j = 0; j < this.repeat; j++) {
// Note on
this.pitch.forEach(function (p, i) {
var noteOnNew;
if (i == 0) {
noteOnNew = new NoteOnEvent({
channel: _this.channel,
wait: _this.wait,
delta: Utils.getTickDuration(_this.wait),
velocity: _this.velocity,
pitch: p,
tick: _this.tick,
});
}
else {
// Running status (can ommit the note on status)
//noteOn = new NoteOnEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]});
noteOnNew = new NoteOnEvent({
channel: _this.channel,
wait: 0,
delta: 0,
velocity: _this.velocity,
pitch: p,
tick: _this.tick,
});
}
_this.events.push(noteOnNew);
});
// Note off
this.pitch.forEach(function (p, i) {
var noteOffNew;
if (i == 0) {
//noteOff = new NoteOffEvent({data: Utils.numberToVariableLength(tickDuration).concat(this.getNoteOffStatus(), Utils.getPitch(p), Utils.convertVelocity(this.velocity))});
noteOffNew = new NoteOffEvent({
channel: _this.channel,
duration: _this.duration,
velocity: _this.velocity,
pitch: p,
tick: _this.tick !== null ? Utils.getTickDuration(_this.duration) + _this.tick : null,
});
}
else {
// Running status (can omit the note off status)
//noteOff = new NoteOffEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]});
noteOffNew = new NoteOffEvent({
channel: _this.channel,
duration: 0,
velocity: _this.velocity,
pitch: p,
tick: _this.tick !== null ? Utils.getTickDuration(_this.duration) + _this.tick : null,
});
}
_this.events.push(noteOffNew);
});
}
}
else {
// Handle repeat
for (var j = 0; j < this.repeat; j++) {
this.pitch.forEach(function (p, i) {
var noteOnNew = new NoteOnEvent({
channel: _this.channel,
wait: (i > 0 ? 0 : _this.wait),
delta: (i > 0 ? 0 : Utils.getTickDuration(_this.wait)),
velocity: _this.velocity,
pitch: p,
tick: _this.tick,
});
var noteOffNew = new NoteOffEvent({
channel: _this.channel,
duration: _this.duration,
velocity: _this.velocity,
pitch: p,
});
_this.events.push(noteOnNew, noteOffNew);
});
}
}
return this;
};
return NoteEvent;
}());
/**
* Holds all data for a "Pitch Bend" MIDI event
* [ -1.0, 0, 1.0 ] -> [ 0, 8192, 16383]
* @param {object} fields { bend : float, channel : int, delta: int }
* @return {PitchBendEvent}
*/
var PitchBendEvent = /** @class */ (function () {
function PitchBendEvent(fields) {
this.channel = fields.channel || 0;
this.delta = fields.delta || 0x00;
this.name = 'PitchBendEvent';
this.status = 0xE0;
var bend14 = this.scale14bits(fields.bend);
var lsbValue = bend14 & 0x7f;
var msbValue = (bend14 >> 7) & 0x7f;
this.data = Utils.numberToVariableLength(this.delta).concat(this.status | this.channel, lsbValue, msbValue);
}
PitchBendEvent.prototype.scale14bits = function (zeroOne) {
if (zeroOne <= 0) {
return Math.floor(16384 * (zeroOne + 1) / 2);
}
return Math.floor(16383 * (zeroOne + 1) / 2);
};
return PitchBendEvent;
}());
/**
* Holds all data for a "program change" MIDI event
* @param {object} fields {instrument: integer, delta: integer}
* @return {ProgramChangeEvent}
*/
var ProgramChangeEvent = /** @class */ (function () {
function ProgramChangeEvent(fields) {
this.channel = fields.channel || 0;
this.delta = fields.delta || 0x00;
this.instrument = fields.instrument;
this.status = 0xC0;
this.name = 'ProgramChangeEvent';
// delta time defaults to 0.
this.data = Utils.numberToVariableLength(this.delta).concat(this.status | this.channel, this.instrument);
}
return ProgramChangeEvent;
}());
/**
* Object representation of a tempo meta event.
* @param {object} fields {bpm: integer, delta: integer}
* @return {TempoEvent}
*/
var TempoEvent = /** @class */ (function () {
function TempoEvent(fields) {
this.bpm = fields.bpm;
this.delta = fields.delta || 0x00;
this.tick = fields.tick;
this.name = 'TempoEvent';
this.type = 0x51;
var tempo = Math.round(60000000 / this.bpm);
// Start with zero time delta
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, [0x03], // Size
Utils.numberToBytes(tempo, 3));
}
return TempoEvent;
}());
/**
* Object representation of a tempo meta event.
* @param {object} fields {text: string, delta: integer}
* @return {TextEvent}
*/
var TextEvent = /** @class */ (function () {
function TextEvent(fields) {
this.delta = fields.delta || 0x00;
this.text = fields.text;
this.name = 'TextEvent';
this.type = 0x01;
var textBytes = Utils.stringToBytes(this.text);
// Start with zero time delta
this.data = Utils.numberToVariableLength(fields.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
textBytes);
}
return TextEvent;
}());
/**
* Object representation of a time signature meta event.
* @return {TimeSignatureEvent}
*/
var TimeSignatureEvent = /** @class */ (function () {
function TimeSignatureEvent(numerator, denominator, midiclockspertick, notespermidiclock) {
this.name = 'TimeSignatureEvent';
this.type = 0x58;
// Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, this.type, [0x04], // Size
Utils.numberToBytes(numerator, 1), // Numerator, 1 bytes
Utils.numberToBytes(Math.log2(denominator), 1), // Denominator is expressed as pow of 2, 1 bytes
Utils.numberToBytes(midiclockspertick || 24, 1), // MIDI Clocks per tick, 1 bytes
Utils.numberToBytes(notespermidiclock || 8, 1));
}
return TimeSignatureEvent;
}());
/**
* Object representation of a tempo meta event.
* @param {object} fields {text: string, delta: integer}
* @return {TrackNameEvent}
*/
var TrackNameEvent = /** @class */ (function () {
function TrackNameEvent(fields) {
this.delta = fields.delta || 0x00;
this.name = 'TrackNameEvent';
this.text = fields.text;
this.type = 0x03;
var textBytes = Utils.stringToBytes(this.text);
// Start with zero time delta
this.data = Utils.numberToVariableLength(this.delta).concat(Constants.META_EVENT_ID, this.type, Utils.numberToVariableLength(textBytes.length), // Size
textBytes);
}
return TrackNameEvent;
}());
/**
* Holds all data for a track.
* @param {object} fields {type: number, data: array, size: array, events: array}
* @return {Track}
*/
var Track = /** @class */ (function () {
function Track() {
this.type = Constants.TRACK_CHUNK_TYPE;
this.data = [];
this.size = [];
this.events = [];
this.explicitTickEvents = [];
// If there are any events with an explicit tick defined then we will create a "sub" track for those
// and merge them in and the end.
this.tickPointer = 0; // Each time an event is added this will increase
}
/**
* Adds any event type to the track.
* Events without a specific startTick property are assumed to be added in order of how they should output.
* Events with a specific startTick property are set aside for now will be merged in during build process.
*
* TODO: Don't put startTick events in their own array. Just lump everything together and sort it out during buildData();
* @param {(NoteEvent|ProgramChangeEvent)} events - Event object or array of Event objects.
* @param {Function} mapFunction - Callback which can be used to apply specific properties to all events.
* @return {Track}
*/
Track.prototype.addEvent = function (events, mapFunction) {
var _this = this;
Utils.toArray(events).forEach(function (event, i) {
if (event instanceof NoteEvent) {
// Handle map function if provided
if (typeof mapFunction === 'function') {
var properties = mapFunction(i, event);
if (typeof properties === 'object') {
Object.assign(event, properties);
}
}
// If this note event has an explicit startTick then we need to set aside for now
if (event.tick !== null) {
_this.explicitTickEvents.push(event);
}
else {
// Push each on/off event to track's event stack
event.buildData().events.forEach(function (e) { return _this.events.push(e); });
}
}
else {
_this.events.push(event);
}
});
return this;
};
/**
* Builds int array of all events.
* @param {object} options
* @return {Track}
*/
Track.prototype.buildData = function (options) {
var _this = this;
if (options === void 0) { options = {}; }
// Reset
this.data = [];
this.size = [];
this.tickPointer = 0;
var precisionLoss = 0;
this.events.forEach(function (event) {
// Build event & add to total tick duration
if (event instanceof NoteOnEvent || event instanceof NoteOffEvent) {
var built = event.buildData(_this, precisionLoss, options);
precisionLoss = Utils.getPrecisionLoss(event.deltaWithPrecisionCorrection || 0);
_this.data = _this.data.concat(built.data);
_this.tickPointer = Utils.getRoundedIfClose(event.tick);
}
else if (event instanceof TempoEvent) {
_this.tickPointer = Utils.getRoundedIfClose(event.tick);
_this.data = _this.data.concat(event.data);
}
else {
_this.data = _this.data.concat(event.data);
}
});
this.mergeExplicitTickEvents();
// If the last event isn't EndTrackEvent, then tack it onto the data.
if (!this.events.length || !(this.events[this.events.length - 1] instanceof EndTrackEvent)) {
this.data = this.data.concat((new EndTrackEvent).data);
}
this.size = Utils.numberToBytes(this.data.length, 4); // 4 bytes long
return this;
};
Track.prototype.mergeExplicitTickEvents = function () {
var _this = this;
if (!this.explicitTickEvents.length)
return;
// First sort asc list of events by startTick
this.explicitTickEvents.sort(function (a, b) { return a.tick - b.tick; });
// Now this.explicitTickEvents is in correct order, and so is this.events naturally.
// For each explicit tick event, splice it into the main list of events and
// adjust the delta on the following events so they still play normally.
this.explicitTickEvents.forEach(function (noteEvent) {
// Convert NoteEvent to it's respective NoteOn/NoteOff events
// Note that as we splice in events the delta for the NoteOff ones will
// Need to change based on what comes before them after the splice.
noteEvent.buildData().events.forEach(function (e) { return e.buildData(_this); });
// Merge each event individually into this track's event list.
noteEvent.events.forEach(function (event) { return _this.mergeSingleEvent(event); });
});
// Hacky way to rebuild track with newly spliced events. Need better solution.
this.explicitTickEvents = [];
this.buildData();
};
/**
* Merges another track's events with this track.
* @param {Track} track
* @return {Track}
*/
Track.prototype.mergeTrack = function (track) {
var _this = this;
// First build this track to populate each event's tick property
this.buildData();
// Then build track to be merged so that tick property is populated on all events & merge each event.
track.buildData().events.forEach(function (event) { return _this.mergeSingleEvent(event); });
return this;
};
/**
* Merges a single event into this track's list of events based on event.tick property.
* @param {AbstractEvent} - event
* @return {Track}
*/
Track.prototype.mergeSingleEvent = function (event) {
// There are no events yet, so just add it in.
if (!this.events.length) {
this.addEvent(event);
return;
}
// Find index of existing event we need to follow with
var lastEventIndex;
for (var i = 0; i < this.events.length; i++) {
if (this.events[i].tick > event.tick)
break;
lastEventIndex = i;
}
var splicedEventIndex = lastEventIndex + 1;
// Need to adjust the delta of this event to ensure it falls on the correct tick.
event.delta = event.tick - this.events[lastEventIndex].tick;
// Splice this event at lastEventIndex + 1
this.events.splice(splicedEventIndex, 0, event);
// Now adjust delta of all following events
for (var i = splicedEventIndex + 1; i < this.events.length; i++) {
// Since each existing event should have a tick value at this point we just need to
// adjust delta to that the event still falls on the correct tick.
this.events[i].delta = this.events[i].tick - this.events[i - 1].tick;
}
};
/**
* Removes all events matching specified type.
* @param {string} eventName - Event type
* @return {Track}
*/
Track.prototype.removeEventsByName = function (eventName) {
var _this = this;
this.events.forEach(function (event, index) {
if (event.name === eventName) {
_this.events.splice(index, 1);
}
});
return this;
};
/**
* Sets tempo of the MIDI file.
* @param {number} bpm - Tempo in beats per minute.
* @param {number} tick - Start tick.
* @return {Track}
*/
Track.prototype.setTempo = function (bpm, tick) {
if (tick === void 0) { tick = 0; }
return this.addEvent(new TempoEvent({ bpm: bpm, tick: tick }));
};
/**
* Sets time signature.
* @param {number} numerator - Top number of the time signature.
* @param {number} denominator - Bottom number of the time signature.
* @param {number} midiclockspertick - Defaults to 24.
* @param {number} notespermidiclock - Defaults to 8.
* @return {Track}
*/
Track.prototype.setTimeSignature = function (numerator, denominator, midiclockspertick, notespermidiclock) {
return this.addEvent(new TimeSignatureEvent(numerator, denominator, midiclockspertick, notespermidiclock));
};
/**
* Sets key signature.
* @param {*} sf -
* @param {*} mi -
* @return {Track}
*/
Track.prototype.setKeySignature = function (sf, mi) {
return this.addEvent(new KeySignatureEvent(sf, mi));
};
/**
* Adds text to MIDI file.
* @param {string} text - Text to add.
* @return {Track}
*/
Track.prototype.addText = function (text) {
return this.addEvent(new TextEvent({ text: text }));
};
/**
* Adds copyright to MIDI file.
* @param {string} text - Text of copyright line.
* @return {Track}
*/
Track.prototype.addCopyright = function (text) {
return this.addEvent(new CopyrightEvent({ text: text }));
};
/**
* Adds Sequence/Track Name.
* @param {string} text - Text of track name.
* @return {Track}
*/
Track.prototype.addTrackName = function (text) {
return this.addEvent(new TrackNameEvent({ text: text }));
};
/**
* Sets instrument name of track.
* @param {string} text - Name of instrument.
* @return {Track}
*/
Track.prototype.addInstrumentName = function (text) {
return this.addEvent(new InstrumentNameEvent({ text: text }));
};
/**
* Adds marker to MIDI file.
* @param {string} text - Marker text.
* @return {Track}
*/
Track.prototype.addMarker = function (text) {
return this.addEvent(new MarkerEvent({ text: text }));
};
/**
* Adds cue point to MIDI file.
* @param {string} text - Text of cue point.
* @return {Track}
*/
Track.prototype.addCuePoint = function (text) {
return this.addEvent(new CuePointEvent({ text: text }));
};
/**
* Adds lyric to MIDI file.
* @param {string} text - Lyric text to add.
* @return {Track}
*/
Track.prototype.addLyric = function (text) {
return this.addEvent(new LyricEvent({ text: text }));
};
/**
* Channel mode messages
* @return {Track}
*/
Track.prototype.polyModeOn = function () {
var event = new NoteOnEvent({ data: [0x00, 0xB0, 0x7E, 0x00] });
return this.addEvent(event);
};
/**
* Sets a pitch bend.
* @param {float} bend - Bend value ranging [-1,1], zero meaning no bend.
* @return {Track}
*/
Track.prototype.setPitchBend = function (bend) {
return this.addEvent(new PitchBendEvent({ bend: bend }));
};
/**
* Adds a controller change event
* @param {number} number - Control number.
* @param {number} value - Control value.
* @param {number} channel - Channel to send controller change event on (1-based).
* @param {number} delta - Track tick offset for cc event.
* @return {Track}
*/
Track.prototype.controllerChange = function (number, value, channel, delta) {
return this.addEvent(new ControllerChangeEvent({ controllerNumber: number, controllerValue: value, channel: channel, delta: delta }));
};
return Track;
}());
var VexFlow = /** @class */ (function () {
function VexFlow() {
}
/**
* Support for converting VexFlow voice into MidiWriterJS track
* @return MidiWriter.Track object
*/
VexFlow.prototype.trackFromVoice = function (voice, options) {
var _this = this;
if (options === void 0) { options = { addRenderedAccidentals: false }; }
var track = new Track;
var wait = [];
voice.tickables.forEach(function (tickable) {
if (tickable.noteType === 'n') {
track.addEvent(new NoteEvent({
pitch: tickable.keys.map(function (pitch, index) { return _this.convertPitch(pitch, index, tickable, options.addRenderedAccidentals); }),
duration: _this.convertDuration(tickable),
wait: wait
}));
// reset wait
wait = [];
}
else if (tickable.noteType === 'r') {
// move on to the next tickable and add this to the stack
// of the `wait` property for the next note event
wait.push(_this.convertDuration(tickable));
}
});
// There may be outstanding rests at the end of the track,
// pad with a ghost note (zero duration and velocity), just to capture the wait.
if (wait.length > 0) {
track.addEvent(new NoteEvent({ pitch: '[c4]', duration: '0', wait: wait, velocity: '0' }));
}
return track;
};
/**
* Converts VexFlow pitch syntax to MidiWriterJS syntax
* @param pitch string
* @param index pitch index
* @param note struct from Vexflow
* @param addRenderedAccidentals adds Vexflow rendered accidentals
*/
VexFlow.prototype.convertPitch = function (pitch, index, note, addRenderedAccidentals) {
var _a;
if (addRenderedAccidentals === void 0) { addRenderedAccidentals = false; }
// Splits note name from octave
var pitchParts = pitch.split('/');
// Retrieves accidentals from pitch
// Removes natural accidentals since they are not accepted in Tonal Midi
var accidentals = pitchParts[0].substring(1).replace('n', '');
if (addRenderedAccidentals) {
(_a = note.getAccidentals()) === null || _a === void 0 ? void 0 : _a.forEach(function (accidental) {
if (accidental.index === index) {
if (accidental.type === 'n') {
accidentals = '';
}
else {
accidentals += accidental.type;
}
}
});
}
return pitchParts[0][0] + accidentals + pitchParts[1];
};
/**
* Converts VexFlow duration syntax to MidiWriterJS syntax
* @param note struct from VexFlow
*/
VexFlow.prototype.convertDuration = function (note) {
return 'd'.repeat(note.dots) + this.convertBaseDuration(note.duration) + (note.tuplet ? 't' + note.tuplet.num_notes : '');
};
/**
* Converts VexFlow base duration syntax to MidiWriterJS syntax
* @param duration Vexflow duration
* @returns MidiWriterJS duration
*/
VexFlow.prototype.convertBaseDuration = function (duration) {
switch (duration) {
case 'w':
return '1';
case 'h':
return '2';
case 'q':
return '4';
default:
return duration;
}
};
return VexFlow;
}());
/**
* Object representation of a header chunk section of a MIDI file.
* @param {number} numberOfTracks - Number of tracks
* @return {Header}
*/
var Header = /** @class */ (function () {
function Header(numberOfTracks) {
this.type = Constants.HEADER_CHUNK_TYPE;
var trackType = numberOfTracks > 1 ? Constants.HEADER_CHUNK_FORMAT1 : Constants.HEADER_CHUNK_FORMAT0;
this.data = trackType.concat(Utils.numberToBytes(numberOfTracks, 2), // two bytes long,
Constants.HEADER_CHUNK_DIVISION);
this.size = [0, 0, 0, this.data.length];
}
return Header;
}());
/**
* Object that puts together tracks and provides methods for file output.
* @param {array|Track} tracks - A single {Track} object or an array of {Track} objects.
* @param {object} options - {middleC: 'C4'}
* @return {Writer}
*/
var Writer = /** @class */ (function () {
function Writer(tracks, options) {
if (options === void 0) { options = {}; }
// Ensure tracks is an array
this.tracks = Utils.toArray(tracks);
this.options = options;
}
/**
* Builds array of data from chunkschunks.
* @return {array}
*/
Writer.prototype.buildData = function () {
var _this = this;
var data = [];
data.push(new Header(this.tracks.length));
// For each track add final end of track event and build data
this.tracks.forEach(function (track) {
data.push(track.buildData(_this.options));
});
return data;
};
/**
* Builds the file into a Uint8Array
* @return {Uint8Array}
*/
Writer.prototype.buildFile = function () {
var build = [];
// Data consists of chunks which consists of data
this.buildData().forEach(function (d) { return build = build.concat(d.type, d.size, d.data); });
return new Uint8Array(build);
};
/**
* Convert file buffer to a base64 string. Different methods depending on if browser or node.
* @return {string}
*/
Writer.prototype.base64 = function () {
if (typeof btoa === 'function') {
var binary = '';
var bytes = this.buildFile();
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
return Buffer.from(this.buildFile()).toString('base64');
};
/**
* Get the data URI.
* @return {string}
*/
Writer.prototype.dataUri = function () {
return 'data:audio/midi;base64,' + this.base64();
};
/**
* Set option on instantiated Writer.
* @param {string} key
* @param {any} value
* @return {Writer}
*/
Writer.prototype.setOptio