smoosic
Version:
<sub>[Github site](https://github.com/Smoosic/smoosic) | [source documentation](https://smoosic.github.io/Smoosic/release/docs/modules.html) | [change notes](https://aarondavidnewman.github.io/Smoosic/changes.html) | [application](https://smoosic.github.i
1,632 lines (1,379 loc) • 51.7 kB
JavaScript
// Credit for Midi functionality goes to:
// https://github.com/grimmdude/MidiWriterJS
import { SmoMusic } from '../smo/data/music';
export var _MidiWriter = function() {
/**
* MIDI file format constants.
* @return {Constants}
*/
var Constants = {
VERSION: 1,
HEADER_CHUNK_TYPE: [0x4d, 0x54, 0x68, 0x64],
// Mthd
HEADER_CHUNK_LENGTH: [0x00, 0x00, 0x00, 0x06],
// Header size for SMF
HEADER_CHUNK_FORMAT0: [0x00, 0x00],
// Midi Type 0 id
HEADER_CHUNK_FORMAT1: [0x00, 0x01],
// Midi Type 1 id
HEADER_CHUNK_DIVISION: [0x00, 0x80],
// Defaults to 128 ticks per beat
TRACK_CHUNK_TYPE: [0x4d, 0x54, 0x72, 0x6b],
// MTrk,
META_EVENT_ID: 0xFF,
META_TEXT_ID: 0x01,
META_COPYRIGHT_ID: 0x02,
META_TRACK_NAME_ID: 0x03,
META_INSTRUMENT_NAME_ID: 0x04,
META_LYRIC_ID: 0x05,
META_MARKER_ID: 0x06,
META_CUE_POINT: 0x07,
META_TEMPO_ID: 0x51,
META_SMTPE_OFFSET: 0x54,
META_TIME_SIGNATURE_ID: 0x58,
META_KEY_SIGNATURE_ID: 0x59,
META_END_OF_TRACK_ID: [0x2F, 0x00],
CONTROLLER_CHANGE_STATUS: 0xB0,
// includes channel number (0)
PROGRAM_CHANGE_STATUS: 0xC0,
// includes channel number (0)
PITCH_BEND_STATUS: 0xE0 // includes channel number (0)
};
function _typeof(obj) {
"@babel/helpers - typeof";
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function (obj) {
return typeof obj;
};
} else {
_typeof = function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof(obj);
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
return true;
} catch (e) {
return false;
}
}
function _construct(Parent, args, Class) {
if (_isNativeReflectConstruct()) {
_construct = Reflect.construct;
} else {
_construct = function _construct(Parent, args, Class) {
var a = [null];
a.push.apply(a, args);
var Constructor = Function.bind.apply(Parent, a);
var instance = new Constructor();
if (Class) _setPrototypeOf(instance, Class.prototype);
return instance;
};
}
return _construct.apply(null, arguments);
}
function _isNativeFunction(fn) {
return Function.toString.call(fn).indexOf("[native code]") !== -1;
}
function _wrapNativeSuper(Class) {
var _cache = typeof Map === "function" ? new Map() : undefined;
_wrapNativeSuper = function _wrapNativeSuper(Class) {
if (Class === null || !_isNativeFunction(Class)) return Class;
if (typeof Class !== "function") {
throw new TypeError("Super expression must either be null or a function");
}
if (typeof _cache !== "undefined") {
if (_cache.has(Class)) return _cache.get(Class);
_cache.set(Class, Wrapper);
}
function Wrapper() {
return _construct(Class, arguments, _getPrototypeOf(this).constructor);
}
Wrapper.prototype = Object.create(Class.prototype, {
constructor: {
value: Wrapper,
enumerable: false,
writable: true,
configurable: true
}
});
return _setPrototypeOf(Wrapper, Class);
};
return _wrapNativeSuper(Class);
}
function _wrapRegExp(re, groups) {
_wrapRegExp = function (re, groups) {
return new BabelRegExp(re, undefined, groups);
};
var _RegExp = _wrapNativeSuper(RegExp);
var _super = RegExp.prototype;
var _groups = new WeakMap();
function BabelRegExp(re, flags, groups) {
var _this = _RegExp.call(this, re, flags);
_groups.set(_this, groups || _groups.get(re));
return _this;
}
_inherits(BabelRegExp, _RegExp);
BabelRegExp.prototype.exec = function (str) {
var result = _super.exec.call(this, str);
if (result) result.groups = buildGroups(result, this);
return result;
};
BabelRegExp.prototype[Symbol.replace] = function (str, substitution) {
if (typeof substitution === "string") {
var groups = _groups.get(this);
return _super[Symbol.replace].call(this, str, substitution.replace(/\$<([^>]+)>/g, function (_, name) {
return "$" + groups[name];
}));
} else if (typeof substitution === "function") {
var _this = this;
return _super[Symbol.replace].call(this, str, function () {
var args = [];
args.push.apply(args, arguments);
if (typeof args[args.length - 1] !== "object") {
args.push(buildGroups(args, _this));
}
return substitution.apply(this, args);
});
} else {
return _super[Symbol.replace].call(this, str, substitution);
}
};
function buildGroups(result, re) {
var g = _groups.get(re);
return Object.keys(g).reduce(function (groups, name) {
groups[name] = result[g[name]];
return groups;
}, Object.create(null));
}
return _wrapRegExp.apply(this, arguments);
}
/**
* Static utility functions used throughout the library.
*/
var Utils = /*#__PURE__*/function () {
function Utils() {
_classCallCheck(this, Utils);
}
_createClass(Utils, null, [{
key: "version",
value:
/**
* Gets MidiWriterJS version number.
* @return {string}
*/
function version() {
return Constants.VERSION;
}
/**
* Convert a string to an array of bytes
* @param {string} string
* @return {array}
*/
}, {
key: "stringToBytes",
value: function stringToBytes(string) {
return string.split('').map(function (_char) {
return _char.charCodeAt();
});
}
/**
* Checks if argument is a valid number.
* @param {*} n - Value to check
* @return {boolean}
*/
}, {
key: "isNumeric",
value: function isNumeric(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
* @return {number}
*/
}, {
key: "getPitch",
value: function getPitch(pitch) {
return SmoMusic.midiPitchToMidiNumber(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
*/
}, {
key: "numberToVariableLength",
value: function numberToVariableLength(ticks) {
ticks = Math.round(ticks);
var buffer = ticks & 0x7F;
while (ticks = ticks >> 7) {
buffer <<= 8;
buffer |= ticks & 0x7F | 0x80;
}
var bList = [];
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 {array}
*/
}, {
key: "stringByteCount",
value: function stringByteCount(s) {
return encodeURI(s).split(/%..|./).length - 1;
}
/**
* Get an int from an array of bytes.
* @param {array} bytes
* @return {number}
*/
}, {
key: "numberFromBytes",
value: function numberFromBytes(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
*/
}, {
key: "numberToBytes",
value: function numberToBytes(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
hexArray = hexArray.map(function (item) {
return parseInt(item, 16);
}); // Prepend empty bytes if we don't have enough
if (hexArray.length < bytesNeeded) {
while (bytesNeeded - hexArray.length > 0) {
hexArray.unshift(0);
}
}
return hexArray;
}
/**
* Converts value to array if needed.
* @param {string} value
* @return {array}
*/
}, {
key: "toArray",
value: function toArray(value) {
if (Array.isArray(value)) return value;
return [value];
}
/**
* Converts velocity to value 0-127
* @param {number} velocity - Velocity value 1-100
* @return {number}
*/
}, {
key: "convertVelocity",
value: function convertVelocity(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}
*/
}, {
key: "getTickDuration",
value: function getTickDuration(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
return parseInt(duration.substring(1));
} // 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}
*/
}, {
key: "getRoundedIfClose",
value: function getRoundedIfClose(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}
*/
}, {
key: "getPrecisionLoss",
value: function getPrecisionLoss(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}
*/
}, {
key: "getDurationMultiplier",
value: function getDurationMultiplier(duration) {
// Need to apply duration here.
// Quarter note == Constants.HEADER_CHUNK_DIVISION ticks.
if (duration === '0') return 0;
var match = duration.match( /*#__PURE__*/_wrapRegExp(/^(d+)?([0-9]+)(?:t([0-9]*))?/, {
dotted: 1,
base: 2,
tuplet: 3
}));
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 _match$groups = match.groups,
dotted = _match$groups.dotted,
tuplet = _match$groups.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 "note on" MIDI event
* @param {object} fields {data: []}
* @return {NoteOnEvent}
*/
var NoteOnEvent = /*#__PURE__*/function () {
function NoteOnEvent(fields) {
_classCallCheck(this, NoteOnEvent);
// Set default fields
fields = Object.assign({
channel: 1,
startTick: null,
velocity: 50,
wait: 0
}, fields);
this.type = 'note-on';
this.channel = fields.channel;
this.pitch = fields.pitch;
this.wait = fields.wait;
this.velocity = fields.velocity;
this.startTick = fields.startTick;
this.midiNumber = Utils.getPitch(this.pitch);
this.tick = null;
this.delta = null;
this.data = fields.data;
}
/**
* Builds int array for this event.
* @param {Track} track - parent track
* @return {NoteOnEvent}
*/
_createClass(NoteOnEvent, [{
key: "buildData",
value: function buildData(track, precisionDelta) {
this.data = []; // Explicitly defined startTick event
if (this.startTick) {
this.tick = Utils.getRoundedIfClose(this.startTick); // 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.getStatusByte(), this.midiNumber, Utils.convertVelocity(this.velocity));
return this;
}
/**
* Gets the note on status code based on the selected channel. 0x9{0-F}
* Note on at channel 0 is 0x90 (144)
* 0 = Ch 1
* @return {number}
*/
}, {
key: "getStatusByte",
value: function getStatusByte() {
return 144 + this.channel - 1;
}
}]);
return NoteOnEvent;
}();
/**
* Holds all data for a "note off" MIDI event
* @param {object} fields {data: []}
* @return {NoteOffEvent}
*/
var NoteOffEvent = /*#__PURE__*/function () {
function NoteOffEvent(fields) {
_classCallCheck(this, NoteOffEvent);
// Set default fields
fields = Object.assign({
channel: 1,
velocity: 50,
tick: null
}, fields);
this.type = 'note-off';
this.channel = fields.channel;
this.pitch = fields.pitch;
this.duration = fields.duration;
this.velocity = fields.velocity;
this.midiNumber = Utils.getPitch(this.pitch);
this.tick = fields.tick;
this.delta = Utils.getTickDuration(this.duration);
this.data = fields.data;
}
/**
* Builds int array for this event.
* @param {Track} track - parent track
* @return {NoteOffEvent}
*/
_createClass(NoteOffEvent, [{
key: "buildData",
value: function buildData(track, precisionDelta) {
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.getStatusByte(), this.midiNumber, Utils.convertVelocity(this.velocity));
return this;
}
/**
* Gets the note off status code based on the selected channel. 0x8{0-F}
* Note off at channel 0 is 0x80 (128)
* 0 = Ch 1
* @return {number}
*/
}, {
key: "getStatusByte",
value: function getStatusByte() {
return 128 + this.channel - 1;
}
}]);
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 = /*#__PURE__*/function () {
function NoteEvent(fields) {
_classCallCheck(this, NoteEvent);
// Set default fields
fields = Object.assign({
channel: 1,
repeat: 1,
sequential: false,
startTick: null,
velocity: 50,
wait: 0
}, fields);
this.data = [];
this.type = 'note';
this.pitch = Utils.toArray(fields.pitch);
this.channel = fields.channel;
this.duration = fields.duration;
this.grace = fields.grace;
this.repeat = fields.repeat;
this.sequential = fields.sequential;
this.startTick = fields.startTick;
this.velocity = fields.velocity;
this.wait = fields.wait;
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}
*/
_createClass(NoteEvent, [{
key: "buildData",
value: function buildData() {
var _this = this;
// Reset data array
this.data = [];
this.tickDuration;
this.restDuration; // 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;
this.grace = Utils.toArray(this.grace);
this.grace.forEach(function (pitch) {
var noteEvent = new NoteEvent({
pitch: _this.grace,
duration: 'T' + graceDuration
});
_this.data = _this.data.concat(noteEvent.data);
});
} // fields.pitch could be an array of pitches.
// 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) {
if (i == 0) {
var noteOnNew = new NoteOnEvent({
channel: _this.channel,
wait: _this.wait,
velocity: _this.velocity,
pitch: p,
startTick: _this.startTick
});
} else {
// Running status (can ommit the note on status)
//noteOn = new NoteOnEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]});
var noteOnNew = new NoteOnEvent({
channel: _this.channel,
wait: 0,
velocity: _this.velocity,
pitch: p,
startTick: _this.startTick
});
}
_this.events.push(noteOnNew);
}); // Note off
this.pitch.forEach(function (p, i) {
if (i == 0) {
//noteOff = new NoteOffEvent({data: Utils.numberToVariableLength(tickDuration).concat(this.getNoteOffStatus(), Utils.getPitch(p), Utils.convertVelocity(this.velocity))});
var noteOffNew = new NoteOffEvent({
channel: _this.channel,
duration: _this.duration,
velocity: _this.velocity,
pitch: p,
tick: _this.startTick !== null ? Utils.getTickDuration(_this.duration) - _this.startTick : null
});
} else {
// Running status (can ommit the note off status)
//noteOff = new NoteOffEvent({data: [0, Utils.getPitch(p), Utils.convertVelocity(this.velocity)]});
var noteOffNew = new NoteOffEvent({
channel: _this.channel,
duration: 0,
velocity: _this.velocity,
pitch: p,
tick: _this.startTick !== null ? Utils.getTickDuration(_this.duration) - _this.startTick : 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,
// wait only applies to first note in repetition
velocity: _this.velocity,
pitch: p,
startTick: _this.startTick
});
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 }
* @return {PitchBendEvent}
*/
var scale14bits = function scale14bits(zeroOne) {
if (zeroOne <= 0) {
return Math.floor(16384 * (zeroOne + 1) / 2);
}
return Math.floor(16383 * (zeroOne + 1) / 2);
};
var PitchBendEvent = function PitchBendEvent(fields) {
_classCallCheck(this, PitchBendEvent);
this.type = 'pitch-bend';
var bend14 = scale14bits(fields.bend);
var channel = fields.channel || 0;
var lsbValue = bend14 & 0x7f;
var msbValue = bend14 >> 7 & 0x7f;
this.data = Utils.numberToVariableLength(0x00).concat(Constants.PITCH_BEND_STATUS | channel, lsbValue, msbValue);
};
/**
* Holds all data for a "program change" MIDI event
* @param {object} fields {instrument: integer}
* @return {ProgramChangeEvent}
*/
var ProgramChangeEvent = function ProgramChangeEvent(fields) {
_classCallCheck(this, ProgramChangeEvent);
this.type = 'program'; // delta time defaults to 0.
this.data = Utils.numberToVariableLength(0x00).concat(Constants.PROGRAM_CHANGE_STATUS, fields.instrument);
};
/**
* Holds all data for a "controller change" MIDI event
* @param {object} fields {controllerNumber: integer, controllerValue: integer}
* @return {ControllerChangeEvent}
*/
var ControllerChangeEvent = function ControllerChangeEvent(fields) {
_classCallCheck(this, ControllerChangeEvent);
this.type = 'controller'; // delta time defaults to 0.
this.data = Utils.numberToVariableLength(0x00).concat(Constants.CONTROLLER_CHANGE_STATUS, fields.controllerNumber, fields.controllerValue);
};
/**
* Object representation of a tempo meta event.
* @param {string} text - Copyright text
* @return {CopyrightEvent}
*/
var CopyrightEvent = function CopyrightEvent(text) {
_classCallCheck(this, CopyrightEvent);
this.type = 'copyright';
var textBytes = Utils.stringToBytes(text); // Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, Constants.META_COPYRIGHT_ID, Utils.numberToVariableLength(textBytes.length), // Size
textBytes // Text
);
};
/**
* Object representation of a cue point meta event.
* @param {string} text - Cue point text
* @return {CuePointEvent}
*/
var CuePointEvent = function CuePointEvent(text) {
_classCallCheck(this, CuePointEvent);
this.type = 'marker';
var textBytes = Utils.stringToBytes(text); // Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, Constants.META_CUE_POINT, Utils.numberToVariableLength(textBytes.length), // Size
textBytes // Text
);
};
/**
* Object representation of a end track meta event.
* @return {EndTrackEvent}
*/
var EndTrackEvent = function EndTrackEvent() {
_classCallCheck(this, EndTrackEvent);
this.type = 'end-track'; // Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, Constants.META_END_OF_TRACK_ID);
};
/**
* Object representation of an instrument name meta event.
* @param {number} bpm - Beats per minute
* @return {InstrumentNameEvent}
*/
var InstrumentNameEvent = function InstrumentNameEvent(text) {
_classCallCheck(this, InstrumentNameEvent);
this.type = 'instrument-name';
var textBytes = Utils.stringToBytes(text); // Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, Constants.META_INSTRUMENT_NAME_ID, Utils.numberToVariableLength(textBytes.length), // Size
textBytes // Instrument name
);
};
/**
* Object representation of a key signature meta event.
* @return {KeySignatureEvent}
*/
var KeySignatureEvent = function KeySignatureEvent(sf, mi) {
_classCallCheck(this, KeySignatureEvent);
this.type = 'key-signature';
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, Constants.META_KEY_SIGNATURE_ID, [0x02], // Size
Utils.numberToBytes(sf, 1), // Number of sharp or flats ( < 0 flat; > 0 sharp)
Utils.numberToBytes(mode, 1) // Mode: 0 major, 1 minor
);
};
/**
* Object representation of a lyric meta event.
* @param {string} text - Lyric text
* @return {LyricEvent}
*/
var LyricEvent = function LyricEvent(text) {
_classCallCheck(this, LyricEvent);
this.type = 'marker';
var textBytes = Utils.stringToBytes(text); // Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, Constants.META_LYRIC_ID, Utils.numberToVariableLength(textBytes.length), // Size
textBytes // Text
);
};
/**
* Object representation of a marker meta event.
* @param {string} text - Marker text
* @return {MarkerEvent}
*/
var MarkerEvent = function MarkerEvent(text) {
_classCallCheck(this, MarkerEvent);
this.type = 'marker';
var textBytes = Utils.stringToBytes(text); // Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, Constants.META_MARKER_ID, Utils.numberToVariableLength(textBytes.length), // Size
textBytes // Text
);
};
/**
* Object representation of a tempo meta event.
* @param {number} bpm - Beats per minute
* @return {TempoEvent}
*/
var TempoEvent = function TempoEvent(bpm) {
_classCallCheck(this, TempoEvent);
this.type = 'tempo';
var tempo = Math.round(60000000 / bpm); // Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, Constants.META_TEMPO_ID, [0x03], // Size
Utils.numberToBytes(tempo, 3) // Tempo, 3 bytes
);
};
/**
* Object representation of a tempo meta event.
* @param {number} bpm - Beats per minute
* @return {TextEvent}
*/
var TextEvent = function TextEvent(text) {
_classCallCheck(this, TextEvent);
this.type = 'text';
var textBytes = Utils.stringToBytes(text); // Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, Constants.META_TEXT_ID, Utils.numberToVariableLength(textBytes.length), // Size
textBytes // Text
);
};
/**
* Object representation of a time signature meta event.
* @return {TimeSignatureEvent}
*/
var TimeSignatureEvent = function TimeSignatureEvent(numerator, denominator, midiclockspertick, notespermidiclock) {
_classCallCheck(this, TimeSignatureEvent);
this.type = 'time-signature'; // Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, Constants.META_TIME_SIGNATURE_ID, [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) // Number of 1/32 notes per MIDI clocks, 1 bytes
);
};
/**
* Object representation of a tempo meta event.
* @param {number} bpm - Beats per minute
* @return {TrackNameEvent}
*/
var TrackNameEvent = function TrackNameEvent(text) {
_classCallCheck(this, TrackNameEvent);
this.type = 'track-name';
var textBytes = Utils.stringToBytes(text); // Start with zero time delta
this.data = Utils.numberToVariableLength(0x00).concat(Constants.META_EVENT_ID, Constants.META_TRACK_NAME_ID, Utils.numberToVariableLength(textBytes.length), // Size
textBytes // Text
);
};
/**
* Holds all data for a track.
* @param {object} fields {type: number, data: array, size: array, events: array}
* @return {Track}
*/
var Track = /*#__PURE__*/function () {
function Track() {
_classCallCheck(this, 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.
* @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}
*/
_createClass(Track, [{
key: "addEvent",
value: function addEvent(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') {
for (var j in properties) {
switch (j) {
case 'channel':
event.channel = properties[j];
break;
case 'duration':
event.duration = properties[j];
break;
case 'sequential':
event.sequential = properties[j];
break;
case 'velocity':
event.velocity = Utils.convertVelocity(properties[j]);
break;
}
}
}
} // If this note event has an explicit startTick then we need to set aside for now
if (event.startTick !== 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.
* @return {Track}
*/
}, {
key: "buildData",
value: function buildData() {
var _this2 = this;
// Remove existing end track event and add one.
// This makes sure it's at the very end of the event list.
this.removeEventsByType('end-track').addEvent(new EndTrackEvent()); // Reset
this.data = [];
this.size = [];
this.tickPointer = 0;
var precisionLoss = 0;
this.events.forEach(function (event, eventIndex) {
// Build event & add to total tick duration
if (event instanceof NoteOnEvent || event instanceof NoteOffEvent) {
var built = event.buildData(_this2, precisionLoss);
precisionLoss = Utils.getPrecisionLoss(event.deltaWithPrecisionCorrection || 0);
_this2.data = _this2.data.concat(built.data);
_this2.tickPointer = Utils.getRoundedIfClose(event.tick);
} else {
_this2.data = _this2.data.concat(event.data);
}
});
this.mergeExplicitTickEvents();
this.size = Utils.numberToBytes(this.data.length, 4); // 4 bytes long
return this;
}
}, {
key: "mergeExplicitTickEvents",
value: function mergeExplicitTickEvents() {
var _this3 = this;
if (!this.explicitTickEvents.length) return; // First sort asc list of events by startTick
this.explicitTickEvents.sort(function (a, b) {
return a.startTick - b.startTick;
}); // 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(_this3);
}); // Merge each event indivually into this track's event list.
noteEvent.events.forEach(function (event) {
return _this3.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}
*/
}, {
key: "mergeTrack",
value: function mergeTrack(track) {
var _this4 = 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 _this4.mergeSingleEvent(event);
});
}
/**
* Merges a single event into this track's list of events based on event.tick property.
* @param {NoteOnEvent|NoteOffEvent} - event
* @return {Track}
*/
}, {
key: "mergeSingleEvent",
value: function mergeSingleEvent(event) {
// Find index of existing event we need to follow with
var lastEventIndex = 0;
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} eventType - Event type
* @return {Track}
*/
}, {
key: "removeEventsByType",
value: function removeEventsByType(eventType) {
var _this5 = this;
this.events.forEach(function (event, index) {
if (event.type === eventType) {
_this5.events.splice(index, 1);
}
});
return this;
}
/**
* Sets tempo of the MIDI file.
* @param {number} bpm - Tempo in beats per minute.
* @return {Track}
*/
}, {
key: "setTempo",
value: function setTempo(bpm) {
return this.addEvent(new TempoEvent(bpm));
}
/**
* 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}
*/
}, {
key: "setTimeSignature",
value: function setTimeSignature(numerator, denominator, midiclockspertick, notespermidiclock) {
return this.addEvent(new TimeSignatureEvent(numerator, denominator, midiclockspertick, notespermidiclock));
}
/**
* Sets key signature.
* @param {*} sf -
* @param {*} mi -
* @return {Track}
*/
}, {
key: "setKeySignature",
value: function setKeySignature(sf, mi) {
return this.addEvent(new KeySignatureEvent(sf, mi));
}
/**
* Adds text to MIDI file.
* @param {string} text - Text to add.
* @return {Track}
*/
}, {
key: "addText",
value: function addText(text) {
return this.addEvent(new TextEvent(text));
}
/**
* Adds copyright to MIDI file.
* @param {string} text - Text of copyright line.
* @return {Track}
*/
}, {
key: "addCopyright",
value: function addCopyright(text) {
return this.addEvent(new CopyrightEvent(text));
}
/**
* Adds Sequence/Track Name.
* @param {string} text - Text of track name.
* @return {Track}
*/
}, {
key: "addTrackName",
value: function addTrackName(text) {
return this.addEvent(new TrackNameEvent(text));
}
/**
* Sets instrument name of track.
* @param {string} text - Name of instrument.
* @return {Track}
*/
}, {
key: "addInstrumentName",
value: function addInstrumentName(text) {
return this.addEvent(new InstrumentNameEvent(text));
}
/**
* Adds marker to MIDI file.
* @param {string} text - Marker text.
* @return {Track}
*/
}, {
key: "addMarker",
value: function addMarker(text) {
return this.addEvent(new MarkerEvent(text));
}
/**
* Adds cue point to MIDI file.
* @param {string} text - Text of cue point.
* @return {Track}
*/
}, {
key: "addCuePoint",
value: function addCuePoint(text) {
return this.addEvent(new CuePointEvent(text));
}
/**
* Adds lyric to MIDI file.
* @param {string} text - Lyric text to add.
* @return {Track}
*/
}, {
key: "addLyric",
value: function addLyric(text) {
return this.addEvent(new LyricEvent(text));
}
/**
* Channel mode messages
* @return {Track}
*/
}, {
key: "polyModeOn",
value: function polyModeOn() {
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}
*/
}, {
key: "setPitchBend",
value: function setPitchBend(bend) {
return this.addEvent(new PitchBendEvent({
bend: bend
}));
}
/**
* Adds a controller change event
* @param {number} number - Control number.
* @param {number} value - Control value.
* @return {Track}
*/
}, {
key: "controllerChange",
value: function controllerChange(number, value) {
return this.addEvent(new ControllerChangeEvent({
controllerNumber: number,
controllerValue: value
}));
}
}]);
return Track;
}();
var VexFlow = /*#__PURE__*/function () {
function VexFlow() {
_classCallCheck(this, VexFlow);
}
_createClass(VexFlow, [{
key: "trackFromVoice",
value:
/**
* Support for converting VexFlow voice into MidiWriterJS track
* @return MidiWriter.Track object
*/
function trackFromVoice(voice) {
var _this = this;
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
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));
return;
}
}); // 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
*/
}, {
key: "convertPitch",
value: function convertPitch(pitch, index, note) {
var addRenderedAccidentals = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 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) {
var _note$getAccidentals;
(_note$getAccidentals = note.getAccidentals()) === null || _note$getAccidentals === void 0 ? void 0 : _note$getAccidentals.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
*/
}, {
key: "convertDuration",
value: function convertDuration(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
*/
}, {
key: "convertBaseDuration",
value: function convertBaseDuration(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 {HeaderChunk}
*/
var HeaderChunk = function HeaderChunk(numberOfTracks) {
_classCallCheck(this, HeaderChunk);
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];
};
/**
* 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.
* @return {Writer}
*/
var Writer = /*#__PURE__*/function () {
function Writer(tracks) {
var _this = this;
_classCallCheck(this, Writer);
// Ensure track is an array
tracks = Utils.toArray(tracks);
this.data = [];
this.data.push(new HeaderChunk(tracks.length)); // For each track add final end of track event and build data
tracks.forEach(function (track, i) {
_this.data.push(track.buildData());
});
}
/**
* Builds the file into a Uint8Array
* @return {Uint8Array}
*/
_createClass(Writer, [{
key: "buildFile",
value: function buildFile() {
var build = []; //