webmidi
Version:
WEBMIDI.js makes it easy to talk to MIDI instruments from a browser or from Node.js. It simplifies the control of external or virtual MIDI instruments with functions such as playNote(), sendPitchBend(), sendControlChange(), etc. It also allows reacting to
11,630 lines • 516 kB
JavaScript
/**
* WEBMIDI.js v3.1.12
* A JavaScript library to kickstart your MIDI projects
* https://webmidijs.org
* Build generated on November 22nd, 2024.
*
* © Copyright 2015-2024, Jean-Philippe Côté.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
/* Version: 3.1.12 - November 22, 2024 18:55:13 */
(function (exports) {
'use strict';
/**
* The `EventEmitter` class provides methods to implement the _observable_ design pattern. This
* pattern allows one to _register_ a function to execute when a specific event is _emitted_ by the
* emitter.
*
* It is intended to be an abstract class meant to be extended by (or mixed into) other objects.
*/
class EventEmitter {
/**
* Creates a new `EventEmitter`object.
*
* @param {boolean} [eventsSuspended=false] Whether the `EventEmitter` is initially in a suspended
* state (i.e. not executing callbacks).
*/
constructor(eventsSuspended = false) {
/**
* An object containing a property for each event with at least one registered listener. Each
* event property contains an array of all the [`Listener`]{@link Listener} objects registered
* for the event.
*
* @type {Object}
* @readonly
*/
this.eventMap = {};
/**
* Whether or not the execution of callbacks is currently suspended for this emitter.
*
* @type {boolean}
*/
this.eventsSuspended = eventsSuspended == true ? true : false;
}
/**
* The callback function is executed when the associated event is triggered via [`emit()`](#emit).
* The [`emit()`](#emit) method relays all additional arguments it received to the callback
* functions. Since [`emit()`](#emit) can be passed a variable number of arguments, it is up to
* the developer to make sure the arguments match those of the associated callback. In addition,
* the callback also separately receives all the arguments present in the listener's
* [`arguments`](Listener#arguments) property. This makes it easy to pass data from where the
* listener is added to where the listener is executed.
*
* @callback EventEmitter~callback
* @param {...*} [args] A variable number of arguments matching the ones (if any) that were passed
* to the [`emit()`](#emit) method (except, the first one) followed by the arguments found in the
* listener's [`arguments`](Listener#arguments) array.
*/
/**
* Adds a listener for the specified event. It returns the [`Listener`]{@link Listener} object
* that was created and attached to the event.
*
* To attach a global listener that will be triggered for any events, use
* [`EventEmitter.ANY_EVENT`]{@link #ANY_EVENT} as the first parameter. Note that a global
* listener will also be triggered by non-registered events.
*
* @param {string|Symbol} event The event to listen to.
* @param {EventEmitter~callback} callback The callback function to execute when the event occurs.
* @param {Object} [options={}]
* @param {Object} [options.context=this] The value of `this` in the callback function.
* @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning
* of the listeners array and thus executed first.
* @param {number} [options.duration=Infinity] The number of milliseconds before the listener
* automatically expires.
* @param {number} [options.remaining=Infinity] The number of times after which the callback
* should automatically be removed.
* @param {array} [options.arguments] An array of arguments which will be passed separately to the
* callback function. This array is stored in the [`arguments`]{@link Listener#arguments}
* property of the [`Listener`]{@link Listener} object and can be retrieved or modified as
* desired.
*
* @returns {Listener} The newly created [`Listener`]{@link Listener} object.
*
* @throws {TypeError} The `event` parameter must be a string or
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}.
* @throws {TypeError} The `callback` parameter must be a function.
*/
addListener(event, callback, options = {}) {
if (typeof event === "string" && event.length < 1 || event instanceof String && event.length < 1 || typeof event !== "string" && !(event instanceof String) && event !== EventEmitter.ANY_EVENT) {
throw new TypeError("The 'event' parameter must be a string or EventEmitter.ANY_EVENT.");
}
if (typeof callback !== "function") throw new TypeError("The callback must be a function.");
const listener = new Listener(event, this, callback, options);
if (!this.eventMap[event]) this.eventMap[event] = [];
if (options.prepend) {
this.eventMap[event].unshift(listener);
} else {
this.eventMap[event].push(listener);
}
return listener;
}
/**
* Adds a one-time listener for the specified event. The listener will be executed once and then
* destroyed. It returns the [`Listener`]{@link Listener} object that was created and attached
* to the event.
*
* To attach a global listener that will be triggered for any events, use
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} as the first parameter. Note that a
* global listener will also be triggered by non-registered events.
*
* @param {string|Symbol} event The event to listen to
* @param {EventEmitter~callback} callback The callback function to execute when the event occurs
* @param {Object} [options={}]
* @param {Object} [options.context=this] The context to invoke the callback function in.
* @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning
* of the listeners array and thus executed first.
* @param {number} [options.duration=Infinity] The number of milliseconds before the listener
* automatically expires.
* @param {array} [options.arguments] An array of arguments which will be passed separately to the
* callback function. This array is stored in the [`arguments`]{@link Listener#arguments}
* property of the [`Listener`]{@link Listener} object and can be retrieved or modified as
* desired.
*
* @returns {Listener} The newly created [`Listener`]{@link Listener} object.
*
* @throws {TypeError} The `event` parameter must be a string or
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}.
* @throws {TypeError} The `callback` parameter must be a function.
*/
addOneTimeListener(event, callback, options = {}) {
options.remaining = 1;
this.addListener(event, callback, options);
}
/**
* Identifier to use when adding or removing a listener that should be triggered when any events
* occur.
*
* @type {Symbol}
*/
static get ANY_EVENT() {
return Symbol.for("Any event");
}
/**
* Returns `true` if the specified event has at least one registered listener. If no event is
* specified, the method returns `true` if any event has at least one listener registered (this
* includes global listeners registered to
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}).
*
* Note: to specifically check for global listeners added with
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}, use
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} as the parameter.
*
* @param {string|Symbol} [event=(any event)] The event to check
* @param {function|Listener} [callback=(any callback)] The actual function that was added to the
* event or the {@link Listener} object returned by `addListener()`.
* @returns {boolean}
*/
hasListener(event, callback) {
if (event === undefined) {
// Check for ANY_EVENT
if (this.eventMap[EventEmitter.ANY_EVENT] && this.eventMap[EventEmitter.ANY_EVENT].length > 0) {
return true;
}
// Check for any regular events
return Object.entries(this.eventMap).some(([, value]) => {
return value.length > 0;
});
} else {
if (this.eventMap[event] && this.eventMap[event].length > 0) {
if (callback instanceof Listener) {
let result = this.eventMap[event].filter(listener => listener === callback);
return result.length > 0;
} else if (typeof callback === "function") {
let result = this.eventMap[event].filter(listener => listener.callback === callback);
return result.length > 0;
} else if (callback != undefined) {
return false;
}
return true;
} else {
return false;
}
}
}
/**
* An array of all the unique event names for which the emitter has at least one registered
* listener.
*
* Note: this excludes global events registered with
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} because they are not tied to a
* specific event.
*
* @type {string[]}
* @readonly
*/
get eventNames() {
return Object.keys(this.eventMap);
}
/**
* Returns an array of all the [`Listener`]{@link Listener} objects that have been registered for
* a specific event.
*
* Please note that global events (those added with
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}) are not returned for "regular"
* events. To get the list of global listeners, specifically use
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} as the parameter.
*
* @param {string|Symbol} event The event to get listeners for.
* @returns {Listener[]} An array of [`Listener`]{@link Listener} objects.
*/
getListeners(event) {
return this.eventMap[event] || [];
}
/**
* Suspends execution of all callbacks functions registered for the specified event type.
*
* You can suspend execution of callbacks registered with
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} by passing
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} to `suspendEvent()`. Beware that this
* will not suspend all callbacks but only those registered with
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}. While this may seem counter-intuitive
* at first glance, it allows the selective suspension of global listeners while leaving other
* listeners alone. If you truly want to suspends all callbacks for a specific
* [`EventEmitter`]{@link EventEmitter}, simply set its `eventsSuspended` property to `true`.
*
* @param {string|Symbol} event The event name (or `EventEmitter.ANY_EVENT`) for which to suspend
* execution of all callback functions.
*/
suspendEvent(event) {
this.getListeners(event).forEach(listener => {
listener.suspended = true;
});
}
/**
* Resumes execution of all suspended callback functions registered for the specified event type.
*
* You can resume execution of callbacks registered with
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} by passing
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} to `unsuspendEvent()`. Beware that
* this will not resume all callbacks but only those registered with
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}. While this may seem
* counter-intuitive, it allows the selective unsuspension of global listeners while leaving other
* callbacks alone.
*
* @param {string|Symbol} event The event name (or `EventEmitter.ANY_EVENT`) for which to resume
* execution of all callback functions.
*/
unsuspendEvent(event) {
this.getListeners(event).forEach(listener => {
listener.suspended = false;
});
}
/**
* Returns the number of listeners registered for a specific event.
*
* Please note that global events (those added with
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}) do not count towards the remaining
* number for a "regular" event. To get the number of global listeners, specifically use
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} as the parameter.
*
* @param {string|Symbol} event The event which is usually a string but can also be the special
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} symbol.
* @returns {number} An integer representing the number of listeners registered for the specified
* event.
*/
getListenerCount(event) {
return this.getListeners(event).length;
}
/**
* Executes the callback function of all the [`Listener`]{@link Listener} objects registered for
* a given event. The callback functions are passed the additional arguments passed to `emit()`
* (if any) followed by the arguments present in the [`arguments`](Listener#arguments) property of
* the [`Listener`](Listener) object (if any).
*
* If the [`eventsSuspended`]{@link #eventsSuspended} property is `true` or the
* [`Listener.suspended`]{@link Listener#suspended} property is `true`, the callback functions
* will not be executed.
*
* This function returns an array containing the return values of each of the callbacks.
*
* It should be noted that the regular listeners are triggered first followed by the global
* listeners (those added with [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}).
*
* @param {string} event The event
* @param {...*} args Arbitrary number of arguments to pass along to the callback functions
*
* @returns {Array} An array containing the return value of each of the executed listener
* functions.
*
* @throws {TypeError} The `event` parameter must be a string.
*/
emit(event, ...args) {
if (typeof event !== "string" && !(event instanceof String)) {
throw new TypeError("The 'event' parameter must be a string.");
}
if (this.eventsSuspended) return;
// We collect return values from all listeners here
let results = [];
// We must make sure that we do not have undefined otherwise concat() will add an undefined
// entry in the array.
let listeners = this.eventMap[EventEmitter.ANY_EVENT] || [];
if (this.eventMap[event]) listeners = listeners.concat(this.eventMap[event]);
listeners.forEach(listener => {
// This is the per-listener suspension check
if (listener.suspended) return;
let params = [...args];
if (Array.isArray(listener.arguments)) params = params.concat(listener.arguments);
if (listener.remaining > 0) {
results.push(listener.callback.apply(listener.context, params));
listener.count++;
}
if (--listener.remaining < 1) listener.remove();
});
return results;
}
/**
* Removes all the listeners that were added to the object upon which the method is called and
* that match the specified criterias. If no parameters are passed, all listeners added to this
* object will be removed. If only the `event` parameter is passed, all listeners for that event
* will be removed from that object. You can remove global listeners by using
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} as the first parameter.
*
* To use more granular options, you must at least define the `event`. Then, you can specify the
* callback to match or one or more of the additional options.
*
* @param {string} [event] The event name.
* @param {EventEmitter~callback} [callback] Only remove the listeners that match this exact
* callback function.
* @param {Object} [options]
* @param {*} [options.context] Only remove the listeners that have this exact context.
* @param {number} [options.remaining] Only remove the listener if it has exactly that many
* remaining times to be executed.
*/
removeListener(event, callback, options = {}) {
if (event === undefined) {
this.eventMap = {};
return;
} else if (!this.eventMap[event]) {
return;
}
// Find listeners that do not match the criterias (those are the ones we will keep)
let listeners = this.eventMap[event].filter(listener => {
return callback && listener.callback !== callback || options.remaining && options.remaining !== listener.remaining || options.context && options.context !== listener.context;
});
if (listeners.length) {
this.eventMap[event] = listeners;
} else {
delete this.eventMap[event];
}
}
/**
* The `waitFor()` method is an async function which returns a promise. The promise is fulfilled
* when the specified event occurs. The event can be a regular event or
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} (if you want to resolve as soon as any
* event is emitted).
*
* If the `duration` option is set, the promise will only be fulfilled if the event is emitted
* within the specified duration. If the event has not been fulfilled after the specified
* duration, the promise is rejected. This makes it super easy to wait for an event and timeout
* after a certain time if the event is not triggered.
*
* @param {string|Symbol} event The event to wait for
* @param {Object} [options={}]
* @param {number} [options.duration=Infinity] The number of milliseconds to wait before the
* promise is automatically rejected.
*/
async waitFor(event, options = {}) {
options.duration = parseInt(options.duration);
if (isNaN(options.duration) || options.duration <= 0) options.duration = Infinity;
return new Promise((resolve, reject) => {
let timeout;
let listener = this.addListener(event, () => {
clearTimeout(timeout);
resolve();
}, {
remaining: 1
});
if (options.duration !== Infinity) {
timeout = setTimeout(() => {
listener.remove();
reject("The duration expired before the event was emitted.");
}, options.duration);
}
});
}
/**
* The number of unique events that have registered listeners.
*
* Note: this excludes global events registered with
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT} because they are not tied to a
* specific event.
*
* @type {number}
* @readonly
*/
get eventCount() {
return Object.keys(this.eventMap).length;
}
}
/**
* The `Listener` class represents a single event listener object. Such objects keep all relevant
* contextual information such as the event being listened to, the object the listener was attached
* to, the callback function and so on.
*
*/
class Listener {
/**
* Creates a new `Listener` object
*
* @param {string|Symbol} event The event being listened to
* @param {EventEmitter} target The [`EventEmitter`]{@link EventEmitter} object that the listener
* is attached to.
* @param {EventEmitter~callback} callback The function to call when the listener is triggered
* @param {Object} [options={}]
* @param {Object} [options.context=target] The context to invoke the listener in (a.k.a. the
* value of `this` inside the callback function).
* @param {number} [options.remaining=Infinity] The remaining number of times after which the
* callback should automatically be removed.
* @param {array} [options.arguments] An array of arguments that will be passed separately to the
* callback function upon execution. The array is stored in the [`arguments`]{@link #arguments}
* property and can be retrieved or modified as desired.
*
* @throws {TypeError} The `event` parameter must be a string or
* [`EventEmitter.ANY_EVENT`]{@link EventEmitter#ANY_EVENT}.
* @throws {ReferenceError} The `target` parameter is mandatory.
* @throws {TypeError} The `callback` must be a function.
*/
constructor(event, target, callback, options = {}) {
if (typeof event !== "string" && !(event instanceof String) && event !== EventEmitter.ANY_EVENT) {
throw new TypeError("The 'event' parameter must be a string or EventEmitter.ANY_EVENT.");
}
if (!target) {
throw new ReferenceError("The 'target' parameter is mandatory.");
}
if (typeof callback !== "function") {
throw new TypeError("The 'callback' must be a function.");
}
// Convert single value argument to array
if (options.arguments !== undefined && !Array.isArray(options.arguments)) {
options.arguments = [options.arguments];
}
// Define default options and merge declared options into them,
options = Object.assign({
context: target,
remaining: Infinity,
arguments: undefined,
duration: Infinity
}, options);
// Make sure it is eventually deleted if a duration is supplied
if (options.duration !== Infinity) {
setTimeout(() => this.remove(), options.duration);
}
/**
* An array of arguments to pass to the callback function upon execution.
* @type {array}
*/
this.arguments = options.arguments;
/**
* The callback function to execute.
* @type {Function}
*/
this.callback = callback;
/**
* The context to execute the callback function in (a.k.a. the value of `this` inside the
* callback function)
* @type {Object}
*/
this.context = options.context;
/**
* The number of times the listener function was executed.
* @type {number}
*/
this.count = 0;
/**
* The event name.
* @type {string}
*/
this.event = event;
/**
* The remaining number of times after which the callback should automatically be removed.
* @type {number}
*/
this.remaining = parseInt(options.remaining) >= 1 ? parseInt(options.remaining) : Infinity;
/**
* Whether this listener is currently suspended or not.
* @type {boolean}
*/
this.suspended = false;
/**
* The object that the event is attached to (or that emitted the event).
* @type {EventEmitter}
*/
this.target = target;
}
/**
* Removes the listener from its target.
*/
remove() {
this.target.removeListener(this.event, this.callback, {
context: this.context,
remaining: this.remaining
});
}
}
/**
* The `Enumerations` class contains enumerations and arrays of elements used throughout the
* library. All its properties are static and should be referenced using the class name. For
* example: `Enumerations.CHANNEL_MESSAGES`.
*
* @license Apache-2.0
* @since 3.0.0
*/
class Enumerations {
/**
* @enum {Object.<string, number>}
* @readonly
* @deprecated since 3.1 (use Enumerations.CHANNEL_MESSAGES instead)
* @private
* @static
*/
static get MIDI_CHANNEL_MESSAGES() {
if (this.validation) {
console.warn("The MIDI_CHANNEL_MESSAGES enum has been deprecated. Use the " + "Enumerations.CHANNEL_MESSAGES enum instead.");
}
return Enumerations.CHANNEL_MESSAGES;
}
/**
* Enumeration of all MIDI channel message names and their associated 4-bit numerical value:
*
* | Message Name | Hexadecimal | Decimal |
* |---------------------|-------------|---------|
* | `noteoff` | 0x8 | 8 |
* | `noteon` | 0x9 | 9 |
* | `keyaftertouch` | 0xA | 10 |
* | `controlchange` | 0xB | 11 |
* | `programchange` | 0xC | 12 |
* | `channelaftertouch` | 0xD | 13 |
* | `pitchbend` | 0xE | 14 |
*
* @enum {Object.<string, number>}
* @readonly
* @since 3.1
* @static
*/
static get CHANNEL_MESSAGES() {
return {
noteoff: 0x8,
// 8
noteon: 0x9,
// 9
keyaftertouch: 0xA,
// 10
controlchange: 0xB,
// 11
programchange: 0xC,
// 12
channelaftertouch: 0xD,
// 13
pitchbend: 0xE // 14
};
}
/**
* A simple array of the 16 valid MIDI channel numbers (`1` to `16`):
*
* @type {number[]}
* @readonly
* @since 3.1
* @static
*/
static get CHANNEL_NUMBERS() {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
}
/**
* @type {number[]}
* @readonly
* @deprecated since 3.1 (use Enumerations.CHANNEL_NUMBERS instead)
* @private
* @static
*/
static get MIDI_CHANNEL_NUMBERS() {
if (this.validation) {
console.warn("The MIDI_CHANNEL_NUMBERS array has been deprecated. Use the " + "Enumerations.CHANNEL_NUMBERS array instead.");
}
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
}
/**
* Enumeration of all MIDI channel mode message names and their associated numerical value:
*
*
* | Message Name | Hexadecimal | Decimal |
* |-----------------------|-------------|---------|
* | `allsoundoff` | 0x78 | 120 |
* | `resetallcontrollers` | 0x79 | 121 |
* | `localcontrol` | 0x7A | 122 |
* | `allnotesoff` | 0x7B | 123 |
* | `omnimodeoff` | 0x7C | 124 |
* | `omnimodeon` | 0x7D | 125 |
* | `monomodeon` | 0x7E | 126 |
* | `polymodeon` | 0x7F | 127 |
*
* @enum {Object.<string, number>}
* @readonly
* @since 3.1
* @static
*/
static get CHANNEL_MODE_MESSAGES() {
return {
allsoundoff: 120,
resetallcontrollers: 121,
localcontrol: 122,
allnotesoff: 123,
omnimodeoff: 124,
omnimodeon: 125,
monomodeon: 126,
polymodeon: 127
};
}
/**
* @enum {Object.<string, number>}
* @deprecated since 3.1 (use Enumerations.CHANNEL_MODE_MESSAGES instead)
* @private
* @readonly
* @static
*/
static get MIDI_CHANNEL_MODE_MESSAGES() {
if (this.validation) {
console.warn("The MIDI_CHANNEL_MODE_MESSAGES enum has been deprecated. Use the " + "Enumerations.CHANNEL_MODE_MESSAGES enum instead.");
}
return Enumerations.CHANNEL_MODE_MESSAGES;
}
/**
* @enum {Object.<string, number>}
* @readonly
* @static
* @private
* @deprecated since version 3.0.26 (use `CONTROL_CHANGE_MESSAGES` instead)
*/
static get MIDI_CONTROL_CHANGE_MESSAGES() {
if (this.validation) {
console.warn("The MIDI_CONTROL_CHANGE_MESSAGES enum has been deprecated. Use the " + "Enumerations.CONTROL_CHANGE_MESSAGES array instead.");
}
return {
bankselectcoarse: 0,
modulationwheelcoarse: 1,
breathcontrollercoarse: 2,
controller3: 3,
footcontrollercoarse: 4,
portamentotimecoarse: 5,
dataentrycoarse: 6,
volumecoarse: 7,
balancecoarse: 8,
controller9: 9,
pancoarse: 10,
expressioncoarse: 11,
effectcontrol1coarse: 12,
effectcontrol2coarse: 13,
controller14: 14,
controller15: 15,
generalpurposeslider1: 16,
generalpurposeslider2: 17,
generalpurposeslider3: 18,
generalpurposeslider4: 19,
controller20: 20,
controller21: 21,
controller22: 22,
controller23: 23,
controller24: 24,
controller25: 25,
controller26: 26,
controller27: 27,
controller28: 28,
controller29: 29,
controller30: 30,
controller31: 31,
bankselectfine: 32,
modulationwheelfine: 33,
breathcontrollerfine: 34,
controller35: 35,
footcontrollerfine: 36,
portamentotimefine: 37,
dataentryfine: 38,
volumefine: 39,
balancefine: 40,
controller41: 41,
panfine: 42,
expressionfine: 43,
effectcontrol1fine: 44,
effectcontrol2fine: 45,
controller46: 46,
controller47: 47,
controller48: 48,
controller49: 49,
controller50: 50,
controller51: 51,
controller52: 52,
controller53: 53,
controller54: 54,
controller55: 55,
controller56: 56,
controller57: 57,
controller58: 58,
controller59: 59,
controller60: 60,
controller61: 61,
controller62: 62,
controller63: 63,
holdpedal: 64,
portamento: 65,
sustenutopedal: 66,
softpedal: 67,
legatopedal: 68,
hold2pedal: 69,
soundvariation: 70,
resonance: 71,
soundreleasetime: 72,
soundattacktime: 73,
brightness: 74,
soundcontrol6: 75,
soundcontrol7: 76,
soundcontrol8: 77,
soundcontrol9: 78,
soundcontrol10: 79,
generalpurposebutton1: 80,
generalpurposebutton2: 81,
generalpurposebutton3: 82,
generalpurposebutton4: 83,
controller84: 84,
controller85: 85,
controller86: 86,
controller87: 87,
controller88: 88,
controller89: 89,
controller90: 90,
reverblevel: 91,
tremololevel: 92,
choruslevel: 93,
celestelevel: 94,
phaserlevel: 95,
databuttonincrement: 96,
databuttondecrement: 97,
nonregisteredparametercoarse: 98,
nonregisteredparameterfine: 99,
registeredparametercoarse: 100,
registeredparameterfine: 101,
controller102: 102,
controller103: 103,
controller104: 104,
controller105: 105,
controller106: 106,
controller107: 107,
controller108: 108,
controller109: 109,
controller110: 110,
controller111: 111,
controller112: 112,
controller113: 113,
controller114: 114,
controller115: 115,
controller116: 116,
controller117: 117,
controller118: 118,
controller119: 119,
allsoundoff: 120,
resetallcontrollers: 121,
localcontrol: 122,
allnotesoff: 123,
omnimodeoff: 124,
omnimodeon: 125,
monomodeon: 126,
polymodeon: 127
};
}
/**
* An array of objects, ordered by control number, describing control change messages. Each object
* in the array has 3 properties with some objects having a fourth one (`position`) :
*
* * `number`: MIDI control number (0-127);
* * `name`: name of emitted event (eg: `bankselectcoarse`, `choruslevel`, etc) that can be
* listened to;
* * `description`: user-friendly description of the controller's purpose;
* * `position` (optional): whether this controller's value should be considered an `msb` or
* `lsb`
*
* Not all controllers have a predefined function. For those that don't, `name` is the word
* "controller" followed by the number (e.g. `controller112`).
*
* | Event name | Control Number |
* |--------------------------------|----------------|
* | `bankselectcoarse` | 0 |
* | `modulationwheelcoarse` | 1 |
* | `breathcontrollercoarse` | 2 |
* | `controller3` | 3 |
* | `footcontrollercoarse` | 4 |
* | `portamentotimecoarse` | 5 |
* | `dataentrycoarse` | 6 |
* | `volumecoarse` | 7 |
* | `balancecoarse` | 8 |
* | `controller9` | 9 |
* | `pancoarse` | 10 |
* | `expressioncoarse` | 11 |
* | `effectcontrol1coarse` | 12 |
* | `effectcontrol2coarse` | 13 |
* | `controller14` | 14 |
* | `controller15` | 15 |
* | `generalpurposecontroller1` | 16 |
* | `generalpurposecontroller2` | 17 |
* | `generalpurposecontroller3` | 18 |
* | `generalpurposecontroller4` | 19 |
* | `controller20` | 20 |
* | `controller21` | 21 |
* | `controller22` | 22 |
* | `controller23` | 23 |
* | `controller24` | 24 |
* | `controller25` | 25 |
* | `controller26` | 26 |
* | `controller27` | 27 |
* | `controller28` | 28 |
* | `controller29` | 29 |
* | `controller30` | 30 |
* | `controller31` | 31 |
* | `bankselectfine` | 32 |
* | `modulationwheelfine` | 33 |
* | `breathcontrollerfine` | 34 |
* | `controller35` | 35 |
* | `footcontrollerfine` | 36 |
* | `portamentotimefine` | 37 |
* | `dataentryfine` | 38 |
* | `channelvolumefine` | 39 |
* | `balancefine` | 40 |
* | `controller41` | 41 |
* | `panfine` | 42 |
* | `expressionfine` | 43 |
* | `effectcontrol1fine` | 44 |
* | `effectcontrol2fine` | 45 |
* | `controller46` | 46 |
* | `controller47` | 47 |
* | `controller48` | 48 |
* | `controller49` | 49 |
* | `controller50` | 50 |
* | `controller51` | 51 |
* | `controller52` | 52 |
* | `controller53` | 53 |
* | `controller54` | 54 |
* | `controller55` | 55 |
* | `controller56` | 56 |
* | `controller57` | 57 |
* | `controller58` | 58 |
* | `controller59` | 59 |
* | `controller60` | 60 |
* | `controller61` | 61 |
* | `controller62` | 62 |
* | `controller63` | 63 |
* | `damperpedal` | 64 |
* | `portamento` | 65 |
* | `sostenuto` | 66 |
* | `softpedal` | 67 |
* | `legatopedal` | 68 |
* | `hold2` | 69 |
* | `soundvariation` | 70 |
* | `resonance` | 71 |
* | `releasetime` | 72 |
* | `attacktime` | 73 |
* | `brightness` | 74 |
* | `decaytime` | 75 |
* | `vibratorate` | 76 |
* | `vibratodepth` | 77 |
* | `vibratodelay` | 78 |
* | `controller79` | 79 |
* | `generalpurposecontroller5` | 80 |
* | `generalpurposecontroller6` | 81 |
* | `generalpurposecontroller7` | 82 |
* | `generalpurposecontroller8` | 83 |
* | `portamentocontrol` | 84 |
* | `controller85` | 85 |
* | `controller86` | 86 |
* | `controller87` | 87 |
* | `highresolutionvelocityprefix` | 88 |
* | `controller89` | 89 |
* | `controller90` | 90 |
* | `effect1depth` | 91 |
* | `effect2depth` | 92 |
* | `effect3depth` | 93 |
* | `effect4depth` | 94 |
* | `effect5depth` | 95 |
* | `dataincrement` | 96 |
* | `datadecrement` | 97 |
* | `nonregisteredparameterfine` | 98 |
* | `nonregisteredparametercoarse` | 99 |
* | `nonregisteredparameterfine` | 100 |
* | `registeredparametercoarse` | 101 |
* | `controller102` | 102 |
* | `controller103` | 103 |
* | `controller104` | 104 |
* | `controller105` | 105 |
* | `controller106` | 106 |
* | `controller107` | 107 |
* | `controller108` | 108 |
* | `controller109` | 109 |
* | `controller110` | 110 |
* | `controller111` | 111 |
* | `controller112` | 112 |
* | `controller113` | 113 |
* | `controller114` | 114 |
* | `controller115` | 115 |
* | `controller116` | 116 |
* | `controller117` | 117 |
* | `controller118` | 118 |
* | `controller119` | 119 |
* | `allsoundoff` | 120 |
* | `resetallcontrollers` | 121 |
* | `localcontrol` | 122 |
* | `allnotesoff` | 123 |
* | `omnimodeoff` | 124 |
* | `omnimodeon` | 125 |
* | `monomodeon` | 126 |
* | `polymodeon` | 127 |
*
* @type {object[]}
* @readonly
* @static
* @since 3.1
*/
static get CONTROL_CHANGE_MESSAGES() {
return [{
number: 0,
name: "bankselectcoarse",
description: "Bank Select (Coarse)",
position: "msb"
}, {
number: 1,
name: "modulationwheelcoarse",
description: "Modulation Wheel (Coarse)",
position: "msb"
}, {
number: 2,
name: "breathcontrollercoarse",
description: "Breath Controller (Coarse)",
position: "msb"
}, {
number: 3,
name: "controller3",
description: "Undefined",
position: "msb"
}, {
number: 4,
name: "footcontrollercoarse",
description: "Foot Controller (Coarse)",
position: "msb"
}, {
number: 5,
name: "portamentotimecoarse",
description: "Portamento Time (Coarse)",
position: "msb"
}, {
number: 6,
name: "dataentrycoarse",
description: "Data Entry (Coarse)",
position: "msb"
}, {
number: 7,
name: "volumecoarse",
description: "Channel Volume (Coarse)",
position: "msb"
}, {
number: 8,
name: "balancecoarse",
description: "Balance (Coarse)",
position: "msb"
}, {
number: 9,
name: "controller9",
description: "Controller 9 (Coarse)",
position: "msb"
}, {
number: 10,
name: "pancoarse",
description: "Pan (Coarse)",
position: "msb"
}, {
number: 11,
name: "expressioncoarse",
description: "Expression Controller (Coarse)",
position: "msb"
}, {
number: 12,
name: "effectcontrol1coarse",
description: "Effect Control 1 (Coarse)",
position: "msb"
}, {
number: 13,
name: "effectcontrol2coarse",
description: "Effect Control 2 (Coarse)",
position: "msb"
}, {
number: 14,
name: "controller14",
description: "Undefined",
position: "msb"
}, {
number: 15,
name: "controller15",
description: "Undefined",
position: "msb"
}, {
number: 16,
name: "generalpurposecontroller1",
description: "General Purpose Controller 1 (Coarse)",
position: "msb"
}, {
number: 17,
name: "generalpurposecontroller2",
description: "General Purpose Controller 2 (Coarse)",
position: "msb"
}, {
number: 18,
name: "generalpurposecontroller3",
description: "General Purpose Controller 3 (Coarse)",
position: "msb"
}, {
number: 19,
name: "generalpurposecontroller4",
description: "General Purpose Controller 4 (Coarse)",
position: "msb"
}, {
number: 20,
name: "controller20",
description: "Undefined",
position: "msb"
}, {
number: 21,
name: "controller21",
description: "Undefined",
position: "msb"
}, {
number: 22,
name: "controller22",
description: "Undefined",
position: "msb"
}, {
number: 23,
name: "controller23",
description: "Undefined",
position: "msb"
}, {
number: 24,
name: "controller24",
description: "Undefined",
position: "msb"
}, {
number: 25,
name: "controller25",
description: "Undefined",
position: "msb"
}, {
number: 26,
name: "controller26",
description: "Undefined",
position: "msb"
}, {
number: 27,
name: "controller27",
description: "Undefined",
position: "msb"
}, {
number: 28,
name: "controller28",
description: "Undefined",
position: "msb"
}, {
number: 29,
name: "controller29",
description: "Undefined",
position: "msb"
}, {
number: 30,
name: "controller30",
description: "Undefined",
position: "msb"
}, {
number: 31,
name: "controller31",
description: "Undefined",
position: "msb"
}, {
number: 32,
name: "bankselectfine",
description: "Bank Select (Fine)",
position: "lsb"
}, {
number: 33,
name: "modulationwheelfine",
description: "Modulation Wheel (Fine)",
position: "lsb"
}, {
number: 34,
name: "breathcontrollerfine",
description: "Breath Controller (Fine)",
position: "lsb"
}, {
number: 35,
name: "controller35",
description: "Undefined",
position: "lsb"
}, {
number: 36,
name: "footcontrollerfine",
description: "Foot Controller (Fine)",
position: "lsb"
}, {
number: 37,
name: "portamentotimefine",
description: "Portamento Time (Fine)",
position: "lsb"
}, {
number: 38,
name: "dataentryfine",
description: "Data Entry (Fine)",
position: "lsb"
}, {
number: 39,
name: "channelvolumefine",
description: "Channel Volume (Fine)",
position: "lsb"
}, {
number: 40,
name: "balancefine",
description: "Balance (Fine)",
position: "lsb"
}, {
number: 41,
name: "controller41",
description: "Undefined",
position: "lsb"
}, {
number: 42,
name: "panfine",
description: "Pan (Fine)",
position: "lsb"
}, {
number: 43,
name: "expressionfine",
description: "Expression Controller (Fine)",
position: "lsb"
}, {
number: 44,
name: "effectcontrol1fine",
description: "Effect control 1 (Fine)",
position: "lsb"
}, {
number: 45,
name: "effectcontrol2fine",
description: "Effect control 2 (Fine)",
position: "lsb"
}, {
number: 46,
name: "controller46",
description: "Undefined",
position: "lsb"
}, {
number: 47,
name: "controller47",
description: "Undefined",
position: "lsb"
}, {
number: 48,
name: "controller48",
description: "General Purpose Controller 1 (Fine)",
position: "lsb"
}, {
number: 49,
name: "controller49",
description: "General Purpose Controller 2 (Fine)",
position: "lsb"
}, {
number: 50,
name: "controller50",
description: "General Purpose Controller 3 (Fine)",
position: "lsb"
}, {
number: 51,
name: "controller51",
description: "General Purpose Controller 4 (Fine)",
position: "lsb"
}, {
number: 52,
name: "controller52",
description: "Undefined",
position: "lsb"
}, {
number: 53,
name: "controller53",
description: "Undefined",
position: "lsb"
}, {
number: 54,
name: "controller54",
description: "Undefined",
position: "lsb"
}, {
number: 55,
name: "controller55",
description: "Undefined",
position: "lsb"
}, {
number: 56,
name: "controller56",
description: "Undefined",
position: "lsb"
}, {
number: 57,
name: "controller57",
description: "Undefined",
position: "lsb"
}, {
number: 58,
name: "controller58",
description: "Undefined",
position: "lsb"
}, {
number: 59,
name: "controller59",
description: "Undefined",
position: "lsb"
}, {
number: 60,
name: "controller60",
description: "Undefined",
position: "lsb"
}, {
number: 61,
name: "controller61",
description: "Undefined",
position: "lsb"
}, {
number: 62,
name: "controller62",
description: "Undefined",
position: "lsb"
}, {
number: 63,
name: "controller63",
description: "Undefined",
position: "lsb"
}, {
number: 64,
name: "damperpedal",
description: "Damper Pedal On/Off"
}, {
number: 65,
name: "portamento",
description: "Portamento On/Off"
}, {
number: 66,
name: "sostenuto",
description: "Sostenuto On/Off"
}, {
number: 67,
name: "softpedal",
description: "Soft Pedal On/Off"
}, {
number: 68,
name: "legatopedal",
description: "Legato Pedal On/Off"
}, {
number: 69,
name: "hold2",
description: "Hold 2 On/Off"
}, {
number: 70,
name: "soundvariation",
description: "Sound Variation",
position: "lsb"
}, {
number: 71,
name: "resonance",
description: "Resonance",
position: "lsb"
}, {
number: 72,
name: "releasetime",
description: "Release Time",
position: "lsb"
}, {
number: 73,
name: "attacktime",
description: "Attack Time",
position: "lsb"
}, {
number: 74,
name: "brightness",
description: "Brightness",
position: "lsb"
}, {
number: 75,
name: "decaytime",
description: "Decay Time",
position: "lsb"
}, {
number: 76,
name: "vibratorate",
description: "Vibrato Rate",
position: "lsb"
}, {
number: 77,
name: "vibratodepth",
description: "Vibrato Depth",
position: "lsb"
}, {
number: 78,
name: "vibratodelay",
description: "Vibrato Delay",
position: "lsb"
}, {
number: 79,
name: "controller79",
description: "Undefined",
position: "lsb"
}, {
number: 80,
name: "generalpurposecontroller5",
description: "General Purpose Controller 5",
position: "lsb"
}, {
number: 81,
name: "generalpurposecontroller6",
description: "General Purpose Controller 6",
position: "lsb"
}, {
number: 82,
name: "generalpurposecontroller7",
description: "General Purpose Controller 7",
position: "lsb"
}, {
number: 83,
name: "generalpurposecontroller8",
description: "General Purpose Controller 8",
position: "lsb"
}, {
number: 84,
name: "portamentocontrol",
description: "Portamento Control",
position: "lsb"
}, {
number: 85,
name: "controller85",
description: "Undefined"
}, {
number: 86,
name: "controller86",
description: "Undefined"
}, {
number: 87,
name: "controller87",
description: "Undefined"
}, {
number: 88,
name: "highresolutionvelocityprefix",
description: "High Resolution Velocity Prefix",
position: "lsb"
}, {
number: 89,
name: "controller89",
description: "Undefined"
}, {
number: 90,
name: "controller90",
description: "Undefined"
}, {
number: 91,
name: "effect1depth",
description: "Effects 1 Depth (Reverb Send Level)"
}, {
number: 92,
name: "effect2depth",
description: "Effects 2 Depth"
}, {
number: 93,
name: "effect3depth",
description: "Effects 3 Depth (Chorus Send Level)"
}, {
number: 94,
name: "effect4depth",
description: "Effects 4 Depth"
}, {
number: 95,
name: "effect5depth",
description: "Effects 5 Depth"
}, {
number: 96,
name: "dataincrement",
description: "Data Increment"
}, {
number: 97,
name: "datadecrement",
description: "Data Decrement"
}, {
number: 98,
name: "nonregisteredparameterfine",
description: "Non-Registered Parameter Number (Fine)",
position: "lsb"
}, {
number: 99,
name: "nonregisteredparametercoarse",
description: "Non-Registered Parameter Number (Coarse)",
position: "msb"
}, {
number: 100,
name: "registeredparameterfine",
description: "Registered Parameter Number (Fine)",
position: "lsb"
}, {
number: 101,
name: "registeredparametercoarse",
description: "Registered Parameter Number (Coarse)",
position: "msb"
}, {
number: 102,
name: "controller102",
description: "Undefined"
}, {
number: 103,
name: "controller103",
description: "Undefined"
}, {
number: 104,
name: "controller104",
description: "Undefined"
}, {
number: 105,
name: "controller105",
description: "Undefined"
}, {
number: 106,
name: "controller106",
description: "Undefined"
}, {
number: 107,
name: "controller107",
description: "Undefined"
}, {
number: 108,
name: "controller108",
description: "Undefined"
}, {
number: 109,
name: "controller109",
description: "Undefined"
}, {
number: 110,
name: "controller110",
description: "Undefined"
}, {
number: 111,
name: "controller111",
description: "Undefined"
}, {
number: 112,
name: "controller112",
description: "Undefined"
}, {
number: 113,
name: "controller113",
description: "Undefined"
}, {
number: 114,
name: "controller114",
description: "Undefined"
}, {
number: 115,
name: "controller115",
description: "Undefined"
}, {
number: 116,
name: "controller116",
description: "Undefined"
}, {
number: 117,
name: "controller117",
description: "Undefined"
}, {
number: 118,
name: "controller118",
description: "Undefined"
}, {
number: 119,
name: "controller119",
description: "Undefined"
}, {
number: 120,
name: "allsoundoff",
description: "All Sound Off"
}, {
number: 121,
name: "resetallcontrollers",
description: "Reset All Controllers"
}, {
number: 122,
name: "localcontrol",
description: "Local Control On/Off"
}, {
number: 123,
name: "allnotesoff",
description: "All Notes Off"
}, {
number: 124,
name: "omnimodeoff",
description: "Omni Mode Off"
}, {
number: 125,
name: "omnimodeon",
description: "Omni Mode On"
}, {
number: 126,
name: "monomodeon",
description: "Mono Mode On"
}, {
number: 127,
name: "polymodeon",
description: "Poly Mode On"
}];
}
/**
* Enumeration of all MIDI registered parameters and their associated pair of numerical values.
* MIDI registered parameters extend the original list of control change messages. Currently,
* there are only a limited number of them:
*
*
* | Control Function | [LSB, MSB] |
* |------------------------------|--------------|
* | `pitchbendrange` | [0x00, 0x00] |
* | `channelfinetuning` | [0x00, 0x01] |
* | `channelcoarsetuning` | [0x00, 0x02] |
* | `tuningprogram` | [0x00, 0x03] |
* | `tuningbank` | [0x00, 0x04] |
* | `modulationrange` | [0x00, 0x05] |
* | `azimuthangle` | [0x3D, 0x00] |
* | `elevationangle` | [0x3D, 0x01] |
* | `gain` | [0x3D, 0x02] |
* | `distanceratio` | [0x3D, 0x03] |
* | `maximumdistance` | [0x3D, 0x04] |
* | `maximumdistancegain` | [0x3D, 0x05] |
* | `referencedistanceratio` | [0x3D, 0x06] |
* | `panspreadangle` | [0x3D, 0x07] |
* | `rollangle` | [0x3D, 0x08] |
*
* @enum {Object.<string, number[]>}
* @readonly
* @since 3.1
* @static
*/
static get REGISTERED_PARAMETERS() {
return {
pitchbendrange: [0x00, 0x00],
channelfinetuning: [0x00, 0x01],
channelcoarsetuning: [0x00, 0x02],
tuningprogram: [0x00, 0x03],
tuningbank: [0x00, 0x04],
modulationrange: [0x00, 0x05],
azimuthangle: [0x3D, 0x00],
elevationangle: [0x3D, 0x01],
gain: [0x3D, 0x02],
distanceratio: [0x3D, 0x03],
maximumdistance: [0x3D, 0x04],
maximumdistancegain: [0x3D, 0x05],
referencedistanceratio: [0x3D, 0x06],
panspreadangle: [0x3D, 0x07],
rollangle: [0x3D, 0x08]
};
}
/**
* @enum {Object.<string, number[]>}
* @readonly
* @deprecated since 3.1 (use Enumerations.REGISTERED_PARAMETERS instead)
* @private
* @static
*/
static get MIDI_REGISTERED_PARAMETERS() {
if (this.validation) {
console.warn("The MIDI_REGISTERED_PARAMETERS enum has been deprecated. Use the " + "Enumerations.REGISTERED_PARAMETERS enum instead.");
}
return Enumerations.MIDI_REGISTERED_PARAMETERS;
}
/**
* Enumeration of all valid MIDI system messages and matching numerical values. This library also
* uses two additional custom messages.
*
* **System Common Messages**
*
* | Function | Hexadecimal | Decimal |
* |------------------------|-------------|---------|
* | `sysex` | 0xF0 | 240 |
* | `timecode` | 0xF1 | 241 |
* | `songposition` | 0xF2 | 242 |
* | `songselect` | 0xF3 | 243 |
* | `tunerequest` | 0xF6 | 246 |
* | `sysexend` | 0xF7 | 247 |
*
* The `sysexend` message is never actually received. It simply ends a sysex stream.
*
* **System Real-Time Messages**
*
* | Function | Hexadecimal | Decimal |
* |------------------------|-------------|---------|
* | `clock` | 0xF8 | 248 |
* | `start` | 0xFA | 250 |
* | `continue` | 0xFB | 251 |
* | `stop` | 0xFC | 252 |
* | `activesensing` | 0xFE | 254 |
* | `reset` | 0xFF | 255 |
*
* Values 249 and 253 are relayed by the
* [Web MIDI API](https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API) but they do not
* serve any specific purpose. The
* [MIDI 1.0 spec](https://www.midi.org/specifications/item/table-1-summary-of-midi-message)
* simply states that they are undefined/reserved.
*
* **Custom Messages**
*
* These two messages are mostly for internal use. They are not MIDI messages and cannot be sent
* or forwarded.
*
* | Function | Hexadecimal | Decimal |
* |------------------------|-------------|---------|
* | `midimessage` | | 0 |
* | `unknownsystemmessage` | | -1 |
*
* @enum {Object.<string, number>}
* @readonly
* @since 3.1
* @static
*/
static get SYSTEM_MESSAGES() {
return {
// System common messages
sysex: 0xF0,
// 240
timecode: 0xF1,
// 241
songposition: 0xF2,
// 242
songselect: 0xF3,
// 243
tunerequest: 0xF6,
// 246
tuningrequest: 0xF6,
// for backwards-compatibility (deprecated in version 3.0)
sysexend: 0xF7,
// 247 (never actually received - simply ends a sysex)
// System real-time messages
clock: 0xF8,
// 248
start: 0xFA,
// 250
continue: 0xFB,
// 251
stop: 0xFC,
// 252
activesensing: 0xFE,
// 254
reset: 0xFF,
// 255
// Custom WebMidi.js messages
midimessage: 0,
unknownsystemmessage: -1
};
}
/**
* @enum {Object.<string, number>}
* @readonly
* @deprecated since 3.1 (use Enumerations.SYSTEM_MESSAGES instead)
* @private
* @static
*/
static get MIDI_SYSTEM_MESSAGES() {
if (this.validation) {
console.warn("The MIDI_SYSTEM_MESSAGES enum has been deprecated. Use the " + "Enumerations.SYSTEM_MESSAGES enum instead.");
}
return Enumerations.SYSTEM_MESSAGES;
}
/**
* Array of channel-specific event names that can be listened for. This includes channel mode
* events and RPN/NRPN events.
*
* @type {string[]}
* @readonly
*/
static get CHANNEL_EVENTS() {
return [
// MIDI channel message events
"noteoff", "controlchange", "noteon", "keyaftertouch", "programchange", "channelaftertouch", "pitchbend",
// MIDI channel mode events
"allnotesoff", "allsoundoff", "localcontrol", "monomode", "omnimode", "resetallcontrollers",
// RPN/NRPN events
"nrpn", "nrpn-dataentrycoarse", "nrpn-dataentryfine", "nrpn-dataincrement", "nrpn-datadecrement", "rpn", "rpn-dataentrycoarse", "rpn-dataentryfine", "rpn-dataincrement", "rpn-datadecrement",
// Legacy (remove in v4)
"nrpn-databuttonincrement", "nrpn-databuttondecrement", "rpn-databuttonincrement", "rpn-databuttondecrement"];
}
}
/**
* The `Note` class represents a single musical note such as `"D3"`, `"G#4"`, `"F-1"`, `"Gb7"`, etc.
*
* `Note` objects can be played back on a single channel by calling
* [`OutputChannel.playNote()`]{@link OutputChannel#playNote} or, on multiple channels of the same
* output, by calling [`Output.playNote()`]{@link Output#playNote}.
*
* The note has [`attack`](#attack) and [`release`](#release) velocities set at `0.5` by default.
* These can be changed by passing in the appropriate option. It is also possible to set a
* system-wide default for attack and release velocities by using the
* [`WebMidi.defaults`](WebMidi#defaults) property.
*
* If you prefer to work with raw MIDI values (`0` to `127`), you can use [`rawAttack`](#rawAttack) and
* [`rawRelease`](#rawRelease) to both get and set the values.
*
* The note may have a [`duration`](#duration). If it does, playback will be automatically stopped
* when the duration has elapsed by sending a `"noteoff"` event. By default, the duration is set to
* `Infinity`. In this case, it will never stop playing unless explicitly stopped by calling a
* method such as [`OutputChannel.stopNote()`]{@link OutputChannel#stopNote},
* [`Output.stopNote()`]{@link Output#stopNote} or similar.
*
* @license Apache-2.0
* @since 3.0.0
*/
class Note {
/**
* Creates a `Note` object.
*
* @param value {string|number} The value used to create the note. If an identifier string is used,
* it must start with the note letter, optionally followed by an accidental and followed by the
* octave number (`"C3"`, `"G#4"`, `"F-1"`, `"Db7"`, etc.). If a number is used, it must be an
* integer between 0 and 127. In this case, middle C is considered to be C4 (note number 60).
*
* @param {object} [options={}]
*
* @param {number} [options.duration=Infinity] The number of milliseconds before the note should be
* explicitly stopped.
*
* @param {number} [options.attack=0.5] The note's attack velocity as a float between 0 and 1. If
* you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both
* `attack` and `rawAttack` are specified, the latter has precedence.
*
* @param {number} [options.release=0.5] The note's release velocity as a float between 0 and 1. If
* you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both
* `release` and `rawRelease` are specified, the latter has precedence.
*
* @param {number} [options.rawAttack=64] The note's attack velocity as an integer between 0 and
* 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both
* `attack` and `rawAttack` are specified, the latter has precedence.
*
* @param {number} [options.rawRelease=64] The note's release velocity as an integer between 0 and
* 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both
* `release` and `rawRelease` are specified, the latter has precedence.
*
* @throws {Error} Invalid note identifier
* @throws {RangeError} Invalid name value
* @throws {RangeError} Invalid accidental value
* @throws {RangeError} Invalid octave value
* @throws {RangeError} Invalid duration value
* @throws {RangeError} Invalid attack value
* @throws {RangeError} Invalid release value
*/
constructor(value, options = {}) {
// Assign property defaults
this.duration = wm.defaults.note.duration;
this.attack = wm.defaults.note.attack;
this.release = wm.defaults.note.release;
// Assign property values from options (validation occurs in setter)
if (options.duration != undefined) this.duration = options.duration;
if (options.attack != undefined) this.attack = options.attack;
if (options.rawAttack != undefined) this.attack = Utilities.from7bitToFloat(options.rawAttack);
if (options.release != undefined) this.release = options.release;
if (options.rawRelease != undefined) {
this.release = Utilities.from7bitToFloat(options.rawRelease);
}
// Assign note depending on the way it was specified (name or number)
if (Number.isInteger(value)) {
this.identifier = Utilities.toNoteIdentifier(value);
} else {
this.identifier = value;
}
}
/**
* The name, optional accidental and octave of the note, as a string.
* @type {string}
* @since 3.0.0
*/
get identifier() {
return this._name + (this._accidental || "") + this._octave;
}
set identifier(value) {
const fragments = Utilities.getNoteDetails(value);
if (wm.validation) {
if (!value) throw new Error("Invalid note identifier");
}
this._name = fragments.name;
this._accidental = fragments.accidental;
this._octave = fragments.octave;
}
/**
* The name (letter) of the note. If you need the full name with octave and accidental, you can
* use the [`identifier`]{@link Note#identifier} property instead.
* @type {string}
* @since 3.0.0
*/
get name() {
return this._name;
}
set name(value) {
if (wm.validation) {
value = value.toUpperCase();
if (!["C", "D", "E", "F", "G", "A", "B"].includes(value)) {
throw new Error("Invalid name value");
}
}
this._name = value;
}
/**
* The accidental (#, ##, b or bb) of the note.
* @type {string}
* @since 3.0.0
*/
get accidental() {
return this._accidental;
}
set accidental(value) {
if (wm.validation) {
value = value.toLowerCase();
if (!["#", "##", "b", "bb"].includes(value)) throw new Error("Invalid accidental value");
}
this._accidental = value;
}
/**
* The octave of the note.
* @type {number}
* @since 3.0.0
*/
get octave() {
return this._octave;
}
set octave(value) {
if (wm.validation) {
value = parseInt(value);
if (isNaN(value)) throw new Error("Invalid octave value");
}
this._octave = value;
}
/**
* The duration of the note as a positive decimal number representing the number of milliseconds
* that the note should play for.
*
* @type {number}
* @since 3.0.0
*/
get duration() {
return this._duration;
}
set duration(value) {
if (wm.validation) {
value = parseFloat(value);
if (isNaN(value) || value === null || value < 0) {
throw new RangeError("Invalid duration value.");
}
}
this._duration = value;
}
/**
* The attack velocity of the note as an integer between 0 and 1.
* @type {number}
* @since 3.0.0
*/
get attack() {
return this._attack;
}
set attack(value) {
if (wm.validation) {
value = parseFloat(value);
if (isNaN(value) || !(value >= 0 && value <= 1)) {
throw new RangeError("Invalid attack value.");
}
}
this._attack = value;
}
/**
* The release velocity of the note as an integer between 0 and 1.
* @type {number}
* @since 3.0.0
*/
get release() {
return this._release;
}
set release(value) {
if (wm.validation) {
value = parseFloat(value);
if (isNaN(value) || !(value >= 0 && value <= 1)) {
throw new RangeError("Invalid release value.");
}
}
this._release = value;
}
/**
* The attack velocity of the note as a positive integer between 0 and 127.
* @type {number}
* @since 3.0.0
*/
get rawAttack() {
return Utilities.fromFloatTo7Bit(this._attack);
}
set rawAttack(value) {
this._attack = Utilities.from7bitToFloat(value);
}
/**
* The release velocity of the note as a positive integer between 0 and 127.
* @type {number}
* @since 3.0.0
*/
get rawRelease() {
return Utilities.fromFloatTo7Bit(this._release);
}
set rawRelease(value) {
this._release = Utilities.from7bitToFloat(value);
}
/**
* The MIDI number of the note (`0` - `127`). This number is derived from the note identifier
* using C4 as a reference for middle C.
*
* @type {number}
* @readonly
* @since 3.0.0
*/
get number() {
return Utilities.toNoteNumber(this.identifier);
}
/**
* Returns a MIDI note number offset by octave and/or semitone. If the calculated value is less
* than 0, 0 will be returned. If the calculated value is more than 127, 127 will be returned. If
* an invalid value is supplied, 0 will be used.
*
* @param [octaveOffset] {number} An integer to offset the note number by octave.
* @param [semitoneOffset] {number} An integer to offset the note number by semitone.
* @returns {number} An integer between 0 and 127
*/
getOffsetNumber(octaveOffset = 0, semitoneOffset = 0) {
if (wm.validation) {
octaveOffset = parseInt(octaveOffset) || 0;
semitoneOffset = parseInt(semitoneOffset) || 0;
}
return Math.min(Math.max(this.number + octaveOffset * 12 + semitoneOffset, 0), 127);
}
}
/**
* The `Utilities` class contains general-purpose utility methods. All methods are static and
* should be called using the class name. For example: `Utilities.getNoteDetails("C4")`.
*
* @license Apache-2.0
* @since 3.0.0
*/
class Utilities {
/**
* Returns a MIDI note number matching the identifier passed in the form of a string. The
* identifier must include the octave number. The identifier also optionally include a sharp (#),
* a double sharp (##), a flat (b) or a double flat (bb) symbol. For example, these are all valid
* identifiers: C5, G4, D#-1, F0, Gb7, Eb-1, Abb4, B##6, etc.
*
* When converting note identifiers to numbers, C4 is considered to be middle C (MIDI note number
* 60) as per the scientific pitch notation standard.
*
* The resulting note number can be offset by using the `octaveOffset` parameter.
*
* @param identifier {string} The identifier in the form of a letter, followed by an optional "#",
* "##", "b" or "bb" followed by the octave number. For exemple: C5, G4, D#-1, F0, Gb7, Eb-1,
* Abb4, B##6, etc.
*
* @param {number} [octaveOffset=0] A integer to offset the octave by.
*
* @returns {number} The MIDI note number (an integer between 0 and 127).
*
* @throws RangeError Invalid 'octaveOffset' value
*
* @throws TypeError Invalid note identifier
*
* @license Apache-2.0
* @since 3.0.0
* @static
*/
static toNoteNumber(identifier, octaveOffset = 0) {
// Validation
octaveOffset = octaveOffset == undefined ? 0 : parseInt(octaveOffset);
if (isNaN(octaveOffset)) throw new RangeError("Invalid 'octaveOffset' value");
if (typeof identifier !== "string") identifier = "";
const fragments = this.getNoteDetails(identifier);
if (!fragments) throw new TypeError("Invalid note identifier");
const notes = {
C: 0,
D: 2,
E: 4,
F: 5,
G: 7,
A: 9,
B: 11
};
let result = (fragments.octave + 1 + octaveOffset) * 12;
result += notes[fragments.name];
if (fragments.accidental) {
if (fragments.accidental.startsWith("b")) {
result -= fragments.accidental.length;
} else {
result += fragments.accidental.length;
}
}
if (result < 0 || result > 127) throw new RangeError("Invalid octaveOffset value");
return result;
}
/**
* Given a proper note identifier (`C#4`, `Gb-1`, etc.) or a valid MIDI note number (0-127), this
* method returns an object containing broken down details about the specified note (uppercase
* letter, accidental and octave).
*
* When a number is specified, the translation to note is done using a value of 60 for middle C
* (C4 = middle C).
*
* @param value {string|number} A note identifier A atring ("C#4", "Gb-1", etc.) or a MIDI note
* number (0-127).
*
* @returns {{accidental: string, identifier: string, name: string, octave: number }}
*
* @throws TypeError Invalid note identifier
*
* @since 3.0.0
* @static
*/
static getNoteDetails(value) {
if (Number.isInteger(value)) value = this.toNoteIdentifier(value);
const matches = value.match(/^([CDEFGAB])(#{0,2}|b{0,2})(-?\d+)$/i);
if (!matches) throw new TypeError("Invalid note identifier");
const name = matches[1].toUpperCase();
const octave = parseInt(matches[3]);
let accidental = matches[2].toLowerCase();
accidental = accidental === "" ? undefined : accidental;
const fragments = {
accidental: accidental,
identifier: name + (accidental || "") + octave,
name: name,
octave: octave
};
return fragments;
}
/**
* Returns a sanitized array of valid MIDI channel numbers (1-16). The parameter should be a
* single integer or an array of integers.
*
* For backwards-compatibility, passing `undefined` as a parameter to this method results in all
* channels being returned (1-16). Otherwise, parameters that cannot successfully be parsed to
* integers between 1 and 16 are silently ignored.
*
* @param [channel] {number|number[]} An integer or an array of integers to parse as channel
* numbers.
*
* @returns {number[]} An array of 0 or more valid MIDI channel numbers.
*
* @since 3.0.0
* @static
*/
static sanitizeChannels(channel) {
let channels;
if (wm.validation) {
if (channel === "all") {
// backwards-compatibility
channels = ["all"];
} else if (channel === "none") {
// backwards-compatibility
return [];
}
}
if (!Array.isArray(channel)) {
channels = [channel];
} else {
channels = channel;
}
// In order to preserve backwards-compatibility, we let this assignment as it is.
if (channels.indexOf("all") > -1) {
channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
return channels.map(function (ch) {
return parseInt(ch);
}).filter(function (ch) {
return ch >= 1 && ch <= 16;
});
}
/**
* Returns a valid timestamp, relative to the navigation start of the document, derived from the
* `time` parameter. If the parameter is a string starting with the "+" sign and followed by a
* number, the resulting timestamp will be the sum of the current timestamp plus that number. If
* the parameter is a positive number, it will be returned as is. Otherwise, false will be
* returned.
*
* @param [time] {number|string} The time string (e.g. `"+2000"`) or number to parse
* @return {number|false} A positive number or `false` (if the time cannot be converted)
*
* @since 3.0.0
* @static
*/
static toTimestamp(time) {
let value = false;
const parsed = parseFloat(time);
if (isNaN(parsed)) return false;
if (typeof time === "string" && time.substring(0, 1) === "+") {
if (parsed >= 0) value = wm.time + parsed;
} else {
if (parsed >= 0) value = parsed;
}
return value;
}
/**
* Returns a valid MIDI note number (0-127) given the specified input. The input usually is a
* string containing a note identifier (`"C3"`, `"F#4"`, `"D-2"`, `"G8"`, etc.). If an integer
* between 0 and 127 is passed, it will simply be returned as is (for convenience). Other strings
* will be parsed for integer value, if possible.
*
* If the input is an identifier, the resulting note number is offset by the `octaveOffset`
* parameter. For example, if you pass in "C4" (note number 60) and the `octaveOffset` value is
* -2, the resulting MIDI note number will be 36.
*
* @param input {string|number} A string or number to extract the MIDI note number from.
* @param octaveOffset {number} An integer to offset the octave by
*
* @returns {number|false} A valid MIDI note number (0-127) or `false` if the input could not
* successfully be parsed to a note number.
*
* @since 3.0.0
* @static
*/
static guessNoteNumber(input, octaveOffset) {
// Validate and, if necessary, assign default
octaveOffset = parseInt(octaveOffset) || 0;
let output = false;
// Check input type
if (Number.isInteger(input) && input >= 0 && input <= 127) {
// uint
output = parseInt(input);
} else if (parseInt(input) >= 0 && parseInt(input) <= 127) {
// float or uint as string
output = parseInt(input);
} else if (typeof input === "string" || input instanceof String) {
// string
try {
output = this.toNoteNumber(input.trim(), octaveOffset);
} catch (e) {
return false;
}
}
return output;
}
/**
* Returns an identifier string representing a note name (with optional accidental) followed by an
* octave number. The octave can be offset by using the `octaveOffset` parameter.
*
* @param {number} number The MIDI note number to convert to a note identifier
* @param {number} octaveOffset An offset to apply to the resulting octave
*
* @returns {string}
*
* @throws RangeError Invalid note number
* @throws RangeError Invalid octaveOffset value
*
* @since 3.0.0
* @static
*/
static toNoteIdentifier(number, octaveOffset) {
number = parseInt(number);
if (isNaN(number) || number < 0 || number > 127) throw new RangeError("Invalid note number");
octaveOffset = octaveOffset == undefined ? 0 : parseInt(octaveOffset);
if (isNaN(octaveOffset)) throw new RangeError("Invalid octaveOffset value");
const notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
const octave = Math.floor(number / 12 - 1) + octaveOffset;
return notes[number % 12] + octave.toString();
}
/**
* Converts the `input` parameter to a valid [`Note`]{@link Note} object. The input usually is an
* unsigned integer (0-127) or a note identifier (`"C4"`, `"G#5"`, etc.). If the input is a
* [`Note`]{@link Note} object, it will be returned as is.
*
* If the input is a note number or identifier, it is possible to specify options by providing the
* `options` parameter.
*
* @param [input] {number|string|Note}
*
* @param {object} [options={}]
*
* @param {number} [options.duration=Infinity] The number of milliseconds before the note should
* be explicitly stopped.
*
* @param {number} [options.attack=0.5] The note's attack velocity as a float between 0 and 1. If
* you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both
* `attack` and `rawAttack` are specified, the latter has precedence.
*
* @param {number} [options.release=0.5] The note's release velocity as a float between 0 and 1. If
* you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both
* `release` and `rawRelease` are specified, the latter has precedence.
*
* @param {number} [options.rawAttack=64] The note's attack velocity as an integer between 0 and
* 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both
* `attack` and `rawAttack` are specified, the latter has precedence.
*
* @param {number} [options.rawRelease=64] The note's release velocity as an integer between 0 and
* 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both
* `release` and `rawRelease` are specified, the latter has precedence.
*
* @param {number} [options.octaveOffset=0] An integer to offset the octave by. **This is only
* used when the input value is a note identifier.**
*
* @returns {Note}
*
* @throws TypeError The input could not be parsed to a note
*
* @since version 3.0.0
* @static
*/
static buildNote(input, options = {}) {
options.octaveOffset = parseInt(options.octaveOffset) || 0;
// If it's already a Note, we're done
if (input instanceof Note) return input;
let number = this.guessNoteNumber(input, options.octaveOffset);
if (number === false) {
// We use a comparison b/c the note can be 0 (which equates to false)
throw new TypeError(`The input could not be parsed as a note (${input})`);
}
// If we got here, we have a proper note number. Before creating the new note, we strip out
// 'octaveOffset' because it has already been factored in when calling guessNoteNumber().
options.octaveOffset = undefined;
return new Note(number, options);
}
/**
* Converts an input value, which can be an unsigned integer (0-127), a note identifier, a
* [`Note`]{@link Note} object or an array of the previous types, to an array of
* [`Note`]{@link Note} objects.
*
* [`Note`]{@link Note} objects are returned as is. For note numbers and identifiers, a
* [`Note`]{@link Note} object is created with the options specified. An error will be thrown when
* encountering invalid input.
*
* Note: if both the `attack` and `rawAttack` options are specified, the later has priority. The
* same goes for `release` and `rawRelease`.
*
* @param [notes] {number|string|Note|number[]|string[]|Note[]}
*
* @param {object} [options={}]
*
* @param {number} [options.duration=Infinity] The number of milliseconds before the note should
* be explicitly stopped.
*
* @param {number} [options.attack=0.5] The note's attack velocity as a float between 0 and 1. If
* you wish to use an integer between 0 and 127, use the `rawAttack` option instead. If both
* `attack` and `rawAttack` are specified, the latter has precedence.
*
* @param {number} [options.release=0.5] The note's release velocity as a float between 0 and 1. If
* you wish to use an integer between 0 and 127, use the `rawRelease` option instead. If both
* `release` and `rawRelease` are specified, the latter has precedence.
*
* @param {number} [options.rawAttack=64] The note's attack velocity as an integer between 0 and
* 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both
* `attack` and `rawAttack` are specified, the latter has precedence.
*
* @param {number} [options.rawRelease=64] The note's release velocity as an integer between 0 and
* 127. If you wish to use a float between 0 and 1, use the `release` option instead. If both
* `release` and `rawRelease` are specified, the latter has precedence.
*
* @param {number} [options.octaveOffset=0] An integer to offset the octave by. **This is only
* used when the input value is a note identifier.**
*
* @returns {Note[]}
*
* @throws TypeError An element could not be parsed as a note.
*
* @since 3.0.0
* @static
*/
static buildNoteArray(notes, options = {}) {
let result = [];
if (!Array.isArray(notes)) notes = [notes];
notes.forEach(note => {
result.push(this.buildNote(note, options));
});
return result;
}
/**
* Returns a number between 0 and 1 representing the ratio of the input value divided by 127 (7
* bit). The returned value is restricted between 0 and 1 even if the input is greater than 127 or
* smaller than 0.
*
* Passing `Infinity` will return `1` and passing `-Infinity` will return `0`. Otherwise, when the
* input value cannot be converted to an integer, the method returns 0.
*
* @param value {number} A positive integer between 0 and 127 (inclusive)
* @returns {number} A number between 0 and 1 (inclusive)
* @static
*/
static from7bitToFloat(value) {
if (value === Infinity) value = 127;
value = parseInt(value) || 0;
return Math.min(Math.max(value / 127, 0), 1);
}
/**
* Returns an integer between 0 and 127 which is the result of multiplying the input value by
* 127. The input value should be a number between 0 and 1 (inclusively). The returned value is
* restricted between 0 and 127 even if the input is greater than 1 or smaller than 0.
*
* Passing `Infinity` will return `127` and passing `-Infinity` will return `0`. Otherwise, when
* the input value cannot be converted to a number, the method returns 0.
*
* @param value {number} A positive float between 0 and 1 (inclusive)
* @returns {number} A number between 0 and 127 (inclusive)
* @static
*/
static fromFloatTo7Bit(value) {
if (value === Infinity) value = 1;
value = parseFloat(value) || 0;
return Math.min(Math.max(Math.round(value * 127), 0), 127);
}
/**
* Combines and converts MSB and LSB values (0-127) to a float between 0 and 1. The returned value
* is within between 0 and 1 even if the result is greater than 1 or smaller than 0.
*
* @param msb {number} The most significant byte as a integer between 0 and 127.
* @param [lsb=0] {number} The least significant byte as a integer between 0 and 127.
* @returns {number} A float between 0 and 1.
*/
static fromMsbLsbToFloat(msb, lsb = 0) {
if (wm.validation) {
msb = Math.min(Math.max(parseInt(msb) || 0, 0), 127);
lsb = Math.min(Math.max(parseInt(lsb) || 0, 0), 127);
}
const value = ((msb << 7) + lsb) / 16383;
return Math.min(Math.max(value, 0), 1);
}
/**
* Extracts 7bit MSB and LSB values from the supplied float.
*
* @param value {number} A float between 0 and 1
* @returns {{lsb: number, msb: number}}
*/
static fromFloatToMsbLsb(value) {
if (wm.validation) {
value = Math.min(Math.max(parseFloat(value) || 0, 0), 1);
}
const multiplied = Math.round(value * 16383);
return {
msb: multiplied >> 7,
lsb: multiplied & 0x7F
};
}
/**
* Returns the supplied MIDI note number offset by the requested octave and semitone values. If
* the calculated value is less than 0, 0 will be returned. If the calculated value is more than
* 127, 127 will be returned. If an invalid offset value is supplied, 0 will be used.
*
* @param number {number} The MIDI note to offset as an integer between 0 and 127.
* @param octaveOffset {number} An integer to offset the note by (in octave)
* @param octaveOffset {number} An integer to offset the note by (in semitones)
* @returns {number} An integer between 0 and 127
*
* @throws {Error} Invalid note number
* @static
*/
static offsetNumber(number, octaveOffset = 0, semitoneOffset = 0) {
if (wm.validation) {
number = parseInt(number);
if (isNaN(number)) throw new Error("Invalid note number");
octaveOffset = parseInt(octaveOffset) || 0;
semitoneOffset = parseInt(semitoneOffset) || 0;
}
return Math.min(Math.max(number + octaveOffset * 12 + semitoneOffset, 0), 127);
}
/**
* Returns the name of the first property of the supplied object whose value is equal to the one
* supplied. If nothing is found, `undefined` is returned.
*
* @param object {object} The object to look for the property in.
* @param value {*} Any value that can be expected to be found in the object's properties.
* @returns {string|undefined} The name of the matching property or `undefined` if nothing is
* found.
* @static
*/
static getPropertyByValue(object, value) {
return Object.keys(object).find(key => object[key] === value);
}
/**
* Returns the name of a control change message matching the specified number (0-127). Some valid
* control change numbers do not have a specific name or purpose assigned in the MIDI
* [spec](https://midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2).
* In these cases, the method returns `controllerXXX` (where XXX is the number).
*
* @param {number} number An integer (0-127) representing the control change message
* @returns {string|undefined} The matching control change name or `undefined` if no match was
* found.
*
* @static
*/
static getCcNameByNumber(number) {
if (wm.validation) {
number = parseInt(number);
if (!(number >= 0 && number <= 127)) return undefined;
}
return Enumerations.CONTROL_CHANGE_MESSAGES[number].name;
}
/**
* Returns the number of a control change message matching the specified name.
*
* @param {string} name A string representing the control change message
* @returns {string|undefined} The matching control change number or `undefined` if no match was
* found.
*
* @since 3.1
* @static
*/
static getCcNumberByName(name) {
let message = Enumerations.CONTROL_CHANGE_MESSAGES.find(element => element.name === name);
if (message) {
return message.number;
} else {
// Legacy (remove in v4)
return Enumerations.MIDI_CONTROL_CHANGE_MESSAGES[name];
}
}
/**
* Returns the channel mode name matching the specified number. If no match is found, the function
* returns `false`.
*
* @param {number} number An integer representing the channel mode message (120-127)
* @returns {string|false} The name of the matching channel mode or `false` if no match could be
* found.
*
* @since 2.0.0
*/
static getChannelModeByNumber(number) {
if (!(number >= 120 && number <= 127)) return false;
for (let cm in Enumerations.CHANNEL_MODE_MESSAGES) {
if (Enumerations.CHANNEL_MODE_MESSAGES.hasOwnProperty(cm) && number === Enumerations.CHANNEL_MODE_MESSAGES[cm]) {
return cm;
}
}
return false;
}
/**
* Indicates whether the execution environment is Node.js (`true`) or not (`false`)
* @type {boolean}
*/
static get isNode() {
return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
}
/**
* Indicates whether the execution environment is a browser (`true`) or not (`false`)
* @type {boolean}
*/
static get isBrowser() {
return typeof window !== "undefined" && typeof window.document !== "undefined";
}
}
/**
* The `OutputChannel` class represents a single output MIDI channel. `OutputChannel` objects are
* provided by an [`Output`](Output) port which, itself, is made available by a device. The
* `OutputChannel` object is derived from the host's MIDI subsystem and should not be instantiated
* directly.
*
* All 16 `OutputChannel` objects can be found inside the parent output's
* [`channels`]{@link Output#channels} property.
*
* @param {Output} output The [`Output`](Output) this channel belongs to.
* @param {number} number The MIDI channel number (`1` - `16`).
*
* @extends EventEmitter
* @license Apache-2.0
* @since 3.0.0
*/
class OutputChannel extends EventEmitter {
/**
* Creates an `OutputChannel` object.
*
* @param {Output} output The [`Output`](Output) this channel belongs to.
* @param {number} number The MIDI channel number (`1` - `16`).
*/
constructor(output, number) {
super();
/**
* @type {Output}
* @private
*/
this._output = output;
/**
* @type {number}
* @private
*/
this._number = number;
/**
* @type {number}
* @private
*/
this._octaveOffset = 0;
}
/**
* Unlinks the MIDI subsystem, removes all listeners attached to the channel and nulls the channel
* number. This method is mostly for internal use. It has not been prefixed with an underscore
* since it is called by other objects such as the `Output` object.
*
* @private
*/
destroy() {
this._output = null;
this._number = null;
this._octaveOffset = 0;
this.removeListener();
}
/**
* Sends a MIDI message on the MIDI output port. If no time is specified, the message will be
* sent immediately. The message should be an array of 8-bit unsigned integers (`0` - `225`),
* a
* [`Uint8Array`]{@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array}
* object or a [`Message`](Message) object.
*
* It is usually not necessary to use this method directly as you can use one of the simpler
* helper methods such as [`playNote()`](#playNote), [`stopNote()`](#stopNote),
* [`sendControlChange()`](#sendControlChange), etc.
*
* Details on the format of MIDI messages are available in the summary of
* [MIDI messages]{@link https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message}
* from the MIDI Manufacturers Association.
*
* @param message {number[]|Uint8Array|Message} A `Message` object, an array of 8-bit unsigned
* integers or a `Uint8Array` object (not available in Node.js) containing the message bytes.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The first byte (status) must be an integer between 128 and 255.
*
* @throws {RangeError} Data bytes must be integers between 0 and 255.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
send(message, options = {
time: 0
}) {
this.output.send(message, options);
return this;
}
/**
* Sends a MIDI **key aftertouch** message at the scheduled time. This is a key-specific
* aftertouch. For a channel-wide aftertouch message, use
* [`sendChannelAftertouch()`]{@link #sendChannelAftertouch}.
*
* @param target {number|Note|string|number[]|Note[]|string[]} The note(s) for which you are sending
* an aftertouch value. The notes can be specified by using a MIDI note number (`0` - `127`), a
* [`Note`](Note) object, a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the
* previous types. When using a note identifier, octave range must be between `-1` and `9`. The
* lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number
* `127`).
*
* When using a note identifier, the octave value will be offset by the local
* [`octaveOffset`](#octaveOffset) and by
* [`Output.octaveOffset`](Output#octaveOffset) and [`WebMidi.octaveOffset`](WebMidi#octaveOffset)
* (if those values are not `0`). When using a key number, `octaveOffset` values are ignored.
*
* @param [pressure=0.5] {number} The pressure level (between `0` and `1`). An invalid pressure
* value will silently trigger the default behaviour. If the `rawValue` option is set to `true`,
* the pressure is defined by using an integer between `0` and `127`.
*
* @param {object} [options={}]
*
* @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be
* considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @return {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*
* @throws RangeError Invalid key aftertouch value.
*/
sendKeyAftertouch(target, pressure, options = {}) {
if (wm.validation) {
// Legacy support
if (options.useRawValue) options.rawValue = options.useRawValue;
if (isNaN(parseFloat(pressure))) {
throw new RangeError("Invalid key aftertouch value.");
}
if (options.rawValue) {
if (!(pressure >= 0 && pressure <= 127 && Number.isInteger(pressure))) {
throw new RangeError("Key aftertouch raw value must be an integer between 0 and 127.");
}
} else {
if (!(pressure >= 0 && pressure <= 1)) {
throw new RangeError("Key aftertouch value must be a float between 0 and 1.");
}
}
}
// Normalize pressure to integer
if (!options.rawValue) pressure = Utilities.fromFloatTo7Bit(pressure);
// Plot total offset
const offset = wm.octaveOffset + this.output.octaveOffset + this.octaveOffset;
// Make sure we are dealing with an array
if (!Array.isArray(target)) target = [target];
Utilities.buildNoteArray(target).forEach(n => {
this.send([(Enumerations.CHANNEL_MESSAGES.keyaftertouch << 4) + (this.number - 1), n.getOffsetNumber(offset), pressure], {
time: Utilities.toTimestamp(options.time)
});
});
return this;
}
/**
* Sends a MIDI **control change** message to the channel at the scheduled time. The control
* change message to send can be specified numerically (`0` to `127`) or by using one of the
* following common names:
*
* | Number | Name |
* |--------|-------------------------------|
* | 0 |`bankselectcoarse` |
* | 1 |`modulationwheelcoarse` |
* | 2 |`breathcontrollercoarse` |
* | 4 |`footcontrollercoarse` |
* | 5 |`portamentotimecoarse` |
* | 6 |`dataentrycoarse` |
* | 7 |`volumecoarse` |
* | 8 |`balancecoarse` |
* | 10 |`pancoarse` |
* | 11 |`expressioncoarse` |
* | 12 |`effectcontrol1coarse` |
* | 13 |`effectcontrol2coarse` |
* | 18 |`generalpurposeslider3` |
* | 19 |`generalpurposeslider4` |
* | 32 |`bankselectfine` |
* | 33 |`modulationwheelfine` |
* | 34 |`breathcontrollerfine` |
* | 36 |`footcontrollerfine` |
* | 37 |`portamentotimefine` |
* | 38 |`dataentryfine` |
* | 39 |`volumefine` |
* | 40 |`balancefine` |
* | 42 |`panfine` |
* | 43 |`expressionfine` |
* | 44 |`effectcontrol1fine` |
* | 45 |`effectcontrol2fine` |
* | 64 |`holdpedal` |
* | 65 |`portamento` |
* | 66 |`sustenutopedal` |
* | 67 |`softpedal` |
* | 68 |`legatopedal` |
* | 69 |`hold2pedal` |
* | 70 |`soundvariation` |
* | 71 |`resonance` |
* | 72 |`soundreleasetime` |
* | 73 |`soundattacktime` |
* | 74 |`brightness` |
* | 75 |`soundcontrol6` |
* | 76 |`soundcontrol7` |
* | 77 |`soundcontrol8` |
* | 78 |`soundcontrol9` |
* | 79 |`soundcontrol10` |
* | 80 |`generalpurposebutton1` |
* | 81 |`generalpurposebutton2` |
* | 82 |`generalpurposebutton3` |
* | 83 |`generalpurposebutton4` |
* | 91 |`reverblevel` |
* | 92 |`tremololevel` |
* | 93 |`choruslevel` |
* | 94 |`celestelevel` |
* | 95 |`phaserlevel` |
* | 96 |`dataincrement` |
* | 97 |`datadecrement` |
* | 98 |`nonregisteredparametercoarse` |
* | 99 |`nonregisteredparameterfine` |
* | 100 |`registeredparametercoarse` |
* | 101 |`registeredparameterfine` |
* | 120 |`allsoundoff` |
* | 121 |`resetallcontrollers` |
* | 122 |`localcontrol` |
* | 123 |`allnotesoff` |
* | 124 |`omnimodeoff` |
* | 125 |`omnimodeon` |
* | 126 |`monomodeon` |
* | 127 |`polymodeon` |
*
* As you can see above, not all control change message have a matching name. This does not mean
* you cannot use the others. It simply means you will need to use their number
* (`0` to `127`) instead of their name. While you can still use them, numbers `120` to `127` are
* usually reserved for *channel mode* messages. See
* [`sendChannelMode()`]{@link OutputChannel#sendChannelMode} method for more info.
*
* To view a detailed list of all available **control change** messages, please consult "Table 3 -
* Control Change Messages" from the [MIDI Messages](
* https://www.midi.org/specifications/item/table-3-control-change-messages-data-bytes-2)
* specification.
*
* **Note**: messages #0-31 (MSB) are paired with messages #32-63 (LSB). For example, message #1
* (`modulationwheelcoarse`) can be accompanied by a second control change message for
* `modulationwheelfine` to achieve a greater level of precision. if you want to specify both MSB
* and LSB for messages between `0` and `31`, you can do so by passing a 2-value array as the
* second parameter.
*
* @param {number|string} controller The MIDI controller name or number (`0` - `127`).
*
* @param {number|number[]} value The value to send (0-127). You can also use a two-position array
* for controllers 0 to 31. In this scenario, the first value will be sent as usual and the second
* value will be sent to the matching LSB controller (which is obtained by adding 32 to the first
* controller)
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} Controller numbers must be between 0 and 127.
* @throws {RangeError} Invalid controller name.
* @throws {TypeError} The value array must have a length of 2.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*
* @license Apache-2.0
* @since 3.0.0
*/
sendControlChange(controller, value, options = {}) {
if (typeof controller === "string") {
controller = Utilities.getCcNumberByName(controller);
}
if (!Array.isArray(value)) value = [value];
if (wm.validation) {
if (controller === undefined) {
throw new TypeError("Control change must be identified with a valid name or an integer between 0 and 127.");
}
if (!Number.isInteger(controller) || !(controller >= 0 && controller <= 127)) {
throw new TypeError("Control change number must be an integer between 0 and 127.");
}
value = value.map(item => {
const output = Math.min(Math.max(parseInt(item), 0), 127);
if (isNaN(output)) throw new TypeError("Values must be integers between 0 and 127");
return output;
});
if (value.length === 2 && controller >= 32) {
throw new TypeError("To use a value array, the controller must be between 0 and 31");
}
}
value.forEach((item, index) => {
this.send([(Enumerations.CHANNEL_MESSAGES.controlchange << 4) + (this.number - 1), controller + index * 32, value[index]], {
time: Utilities.toTimestamp(options.time)
});
});
return this;
}
/**
* Selects a MIDI non-registered parameter so it is affected by upcoming data entry, data
* increment and data decrement messages.
*
* @param parameter {number[]} A two-position array specifying the two control bytes that identify
* the registered parameter. The NRPN MSB (99 or 0x63) is a position 0. The NRPN LSB (98 or 0x62)
* is at position 1.
*
* @private
*
* @param {object} [options={}]
*
* @param {number|string} [options.time] If `time` is a string prefixed with `"+"` and followed by
* a number, the message will be delayed by that many milliseconds. If the value is a number, the
* operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
_selectNonRegisteredParameter(parameter, options = {}) {
// parameter[0] = Math.floor(parameter[0]);
// if (!(parameter[0] >= 0 && parameter[0] <= 127)) {
// throw new RangeError("The control63 value must be between 0 and 127.");
// }
//
// parameter[1] = Math.floor(parameter[1]);
// if (!(parameter[1] >= 0 && parameter[1] <= 127)) {
// throw new RangeError("The control62 value must be between 0 and 127.");
// }
this.sendControlChange(0x63, parameter[0], options);
this.sendControlChange(0x62, parameter[1], options);
return this;
}
/**
* Deselects the currently active MIDI registered parameter so it is no longer affected by data
* entry, data increment and data decrement messages.
*
* Current best practice recommends doing that after each call to
* [_setCurrentParameter()]{@link #_setCurrentParameter}.
*
* @private
*
* @param {object} [options={}]
*
* @param {number|string} [options.time] If `time` is a string prefixed with `"+"` and followed by
* a number, the message will be delayed by that many milliseconds. If the value is a number, the
* operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
_deselectRegisteredParameter(options = {}) {
this.sendControlChange(0x65, 0x7F, options);
this.sendControlChange(0x64, 0x7F, options);
return this;
}
/**
* Deselects the currently active MIDI non-registered parameter so it is no longer affected by
* data entry, data increment and data decrement messages.
*
* @private
*
* @param {object} [options={}]
*
* @param {number|string} [options.time] If `time` is a string prefixed with `"+"` and followed by
* a number, the message will be delayed by that many milliseconds. If the value is a number, the
* operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
_deselectNonRegisteredParameter(options = {}) {
this.sendControlChange(0x65, 0x7F, options);
this.sendControlChange(0x64, 0x7F, options);
return this;
}
/**
* Selects a MIDI registered parameter so it is affected by upcoming data entry, data increment
* and data decrement messages.
*
* @private
*
* @param parameter {number[]} A two-position array of integers specifying the two control bytes
* (0x65, 0x64) that identify the registered parameter. The integers must be between 0 and 127.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time] If `time` is a string prefixed with `"+"` and followed by
* a number, the message will be delayed by that many milliseconds. If the value is a number, the
* operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
_selectRegisteredParameter(parameter, options = {}) {
this.sendControlChange(0x65, parameter[0], options);
this.sendControlChange(0x64, parameter[1], options);
return this;
}
/**
* Sets the value of the currently selected MIDI registered parameter.
*
* @private
*
* @param data {number|number[]}
*
* @param {object} [options={}]
*
* @param {number|string} [options.time] If `time` is a string prefixed with `"+"` and followed by
* a number, the message will be delayed by that many milliseconds. If the value is a number, the
* operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
_setCurrentParameter(data, options = {}) {
data = [].concat(data);
// MSB
// data[0] = parseInt(data[0]);
// if (!isNaN(data[0]) && data[0] >= 0 && data[0] <= 127) {
this.sendControlChange(0x06, data[0], options);
// } else {
// throw new RangeError("The msb value must be between 0 and 127.");
// }
if (data.length < 2) return this;
// LSB
// data[1] = parseInt(data[1]);
// if (!isNaN(data[1]) && data[1] >= 0 && data[1] <= 127) {
this.sendControlChange(0x26, data[1], options);
// } else {
// throw new RangeError("The lsb value must be between 0 and 127.");
// }
return this;
}
/**
* Decrements the specified MIDI registered parameter by 1. Here is the full list of parameter
* names that can be used with this function:
*
* * Pitchbend Range (0x00, 0x00): `"pitchbendrange"`
* * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"`
* * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"`
* * Tuning Program (0x00, 0x03): `"tuningprogram"`
* * Tuning Bank (0x00, 0x04): `"tuningbank"`
* * Modulation Range (0x00, 0x05): `"modulationrange"`
* * Azimuth Angle (0x3D, 0x00): `"azimuthangle"`
* * Elevation Angle (0x3D, 0x01): `"elevationangle"`
* * Gain (0x3D, 0x02): `"gain"`
* * Distance Ratio (0x3D, 0x03): `"distanceratio"`
* * Maximum Distance (0x3D, 0x04): `"maximumdistance"`
* * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"`
* * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"`
* * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"`
* * Roll Angle (0x3D, 0x08): `"rollangle"`
*
* @param parameter {String|number[]} A string identifying the parameter's name (see above) or a
* two-position array specifying the two control bytes (0x65, 0x64) that identify the registered
* parameter.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws TypeError The specified registered parameter is invalid.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendRpnDecrement(parameter, options = {}) {
if (!Array.isArray(parameter)) parameter = Enumerations.REGISTERED_PARAMETERS[parameter];
if (wm.validation) {
if (parameter === undefined) {
throw new TypeError("The specified registered parameter is invalid.");
}
let valid = false;
Object.getOwnPropertyNames(Enumerations.REGISTERED_PARAMETERS).forEach(p => {
if (Enumerations.REGISTERED_PARAMETERS[p][0] === parameter[0] && Enumerations.REGISTERED_PARAMETERS[p][1] === parameter[1]) {
valid = true;
}
});
if (!valid) throw new TypeError("The specified registered parameter is invalid.");
}
this._selectRegisteredParameter(parameter, options);
this.sendControlChange(0x61, 0, options);
this._deselectRegisteredParameter(options);
return this;
}
/**
* Increments the specified MIDI registered parameter by 1. Here is the full list of parameter
* names that can be used with this function:
*
* * Pitchbend Range (0x00, 0x00): `"pitchbendrange"`
* * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"`
* * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"`
* * Tuning Program (0x00, 0x03): `"tuningprogram"`
* * Tuning Bank (0x00, 0x04): `"tuningbank"`
* * Modulation Range (0x00, 0x05): `"modulationrange"`
* * Azimuth Angle (0x3D, 0x00): `"azimuthangle"`
* * Elevation Angle (0x3D, 0x01): `"elevationangle"`
* * Gain (0x3D, 0x02): `"gain"`
* * Distance Ratio (0x3D, 0x03): `"distanceratio"`
* * Maximum Distance (0x3D, 0x04): `"maximumdistance"`
* * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"`
* * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"`
* * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"`
* * Roll Angle (0x3D, 0x08): `"rollangle"`
*
* @param parameter {String|number[]} A string identifying the parameter's name (see above) or a
* two-position array specifying the two control bytes (0x65, 0x64) that identify the registered
* parameter.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws TypeError The specified registered parameter is invalid.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendRpnIncrement(parameter, options = {}) {
if (!Array.isArray(parameter)) parameter = Enumerations.REGISTERED_PARAMETERS[parameter];
if (wm.validation) {
if (parameter === undefined) {
throw new TypeError("The specified registered parameter is invalid.");
}
let valid = false;
Object.getOwnPropertyNames(Enumerations.REGISTERED_PARAMETERS).forEach(p => {
if (Enumerations.REGISTERED_PARAMETERS[p][0] === parameter[0] && Enumerations.REGISTERED_PARAMETERS[p][1] === parameter[1]) {
valid = true;
}
});
if (!valid) throw new TypeError("The specified registered parameter is invalid.");
}
this._selectRegisteredParameter(parameter, options);
this.sendControlChange(0x60, 0, options);
this._deselectRegisteredParameter(options);
return this;
}
/**
* Plays a note or an array of notes on the channel. The first parameter is the note to play. It
* can be a single value or an array of the following valid values:
*
* - A [`Note`]{@link Note} object
* - A MIDI note number (integer between `0` and `127`)
* - A note name, followed by the octave (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`)
*
* The `playNote()` method sends a **note on** MIDI message for all specified notes. If a
* `duration` is set in the `options` parameter or in the [`Note`]{@link Note} object's
* [`duration`]{@link Note#duration} property, it will also schedule a **note off** message
* to end the note after said duration. If no `duration` is set, the note will simply play until
* a matching **note off** message is sent with [`stopNote()`]{@link OutputChannel#stopNote} or
* [`sendNoteOff()`]{@link OutputChannel#sendNoteOff}.
*
* The execution of the **note on** command can be delayed by using the `time` property of the
* `options` parameter.
*
* When using [`Note`]{@link Note} objects, the durations and velocities defined in the
* [`Note`]{@link Note} objects have precedence over the ones specified via the method's `options`
* parameter.
*
* **Note**: per the MIDI standard, a **note on** message with an attack velocity of `0` is
* functionally equivalent to a **note off** message.
*
* @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to play. The notes can be
* specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`,
* `F-1`, `Db7`), a [`Note`]{@link Note} object or an array of the previous types. When using a
* note identifier, the octave range must be between `-1` and `9`. The lowest note is `C-1` (MIDI
* note number `0`) and the highest note is `G9` (MIDI note number `127`).
*
* @param {object} [options={}]
*
* @param {number} [options.duration] A positive decimal number larger than `0` representing the
* number of milliseconds to wait before sending a **note off** message. If invalid or left
* undefined, only a **note on** message will be sent.
*
* @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and
* `1`). If the `rawAttack` option is also defined, it will have priority. An invalid velocity
* value will silently trigger the default of `0.5`.
*
* @param {number} [options.rawAttack=64] The attack velocity at which to play the note (between
* `0` and `127`). This has priority over the `attack` property. An invalid velocity value will
* silently trigger the default of 64.
*
* @param {number} [options.release=0.5] The velocity at which to release the note (between `0`
* and `1`). If the `rawRelease` option is also defined, it will have priority. An invalid
* velocity value will silently trigger the default of `0.5`. This is only used with the
* **note off** event triggered when `options.duration` is set.
*
* @param {number} [options.rawRelease=64] The velocity at which to release the note (between `0`
* and `127`). This has priority over the `release` property. An invalid velocity value will
* silently trigger the default of 64. This is only used with the **note off** event triggered
* when `options.duration` is set.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
playNote(note, options = {}) {
// Send note on and, optionally, note off message (if duration is a positive number)
this.sendNoteOn(note, options);
const notes = Array.isArray(note) ? note : [note];
for (let note of notes) {
if (parseInt(note.duration) > 0) {
const noteOffOptions = {
time: (Utilities.toTimestamp(options.time) || wm.time) + parseInt(note.duration),
release: note.release,
rawRelease: note.rawRelease
};
this.sendNoteOff(note, noteOffOptions);
} else if (parseInt(options.duration) > 0) {
const noteOffOptions = {
time: (Utilities.toTimestamp(options.time) || wm.time) + parseInt(options.duration),
release: options.release,
rawRelease: options.rawRelease
};
this.sendNoteOff(note, noteOffOptions);
}
}
return this;
}
/**
* Sends a **note off** message for the specified notes on the channel. The first parameter is the
* note. It can be a single value or an array of the following valid values:
*
* - A MIDI note number (integer between `0` and `127`)
* - A note name, followed by the octave (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`)
* - A [`Note`]{@link Note} object
*
* The execution of the **note off** command can be delayed by using the `time` property of the
* `options` parameter.
*
* When using [`Note`]{@link Note} objects, the release velocity defined in the
* [`Note`]{@link Note} objects has precedence over the one specified via the method's `options`
* parameter.
*
* @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to stop. The notes can be
* specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a
* [`Note`]{@link Note} object or an array of the previous types. When using a note name, octave
* range must be between -1 and 9. The lowest note is C-1 (MIDI note number 0) and the highest
* note is G9 (MIDI note number 127).
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @param {number} [options.release=0.5] The velocity at which to release the note
* (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have
* priority. An invalid velocity value will silently trigger the default of `0.5`.
*
* @param {number} [options.rawRelease=64] The velocity at which to release the note
* (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have
* priority. An invalid velocity value will silently trigger the default of `64`.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendNoteOff(note, options = {}) {
if (wm.validation) {
if (options.rawRelease != undefined && !(options.rawRelease >= 0 && options.rawRelease <= 127)) {
throw new RangeError("The 'rawRelease' option must be an integer between 0 and 127");
}
if (options.release != undefined && !(options.release >= 0 && options.release <= 1)) {
throw new RangeError("The 'release' option must be an number between 0 and 1");
}
// Legacy compatibility warnings
if (options.rawVelocity) {
options.rawRelease = options.velocity;
console.warn("The 'rawVelocity' option is deprecated. Use 'rawRelease' instead.");
}
if (options.velocity) {
options.release = options.velocity;
console.warn("The 'velocity' option is deprecated. Use 'attack' instead.");
}
}
let nVelocity = 64;
if (options.rawRelease != undefined) {
nVelocity = options.rawRelease;
} else {
if (!isNaN(options.release)) nVelocity = Math.round(options.release * 127);
}
// Plot total octave offset
const offset = wm.octaveOffset + this.output.octaveOffset + this.octaveOffset;
Utilities.buildNoteArray(note, {
rawRelease: parseInt(nVelocity)
}).forEach(n => {
this.send([(Enumerations.CHANNEL_MESSAGES.noteoff << 4) + (this.number - 1), n.getOffsetNumber(offset), n.rawRelease], {
time: Utilities.toTimestamp(options.time)
});
});
return this;
}
/**
* Sends a **note off** message for the specified MIDI note number. The first parameter is the
* note to stop. It can be a single value or an array of the following valid values:
*
* - A MIDI note number (integer between `0` and `127`)
* - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`)
* - A [`Note`](Note) object
*
* The execution of the **note off** command can be delayed by using the `time` property of the
* `options` parameter.
*
* @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be
* specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`,
* `Db7`) or an array of the previous types. When using a note identifier, octave range must be
* between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is
* `G9` (MIDI note number `127`).
*
* @param {Object} [options={}]
*
* @param {number} [options.release=0.5] The velocity at which to release the note
* (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have
* priority. An invalid velocity value will silently trigger the default of `0.5`.
*
* @param {number} [options.rawRelease=64] The velocity at which to release the note
* (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have
* priority. An invalid velocity value will silently trigger the default of `64`.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
stopNote(note, options = {}) {
return this.sendNoteOff(note, options);
}
/**
* Sends a **note on** message for the specified note(s) on the channel. The first parameter is
* the note. It can be a single value or an array of the following valid values:
*
* - A [`Note`]{@link Note} object
* - A MIDI note number (integer between `0` and `127`)
* - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`)
*
* When passing a [`Note`]{@link Note}object or a note name, the `octaveOffset` will be applied.
* This is not the case when using a note number. In this case, we assume you know exactly which
* MIDI note number should be sent out.
*
* The execution of the **note on** command can be delayed by using the `time` property of the
* `options` parameter.
*
* When using [`Note`]{@link Note} objects, the attack velocity defined in the
* [`Note`]{@link Note} objects has precedence over the one specified via the method's `options`
* parameter. Also, the `duration` is ignored. If you want to also send a **note off** message,
* use the [`playNote()`]{@link #playNote} method instead.
*
* **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is
* functionally equivalent to a **note off** message.
*
* @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to play. The notes can be
* specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a
* [`Note`]{@link Note} object or an array of the previous types.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and
* `1`). If the `rawAttack` option is also defined, `rawAttack` will have priority. An invalid
* velocity value will silently trigger the default of `0.5`.
*
* @param {number} [options.rawAttack=64] The velocity at which to release the note (between `0`
* and `127`). If the `attack` option is also defined, `rawAttack` will have priority. An invalid
* velocity value will silently trigger the default of `64`.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendNoteOn(note, options = {}) {
if (wm.validation) {
if (options.rawAttack != undefined && !(options.rawAttack >= 0 && options.rawAttack <= 127)) {
throw new RangeError("The 'rawAttack' option must be an integer between 0 and 127");
}
if (options.attack != undefined && !(options.attack >= 0 && options.attack <= 1)) {
throw new RangeError("The 'attack' option must be an number between 0 and 1");
}
// Legacy compatibility warnings
if (options.rawVelocity) {
options.rawAttack = options.velocity;
options.rawRelease = options.release;
console.warn("The 'rawVelocity' option is deprecated. Use 'rawAttack' or 'rawRelease'.");
}
if (options.velocity) {
options.attack = options.velocity;
console.warn("The 'velocity' option is deprecated. Use 'attack' instead.");
}
}
let nVelocity = 64;
if (options.rawAttack != undefined) {
nVelocity = options.rawAttack;
} else {
if (!isNaN(options.attack)) nVelocity = Math.round(options.attack * 127);
}
// Plot total octave offset
const offset = wm.octaveOffset + this.output.octaveOffset + this.octaveOffset;
Utilities.buildNoteArray(note, {
rawAttack: nVelocity
}).forEach(n => {
this.send([(Enumerations.CHANNEL_MESSAGES.noteon << 4) + (this.number - 1), n.getOffsetNumber(offset), n.rawAttack], {
time: Utilities.toTimestamp(options.time)
});
});
return this;
}
/**
* Sends a MIDI **channel mode** message. The channel mode message to send can be specified
* numerically or by using one of the following common names:
*
* | Type |Number| Shortcut Method |
* | ---------------------|------|-------------------------------------------------------------- |
* | `allsoundoff` | 120 | [`sendAllSoundOff()`]{@link #sendAllSoundOff} |
* | `resetallcontrollers`| 121 | [`sendResetAllControllers()`]{@link #sendResetAllControllers} |
* | `localcontrol` | 122 | [`sendLocalControl()`]{@link #sendLocalControl} |
* | `allnotesoff` | 123 | [`sendAllNotesOff()`]{@link #sendAllNotesOff} |
* | `omnimodeoff` | 124 | [`sendOmniMode(false)`]{@link #sendOmniMode} |
* | `omnimodeon` | 125 | [`sendOmniMode(true)`]{@link #sendOmniMode} |
* | `monomodeon` | 126 | [`sendPolyphonicMode("mono")`]{@link #sendPolyphonicMode} |
* | `polymodeon` | 127 | [`sendPolyphonicMode("poly")`]{@link #sendPolyphonicMode} |
*
* **Note**: as you can see above, to make it easier, all channel mode messages also have a matching
* helper method.
*
* It should be noted that, per the MIDI specification, only `localcontrol` and `monomodeon` may
* require a value that's not zero. For that reason, the `value` parameter is optional and
* defaults to 0.
*
* @param {number|string} command The numerical identifier of the channel mode message (integer
* between `120` and `127`) or its name as a string.
*
* @param {number} [value=0] The value to send (integer between `0` - `127`).
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendChannelMode(command, value = 0, options = {}) {
// Normalize command to integer
if (typeof command === "string") command = Enumerations.CHANNEL_MODE_MESSAGES[command];
if (wm.validation) {
if (command === undefined) {
throw new TypeError("Invalid channel mode message name or number.");
}
if (isNaN(command) || !(command >= 120 && command <= 127)) {
throw new TypeError("Invalid channel mode message number.");
}
if (isNaN(parseInt(value)) || value < 0 || value > 127) {
throw new RangeError("Value must be an integer between 0 and 127.");
}
}
this.send([(Enumerations.CHANNEL_MESSAGES.controlchange << 4) + (this.number - 1), command, value], {
time: Utilities.toTimestamp(options.time)
});
return this;
}
/**
* Sets OMNI mode to `"on"` or `"off"`. MIDI's OMNI mode causes the instrument to respond to
* messages from all channels.
*
* It should be noted that support for OMNI mode is not as common as it used to be.
*
* @param [state=true] {boolean} Whether to activate OMNI mode (`true`) or not (`false`).
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {TypeError} Invalid channel mode message name.
* @throws {RangeError} Channel mode controller numbers must be between 120 and 127.
* @throws {RangeError} Value must be an integer between 0 and 127.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendOmniMode(state, options = {}) {
if (state === undefined || state) {
this.sendChannelMode("omnimodeon", 0, options);
} else {
this.sendChannelMode("omnimodeoff", 0, options);
}
return this;
}
/**
* Sends a MIDI **channel aftertouch** message. For key-specific aftertouch, you should instead
* use [`sendKeyAftertouch()`]{@link #sendKeyAftertouch}.
*
* @param [pressure] {number} The pressure level (between `0` and `1`). If the `rawValue` option
* is set to `true`, the pressure can be defined by using an integer between `0` and `127`.
*
* @param {object} [options={}]
*
* @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be
* considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*
* @throws RangeError Invalid channel aftertouch value.
*/
sendChannelAftertouch(pressure, options = {}) {
if (wm.validation) {
if (isNaN(parseFloat(pressure))) {
throw new RangeError("Invalid channel aftertouch value.");
}
if (options.rawValue) {
if (!(pressure >= 0 && pressure <= 127 && Number.isInteger(pressure))) {
throw new RangeError("Channel aftertouch raw value must be an integer between 0 and 127.");
}
} else {
if (!(pressure >= 0 && pressure <= 1)) {
throw new RangeError("Channel aftertouch value must be a float between 0 and 1.");
}
}
}
// Normalize pressure to integer
if (!options.rawValue) pressure = Utilities.fromFloatTo7Bit(pressure);
this.send([(Enumerations.CHANNEL_MESSAGES.channelaftertouch << 4) + (this.number - 1), Math.round(pressure)], {
time: Utilities.toTimestamp(options.time)
});
return this;
}
/**
* Sends a **master tuning** message. The value is decimal and must be larger than -65 semitones
* and smaller than 64 semitones.
*
* Because of the way the MIDI specification works, the decimal portion of the value will be
* encoded with a resolution of 14bit. The integer portion must be between -64 and 63
* inclusively. This function actually generates two MIDI messages: a **Master Coarse Tuning** and
* a **Master Fine Tuning** RPN messages.
*
* @param [value=0.0] {number} The desired decimal adjustment value in semitones (-65 < x < 64)
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The value must be a decimal number between larger than -65 and smaller
* than 64.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendMasterTuning(value, options = {}) {
// @todo allow passing value as msb/lsb pair (the same as pitch bend range)
value = parseFloat(value) || 0.0;
if (wm.validation) {
if (!(value > -65 && value < 64)) {
throw new RangeError("The value must be a decimal number larger than -65 and smaller than 64.");
}
}
let coarse = Math.floor(value) + 64;
let fine = value - Math.floor(value);
// Calculate MSB and LSB for fine adjustment (14bit resolution)
fine = Math.round((fine + 1) / 2 * 16383);
let msb = fine >> 7 & 0x7F;
let lsb = fine & 0x7F;
this.sendRpnValue("channelcoarsetuning", coarse, options);
this.sendRpnValue("channelfinetuning", [msb, lsb], options);
return this;
}
/**
* Sends a **modulation depth range** message to adjust the depth of the modulation wheel's range.
* The range can be specified with the `semitones` parameter, the `cents` parameter or by
* specifying both parameters at the same time.
*
* @param {number} semitones The desired adjustment value in semitones (integer between 0 and
* 127).
*
* @param {number} [cents=0] The desired adjustment value in cents (integer between 0 and 127).
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendModulationRange(semitones, cents, options = {}) {
// @todo allow passing value as msb/lsb pair (the same as pitch bend range)
// when passing a single argument, semitones and cents shoud be combined
if (wm.validation) {
if (!Number.isInteger(semitones) || !(semitones >= 0 && semitones <= 127)) {
throw new RangeError("The semitones value must be an integer between 0 and 127.");
}
if (!(cents == undefined) && (!Number.isInteger(cents) || !(cents >= 0 && cents <= 127))) {
throw new RangeError("If specified, the cents value must be an integer between 0 and 127.");
}
}
// Default value for cents
if (!(cents >= 0 && cents <= 127)) cents = 0;
this.sendRpnValue("modulationrange", [semitones, cents], options);
return this;
}
/**
* Sets a non-registered parameter (NRPN) to the specified value. The NRPN is selected by passing
* in a two-position array specifying the values of the two control bytes. The value is specified
* by passing in a single integer (most cases) or an array of two integers.
*
* NRPNs are not standardized in any way. Each manufacturer is free to implement them any way
* they see fit. For example, according to the Roland GS specification, you can control the
* **vibrato rate** using NRPN (1, 8). Therefore, to set the **vibrato rate** value to **123** you
* would use:
*
* ```js
* WebMidi.outputs[0].channels[0].sendNrpnValue([1, 8], 123);
* ```
*
* In some rarer cases, you need to send two values with your NRPN messages. In such cases, you
* would use a 2-position array. For example, for its **ClockBPM** parameter (2, 63), Novation
* uses a 14-bit value that combines an MSB and an LSB (7-bit values). So, for example, if the
* value to send was 10, you could use:
*
* ```js
* WebMidi.outputs[0].channels[0].sendNrpnValue([2, 63], [0, 10]);
* ```
*
* For further implementation details, refer to the manufacturer's documentation.
*
* @param nrpn {number[]} A two-position array specifying the two control bytes (0x63,
* 0x62) that identify the non-registered parameter.
*
* @param [data=[]] {number|number[]} An integer or an array of integers with a length of 1 or 2
* specifying the desired data.
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The control value must be between 0 and 127.
* @throws {RangeError} The msb value must be between 0 and 127
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendNrpnValue(nrpn, data, options = {}) {
data = [].concat(data);
if (wm.validation) {
if (!Array.isArray(nrpn) || !Number.isInteger(nrpn[0]) || !Number.isInteger(nrpn[1])) {
throw new TypeError("The specified NRPN is invalid.");
}
if (!(nrpn[0] >= 0 && nrpn[0] <= 127)) {
throw new RangeError("The first byte of the NRPN must be between 0 and 127.");
}
if (!(nrpn[1] >= 0 && nrpn[1] <= 127)) {
throw new RangeError("The second byte of the NRPN must be between 0 and 127.");
}
data.forEach(value => {
if (!(value >= 0 && value <= 127)) {
throw new RangeError("The data bytes of the NRPN must be between 0 and 127.");
}
});
}
this._selectNonRegisteredParameter(nrpn, options);
this._setCurrentParameter(data, options);
this._deselectNonRegisteredParameter(options);
return this;
}
/**
* Sends a MIDI **pitch bend** message at the scheduled time. The resulting bend is relative to
* the pitch bend range that has been defined. The range can be set with
* [`sendPitchBendRange()`]{@link #sendPitchBendRange}. So, for example, if the pitch
* bend range has been set to 12 semitones, using a bend value of -1 will bend the note 1 octave
* below its nominal value.
*
* @param {number|number[]} [value] The intensity of the bend (between -1.0 and 1.0). A value of
* zero means no bend. If the `rawValue` option is set to `true`, the intensity of the bend can be
* defined by either using a single integer between 0 and 127 (MSB) or an array of two integers
* between 0 and 127 representing, respectively, the MSB (most significant byte) and the LSB
* (least significant byte). The MSB is expressed in semitones with `64` meaning no bend. A value
* lower than `64` bends downwards while a value higher than `64` bends upwards. The LSB is
* expressed in cents (1/100 of a semitone). An LSB of `64` also means no bend.
*
* @param {Object} [options={}]
*
* @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be
* considered as a float between -1.0 and 1.0 (default) or as raw integer between 0 and 127 (or
* an array of 2 integers if using both MSB and LSB).
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendPitchBend(value, options = {}) {
// @todo standardize the way msb/lsb are passed in
if (wm.validation) {
if (options.rawValue && Array.isArray(value)) {
if (!(value[0] >= 0 && value[0] <= 127)) {
throw new RangeError("The pitch bend MSB must be an integer between 0 and 127.");
}
if (!(value[1] >= 0 && value[1] <= 127)) {
throw new RangeError("The pitch bend LSB must be an integer between 0 and 127.");
}
} else if (options.rawValue && !Array.isArray(value)) {
if (!(value >= 0 && value <= 127)) {
throw new RangeError("The pitch bend MSB must be an integer between 0 and 127.");
}
} else {
if (isNaN(value) || value === null) {
throw new RangeError("Invalid pitch bend value.");
}
if (!(value >= -1 && value <= 1)) {
throw new RangeError("The pitch bend value must be a float between -1 and 1.");
}
}
}
let msb = 0;
let lsb = 0;
// Calculate MSB and LSB for both scenarios
if (options.rawValue && Array.isArray(value)) {
msb = value[0];
lsb = value[1];
} else if (options.rawValue && !Array.isArray(value)) {
msb = value;
} else {
const result = Utilities.fromFloatToMsbLsb((value + 1) / 2); // b/c value is -1 to 1
msb = result.msb;
lsb = result.lsb;
}
this.send([(Enumerations.CHANNEL_MESSAGES.pitchbend << 4) + (this.number - 1), lsb, msb], {
time: Utilities.toTimestamp(options.time)
});
return this;
}
/**
* Sends a **pitch bend range** message at the scheduled time to adjust the range used by the
* pitch bend lever. The range is specified by using the `semitones` and `cents` parameters. For
* example, setting the `semitones` parameter to `12` means that the pitch bend range will be 12
* semitones above and below the nominal pitch.
*
* @param semitones {number} The desired adjustment value in semitones (between 0 and 127). While
* nothing imposes that in the specification, it is very common for manufacturers to limit the
* range to 2 octaves (-12 semitones to 12 semitones).
*
* @param [cents=0] {number} The desired adjustment value in cents (integer between 0-127).
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The semitones value must be an integer between 0 and 127.
* @throws {RangeError} The cents value must be an integer between 0 and 127.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendPitchBendRange(semitones, cents, options = {}) {
// @todo use single value as parameter or pair of msb/lsb
if (wm.validation) {
if (!Number.isInteger(semitones) || !(semitones >= 0 && semitones <= 127)) {
throw new RangeError("The semitones value must be an integer between 0 and 127.");
}
if (!Number.isInteger(cents) || !(cents >= 0 && cents <= 127)) {
throw new RangeError("The cents value must be an integer between 0 and 127.");
}
}
this.sendRpnValue("pitchbendrange", [semitones, cents], options);
return this;
}
/**
* Sends a MIDI **program change** message at the scheduled time.
*
* @param [program=1] {number} The MIDI patch (program) number (integer between `0` and `127`).
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {TypeError} Failed to execute 'send' on 'MIDIOutput': The value at index 1 is greater
* than 0xFF.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*
*/
sendProgramChange(program, options = {}) {
program = parseInt(program) || 0;
if (wm.validation) {
if (!(program >= 0 && program <= 127)) {
throw new RangeError("The program number must be between 0 and 127.");
}
}
this.send([(Enumerations.CHANNEL_MESSAGES.programchange << 4) + (this.number - 1), program], {
time: Utilities.toTimestamp(options.time)
});
return this;
}
/**
* Sets the specified MIDI registered parameter to the desired value. The value is defined with
* up to two bytes of data (msb, lsb) that each can go from 0 to 127.
*
* MIDI
* [registered parameters](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2)
* extend the original list of control change messages. The MIDI 1.0 specification lists only a
* limited number of them:
*
* | Numbers | Function |
* |--------------|--------------------------|
* | (0x00, 0x00) | `pitchbendrange` |
* | (0x00, 0x01) | `channelfinetuning` |
* | (0x00, 0x02) | `channelcoarsetuning` |
* | (0x00, 0x03) | `tuningprogram` |
* | (0x00, 0x04) | `tuningbank` |
* | (0x00, 0x05) | `modulationrange` |
* | (0x3D, 0x00) | `azimuthangle` |
* | (0x3D, 0x01) | `elevationangle` |
* | (0x3D, 0x02) | `gain` |
* | (0x3D, 0x03) | `distanceratio` |
* | (0x3D, 0x04) | `maximumdistance` |
* | (0x3D, 0x05) | `maximumdistancegain` |
* | (0x3D, 0x06) | `referencedistanceratio` |
* | (0x3D, 0x07) | `panspreadangle` |
* | (0x3D, 0x08) | `rollangle` |
*
* Note that the **Tuning Program** and **Tuning Bank** parameters are part of the *MIDI Tuning
* Standard*, which is not widely implemented.
*
* @param rpn {string|number[]} A string identifying the parameter's name (see above) or a
* two-position array specifying the two control bytes (e.g. `[0x65, 0x64]`) that identify the
* registered parameter.
*
* @param [data=[]] {number|number[]} An single integer or an array of integers with a maximum
* length of 2 specifying the desired data.
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendRpnValue(rpn, data, options = {}) {
if (!Array.isArray(rpn)) rpn = Enumerations.REGISTERED_PARAMETERS[rpn];
if (wm.validation) {
if (!Number.isInteger(rpn[0]) || !Number.isInteger(rpn[1])) {
throw new TypeError("The specified NRPN is invalid.");
}
if (!(rpn[0] >= 0 && rpn[0] <= 127)) {
throw new RangeError("The first byte of the RPN must be between 0 and 127.");
}
if (!(rpn[1] >= 0 && rpn[1] <= 127)) {
throw new RangeError("The second byte of the RPN must be between 0 and 127.");
}
[].concat(data).forEach(value => {
if (!(value >= 0 && value <= 127)) {
throw new RangeError("The data bytes of the RPN must be between 0 and 127.");
}
});
}
this._selectRegisteredParameter(rpn, options);
this._setCurrentParameter(data, options);
this._deselectRegisteredParameter(options);
return this;
}
/**
* Sets the MIDI tuning bank to use. Note that the **Tuning Bank** parameter is part of the
* *MIDI Tuning Standard*, which is not widely implemented.
*
* @param value {number} The desired tuning bank (integer between `0` and `127`).
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The bank value must be between 0 and 127.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendTuningBank(value, options = {}) {
if (wm.validation) {
if (!Number.isInteger(value) || !(value >= 0 && value <= 127)) {
throw new RangeError("The tuning bank number must be between 0 and 127.");
}
}
this.sendRpnValue("tuningbank", value, options);
return this;
}
/**
* Sets the MIDI tuning program to use. Note that the **Tuning Program** parameter is part of the
* *MIDI Tuning Standard*, which is not widely implemented.
*
* @param value {number} The desired tuning program (integer between `0` and `127`).
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The program value must be between 0 and 127.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendTuningProgram(value, options = {}) {
if (wm.validation) {
if (!Number.isInteger(value) || !(value >= 0 && value <= 127)) {
throw new RangeError("The tuning program number must be between 0 and 127.");
}
}
this.sendRpnValue("tuningprogram", value, options);
return this;
}
/**
* Turns local control on or off. Local control is usually enabled by default. If you disable it,
* the instrument will no longer trigger its own sounds. It will only send the MIDI messages to
* its out port.
*
* @param [state=false] {boolean} Whether to activate local control (`true`) or disable it
* (`false`).
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendLocalControl(state, options = {}) {
if (state) {
return this.sendChannelMode("localcontrol", 127, options);
} else {
return this.sendChannelMode("localcontrol", 0, options);
}
}
/**
* Sends an **all notes off** channel mode message. This will make all currently playing notes
* fade out just as if their key had been released. This is different from the
* [`sendAllSoundOff()`]{@link #sendAllSoundOff} method which mutes all sounds immediately.
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendAllNotesOff(options = {}) {
return this.sendChannelMode("allnotesoff", 0, options);
}
/**
* Sends an **all sound off** channel mode message. This will silence all sounds playing on that
* channel but will not prevent new sounds from being triggered.
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendAllSoundOff(options = {}) {
return this.sendChannelMode("allsoundoff", 0, options);
}
/**
* Sends a **reset all controllers** channel mode message. This resets all controllers, such as
* the pitch bend, to their default value.
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendResetAllControllers(options = {}) {
return this.sendChannelMode("resetallcontrollers", 0, options);
}
/**
* Sets the polyphonic mode. In `"poly"` mode (usually the default), multiple notes can be played
* and heard at the same time. In `"mono"` mode, only one note will be heard at once even if
* multiple notes are being played.
*
* @param {string} [mode=poly] The mode to use: `"mono"` or `"poly"`.
*
* @param {Object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {OutputChannel} Returns the `OutputChannel` object so methods can be chained.
*/
sendPolyphonicMode(mode, options = {}) {
if (mode === "mono") {
return this.sendChannelMode("monomodeon", 0, options);
} else {
return this.sendChannelMode("polymodeon", 0, options);
}
}
/**
* An integer to offset the reported octave of outgoing note-specific messages (`noteon`,
* `noteoff` and `keyaftertouch`). By default, middle C (MIDI note number 60) is placed on the 4th
* octave (C4).
*
* Note that this value is combined with the global offset value defined in
* [`WebMidi.octaveOffset`](WebMidi#octaveOffset) and with the parent value defined in
* [`Output.octaveOffset`]{@link Output#octaveOffset}.
*
* @type {number}
*
* @since 3.0
*/
get octaveOffset() {
return this._octaveOffset;
}
set octaveOffset(value) {
if (this.validation) {
value = parseInt(value);
if (isNaN(value)) throw new TypeError("The 'octaveOffset' property must be an integer.");
}
this._octaveOffset = value;
}
/**
* The parent [`Output`]{@link Output} this channel belongs to.
* @type {Output}
* @since 3.0
*/
get output() {
return this._output;
}
/**
* This channel's MIDI number (`1` - `16`).
* @type {number}
* @since 3.0
*/
get number() {
return this._number;
}
}
/**
* The `Output` class represents a single MIDI output port (not to be confused with a MIDI channel).
* A port is made available by a MIDI device. A MIDI device can advertise several input and output
* ports. Each port has 16 MIDI channels which can be accessed via the [`channels`](#channels)
* property.
*
* The `Output` object is automatically instantiated by the library according to the host's MIDI
* subsystem and should not be directly instantiated.
*
* You can access all available `Output` objects by referring to the
* [`WebMidi.outputs`](WebMidi#outputs) array or by using methods such as
* [`WebMidi.getOutputByName()`](WebMidi#getOutputByName) or
* [`WebMidi.getOutputById()`](WebMidi#getOutputById).
*
* @fires Output#opened
* @fires Output#disconnected
* @fires Output#closed
*
* @extends EventEmitter
* @license Apache-2.0
*/
class Output extends EventEmitter {
/**
* Creates an `Output` object.
*
* @param {MIDIOutput} midiOutput [`MIDIOutput`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIOutput)
* object as provided by the MIDI subsystem.
*/
constructor(midiOutput) {
super();
/**
* A reference to the `MIDIOutput` object
* @type {MIDIOutput}
* @private
*/
this._midiOutput = midiOutput;
/**
* @type {number}
* @private
*/
this._octaveOffset = 0;
/**
* Array containing the 16 [`OutputChannel`]{@link OutputChannel} objects available provided by
* this `Output`. The channels are numbered 1 through 16.
*
* @type {OutputChannel[]}
*/
this.channels = [];
for (let i = 1; i <= 16; i++) this.channels[i] = new OutputChannel(this, i);
this._midiOutput.onstatechange = this._onStateChange.bind(this);
}
/**
* Destroys the `Output`. All listeners are removed, all channels are destroyed and the MIDI
* subsystem is unlinked.
* @returns {Promise<void>}
*/
async destroy() {
this.removeListener();
this.channels.forEach(ch => ch.destroy());
this.channels = [];
if (this._midiOutput) this._midiOutput.onstatechange = null;
await this.close();
this._midiOutput = null;
}
/**
* @private
*/
_onStateChange(e) {
let event = {
timestamp: wm.time
};
if (e.port.connection === "open") {
/**
* Event emitted when the {@link Output} has been opened by calling the
* [open()]{@link Output#open} method.
*
* @event Output#opened
* @type {object}
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `"opened"`
* @property {Output} target The object to which the listener was originally added (`Output`).
* @property {Output} port The port that was opened
*/
event.type = "opened";
event.target = this;
event.port = event.target; // for consistency
this.emit("opened", event);
} else if (e.port.connection === "closed" && e.port.state === "connected") {
/**
* Event emitted when the {@link Output} has been closed by calling the
* [close()]{@link Output#close} method.
*
* @event Output#closed
* @type {object}
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `"closed"`
* @property {Output} target The object to which the listener was originally added (`Output`).
* @property {Output} port The port that was closed
*/
event.type = "closed";
event.target = this;
event.port = event.target; // for consistency
this.emit("closed", event);
} else if (e.port.connection === "closed" && e.port.state === "disconnected") {
/**
* Event emitted when the {@link Output} becomes unavailable. This event is typically fired
* when the MIDI device is unplugged.
*
* @event Output#disconnected
* @type {object}
* @property {number} timestamp The moment (DOMHighResTimeStamp0 when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `"disconnected"`
* @property {Output} target The object to which the listener was originally added (`Output`).
* @property {object} port Object with properties describing the {@link Output} that was
* disconnected. This is not the actual `Output` as it is no longer available.
*/
event.type = "disconnected";
event.port = {
connection: e.port.connection,
id: e.port.id,
manufacturer: e.port.manufacturer,
name: e.port.name,
state: e.port.state,
type: e.port.type
};
this.emit("disconnected", event);
} else if (e.port.connection === "pending" && e.port.state === "disconnected") ; else {
console.warn("This statechange event was not caught:", e.port.connection, e.port.state);
}
}
/**
* Opens the output for usage. When the library is enabled, all ports are automatically opened.
* This method is only useful for ports that have been manually closed.
*
* @returns {Promise<Output>} The promise is fulfilled with the `Output` object.
*/
async open() {
// Explicitly opens the port for usage. This is not mandatory. When the port is not explicitly
// opened, it is implicitly opened (asynchronously) when calling `send()` on the `MIDIOutput`.
// We do it explicitly so that 'connected' events are dispatched immediately and we are ready to
// send.
try {
await this._midiOutput.open();
return Promise.resolve(this);
} catch (err) {
return Promise.reject(err);
}
}
/**
* Closes the output connection. When an output is closed, it cannot be used to send MIDI messages
* until the output is opened again by calling [`open()`]{@link #open}. You can check
* the connection status by looking at the [`connection`]{@link #connection} property.
*
* @returns {Promise<void>}
*/
async close() {
// We close the port. This triggers a 'statechange' event which we listen to to re-trigger the
// 'closed' event.
if (this._midiOutput) {
await this._midiOutput.close();
} else {
await Promise.resolve();
}
}
/**
* Sends a MIDI message on the MIDI output port. If no time is specified, the message will be
* sent immediately. The message should be an array of 8 bit unsigned integers (0-225), a
* [`Uint8Array`]{@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array}
* object or a [`Message`](Message) object.
*
* It is usually not necessary to use this method directly as you can use one of the simpler
* helper methods such as [`playNote()`](#playNote), [`stopNote()`](#stopNote),
* [`sendControlChange()`](#sendControlChange), etc.
*
* Details on the format of MIDI messages are available in the summary of
* [MIDI messages]{@link https://www.midi.org/specifications-old/item/table-1-summary-of-midi-message}
* from the MIDI Manufacturers Association.
*
* @param message {number[]|Uint8Array|Message} An array of 8bit unsigned integers, a `Uint8Array`
* object (not available in Node.js) containing the message bytes or a `Message` object.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The first byte (status) must be an integer between 128 and 255.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*
* @license Apache-2.0
*/
send(message, options = {
time: 0
}, legacy = 0) {
// If a Message object is passed in we extract the message data (the jzz plugin used on Node.js
// does not support using Uint8Array).
if (message instanceof Message) {
message = Utilities.isNode ? message.data : message.rawData;
}
// If the data is a Uint8Array and we are on Node, we must convert it to array so it works with
// the jzz module.
if (message instanceof Uint8Array && Utilities.isNode) {
message = Array.from(message);
}
// Validation
if (wm.validation) {
// If message is neither an array nor a Uint8Array, then we are in legacy mode
if (!Array.isArray(message) && !(message instanceof Uint8Array)) {
message = [message];
if (Array.isArray(options)) message = message.concat(options);
options = isNaN(legacy) ? {
time: 0
} : {
time: legacy
};
}
if (!(parseInt(message[0]) >= 128 && parseInt(message[0]) <= 255)) {
throw new RangeError("The first byte (status) must be an integer between 128 and 255.");
}
message.slice(1).forEach(value => {
value = parseInt(value);
if (!(value >= 0 && value <= 255)) {
throw new RangeError("Data bytes must be integers between 0 and 255.");
}
});
if (!options) options = {
time: 0
};
}
// Send message and return `Output` for chaining
this._midiOutput.send(message, Utilities.toTimestamp(options.time));
return this;
}
/**
* Sends a MIDI [**system exclusive**]{@link
* https://www.midi.org/specifications-old/item/table-4-universal-system-exclusive-messages}
* (*sysex*) message. There are two categories of system exclusive messages: manufacturer-specific
* messages and universal messages. Universal messages are further divided into three subtypes:
*
* * Universal non-commercial (for research and testing): `0x7D`
* * Universal non-realtime: `0x7E`
* * Universal realtime: `0x7F`
*
* The method's first parameter (`identification`) identifies the type of message. If the value of
* `identification` is `0x7D` (125), `0x7E` (126) or `0x7F` (127), the message will be identified
* as a **universal non-commercial**, **universal non-realtime** or **universal realtime** message
* (respectively).
*
* If the `identification` value is an array or an integer between 0 and 124, it will be used to
* identify the manufacturer targeted by the message. The *MIDI Manufacturers Association*
* maintains a full list of
* [Manufacturer ID Numbers](https://www.midi.org/specifications-old/item/manufacturer-id-numbers).
*
* The `data` parameter should only contain the data of the message. When sending out the actual
* MIDI message, WEBMIDI.js will automatically prepend the data with the **sysex byte** (`0xF0`)
* and the identification byte(s). It will also automatically terminate the message with the
* **sysex end byte** (`0xF7`).
*
* To use the `sendSysex()` method, system exclusive message support must have been enabled. To
* do so, you must set the `sysex` option to `true` when calling
* [`WebMidi.enable()`]{@link WebMidi#enable}:
*
* ```js
* WebMidi.enable({sysex: true})
* .then(() => console.log("System exclusive messages are enabled");
* ```
*
* ##### Examples of manufacturer-specific system exclusive messages
*
* If you want to send a sysex message to a Korg device connected to the first output, you would
* use the following code:
*
* ```js
* WebMidi.outputs[0].sendSysex(0x42, [0x1, 0x2, 0x3, 0x4, 0x5]);
* ```
* In this case `0x42` is the ID of the manufacturer (Korg) and `[0x1, 0x2, 0x3, 0x4, 0x5]` is the
* data being sent.
*
* The parameters can be specified using any number notation (decimal, hex, binary, etc.).
* Therefore, the code above is equivalent to this code:
*
* ```js
* WebMidi.outputs[0].sendSysex(66, [1, 2, 3, 4, 5]);
* ```
*
* Some manufacturers are identified using 3 bytes. In this case, you would use a 3-position array
* as the first parameter. For example, to send the same sysex message to a
* *Native Instruments* device:
*
* ```js
* WebMidi.outputs[0].sendSysex([0x00, 0x21, 0x09], [0x1, 0x2, 0x3, 0x4, 0x5]);
* ```
*
* There is no limit for the length of the data array. However, it is generally suggested to keep
* system exclusive messages to 64Kb or less.
*
* ##### Example of universal system exclusive message
*
* If you want to send a universal sysex message, simply assign the correct identification number
* in the first parameter. Number `0x7D` (125) is for non-commercial, `0x7E` (126) is for
* non-realtime and `0x7F` (127) is for realtime.
*
* So, for example, if you wanted to send an identity request non-realtime message (`0x7E`), you
* could use the following:
*
* ```js
* WebMidi.outputs[0].sendSysex(0x7E, [0x7F, 0x06, 0x01]);
* ```
*
* For more details on the format of universal messages, consult the list of
* [universal sysex messages](https://www.midi.org/specifications-old/item/table-4-universal-system-exclusive-messages).
*
* @param {number|number[]} identification An unsigned integer or an array of three unsigned
* integers between `0` and `127` that either identify the manufacturer or sets the message to be
* a **universal non-commercial message** (`0x7D`), a **universal non-realtime message** (`0x7E`)
* or a **universal realtime message** (`0x7F`). The *MIDI Manufacturers Association* maintains a
* full list of
* [Manufacturer ID Numbers](https://www.midi.org/specifications-old/item/manufacturer-id-numbers).
*
* @param {number[]|Uint8Array} [data] A `Uint8Array` or an array of unsigned integers between `0`
* and `127`. This is the data you wish to transfer.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {DOMException} Failed to execute 'send' on 'MIDIOutput': System exclusive message is
* not allowed.
*
* @throws {TypeError} Failed to execute 'send' on 'MIDIOutput': The value at index x is greater
* than 0xFF.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendSysex(identification, data = [], options = {}) {
identification = [].concat(identification);
// Check if data is Uint8Array
if (data instanceof Uint8Array) {
const merged = new Uint8Array(1 + identification.length + data.length + 1);
merged[0] = Enumerations.SYSTEM_MESSAGES.sysex;
merged.set(Uint8Array.from(identification), 1);
merged.set(data, 1 + identification.length);
merged[merged.length - 1] = Enumerations.SYSTEM_MESSAGES.sysexend;
this.send(merged, {
time: options.time
});
} else {
const merged = identification.concat(data, Enumerations.SYSTEM_MESSAGES.sysexend);
this.send([Enumerations.SYSTEM_MESSAGES.sysex].concat(merged), {
time: options.time
});
}
return this;
}
/**
* Clears all MIDI messages that have been queued and scheduled but not yet sent.
*
* **Warning**: this method is defined in the
* [Web MIDI API specification](https://www.w3.org/TR/webmidi/#MIDIOutput) but has not been
* implemented by all browsers yet. You can follow
* [this issue](https://github.com/djipco/webmidi/issues/52) for more info.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
clear() {
if (this._midiOutput.clear) {
this._midiOutput.clear();
} else {
if (wm.validation) {
console.warn("The 'clear()' method has not yet been implemented in your environment.");
}
}
return this;
}
/**
* Sends a MIDI **timecode quarter frame** message. Please note that no processing is being done
* on the data. It is up to the developer to format the data according to the
* [MIDI Timecode](https://en.wikipedia.org/wiki/MIDI_timecode) format.
*
* @param value {number} The quarter frame message content (integer between 0 and 127).
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendTimecodeQuarterFrame(value, options = {}) {
if (wm.validation) {
value = parseInt(value);
if (isNaN(value) || !(value >= 0 && value <= 127)) {
throw new RangeError("The value must be an integer between 0 and 127.");
}
}
this.send([Enumerations.SYSTEM_MESSAGES.timecode, value], {
time: options.time
});
return this;
}
/**
* Sends a **song position** MIDI message. The value is expressed in MIDI beats (between `0` and
* `16383`) which are 16th note. Position `0` is always the start of the song.
*
* @param {number} [value=0] The MIDI beat to cue to (integer between `0` and `16383`).
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendSongPosition(value = 0, options = {}) {
// @todo allow passing in 2-entries array for msb/lsb
value = Math.floor(value) || 0;
var msb = value >> 7 & 0x7F;
var lsb = value & 0x7F;
this.send([Enumerations.SYSTEM_MESSAGES.songposition, msb, lsb], {
time: options.time
});
return this;
}
/**
* Sends a **song select** MIDI message.
*
* @param {number} [value=0] The number of the song to select (integer between `0` and `127`).
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws The song number must be between 0 and 127.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendSongSelect(value = 0, options = {}) {
if (wm.validation) {
value = parseInt(value);
if (isNaN(value) || !(value >= 0 && value <= 127)) {
throw new RangeError("The program value must be between 0 and 127");
}
}
this.send([Enumerations.SYSTEM_MESSAGES.songselect, value], {
time: options.time
});
return this;
}
/**
* Sends a MIDI **tune request** real-time message.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendTuneRequest(options = {}) {
this.send([Enumerations.SYSTEM_MESSAGES.tunerequest], {
time: options.time
});
return this;
}
/**
* Sends a MIDI **clock** real-time message. According to the standard, there are 24 MIDI clocks
* for every quarter note.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendClock(options = {}) {
this.send([Enumerations.SYSTEM_MESSAGES.clock], {
time: options.time
});
return this;
}
/**
* Sends a **start** real-time message. A MIDI Start message starts the playback of the current
* song at beat 0. To start playback elsewhere in the song, use the
* [`sendContinue()`]{@link #sendContinue} method.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendStart(options = {}) {
this.send([Enumerations.SYSTEM_MESSAGES.start], {
time: options.time
});
return this;
}
/**
* Sends a **continue** real-time message. This resumes song playback where it was previously
* stopped or where it was last cued with a song position message. To start playback from the
* start, use the [`sendStart()`]{@link Output#sendStart}` method.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendContinue(options = {}) {
this.send([Enumerations.SYSTEM_MESSAGES.continue], {
time: options.time
});
return this;
}
/**
* Sends a **stop** real-time message. This tells the device connected to this output to stop
* playback immediately (or at the scheduled time, if specified).
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendStop(options = {}) {
this.send([Enumerations.SYSTEM_MESSAGES.stop], {
time: options.time
});
return this;
}
/**
* Sends an **active sensing** real-time message. This tells the device connected to this port
* that the connection is still good. Active sensing messages are often sent every 300 ms if there
* was no other activity on the MIDI port.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendActiveSensing(options = {}) {
this.send([Enumerations.SYSTEM_MESSAGES.activesensing], {
time: options.time
});
return this;
}
/**
* Sends a **reset** real-time message. This tells the device connected to this output that it
* should reset itself to a default state.
*
* @param {object} [options={}]
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendReset(options = {}) {
this.send([Enumerations.SYSTEM_MESSAGES.reset], {
time: options.time
});
return this;
}
/**
* @private
* @deprecated since version 3.0
*/
sendTuningRequest(options = {}) {
if (wm.validation) {
console.warn("The sendTuningRequest() method has been deprecated. Use sendTuningRequest() instead.");
}
return this.sendTuneRequest(options);
}
/**
* Sends a MIDI **key aftertouch** message to the specified channel(s) at the scheduled time. This
* is a key-specific aftertouch. For a channel-wide aftertouch message, use
* [`setChannelAftertouch()`]{@link #setChannelAftertouch}.
*
* @param note {number|Note|string|number[]|Note[]|string[]} The note(s) for which you are sending
* an aftertouch value. The notes can be specified by using a MIDI note number (`0` - `127`), a
* [`Note`](Note) object, a note identifier (e.g. `C3`, `G#4`, `F-1`, `Db7`) or an array of the
* previous types. When using a note identifier, octave range must be between `-1` and `9`. The
* lowest note is `C-1` (MIDI note number `0`) and the highest note is `G9` (MIDI note number
* `127`).
*
* @param [pressure=0.5] {number} The pressure level (between 0 and 1). An invalid pressure value
* will silently trigger the default behaviour. If the `rawValue` option is set to `true`, the
* pressure can be defined by using an integer between 0 and 127.
*
* @param {object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be
* considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @return {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendKeyAftertouch(note, pressure, options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendKeyAftertouch(note, pressure, options);
});
return this;
}
/**
* Sends a MIDI **control change** message to the specified channel(s) at the scheduled time. The
* control change message to send can be specified numerically (0-127) or by using one of the
* following common names:
*
* | Number | Name |
* |--------|-------------------------------|
* | 0 |`bankselectcoarse` |
* | 1 |`modulationwheelcoarse` |
* | 2 |`breathcontrollercoarse` |
* | 4 |`footcontrollercoarse` |
* | 5 |`portamentotimecoarse` |
* | 6 |`dataentrycoarse` |
* | 7 |`volumecoarse` |
* | 8 |`balancecoarse` |
* | 10 |`pancoarse` |
* | 11 |`expressioncoarse` |
* | 12 |`effectcontrol1coarse` |
* | 13 |`effectcontrol2coarse` |
* | 18 |`generalpurposeslider3` |
* | 19 |`generalpurposeslider4` |
* | 32 |`bankselectfine` |
* | 33 |`modulationwheelfine` |
* | 34 |`breathcontrollerfine` |
* | 36 |`footcontrollerfine` |
* | 37 |`portamentotimefine` |
* | 38 |`dataentryfine` |
* | 39 |`volumefine` |
* | 40 |`balancefine` |
* | 42 |`panfine` |
* | 43 |`expressionfine` |
* | 44 |`effectcontrol1fine` |
* | 45 |`effectcontrol2fine` |
* | 64 |`holdpedal` |
* | 65 |`portamento` |
* | 66 |`sustenutopedal` |
* | 67 |`softpedal` |
* | 68 |`legatopedal` |
* | 69 |`hold2pedal` |
* | 70 |`soundvariation` |
* | 71 |`resonance` |
* | 72 |`soundreleasetime` |
* | 73 |`soundattacktime` |
* | 74 |`brightness` |
* | 75 |`soundcontrol6` |
* | 76 |`soundcontrol7` |
* | 77 |`soundcontrol8` |
* | 78 |`soundcontrol9` |
* | 79 |`soundcontrol10` |
* | 80 |`generalpurposebutton1` |
* | 81 |`generalpurposebutton2` |
* | 82 |`generalpurposebutton3` |
* | 83 |`generalpurposebutton4` |
* | 91 |`reverblevel` |
* | 92 |`tremololevel` |
* | 93 |`choruslevel` |
* | 94 |`celestelevel` |
* | 95 |`phaserlevel` |
* | 96 |`dataincrement` |
* | 97 |`datadecrement` |
* | 98 |`nonregisteredparametercoarse` |
* | 99 |`nonregisteredparameterfine` |
* | 100 |`registeredparametercoarse` |
* | 101 |`registeredparameterfine` |
* | 120 |`allsoundoff` |
* | 121 |`resetallcontrollers` |
* | 122 |`localcontrol` |
* | 123 |`allnotesoff` |
* | 124 |`omnimodeoff` |
* | 125 |`omnimodeon` |
* | 126 |`monomodeon` |
* | 127 |`polymodeon` |
*
* Note: as you can see above, not all control change message have a matching name. This does not
* mean you cannot use the others. It simply means you will need to use their number (`0` - `127`)
* instead of their name. While you can still use them, numbers `120` to `127` are usually
* reserved for *channel mode* messages. See [`sendChannelMode()`]{@link #sendChannelMode} method
* for more info.
*
* To view a list of all available **control change** messages, please consult [Table 3 - Control
* Change Messages](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2)
* from the MIDI specification.
*
* @param controller {number|string} The MIDI controller name or number (0-127).
*
* @param [value=0] {number} The value to send (0-127).
*
* @param {object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} Controller numbers must be between 0 and 127.
* @throws {RangeError} Invalid controller name.
*
* @return {Output} Returns the `Output` object so methods can be chained.
*/
sendControlChange(controller, value, options = {}, legacy = {}) {
if (wm.validation) {
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendControlChange(controller, value, options);
});
return this;
}
/**
* Sends a **pitch bend range** message to the specified channel(s) at the scheduled time so that
* they adjust the range used by their pitch bend lever. The range is specified by using the
* `semitones` and `cents` parameters. For example, setting the `semitones` parameter to `12`
* means that the pitch bend range will be 12 semitones above and below the nominal pitch.
*
* @param {number} [semitones=0] The desired adjustment value in semitones (between `0` and `127`).
* While nothing imposes that in the specification, it is very common for manufacturers to limit
* the range to 2 octaves (-12 semitones to 12 semitones).
*
* @param {number} [cents=0] The desired adjustment value in cents (integer between `0` and
* `127`).
*
* @param {object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The msb value must be between 0 and 127.
* @throws {RangeError} The lsb value must be between 0 and 127.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendPitchBendRange(semitones = 0, cents = 0, options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendPitchBendRange(semitones, cents, options);
});
return this;
}
/**
* @private
* @deprecated since version 3.0
*/
setPitchBendRange(semitones = 0, cents = 0, channel = "all", options = {}) {
if (wm.validation) {
console.warn("The setPitchBendRange() method is deprecated. Use sendPitchBendRange() instead.");
options.channels = channel;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
return this.sendPitchBendRange(semitones, cents, options);
}
/**
* Sets the specified MIDI registered parameter to the desired value. The value is defined with
* up to two bytes of data (msb, lsb) that each can go from `0` to `127`.
*
* MIDI
* [registered parameters](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2)
* extend the original list of control change messages. The MIDI 1.0 specification lists only a
* limited number of them:
*
* | Numbers | Function |
* |--------------|--------------------------|
* | (0x00, 0x00) | `pitchbendrange` |
* | (0x00, 0x01) | `channelfinetuning` |
* | (0x00, 0x02) | `channelcoarsetuning` |
* | (0x00, 0x03) | `tuningprogram` |
* | (0x00, 0x04) | `tuningbank` |
* | (0x00, 0x05) | `modulationrange` |
* | (0x3D, 0x00) | `azimuthangle` |
* | (0x3D, 0x01) | `elevationangle` |
* | (0x3D, 0x02) | `gain` |
* | (0x3D, 0x03) | `distanceratio` |
* | (0x3D, 0x04) | `maximumdistance` |
* | (0x3D, 0x05) | `maximumdistancegain` |
* | (0x3D, 0x06) | `referencedistanceratio` |
* | (0x3D, 0x07) | `panspreadangle` |
* | (0x3D, 0x08) | `rollangle` |
*
* Note that the `tuningprogram` and `tuningbank` parameters are part of the *MIDI Tuning
* Standard*, which is not widely implemented.
*
* @param parameter {string|number[]} A string identifying the parameter's name (see above) or a
* two-position array specifying the two control bytes (e.g. `[0x65, 0x64]`) that identify the
* registered parameter.
*
* @param [data=[]] {number|number[]} A single integer or an array of integers with a maximum
* length of 2 specifying the desired data.
*
* @param {object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendRpnValue(parameter, data, options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendRpnValue(parameter, data, options);
});
return this;
}
/**
* @private
* @deprecated since version 3.0
*/
setRegisteredParameter(parameter, data = [], channel = "all", options = {}) {
if (wm.validation) {
console.warn("The setRegisteredParameter() method is deprecated. Use sendRpnValue() instead.");
options.channels = channel;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
return this.sendRpnValue(parameter, data, options);
}
/**
* Sends a MIDI **channel aftertouch** message to the specified channel(s). For key-specific
* aftertouch, you should instead use [`setKeyAftertouch()`]{@link #setKeyAftertouch}.
*
* @param [pressure=0.5] {number} The pressure level (between `0` and `1`). An invalid pressure
* value will silently trigger the default behaviour. If the `rawValue` option is set to `true`,
* the pressure can be defined by using an integer between `0` and `127`.
*
* @param {object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be
* considered a float between `0` and `1.0` (default) or a raw integer between `0` and `127`.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @return {Output} Returns the `Output` object so methods can be chained.
* @since 3.0.0
*/
sendChannelAftertouch(pressure, options = {}, legacy = {}) {
if (wm.validation) {
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendChannelAftertouch(pressure, options);
});
return this;
}
/**
* Sends a MIDI **pitch bend** message to the specified channel(s) at the scheduled time.
*
* The resulting bend is relative to the pitch bend range that has been defined. The range can be
* set with [`sendPitchBendRange()`]{@link #sendPitchBendRange}. So, for example, if the pitch
* bend range has been set to 12 semitones, using a bend value of `-1` will bend the note 1 octave
* below its nominal value.
*
* @param {number|number[]} value The intensity of the bend (between `-1.0` and `1.0`). A value of
* `0` means no bend. If an invalid value is specified, the nearest valid value will be used
* instead. If the `rawValue` option is set to `true`, the intensity of the bend can be defined by
* either using a single integer between `0` and `127` (MSB) or an array of two integers between
* `0` and `127` representing, respectively, the MSB (most significant byte) and the LSB (least
* significant byte). The MSB is expressed in semitones with `64` meaning no bend. A value lower
* than `64` bends downwards while a value higher than `64` bends upwards. The LSB is expressed
* in cents (1/100 of a semitone). An LSB of `64` also means no bend.
*
* @param {object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {boolean} [options.rawValue=false] A boolean indicating whether the value should be
* considered as a float between `-1.0` and `1.0` (default) or as raw integer between `0` and
* 127` (or an array of 2 integers if using both MSB and LSB).
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendPitchBend(value, options = {}, legacy = {}) {
if (wm.validation) {
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendPitchBend(value, options);
});
return this;
}
/**
* Sends a MIDI **program change** message to the specified channel(s) at the scheduled time.
*
* @param {number} [program=0] The MIDI patch (program) number (integer between `0` and `127`).
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {TypeError} Failed to execute 'send' on 'MIDIOutput': The value at index 1 is greater
* than 0xFF.
*
* @return {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendProgramChange(program = 0, options = {}, legacy = {}) {
if (wm.validation) {
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendProgramChange(program, options);
});
return this;
}
/**
* Sends a **modulation depth range** message to the specified channel(s) so that they adjust the
* depth of their modulation wheel's range. The range can be specified with the `semitones`
* parameter, the `cents` parameter or by specifying both parameters at the same time.
*
* @param [semitones=0] {number} The desired adjustment value in semitones (integer between
* 0 and 127).
*
* @param [cents=0] {number} The desired adjustment value in cents (integer between 0 and 127).
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The msb value must be between 0 and 127
* @throws {RangeError} The lsb value must be between 0 and 127
*
* @return {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendModulationRange(semitones, cents, options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendModulationRange(semitones, cents, options);
});
return this;
}
/**
* @private
* @deprecated since version 3.0
*/
setModulationRange(semitones = 0, cents = 0, channel = "all", options = {}) {
if (wm.validation) {
console.warn("The setModulationRange() method is deprecated. Use sendModulationRange() instead.");
options.channels = channel;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
return this.sendModulationRange(semitones, cents, options);
}
/**
* Sends a master tuning message to the specified channel(s). The value is decimal and must be
* larger than `-65` semitones and smaller than `64` semitones.
*
* Because of the way the MIDI specification works, the decimal portion of the value will be
* encoded with a resolution of 14bit. The integer portion must be between -64 and 63
* inclusively. This function actually generates two MIDI messages: a **Master Coarse Tuning** and
* a **Master Fine Tuning** RPN messages.
*
* @param [value=0.0] {number} The desired decimal adjustment value in semitones (-65 < x < 64)
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The value must be a decimal number between larger than -65 and smaller
* than 64.
*
* @return {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendMasterTuning(value, options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendMasterTuning(value, options);
});
return this;
}
/**
* @private
* @deprecated since version 3.0
*/
setMasterTuning(value, channel = {}, options = {}) {
if (wm.validation) {
console.warn("The setMasterTuning() method is deprecated. Use sendMasterTuning() instead.");
options.channels = channel;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
return this.sendMasterTuning(value, options);
}
/**
* Sets the MIDI tuning program to use. Note that the **Tuning Program** parameter is part of the
* *MIDI Tuning Standard*, which is not widely implemented.
*
* @param value {number} The desired tuning program (integer between `0` and `127`).
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The program value must be between 0 and 127.
*
* @return {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendTuningProgram(value, options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendTuningProgram(value, options);
});
return this;
}
/**
* @private
* @deprecated since version 3.0
*/
setTuningProgram(value, channel = "all", options = {}) {
if (wm.validation) {
console.warn("The setTuningProgram() method is deprecated. Use sendTuningProgram() instead.");
options.channels = channel;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
return this.sendTuningProgram(value, options);
}
/**
* Sets the MIDI tuning bank to use. Note that the **Tuning Bank** parameter is part of the
* *MIDI Tuning Standard*, which is not widely implemented.
*
* @param {number} [value=0] The desired tuning bank (integer between `0` and `127`).
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The bank value must be between 0 and 127.
*
* @return {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendTuningBank(value = 0, options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendTuningBank(value, options);
});
return this;
}
/**
* @private
* @deprecated since version 3.0
*/
setTuningBank(parameter, channel = "all", options = {}) {
if (wm.validation) {
console.warn("The setTuningBank() method is deprecated. Use sendTuningBank() instead.");
options.channels = channel;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
return this.sendTuningBank(parameter, options);
}
/**
* Sends a MIDI **channel mode** message to the specified channel(s). The channel mode message to
* send can be specified numerically or by using one of the following common names:
*
* | Type |Number| Shortcut Method |
* | ---------------------|------|-------------------------------------------------------------- |
* | `allsoundoff` | 120 | [`sendAllSoundOff()`]{@link #sendAllSoundOff} |
* | `resetallcontrollers`| 121 | [`sendResetAllControllers()`]{@link #sendResetAllControllers} |
* | `localcontrol` | 122 | [`sendLocalControl()`]{@link #sendLocalControl} |
* | `allnotesoff` | 123 | [`sendAllNotesOff()`]{@link #sendAllNotesOff} |
* | `omnimodeoff` | 124 | [`sendOmniMode(false)`]{@link #sendOmniMode} |
* | `omnimodeon` | 125 | [`sendOmniMode(true)`]{@link #sendOmniMode} |
* | `monomodeon` | 126 | [`sendPolyphonicMode("mono")`]{@link #sendPolyphonicMode} |
* | `polymodeon` | 127 | [`sendPolyphonicMode("poly")`]{@link #sendPolyphonicMode} |
*
* Note: as you can see above, to make it easier, all channel mode messages also have a matching
* helper method.
*
* It should also be noted that, per the MIDI specification, only `localcontrol` and `monomodeon`
* may require a value that's not zero. For that reason, the `value` parameter is optional and
* defaults to 0.
*
* @param {number|string} command The numerical identifier of the channel mode message (integer
* between 120-127) or its name as a string.
*
* @param {number} [value=0] The value to send (integer between 0-127).
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {TypeError} Invalid channel mode message name.
* @throws {RangeError} Channel mode controller numbers must be between 120 and 127.
* @throws {RangeError} Value must be an integer between 0 and 127.
*
* @return {Output} Returns the `Output` object so methods can be chained.
*
*/
sendChannelMode(command, value = 0, options = {}, legacy = {}) {
if (wm.validation) {
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendChannelMode(command, value, options);
});
return this;
}
/**
* Sends an **all sound off** channel mode message. This will silence all sounds playing on that
* channel but will not prevent new sounds from being triggered.
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output}
*
* @since 3.0.0
*/
sendAllSoundOff(options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendAllSoundOff(options);
});
return this;
}
/**
* Sends an **all notes off** channel mode message. This will make all currently playing notes
* fade out just as if their key had been released. This is different from the
* [`sendAllSoundOff()`]{@link #sendAllSoundOff} method which mutes all sounds immediately.
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output}
*
* @since 3.0.0
*/
sendAllNotesOff(options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendAllNotesOff(options);
});
return this;
}
/**
* Sends a **reset all controllers** channel mode message. This resets all controllers, such as
* the pitch bend, to their default value.
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output}
*/
sendResetAllControllers(options = {}, legacy = {}) {
if (wm.validation) {
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendResetAllControllers(options);
});
return this;
}
/**
* Sets the polyphonic mode. In `poly` mode (usually the default), multiple notes can be played
* and heard at the same time. In `mono` mode, only one note will be heard at once even if
* multiple notes are being played.
*
* @param mode {string} The mode to use: `mono` or `poly`.
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @return {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendPolyphonicMode(mode, options = {}, legacy = {}) {
if (wm.validation) {
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendPolyphonicMode(mode, options);
});
return this;
}
/**
* Turns local control on or off. Local control is usually enabled by default. If you disable it,
* the instrument will no longer trigger its own sounds. It will only send the MIDI messages to
* its out port.
*
* @param [state=false] {boolean} Whether to activate local control (`true`) or disable it
* (`false`).
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @return {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendLocalControl(state, options = {}, legacy = {}) {
if (wm.validation) {
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendLocalControl(state, options);
});
return this;
}
/**
* Sets OMNI mode to **on** or **off** for the specified channel(s). MIDI's OMNI mode causes the
* instrument to respond to messages from all channels.
*
* It should be noted that support for OMNI mode is not as common as it used to be.
*
* @param [state] {boolean} Whether to activate OMNI mode (`true`) or not (`false`).
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {TypeError} Invalid channel mode message name.
* @throws {RangeError} Channel mode controller numbers must be between 120 and 127.
* @throws {RangeError} Value must be an integer between 0 and 127.
*
* @return {Output} Returns the `Output` object so methods can be chained.
*
* @since 3.0.0
*/
sendOmniMode(state, options = {}, legacy = {}) {
if (wm.validation) {
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendOmniMode(state, options);
});
return this;
}
/**
* Sets a non-registered parameter to the specified value. The NRPN is selected by passing a
* two-position array specifying the values of the two control bytes. The value is specified by
* passing a single integer (most cases) or an array of two integers.
*
* NRPNs are not standardized in any way. Each manufacturer is free to implement them any way
* they see fit. For example, according to the Roland GS specification, you can control the
* **vibrato rate** using NRPN (`1`, `8`). Therefore, to set the **vibrato rate** value to `123`
* you would use:
*
* ```js
* WebMidi.outputs[0].sendNrpnValue([1, 8], 123);
* ```
*
* You probably want to should select a channel so the message is not sent to all channels. For
* instance, to send to channel `1` of the first output port, you would use:
*
* ```js
* WebMidi.outputs[0].sendNrpnValue([1, 8], 123, 1);
* ```
*
* In some rarer cases, you need to send two values with your NRPN messages. In such cases, you
* would use a 2-position array. For example, for its **ClockBPM** parameter (`2`, `63`), Novation
* uses a 14-bit value that combines an MSB and an LSB (7-bit values). So, for example, if the
* value to send was `10`, you could use:
*
* ```js
* WebMidi.outputs[0].sendNrpnValue([2, 63], [0, 10], 1);
* ```
*
* For further implementation details, refer to the manufacturer's documentation.
*
* @param parameter {number[]} A two-position array specifying the two control bytes (`0x63`,
* `0x62`) that identify the non-registered parameter.
*
* @param [data=[]] {number|number[]} An integer or an array of integers with a length of 1 or 2
* specifying the desired data.
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws {RangeError} The control value must be between 0 and 127.
* @throws {RangeError} The msb value must be between 0 and 127
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendNrpnValue(parameter, data, options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendNrpnValue(parameter, data, options);
});
return this;
}
/**
* @private
* @deprecated since version 3.0
*/
setNonRegisteredParameter(parameter, data = [], channel = "all", options = {}) {
if (wm.validation) {
console.warn("The setNonRegisteredParameter() method is deprecated. Use sendNrpnValue() instead.");
options.channels = channel;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
return this.sendNrpnValue(parameter, data, options);
}
/**
* Increments the specified MIDI registered parameter by 1. Here is the full list of parameter
* names that can be used with this method:
*
* * Pitchbend Range (0x00, 0x00): `"pitchbendrange"`
* * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"`
* * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"`
* * Tuning Program (0x00, 0x03): `"tuningprogram"`
* * Tuning Bank (0x00, 0x04): `"tuningbank"`
* * Modulation Range (0x00, 0x05): `"modulationrange"`
* * Azimuth Angle (0x3D, 0x00): `"azimuthangle"`
* * Elevation Angle (0x3D, 0x01): `"elevationangle"`
* * Gain (0x3D, 0x02): `"gain"`
* * Distance Ratio (0x3D, 0x03): `"distanceratio"`
* * Maximum Distance (0x3D, 0x04): `"maximumdistance"`
* * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"`
* * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"`
* * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"`
* * Roll Angle (0x3D, 0x08): `"rollangle"`
*
* @param parameter {String|number[]} A string identifying the parameter's name (see above) or a
* two-position array specifying the two control bytes (0x65, 0x64) that identify the registered
* parameter.
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendRpnIncrement(parameter, options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendRpnIncrement(parameter, options);
});
return this;
}
/**
* @private
* @deprecated since version 3.0
*/
incrementRegisteredParameter(parameter, channel = "all", options = {}) {
if (wm.validation) {
console.warn("The incrementRegisteredParameter() method is deprecated. Use sendRpnIncrement() instead.");
options.channels = channel;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
return this.sendRpnIncrement(parameter, options);
}
/**
* Decrements the specified MIDI registered parameter by 1. Here is the full list of parameter
* names that can be used with this method:
*
* * Pitchbend Range (0x00, 0x00): `"pitchbendrange"`
* * Channel Fine Tuning (0x00, 0x01): `"channelfinetuning"`
* * Channel Coarse Tuning (0x00, 0x02): `"channelcoarsetuning"`
* * Tuning Program (0x00, 0x03): `"tuningprogram"`
* * Tuning Bank (0x00, 0x04): `"tuningbank"`
* * Modulation Range (0x00, 0x05): `"modulationrange"`
* * Azimuth Angle (0x3D, 0x00): `"azimuthangle"`
* * Elevation Angle (0x3D, 0x01): `"elevationangle"`
* * Gain (0x3D, 0x02): `"gain"`
* * Distance Ratio (0x3D, 0x03): `"distanceratio"`
* * Maximum Distance (0x3D, 0x04): `"maximumdistance"`
* * Maximum Distance Gain (0x3D, 0x05): `"maximumdistancegain"`
* * Reference Distance Ratio (0x3D, 0x06): `"referencedistanceratio"`
* * Pan Spread Angle (0x3D, 0x07): `"panspreadangle"`
* * Roll Angle (0x3D, 0x08): `"rollangle"`
*
* @param parameter {String|number[]} A string identifying the parameter's name (see above) or a
* two-position array specifying the two control bytes (0x65, 0x64) that identify the registered
* parameter.
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @throws TypeError The specified parameter is not available.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendRpnDecrement(parameter, options = {}) {
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendRpnDecrement(parameter, options);
});
return this;
}
/**
* @private
* @deprecated since version 3.0
*/
decrementRegisteredParameter(parameter, channel = "all", options = {}) {
if (wm.validation) {
console.warn("The decrementRegisteredParameter() method is deprecated. Use sendRpnDecrement() instead.");
options.channels = channel;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
return this.sendRpnDecrement(parameter, options);
}
/**
* Sends a **note off** message for the specified MIDI note number on the specified channel(s).
* The first parameter is the note to stop. It can be a single value or an array of the following
* valid values:
*
* - A MIDI note number (integer between `0` and `127`)
* - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`)
* - A [`Note`](Note) object
*
* The execution of the **note off** command can be delayed by using the `time` property of the
* `options` parameter.
*
* @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be
* specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`,
* `F-1`, `Db7`) or an array of the previous types. When using a note identifier, octave range
* must be between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest
* note is `G9` (MIDI note number `127`).
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number} [options.release=0.5] The velocity at which to release the note
* (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have
* priority. An invalid velocity value will silently trigger the default of `0.5`.
*
* @param {number} [options.rawRelease=64] The velocity at which to release the note
* (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have
* priority. An invalid velocity value will silently trigger the default of `64`.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendNoteOff(note, options = {}, legacy = {}) {
if (wm.validation) {
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendNoteOff(note, options);
});
return this;
}
/**
* Sends a **note off** message for the specified MIDI note number on the specified channel(s).
* The first parameter is the note to stop. It can be a single value or an array of the following
* valid values:
*
* - A MIDI note number (integer between `0` and `127`)
* - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`)
* - A [`Note`](Note) object
*
* The execution of the **note off** command can be delayed by using the `time` property of the
* `options` parameter.
*
* @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be
* specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`,
* `Db7`) or an array of the previous types. When using a note identifier, octave range must be
* between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is
* `G9` (MIDI note number `127`).
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number} [options.release=0.5] The velocity at which to release the note
* (between `0` and `1`). If the `rawRelease` option is also defined, `rawRelease` will have
* priority. An invalid velocity value will silently trigger the default of `0.5`.
*
* @param {number} [options.rawRelease=64] The velocity at which to release the note
* (between `0` and `127`). If the `release` option is also defined, `rawRelease` will have
* priority. An invalid velocity value will silently trigger the default of `64`.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
stopNote(note, options) {
return this.sendNoteOff(note, options);
}
/**
* Plays a note or an array of notes on one or more channels of this output. If you intend to play
* notes on a single channel, you should probably use
* [`OutputChannel.playNote()`](OutputChannel#playNote) instead.
*
* The first parameter is the note to play. It can be a single value or an array of the following
* valid values:
*
* - A MIDI note number (integer between `0` and `127`)
* - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`)
* - A [`Note`]{@link Note} object
*
* The `playNote()` method sends a **note on** MIDI message for all specified notes on all
* specified channels. If no channel is specified, it will send to all channels. If a `duration`
* is set in the `options` parameter or in the [`Note`]{@link Note} object's
* [`duration`]{@link Note#duration} property, it will also schedule a **note off** message to end
* the note after said duration. If no `duration` is set, the note will simply play until a
* matching **note off** message is sent with [`stopNote()`]{@link #stopNote}.
*
* The execution of the **note on** command can be delayed by using the `time` property of the
* `options` parameter.
*
* When using [`Note`]{@link Note} objects, the durations and velocities defined in the
* [`Note`]{@link Note} objects have precedence over the ones specified via the method's `options`
* parameter.
*
* **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is
* functionally equivalent to a **note off** message.
*
* @param note {number|string|Note|number[]|string[]|Note[]} The note(s) to play. The notes can be
* specified by using a MIDI note number (0-127), a note identifier (e.g. C3, G#4, F-1, Db7), a
* [`Note`]{@link Note} object or an array of the previous types. When using a note identifier,
* octave range must be between -1 and 9. The lowest note is C-1 (MIDI note number `0`) and the
* highest note is G9 (MIDI note number `127`).
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number} [options.duration=undefined] The number of milliseconds after which a
* **note off** message will be scheduled. If left undefined, only a **note on** message is sent.
*
* @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and
* `1`). If the `rawAttack` option is also defined, it will have priority. An invalid velocity
* value will silently trigger the default of `0.5`.
*
* @param {number} [options.rawAttack=64] The attack velocity at which to play the note (between
* `0` and `127`). This has priority over the `attack` property. An invalid velocity value will
* silently trigger the default of 64.
*
* @param {number} [options.release=0.5] The velocity at which to release the note (between `0`
* and `1`). If the `rawRelease` option is also defined, it will have priority. An invalid
* velocity value will silently trigger the default of `0.5`. This is only used with the
* **note off** event triggered when `options.duration` is set.
*
* @param {number} [options.rawRelease=64] The velocity at which to release the note (between `0`
* and `127`). This has priority over the `release` property. An invalid velocity value will
* silently trigger the default of 64. This is only used with the **note off** event triggered
* when `options.duration` is set.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
playNote(note, options = {}, legacy = {}) {
if (wm.validation) {
// Legacy-compatibility warnings
if (options.rawVelocity) {
console.warn("The 'rawVelocity' option is deprecated. Use 'rawAttack' instead.");
}
if (options.velocity) {
console.warn("The 'velocity' option is deprecated. Use 'velocity' instead.");
}
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].playNote(note, options);
});
return this;
}
/**
* Sends a **note on** message for the specified MIDI note number on the specified channel(s). The
* first parameter is the number. It can be a single value or an array of the following valid
* values:
*
* - A MIDI note number (integer between `0` and `127`)
* - A note identifier (e.g. `"C3"`, `"G#4"`, `"F-1"`, `"Db7"`)
* - A [`Note`](Note) object
*
* The execution of the **note on** command can be delayed by using the `time` property of the
* `options` parameter.
*
* **Note**: As per the MIDI standard, a **note on** message with an attack velocity of `0` is
* functionally equivalent to a **note off** message.
*
* @param note {number|Note|string|number[]|Note[]|string[]} The note(s) to stop. The notes can be
* specified by using a MIDI note number (`0` - `127`), a note identifier (e.g. `C3`, `G#4`, `F-1`,
* `Db7`) or an array of the previous types. When using a note identifier, octave range must be
* between `-1` and `9`. The lowest note is `C-1` (MIDI note number `0`) and the highest note is
* `G9` (MIDI note number `127`).
*
* @param {Object} [options={}]
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* The MIDI channel number (between `1` and `16`) or an array of channel numbers to use. If no
* channel is specified, all channels will be used.
*
* @param {number} [options.attack=0.5] The velocity at which to play the note (between `0` and
* `1`). If the `rawAttack` option is also defined, `rawAttack` will have priority. An invalid
* velocity value will silently trigger the default of `0.5`.
*
* @param {number} [options.rawAttack=64] The velocity at which to release the note (between `0`
* and `127`). If the `attack` option is also defined, `rawAttack` will have priority. An invalid
* velocity value will silently trigger the default of `64`.
*
* @param {number|string} [options.time=(now)] If `time` is a string prefixed with `"+"` and
* followed by a number, the message will be delayed by that many milliseconds. If the value is a
* positive number
* ([`DOMHighResTimeStamp`]{@link https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp}),
* the operation will be scheduled for that time. The current time can be retrieved with
* [`WebMidi.time`]{@link WebMidi#time}. If `options.time` is omitted, or in the past, the
* operation will be carried out as soon as possible.
*
* @returns {Output} Returns the `Output` object so methods can be chained.
*/
sendNoteOn(note, options = {}, legacy = {}) {
if (wm.validation) {
// Legacy compatibility
if (Array.isArray(options) || Number.isInteger(options) || options === "all") {
const channels = options;
options = legacy;
options.channels = channels;
if (options.channels === "all") options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
}
}
if (options.channels == undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
// This actually supports passing a Note object even if, semantically, this does not make sense.
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].sendNoteOn(note, options);
});
return this;
}
/**
* Name of the MIDI output.
*
* @type {string}
* @readonly
*/
get name() {
return this._midiOutput.name;
}
/**
* ID string of the MIDI output. The ID is host-specific. Do not expect the same ID on different
* platforms. For example, Google Chrome and the Jazz-Plugin report completely different IDs for
* the same port.
*
* @type {string}
* @readonly
*/
get id() {
return this._midiOutput.id;
}
/**
* Output port's connection state: `pending`, `open` or `closed`.
*
* @type {string}
* @readonly
*/
get connection() {
return this._midiOutput.connection;
}
/**
* Name of the manufacturer of the device that makes this output port available.
*
* @type {string}
* @readonly
*/
get manufacturer() {
return this._midiOutput.manufacturer;
}
/**
* State of the output port: `connected` or `disconnected`.
*
* @type {string}
* @readonly
*/
get state() {
return this._midiOutput.state;
}
/**
* Type of the output port (it will always be: `output`).
*
* @type {string}
* @readonly
*/
get type() {
return this._midiOutput.type;
}
/**
* An integer to offset the octave of outgoing notes. By default, middle C (MIDI note number 60)
* is placed on the 4th octave (C4).
*
* Note that this value is combined with the global offset value defined in
* [`WebMidi.octaveOffset`](WebMidi#octaveOffset) (if any).
*
* @type {number}
*
* @since 3.0
*/
get octaveOffset() {
return this._octaveOffset;
}
set octaveOffset(value) {
if (this.validation) {
value = parseInt(value);
if (isNaN(value)) throw new TypeError("The 'octaveOffset' property must be an integer.");
}
this._octaveOffset = value;
}
}
/**
* The `Forwarder` class allows the forwarding of MIDI messages to predetermined outputs. When you
* call its [`forward()`](#forward) method, it will send the specified [`Message`](Message) object
* to all the outputs listed in its [`destinations`](#destinations) property.
*
* If specific channels or message types have been defined in the [`channels`](#channels) or
* [`types`](#types) properties, only messages matching the channels/types will be forwarded.
*
* While it can be manually instantiated, you are more likely to come across a `Forwarder` object as
* the return value of the [`Input.addForwarder()`](Input#addForwarder) method.
*
* @license Apache-2.0
* @since 3.0.0
*/
class Forwarder {
/**
* Creates a `Forwarder` object.
*
* @param {Output|Output[]} [destinations=\[\]] An [`Output`](Output) object, or an array of such
* objects, to forward the message to.
*
* @param {object} [options={}]
* @param {string|string[]} [options.types=(all messages)] A MIDI message type or an array of such
* types (`"noteon"`, `"controlchange"`, etc.), that the specified message must match in order to
* be forwarded. If this option is not specified, all types of messages will be forwarded. Valid
* messages are the ones found in either
* [`SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES)
* or [`CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES).
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* A MIDI channel number or an array of channel numbers that the message must match in order to be
* forwarded. By default all MIDI channels are included (`1` to `16`).
*/
constructor(destinations = [], options = {}) {
/**
* An array of [`Output`](Output) objects to forward the message to.
* @type {Output[]}
*/
this.destinations = [];
/**
* An array of message types (`"noteon"`, `"controlchange"`, etc.) that must be matched in order
* for messages to be forwarded. By default, this array includes all
* [`Enumerations.SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES) and
* [`Enumerations.CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES).
* @type {string[]}
*/
this.types = [...Object.keys(Enumerations.SYSTEM_MESSAGES), ...Object.keys(Enumerations.CHANNEL_MESSAGES)];
/**
* An array of MIDI channel numbers that the message must match in order to be forwarded. By
* default, this array includes all MIDI channels (`1` to `16`).
* @type {number[]}
*/
this.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
/**
* Indicates whether message forwarding is currently suspended or not in this forwarder.
* @type {boolean}
*/
this.suspended = false;
// Make sure parameters are arrays
if (!Array.isArray(destinations)) destinations = [destinations];
if (options.types && !Array.isArray(options.types)) options.types = [options.types];
if (options.channels && !Array.isArray(options.channels)) options.channels = [options.channels];
if (wm.validation) {
// Validate destinations
destinations.forEach(destination => {
if (!(destination instanceof Output)) {
throw new TypeError("Destinations must be of type 'Output'.");
}
});
// Validate types
if (options.types !== undefined) {
options.types.forEach(type => {
if (!Enumerations.SYSTEM_MESSAGES.hasOwnProperty(type) && !Enumerations.CHANNEL_MESSAGES.hasOwnProperty(type)) {
throw new TypeError("Type must be a valid message type.");
}
});
}
// Validate channels
if (options.channels !== undefined) {
options.channels.forEach(channel => {
if (!Enumerations.MIDI_CHANNEL_NUMBERS.includes(channel)) {
throw new TypeError("MIDI channel must be between 1 and 16.");
}
});
}
}
this.destinations = destinations;
if (options.types) this.types = options.types;
if (options.channels) this.channels = options.channels;
}
/**
* Sends the specified message to the forwarder's destination(s) if it matches the specified
* type(s) and channel(s).
*
* @param {Message} message The [`Message`](Message) object to forward.
*/
forward(message) {
// Abort if forwarding is currently suspended
if (this.suspended) return;
// Abort if this message type should not be forwarded
if (!this.types.includes(message.type)) return;
// Abort if this channel should not be forwarded
if (message.channel && !this.channels.includes(message.channel)) return;
// Forward
this.destinations.forEach(destination => {
if (wm.validation && !(destination instanceof Output)) return;
destination.send(message);
});
}
}
/**
* The `InputChannel` class represents a single MIDI input channel (1-16) from a single input
* device. This object is derived from the host's MIDI subsystem and should not be instantiated
* directly.
*
* All 16 `InputChannel` objects can be found inside the input's [`channels`](Input#channels)
* property.
*
* @fires InputChannel#midimessage
* @fires InputChannel#unknownmessage
*
* @fires InputChannel#noteoff
* @fires InputChannel#noteon
* @fires InputChannel#keyaftertouch
* @fires InputChannel#programchange
* @fires InputChannel#channelaftertouch
* @fires InputChannel#pitchbend
*
* @fires InputChannel#allnotesoff
* @fires InputChannel#allsoundoff
* @fires InputChannel#localcontrol
* @fires InputChannel#monomode
* @fires InputChannel#omnimode
* @fires InputChannel#resetallcontrollers
*
* @fires InputChannel#event:nrpn
* @fires InputChannel#event:nrpn-dataentrycoarse
* @fires InputChannel#event:nrpn-dataentryfine
* @fires InputChannel#event:nrpn-dataincrement
* @fires InputChannel#event:nrpn-datadecrement
* @fires InputChannel#event:rpn
* @fires InputChannel#event:rpn-dataentrycoarse
* @fires InputChannel#event:rpn-dataentryfine
* @fires InputChannel#event:rpn-dataincrement
* @fires InputChannel#event:rpn-datadecrement
*
* @fires InputChannel#controlchange
* @fires InputChannel#event:controlchange-controllerxxx
* @fires InputChannel#event:controlchange-bankselectcoarse
* @fires InputChannel#event:controlchange-modulationwheelcoarse
* @fires InputChannel#event:controlchange-breathcontrollercoarse
* @fires InputChannel#event:controlchange-footcontrollercoarse
* @fires InputChannel#event:controlchange-portamentotimecoarse
* @fires InputChannel#event:controlchange-dataentrycoarse
* @fires InputChannel#event:controlchange-volumecoarse
* @fires InputChannel#event:controlchange-balancecoarse
* @fires InputChannel#event:controlchange-pancoarse
* @fires InputChannel#event:controlchange-expressioncoarse
* @fires InputChannel#event:controlchange-effectcontrol1coarse
* @fires InputChannel#event:controlchange-effectcontrol2coarse
* @fires InputChannel#event:controlchange-generalpurposecontroller1
* @fires InputChannel#event:controlchange-generalpurposecontroller2
* @fires InputChannel#event:controlchange-generalpurposecontroller3
* @fires InputChannel#event:controlchange-generalpurposecontroller4
* @fires InputChannel#event:controlchange-bankselectfine
* @fires InputChannel#event:controlchange-modulationwheelfine
* @fires InputChannel#event:controlchange-breathcontrollerfine
* @fires InputChannel#event:controlchange-footcontrollerfine
* @fires InputChannel#event:controlchange-portamentotimefine
* @fires InputChannel#event:controlchange-dataentryfine
* @fires InputChannel#event:controlchange-channelvolumefine
* @fires InputChannel#event:controlchange-balancefine
* @fires InputChannel#event:controlchange-panfine
* @fires InputChannel#event:controlchange-expressionfine
* @fires InputChannel#event:controlchange-effectcontrol1fine
* @fires InputChannel#event:controlchange-effectcontrol2fine
* @fires InputChannel#event:controlchange-damperpedal
* @fires InputChannel#event:controlchange-portamento
* @fires InputChannel#event:controlchange-sostenuto
* @fires InputChannel#event:controlchange-softpedal
* @fires InputChannel#event:controlchange-legatopedal
* @fires InputChannel#event:controlchange-hold2
* @fires InputChannel#event:controlchange-soundvariation
* @fires InputChannel#event:controlchange-resonance
* @fires InputChannel#event:controlchange-releasetime
* @fires InputChannel#event:controlchange-attacktime
* @fires InputChannel#event:controlchange-brightness
* @fires InputChannel#event:controlchange-decaytime
* @fires InputChannel#event:controlchange-vibratorate
* @fires InputChannel#event:controlchange-vibratodepth
* @fires InputChannel#event:controlchange-vibratodelay
* @fires InputChannel#event:controlchange-generalpurposecontroller5
* @fires InputChannel#event:controlchange-generalpurposecontroller6
* @fires InputChannel#event:controlchange-generalpurposecontroller7
* @fires InputChannel#event:controlchange-generalpurposecontroller8
* @fires InputChannel#event:controlchange-portamentocontrol
* @fires InputChannel#event:controlchange-highresolutionvelocityprefix
* @fires InputChannel#event:controlchange-effect1depth
* @fires InputChannel#event:controlchange-effect2depth
* @fires InputChannel#event:controlchange-effect3depth
* @fires InputChannel#event:controlchange-effect4depth
* @fires InputChannel#event:controlchange-effect5depth
* @fires InputChannel#event:controlchange-dataincrement
* @fires InputChannel#event:controlchange-datadecrement
* @fires InputChannel#event:controlchange-nonregisteredparameterfine
* @fires InputChannel#event:controlchange-nonregisteredparametercoarse
* @fires InputChannel#event:controlchange-registeredparameterfine
* @fires InputChannel#event:controlchange-registeredparametercoarse
* @fires InputChannel#event:controlchange-allsoundoff
* @fires InputChannel#event:controlchange-resetallcontrollers
* @fires InputChannel#event:controlchange-localcontrol
* @fires InputChannel#event:controlchange-allnotesoff
* @fires InputChannel#event:controlchange-omnimodeoff
* @fires InputChannel#event:controlchange-omnimodeon
* @fires InputChannel#event:controlchange-monomodeon
* @fires InputChannel#event:controlchange-polymodeon
* @fires InputChannel#event:
*
* @extends EventEmitter
* @license Apache-2.0
* @since 3.0.0
*/
class InputChannel extends EventEmitter {
/**
* Creates an `InputChannel` object.
*
* @param {Input} input The [`Input`](Input) object this channel belongs to.
* @param {number} number The channel's MIDI number (1-16).
*/
constructor(input, number) {
super();
/**
* @type {Input}
* @private
*/
this._input = input;
/**
* @type {number}
* @private
*/
this._number = number;
/**
* @type {number}
* @private
*/
this._octaveOffset = 0;
/**
* An array of messages that form the current NRPN sequence
* @private
* @type {Message[]}
*/
this._nrpnBuffer = [];
/**
* An array of messages that form the current RPN sequence
* @private
* @type {Message[]}
*/
this._rpnBuffer = [];
/**
* Indicates whether events for **Registered Parameter Number** and **Non-Registered Parameter
* Number** should be dispatched. RPNs and NRPNs are composed of a sequence of specific
* **control change** messages. When a valid sequence of such control change messages is
* received, an [`rpn`](#event-rpn) or [`nrpn`](#event-nrpn) event will fire.
*
* If an invalid or out-of-order **control change** message is received, it will fall through
* the collector logic and all buffered **control change** messages will be discarded as
* incomplete.
*
* @type {boolean}
*/
this.parameterNumberEventsEnabled = true;
/**
* Contains the current playing state of all MIDI notes of this channel (0-127). The state is
* `true` for a currently playing note and `false` otherwise.
* @type {boolean[]}
*/
this.notesState = new Array(128).fill(false);
}
/**
* Destroys the `InputChannel` by removing all listeners and severing the link with the MIDI
* subsystem's input.
*/
destroy() {
this._input = null;
this._number = null;
this._octaveOffset = 0;
this._nrpnBuffer = [];
this.notesState = new Array(128).fill(false);
this.parameterNumberEventsEnabled = false;
this.removeListener();
}
/**
* @param e MIDIMessageEvent
* @private
*/
_processMidiMessageEvent(e) {
// Create and emit a new 'midimessage' event based on the incoming one
const event = Object.assign({}, e);
event.port = this.input;
event.target = this;
event.type = "midimessage";
/**
* Event emitted when a MIDI message of any kind is received by an `InputChannel`
*
* @event InputChannel#midimessage
*
* @type {object}
*
* @property {string} type `midimessage`
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*/
this.emit(event.type, event);
// Parse the inbound event for regular MIDI messages
this._parseEventForStandardMessages(event);
}
/**
* Parses incoming channel events and emit standard MIDI message events (noteon, noteoff, etc.)
* @param e Event
* @private
*/
_parseEventForStandardMessages(e) {
const event = Object.assign({}, e);
event.type = event.message.type || "unknownmessage";
const data1 = e.message.dataBytes[0];
const data2 = e.message.dataBytes[1];
if (event.type === "noteoff" || event.type === "noteon" && data2 === 0) {
this.notesState[data1] = false;
event.type = "noteoff"; // necessary for note on with 0 velocity
/**
* Event emitted when a **note off** MIDI message has been received on the channel.
*
* @event InputChannel#noteoff
*
* @type {object}
* @property {string} type `noteoff`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the incoming
* MIDI message.
* @property {number} timestamp The moment
* ([`DOMHighResTimeStamp`](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp))
* when the event occurred (in milliseconds since the navigation start of the document).
*
* @property {object} note A [`Note`](Note) object containing information such as note name,
* octave and release velocity.
* @property {number} value The release velocity amount expressed as a float between 0 and 1.
* @property {number} rawValue The release velocity amount expressed as an integer (between 0
* and 127).
*/
// The object created when a noteoff event arrives is a Note with an attack velocity of 0.
event.note = new Note(Utilities.offsetNumber(data1, this.octaveOffset + this.input.octaveOffset + wm.octaveOffset), {
rawAttack: 0,
rawRelease: data2
});
event.value = Utilities.from7bitToFloat(data2);
event.rawValue = data2;
// Those are kept for backwards-compatibility but are gone from the documentation. They will
// be removed in future versions (@deprecated).
event.velocity = event.note.release;
event.rawVelocity = event.note.rawRelease;
} else if (event.type === "noteon") {
this.notesState[data1] = true;
/**
* Event emitted when a **note on** MIDI message has been received.
*
* @event InputChannel#noteon
*
* @type {object}
* @property {string} type `noteon`
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} note A [`Note`](Note) object containing information such as note name,
* octave and release velocity.
* @property {number} value The attack velocity amount expressed as a float between 0 and 1.
* @property {number} rawValue The attack velocity amount expressed as an integer (between 0
* and 127).
*/
event.note = new Note(Utilities.offsetNumber(data1, this.octaveOffset + this.input.octaveOffset + wm.octaveOffset), {
rawAttack: data2
});
event.value = Utilities.from7bitToFloat(data2);
event.rawValue = data2;
// Those are kept for backwards-compatibility but are gone from the documentation. They will
// be removed in future versions (@deprecated).
event.velocity = event.note.attack;
event.rawVelocity = event.note.rawAttack;
} else if (event.type === "keyaftertouch") {
/**
* Event emitted when a **key-specific aftertouch** MIDI message has been received.
*
* @event InputChannel#keyaftertouch
*
* @type {object}
* @property {string} type `"keyaftertouch"`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} note A [`Note`](Note) object containing information such as note name
* and number.
* @property {number} value The aftertouch amount expressed as a float between 0 and 1.
* @property {number} rawValue The aftertouch amount expressed as an integer (between 0 and
* 127).
*/
event.note = new Note(Utilities.offsetNumber(data1, this.octaveOffset + this.input.octaveOffset + wm.octaveOffset));
// Aftertouch value
event.value = Utilities.from7bitToFloat(data2);
event.rawValue = data2;
// @deprecated
event.identifier = event.note.identifier;
event.key = event.note.number;
event.rawKey = data1;
} else if (event.type === "controlchange") {
/**
* Event emitted when a **control change** MIDI message has been received.
*
* @event InputChannel#controlchange
*
* @type {object}
* @property {string} type `controlchange`
* @property {string} subtype The type of control change message that was received.
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
event.controller = {
number: data1,
name: Enumerations.CONTROL_CHANGE_MESSAGES[data1].name,
description: Enumerations.CONTROL_CHANGE_MESSAGES[data1].description,
position: Enumerations.CONTROL_CHANGE_MESSAGES[data1].position
};
event.subtype = event.controller.name || "controller" + data1;
event.value = Utilities.from7bitToFloat(data2);
event.rawValue = data2;
/**
* Event emitted when a **control change** MIDI message has been received and that message is
* targeting the controller numbered "xxx". Of course, "xxx" should be replaced by a valid
* controller number (0-127).
*
* @event InputChannel#controlchange-controllerxxx
*
* @type {object}
* @property {string} type `controlchange-controllerxxx`
* @property {string} subtype The type of control change message that was received.
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
const numberedEvent = Object.assign({}, event);
numberedEvent.type = `${event.type}-controller${data1}`;
delete numberedEvent.subtype;
this.emit(numberedEvent.type, numberedEvent);
/**
* Event emitted when a **controlchange-bankselectcoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-bankselectcoarse
*
* @type {object}
* @property {string} type `controlchange-bankselectcoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-modulationwheelcoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-modulationwheelcoarse
*
* @type {object}
* @property {string} type `controlchange-modulationwheelcoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-breathcontrollercoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-breathcontrollercoarse
*
* @type {object}
* @property {string} type `controlchange-breathcontrollercoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-footcontrollercoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-footcontrollercoarse
*
* @type {object}
* @property {string} type `controlchange-footcontrollercoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-portamentotimecoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-portamentotimecoarse
*
* @type {object}
* @property {string} type `controlchange-portamentotimecoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-dataentrycoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-dataentrycoarse
*
* @type {object}
* @property {string} type `controlchange-dataentrycoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-volumecoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-volumecoarse
*
* @type {object}
* @property {string} type `controlchange-volumecoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-balancecoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-balancecoarse
*
* @type {object}
* @property {string} type `controlchange-balancecoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-pancoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-pancoarse
*
* @type {object}
* @property {string} type `controlchange-pancoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-expressioncoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-expressioncoarse
*
* @type {object}
* @property {string} type `controlchange-expressioncoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-effectcontrol1coarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-effectcontrol1coarse
*
* @type {object}
* @property {string} type `controlchange-effectcontrol1coarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-effectcontrol2coarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-effectcontrol2coarse
*
* @type {object}
* @property {string} type `controlchange-effectcontrol2coarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-generalpurposecontroller1** MIDI message has been
* received.
*
* @event InputChannel#controlchange-generalpurposecontroller1
*
* @type {object}
* @property {string} type `controlchange-generalpurposecontroller1`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-generalpurposecontroller2** MIDI message has been
* received.
*
* @event InputChannel#controlchange-generalpurposecontroller2
*
* @type {object}
* @property {string} type `controlchange-generalpurposecontroller2`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-generalpurposecontroller3** MIDI message has been
* received.
*
* @event InputChannel#controlchange-generalpurposecontroller3
*
* @type {object}
* @property {string} type `controlchange-generalpurposecontroller3`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-generalpurposecontroller4** MIDI message has been
* received.
*
* @event InputChannel#controlchange-generalpurposecontroller4
*
* @type {object}
* @property {string} type `controlchange-generalpurposecontroller4`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-bankselectfine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-bankselectfine
*
* @type {object}
* @property {string} type `controlchange-bankselectfine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-modulationwheelfine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-modulationwheelfine
*
* @type {object}
* @property {string} type `controlchange-modulationwheelfine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-breathcontrollerfine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-breathcontrollerfine
*
* @type {object}
* @property {string} type `controlchange-breathcontrollerfine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-footcontrollerfine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-footcontrollerfine
*
* @type {object}
* @property {string} type `controlchange-footcontrollerfine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-portamentotimefine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-portamentotimefine
*
* @type {object}
* @property {string} type `controlchange-portamentotimefine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-dataentryfine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-dataentryfine
*
* @type {object}
* @property {string} type `controlchange-dataentryfine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-channelvolumefine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-channelvolumefine
*
* @type {object}
* @property {string} type `controlchange-channelvolumefine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-balancefine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-balancefine
*
* @type {object}
* @property {string} type `controlchange-balancefine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-panfine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-panfine
*
* @type {object}
* @property {string} type `controlchange-panfine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-expressionfine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-expressionfine
*
* @type {object}
* @property {string} type `controlchange-expressionfine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-effectcontrol1fine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-effectcontrol1fine
*
* @type {object}
* @property {string} type `controlchange-effectcontrol1fine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-effectcontrol2fine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-effectcontrol2fine
*
* @type {object}
* @property {string} type `controlchange-effectcontrol2fine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-damperpedal** MIDI message has been
* received.
*
* @event InputChannel#controlchange-damperpedal
*
* @type {object}
* @property {string} type `controlchange-damperpedal`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-portamento** MIDI message has been
* received.
*
* @event InputChannel#controlchange-portamento
*
* @type {object}
* @property {string} type `controlchange-portamento`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-sostenuto** MIDI message has been
* received.
*
* @event InputChannel#controlchange-sostenuto
*
* @type {object}
* @property {string} type `controlchange-sostenuto`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-softpedal** MIDI message has been
* received.
*
* @event InputChannel#controlchange-softpedal
*
* @type {object}
* @property {string} type `controlchange-softpedal`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-legatopedal** MIDI message has been
* received.
*
* @event InputChannel#controlchange-legatopedal
*
* @type {object}
* @property {string} type `controlchange-legatopedal`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-hold2** MIDI message has been
* received.
*
* @event InputChannel#controlchange-hold2
*
* @type {object}
* @property {string} type `controlchange-hold2`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-soundvariation** MIDI message has been
* received.
*
* @event InputChannel#controlchange-soundvariation
*
* @type {object}
* @property {string} type `controlchange-soundvariation`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-resonance** MIDI message has been
* received.
*
* @event InputChannel#controlchange-resonance
*
* @type {object}
* @property {string} type `controlchange-resonance`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-releasetime** MIDI message has been
* received.
*
* @event InputChannel#controlchange-releasetime
*
* @type {object}
* @property {string} type `controlchange-releasetime`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-attacktime** MIDI message has been
* received.
*
* @event InputChannel#controlchange-attacktime
*
* @type {object}
* @property {string} type `controlchange-attacktime`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-brightness** MIDI message has been
* received.
*
* @event InputChannel#controlchange-brightness
*
* @type {object}
* @property {string} type `controlchange-brightness`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-decaytime** MIDI message has been
* received.
*
* @event InputChannel#controlchange-decaytime
*
* @type {object}
* @property {string} type `controlchange-decaytime`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-vibratorate** MIDI message has been
* received.
*
* @event InputChannel#controlchange-vibratorate
*
* @type {object}
* @property {string} type `controlchange-vibratorate`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-vibratodepth** MIDI message has been
* received.
*
* @event InputChannel#controlchange-vibratodepth
*
* @type {object}
* @property {string} type `controlchange-vibratodepth`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-vibratodelay** MIDI message has been
* received.
*
* @event InputChannel#controlchange-vibratodelay
*
* @type {object}
* @property {string} type `controlchange-vibratodelay`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-generalpurposecontroller5** MIDI message has been
* received.
*
* @event InputChannel#controlchange-generalpurposecontroller5
*
* @type {object}
* @property {string} type `controlchange-generalpurposecontroller5`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-generalpurposecontroller6** MIDI message has been
* received.
*
* @event InputChannel#controlchange-generalpurposecontroller6
*
* @type {object}
* @property {string} type `controlchange-generalpurposecontroller6`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-generalpurposecontroller7** MIDI message has been
* received.
*
* @event InputChannel#controlchange-generalpurposecontroller7
*
* @type {object}
* @property {string} type `controlchange-generalpurposecontroller7`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-generalpurposecontroller8** MIDI message has been
* received.
*
* @event InputChannel#controlchange-generalpurposecontroller8
*
* @type {object}
* @property {string} type `controlchange-generalpurposecontroller8`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-portamentocontrol** MIDI message has been
* received.
*
* @event InputChannel#controlchange-portamentocontrol
*
* @type {object}
* @property {string} type `controlchange-portamentocontrol`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-highresolutionvelocityprefix** MIDI message has been
* received.
*
* @event InputChannel#controlchange-highresolutionvelocityprefix
*
* @type {object}
* @property {string} type `controlchange-highresolutionvelocityprefix`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-effect1depth** MIDI message has been
* received.
*
* @event InputChannel#controlchange-effect1depth
*
* @type {object}
* @property {string} type `controlchange-effect1depth`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-effect2depth** MIDI message has been
* received.
*
* @event InputChannel#controlchange-effect2depth
*
* @type {object}
* @property {string} type `controlchange-effect2depth`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-effect3depth** MIDI message has been
* received.
*
* @event InputChannel#controlchange-effect3depth
*
* @type {object}
* @property {string} type `controlchange-effect3depth`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-effect4depth** MIDI message has been
* received.
*
* @event InputChannel#controlchange-effect4depth
*
* @type {object}
* @property {string} type `controlchange-effect4depth`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-effect5depth** MIDI message has been
* received.
*
* @event InputChannel#controlchange-effect5depth
*
* @type {object}
* @property {string} type `controlchange-effect5depth`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-dataincrement** MIDI message has been
* received.
*
* @event InputChannel#controlchange-dataincrement
*
* @type {object}
* @property {string} type `controlchange-dataincrement`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-datadecrement** MIDI message has been
* received.
*
* @event InputChannel#controlchange-datadecrement
*
* @type {object}
* @property {string} type `controlchange-datadecrement`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-nonregisteredparameterfine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-nonregisteredparameterfine
*
* @type {object}
* @property {string} type `controlchange-nonregisteredparameterfine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-nonregisteredparametercoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-nonregisteredparametercoarse
*
* @type {object}
* @property {string} type `controlchange-nonregisteredparametercoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-registeredparameterfine** MIDI message has been
* received.
*
* @event InputChannel#controlchange-registeredparameterfine
*
* @type {object}
* @property {string} type `controlchange-registeredparameterfine`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-registeredparametercoarse** MIDI message has been
* received.
*
* @event InputChannel#controlchange-registeredparametercoarse
*
* @type {object}
* @property {string} type `controlchange-registeredparametercoarse`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-allsoundoff** MIDI message has been
* received.
*
* @event InputChannel#controlchange-allsoundoff
*
* @type {object}
* @property {string} type `controlchange-allsoundoff`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-resetallcontrollers** MIDI message has been
* received.
*
* @event InputChannel#controlchange-resetallcontrollers
*
* @type {object}
* @property {string} type `controlchange-resetallcontrollers`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-localcontrol** MIDI message has been
* received.
*
* @event InputChannel#controlchange-localcontrol
*
* @type {object}
* @property {string} type `controlchange-localcontrol`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-allnotesoff** MIDI message has been
* received.
*
* @event InputChannel#controlchange-allnotesoff
*
* @type {object}
* @property {string} type `controlchange-allnotesoff`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-omnimodeoff** MIDI message has been
* received.
*
* @event InputChannel#controlchange-omnimodeoff
*
* @type {object}
* @property {string} type `controlchange-omnimodeoff`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-omnimodeon** MIDI message has been
* received.
*
* @event InputChannel#controlchange-omnimodeon
*
* @type {object}
* @property {string} type `controlchange-omnimodeon`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-monomodeon** MIDI message has been
* received.
*
* @event InputChannel#controlchange-monomodeon
*
* @type {object}
* @property {string} type `controlchange-monomodeon`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
/**
* Event emitted when a **controlchange-polymodeon** MIDI message has been
* received.
*
* @event InputChannel#controlchange-polymodeon
*
* @type {object}
* @property {string} type `controlchange-polymodeon`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {object} controller
* @property {object} controller.number The number of the controller.
* @property {object} controller.name The usual name or function of the controller.
* @property {object} controller.description A user-friendly representation of the
* controller's default function
* @property {string} controller.position Whether the controller is meant to be an `msb` or `lsb`
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The value expressed as an integer (between 0 and 127).
*/
const namedEvent = Object.assign({}, event);
namedEvent.type = `${event.type}-` + Enumerations.CONTROL_CHANGE_MESSAGES[data1].name;
delete namedEvent.subtype;
// Dispatch controlchange-"function" events only if the "function" is defined (not the generic
// controllerXXX nomenclature)
if (namedEvent.type.indexOf("controller") !== 0) {
this.emit(namedEvent.type, namedEvent);
}
// Trigger channel mode message events (if appropriate)
if (event.message.dataBytes[0] >= 120) this._parseChannelModeMessage(event);
// Parse the inbound event to see if its part of an RPN/NRPN sequence
if (this.parameterNumberEventsEnabled && this._isRpnOrNrpnController(event.message.dataBytes[0])) {
this._parseEventForParameterNumber(event);
}
} else if (event.type === "programchange") {
/**
* Event emitted when a **program change** MIDI message has been received.
*
* @event InputChannel#programchange
*
* @type {object}
* @property {string} type `programchange`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {number} value The value expressed as an integer between 0 and 127.
* @property {number} rawValue The raw MIDI value expressed as an integer between 0 and 127.
*/
event.value = data1;
event.rawValue = event.value;
} else if (event.type === "channelaftertouch") {
/**
* Event emitted when a control change MIDI message has been received.
*
* @event InputChannel#channelaftertouch
*
* @type {object}
* @property {string} type `channelaftertouch`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The raw MIDI value expressed as an integer between 0 and 127.
*/
event.value = Utilities.from7bitToFloat(data1);
event.rawValue = data1;
} else if (event.type === "pitchbend") {
/**
* Event emitted when a pitch bend MIDI message has been received.
*
* @event InputChannel#pitchbend
*
* @type {object}
* @property {string} type `pitchbend`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {number} value The value expressed as a float between 0 and 1.
* @property {number} rawValue The raw MIDI value expressed as an integer (between 0 and
* 16383).
*/
event.value = ((data2 << 7) + data1 - 8192) / 8192;
event.rawValue = (data2 << 7) + data1;
} else {
event.type = "unknownmessage";
}
this.emit(event.type, event);
}
/**
* @param e {Object}
* @private
*/
_parseChannelModeMessage(e) {
// Make a shallow copy of the incoming event so we can use it as the new event.
const event = Object.assign({}, e);
event.type = event.controller.name;
/**
* Event emitted when an "all sound off" channel-mode MIDI message has been received.
*
* @event InputChannel#allsoundoff
*
* @type {object}
* @property {string} type `allsoundoff`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*/
/**
* Event emitted when a "reset all controllers" channel-mode MIDI message has been received.
*
* @event InputChannel#resetallcontrollers
*
* @type {object}
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*/
/**
* Event emitted when a "local control" channel-mode MIDI message has been received. The value
* property of the event is set to either `true` (local control on) of `false` (local control
* off).
*
* @event InputChannel#localcontrol
*
* @type {object}
* @property {string} type `localcontrol`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {boolean} value For local control on, the value is `true`. For local control off,
* the value is `false`.
* @property {boolean} rawValue For local control on, the value is `127`. For local control off,
* the value is `0`.
*/
if (event.type === "localcontrol") {
event.value = event.message.data[2] === 127 ? true : false;
event.rawValue = event.message.data[2];
}
/**
* Event emitted when an "all notes off" channel-mode MIDI message has been received.
*
* @event InputChannel#allnotesoff
*
* @type {object}
* @property {string} type `allnotesoff`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*/
/**
* Event emitted when an "omni mode" channel-mode MIDI message has been received. The value
* property of the event is set to either `true` (omni mode on) of `false` (omni mode off).
*
* @event InputChannel#omnimode
*
* @type {object}
* @property {string} type `"omnimode"`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {boolean} value The value is `true` for omni mode on and false for omni mode off.
* @property {boolean} rawValue The raw MIDI value
*/
if (event.type === "omnimodeon") {
event.type = "omnimode";
event.value = true;
event.rawValue = event.message.data[2];
} else if (event.type === "omnimodeoff") {
event.type = "omnimode";
event.value = false;
event.rawValue = event.message.data[2];
}
/**
* Event emitted when a "mono/poly mode" MIDI message has been received. The value property of
* the event is set to either `true` (mono mode on / poly mode off) or `false` (mono mode off /
* poly mode on).
*
* @event InputChannel#monomode
*
* @type {object}
* @property {string} type `monomode`
*
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
*
* @property {boolean} value The value is `true` for omni mode on and false for omni mode off.
* @property {boolean} rawValue The raw MIDI value
*/
if (event.type === "monomodeon") {
event.type = "monomode";
event.value = true;
event.rawValue = event.message.data[2];
} else if (event.type === "polymodeon") {
event.type = "monomode";
event.value = false;
event.rawValue = event.message.data[2];
}
this.emit(event.type, event);
}
/**
* Parses inbound events to identify RPN/NRPN sequences.
* @param e Event
* @private
*/
_parseEventForParameterNumber(event) {
// To make it more legible
const controller = event.message.dataBytes[0];
const value = event.message.dataBytes[1];
// A. Check if the message is the start of an RPN (101) or NRPN (99) parameter declaration.
if (controller === 99 || controller === 101) {
this._nrpnBuffer = [];
this._rpnBuffer = [];
if (controller === 99) {
// 99
this._nrpnBuffer = [event.message];
} else {
// 101
// 127 is a reset so we ignore it
if (value !== 127) this._rpnBuffer = [event.message];
}
// B. Check if the message is the end of an RPN (100) or NRPN (98) parameter declaration.
} else if (controller === 98 || controller === 100) {
if (controller === 98) {
// 98
// Flush the other buffer (they are mutually exclusive)
this._rpnBuffer = [];
// Check if we are in sequence
if (this._nrpnBuffer.length === 1) {
this._nrpnBuffer.push(event.message);
} else {
this._nrpnBuffer = []; // out of sequence
}
} else {
// 100
// Flush the other buffer (they are mutually exclusive)
this._nrpnBuffer = [];
// 127 is a reset so we ignore it
if (this._rpnBuffer.length === 1 && value !== 127) {
this._rpnBuffer.push(event.message);
} else {
this._rpnBuffer = []; // out of sequence or reset
}
}
// C. Check if the message is for data entry (6, 38, 96 or 97). Those messages trigger events.
} else if (controller === 6 || controller === 38 || controller === 96 || controller === 97) {
if (this._rpnBuffer.length === 2) {
this._dispatchParameterNumberEvent("rpn", this._rpnBuffer[0].dataBytes[1], this._rpnBuffer[1].dataBytes[1], event);
} else if (this._nrpnBuffer.length === 2) {
this._dispatchParameterNumberEvent("nrpn", this._nrpnBuffer[0].dataBytes[1], this._nrpnBuffer[1].dataBytes[1], event);
} else {
this._nrpnBuffer = [];
this._rpnBuffer = [];
}
}
}
/**
* Indicates whether the specified controller can be part of an RPN or NRPN sequence
* @param controller
* @returns {boolean}
* @private
*/
_isRpnOrNrpnController(controller) {
return controller === 6 || controller === 38 || controller === 96 || controller === 97 || controller === 98 || controller === 99 || controller === 100 || controller === 101;
}
/**
* @private
*/
_dispatchParameterNumberEvent(type, paramMsb, paramLsb, e) {
type = type === "nrpn" ? "nrpn" : "rpn";
/**
* Event emitted when an **RPN data entry coarse** message is received on the input. The
* specific parameter to which the message applies can be found in the event's `parameter`
* property. It is one of the ones defined in
* [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS).
*
* @event InputChannel#rpn-dataentrycoarse
*
* @type {object}
*
* @property {string} type `rpn-dataentrycoarse`
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {string} parameter The registered parameter's name
* @property {number} parameterMsb The MSB portion of the registered parameter (0-127)
* @property {number} parameterLsb: The LSB portion of the registered parameter (0-127)
* @property {number} value The received value as a normalized number between 0 and 1.
* @property {number} rawValue The value as received (0-127)
*/
/**
* Event emitted when an **RPN data entry fine** message is received on the input. The
* specific parameter to which the message applies can be found in the event's `parameter`
* property. It is one of the ones defined in
* [`EnumerationsREGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS).
*
* @event InputChannel#rpn-dataentryfine
*
* @type {object}
*
* @property {string} type `rpn-dataentryfine`
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {string} parameter The registered parameter's name
* @property {number} parameterMsb The MSB portion of the registered parameter (0-127)
* @property {number} parameterLsb: The LSB portion of the registered parameter (0-127)
* @property {number} value The received value as a normalized number between 0 and 1.
* @property {number} rawValue The value as received (0-127)
*/
/**
* Event emitted when an **RPN data increment** message is received on the input. The specific
* parameter to which the message applies can be found in the event's `parameter` property. It
* is one of the ones defined in
* [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS).
*
* @event InputChannel#rpn-dataincrement
*
* @type {object}
*
* @property {string} type `rpn-dataincrement`
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {string} parameter The registered parameter's name
* @property {number} parameterMsb The MSB portion of the registered parameter (0-127)
* @property {number} parameterLsb: The LSB portion of the registered parameter (0-127)
* @property {number} value The received value as a normalized number between 0 and 1.
* @property {number} rawValue The value as received (0-127)
*/
/**
* Event emitted when an **RPN data decrement** message is received on the input. The specific
* parameter to which the message applies can be found in the event's `parameter` property. It
* is one of the ones defined in
* [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS).
*
* @event InputChannel#rpn-datadecrement
*
* @type {object}
*
* @property {string} type `rpn-datadecrement`
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {string} parameter The registered parameter's name
* @property {number} parameterMsb The MSB portion of the registered parameter (0-127)
* @property {number} parameterLsb: The LSB portion of the registered parameter (0-127)
* @property {number} value The received value as a normalized number between 0 and 1.
* @property {number} rawValue The value as received (0-127)
*/
/**
* Event emitted when an **NRPN data entry coarse** message is received on the input. The
* specific parameter to which the message applies can be found in the event's `parameter`
* property. It is one of the ones defined in
* [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS).
*
* @event InputChannel#nrpn-dataentrycoarse
*
* @type {object}
*
* @property {string} type `nrpn-dataentrycoarse`
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {string} parameter The registered parameter's name
* @property {number} parameterMsb The MSB portion of the registered parameter (0-127)
* @property {number} parameterLsb: The LSB portion of the registered parameter (0-127)
* @property {number} value The received value as a normalized number between 0 and 1.
* @property {number} rawValue The value as received (0-127)
*/
/**
* Event emitted when an **NRPN data entry fine** message is received on the input. The
* specific parameter to which the message applies can be found in the event's `parameter`
* property. It is one of the ones defined in
* [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS).
*
* @event InputChannel#nrpn-dataentryfine
*
* @type {object}
*
* @property {string} type `nrpn-dataentryfine`
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {string} parameter The registered parameter's name
* @property {number} parameterMsb The MSB portion of the registered parameter (0-127)
* @property {number} parameterLsb: The LSB portion of the registered parameter (0-127)
* @property {number} value The received value as a normalized number between 0 and 1.
* @property {number} rawValue The value as received (0-127)
*/
/**
* Event emitted when an **NRPN data increment** message is received on the input. The specific
* parameter to which the message applies can be found in the event's `parameter` property. It
* is one of the ones defined in
* [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS).
*
* @event InputChannel#nrpn-dataincrement
*
* @type {object}
*
* @property {string} type `nrpn-dataincrement`
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {string} parameter The registered parameter's name
* @property {number} parameterMsb The MSB portion of the registered parameter (0-127)
* @property {number} parameterLsb: The LSB portion of the registered parameter (0-127)
* @property {number} value The received value as a normalized number between 0 and 1.
* @property {number} rawValue The value as received (0-127)
*/
/**
* Event emitted when an **NRPN data decrement** message is received on the input. The specific
* parameter to which the message applies can be found in the event's `parameter` property. It
* is one of the ones defined in
* [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS).
*
* @event InputChannel#nrpn-datadecrement
*
* @type {object}
*
* @property {string} type `nrpn-datadecrement`
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {string} parameter The registered parameter's name
* @property {number} parameterMsb The MSB portion of the registered parameter (0-127)
* @property {number} parameterLsb: The LSB portion of the registered parameter (0-127)
* @property {number} value The received value as a normalized number between 0 and 1.
* @property {number} rawValue The value as received (0-127)
*/
const event = {
target: e.target,
timestamp: e.timestamp,
message: e.message,
parameterMsb: paramMsb,
parameterLsb: paramLsb,
value: Utilities.from7bitToFloat(e.message.dataBytes[1]),
rawValue: e.message.dataBytes[1]
};
// Identify the parameter (by name for RPN and by number for NRPN)
if (type === "rpn") {
event.parameter = Object.keys(Enumerations.REGISTERED_PARAMETERS).find(key => {
return Enumerations.REGISTERED_PARAMETERS[key][0] === paramMsb && Enumerations.REGISTERED_PARAMETERS[key][1] === paramLsb;
});
} else {
event.parameter = (paramMsb << 7) + paramLsb;
}
// Type and subtype
const subtype = Enumerations.CONTROL_CHANGE_MESSAGES[e.message.dataBytes[0]].name;
// Emit specific event
event.type = `${type}-${subtype}`;
this.emit(event.type, event);
// Begin Legacy Block (remove in v4)
const legacyEvent = Object.assign({}, event);
if (legacyEvent.type === "nrpn-dataincrement") {
legacyEvent.type = "nrpn-databuttonincrement";
} else if (legacyEvent.type === "nrpn-datadecrement") {
legacyEvent.type = "nrpn-databuttondecrement";
} else if (legacyEvent.type === "rpn-dataincrement") {
legacyEvent.type = "rpn-databuttonincrement";
} else if (legacyEvent.type === "rpn-datadecrement") {
legacyEvent.type = "rpn-databuttondecrement";
}
this.emit(legacyEvent.type, legacyEvent);
// End Legacy Block
/**
* Event emitted when any NRPN message is received on the input. There are four subtypes of NRPN
* messages:
*
* * `nrpn-dataentrycoarse`
* * `nrpn-dataentryfine`
* * `nrpn-dataincrement`
* * `nrpn-datadecrement`
*
* The parameter to which the message applies can be found in the event's `parameter` property.
*
* @event InputChannel#nrpn
*
* @type {object}
*
* @property {string} type `nrpn`
* @property {string} subtype The precise type of NRPN message that was received.
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} parameter The non-registered parameter number (0-16383)
* @property {number} parameterMsb The MSB portion of the non-registered parameter number
* (0-127)
* @property {number} parameterLsb: The LSB portion of the non-registered parameter number
* (0-127)
* @property {number} value The received value as a normalized number between 0 and 1.
* @property {number} rawValue The value as received (0-127)
*/
/**
* Event emitted when any RPN message is received on the input. There are four subtypes of RPN
* messages:
*
* * `rpn-dataentrycoarse`
* * `rpn-dataentryfine`
* * `rpn-dataincrement`
* * `rpn-datadecrement`
*
* The parameter to which the message applies can be found in the event's `parameter` property.
* It is one of the ones defined in
* [`Enumerations.REGISTERED_PARAMETERS`](Enumerations#REGISTERED_PARAMETERS).
*
* @event InputChannel#rpn
*
* @type {object}
*
* @property {string} type `rpn`
* @property {string} subtype The precise type of RPN message that was received.
* @property {InputChannel} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {string} parameter The registered parameter's name
* @property {number} parameterMsb The MSB portion of the registered parameter (0-127)
* @property {number} parameterLsb: The LSB portion of the registered parameter (0-127)
* @property {number} value The received value as a normalized number between 0 and 1.
* @property {number} rawValue The value as received (0-127)
*/
// Emit general event
event.type = type;
event.subtype = subtype;
this.emit(event.type, event);
}
/**
* @deprecated since version 3.
* @private
*/
getChannelModeByNumber(number) {
if (wm.validation) {
console.warn("The 'getChannelModeByNumber()' method has been moved to the 'Utilities' class.");
number = Math.floor(number);
}
return Utilities.getChannelModeByNumber(number);
}
/**
* @deprecated since version 3.
* @private
*/
getCcNameByNumber(number) {
if (wm.validation) {
console.warn("The 'getCcNameByNumber()' method has been moved to the 'Utilities' class.");
number = parseInt(number);
if (!(number >= 0 && number <= 127)) throw new RangeError("Invalid control change number.");
}
return Utilities.getCcNameByNumber(number);
}
/**
* Returns the playing status of the specified note (`true` if the note is currently playing,
* `false` if it is not). The `note` parameter can be an unsigned integer (0-127), a note
* identifier (`"C4"`, `"G#5"`, etc.) or a [`Note`]{@link Note} object.
*
* IF the note is specified using an integer (0-127), no octave offset will be applied.
*
* @param {number|string|Note} note The note to get the state for. The
* [`octaveOffset`](#octaveOffset) (channel, input and global) will be factored in for note
* identifiers and [`Note`]{@link Note} objects.
* @returns {boolean}
* @since version 3.0.0
*/
getNoteState(note) {
// If it's a note object, we simply use the identifier
if (note instanceof Note) note = note.identifier;
const number = Utilities.guessNoteNumber(note, wm.octaveOffset + this.input.octaveOffset + this.octaveOffset);
return this.notesState[number];
}
/**
* An integer to offset the reported octave of incoming note-specific messages (`noteon`,
* `noteoff` and `keyaftertouch`). By default, middle C (MIDI note number 60) is placed on the 4th
* octave (C4).
*
* If, for example, `octaveOffset` is set to 2, MIDI note number 60 will be reported as C6. If
* `octaveOffset` is set to -1, MIDI note number 60 will be reported as C3.
*
* Note that this value is combined with the global offset value defined by
* [`WebMidi.octaveOffset`](WebMidi#octaveOffset) object and with the value defined on the parent
* input object with [`Input.octaveOffset`](Input#octaveOffset).
*
* @type {number}
*
* @since 3.0
*/
get octaveOffset() {
return this._octaveOffset;
}
set octaveOffset(value) {
if (this.validation) {
value = parseInt(value);
if (isNaN(value)) throw new TypeError("The 'octaveOffset' property must be an integer.");
}
this._octaveOffset = value;
}
/**
* The [`Input`](Input) this channel belongs to.
* @type {Input}
* @since 3.0
*/
get input() {
return this._input;
}
/**
* This channel's MIDI number (1-16).
* @type {number}
* @since 3.0
*/
get number() {
return this._number;
}
/**
* Whether RPN/NRPN events are parsed and dispatched.
* @type {boolean}
* @since 3.0
* @deprecated Use parameterNumberEventsEnabled instead.
* @private
*/
get nrpnEventsEnabled() {
return this.parameterNumberEventsEnabled;
}
set nrpnEventsEnabled(value) {
if (this.validation) {
value = !!value;
}
this.parameterNumberEventsEnabled = value;
}
}
/**
* The `Message` class represents a single MIDI message. It has several properties that make it
* easy to make sense of the binary data it contains.
*
* @license Apache-2.0
* @since 3.0.0
*/
class Message {
/**
* Creates a new `Message` object from raw MIDI data.
*
* @param {Uint8Array} data The raw data of the MIDI message as a
* [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)
* of integers between `0` and `255`.
*/
constructor(data) {
/**
* A
* [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)
* containing the bytes of the MIDI message. Each byte is an integer between `0` and `255`.
*
* @type {Uint8Array}
* @readonly
*/
this.rawData = data;
/**
* An array containing all the bytes of the MIDI message. Each byte is an integer between `0`
* and `255`.
*
* @type {number[]}
* @readonly
*/
this.data = Array.from(this.rawData);
/**
* The MIDI status byte of the message as an integer between `0` and `255`.
*
* @type {number}
* @readonly
*/
this.statusByte = this.rawData[0];
/**
* A
* [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)
* of the data byte(s) of the MIDI message. When the message is a system exclusive message
* (sysex), `rawDataBytes` explicitly excludes the manufacturer ID and the sysex end byte so
* only the actual data is included.
*
* @type {Uint8Array}
* @readonly
*/
this.rawDataBytes = this.rawData.slice(1);
/**
* An array of the the data byte(s) of the MIDI message (as opposed to the status byte). When
* the message is a system exclusive message (sysex), `dataBytes` explicitly excludes the
* manufacturer ID and the sysex end byte so only the actual data is included.
*
* @type {number[]}
* @readonly
*/
this.dataBytes = this.data.slice(1);
/**
* A boolean indicating whether the MIDI message is a channel-specific message.
*
* @type {boolean}
* @readonly
*/
this.isChannelMessage = false;
/**
* A boolean indicating whether the MIDI message is a system message (not specific to a
* channel).
*
* @type {boolean}
* @readonly
*/
this.isSystemMessage = false;
/**
* An integer identifying the MIDI command. For channel-specific messages, the value is 4-bit
* and will be between `8` and `14`. For system messages, the value will be between `240` and
* `255`.
*
* @type {number}
* @readonly
*/
this.command = undefined;
/**
* The MIDI channel number (`1` - `16`) that the message is targeting. This is only for
* channel-specific messages. For system messages, this will be left `undefined`.
*
* @type {number}
* @readonly
*/
this.channel = undefined;
/**
* When the message is a system exclusive message (sysex), this property contains an array with
* either 1 or 3 entries that identify the manufacturer targeted by the message.
*
* To know how to translate these entries into manufacturer names, check out the official list:
* https://www.midi.org/specifications-old/item/manufacturer-id-numbers
*
* @type {number[]}
* @readonly
*/
this.manufacturerId = undefined;
/**
* The type of message as a string (`"noteon"`, `"controlchange"`, `"sysex"`, etc.)
*
* @type {string}
* @readonly
*/
this.type = undefined;
// Assign values to property that vary according to whether they are channel-specific or system
if (this.statusByte < 240) {
this.isChannelMessage = true;
this.command = this.statusByte >> 4;
this.channel = (this.statusByte & 0b00001111) + 1;
} else {
this.isSystemMessage = true;
this.command = this.statusByte;
}
// Assign type (depending on whether the message is channel-specific or system)
if (this.isChannelMessage) {
this.type = Utilities.getPropertyByValue(Enumerations.CHANNEL_MESSAGES, this.command);
} else if (this.isSystemMessage) {
this.type = Utilities.getPropertyByValue(Enumerations.SYSTEM_MESSAGES, this.command);
}
// When the message is a sysex message, we add a manufacturer property and strip out the id from
// dataBytes and rawDataBytes.
if (this.statusByte === Enumerations.SYSTEM_MESSAGES.sysex) {
if (this.dataBytes[0] === 0) {
this.manufacturerId = this.dataBytes.slice(0, 3);
this.dataBytes = this.dataBytes.slice(3, this.rawDataBytes.length - 1);
this.rawDataBytes = this.rawDataBytes.slice(3, this.rawDataBytes.length - 1);
} else {
this.manufacturerId = [this.dataBytes[0]];
this.dataBytes = this.dataBytes.slice(1, this.dataBytes.length - 1);
this.rawDataBytes = this.rawDataBytes.slice(1, this.rawDataBytes.length - 1);
}
}
}
}
/**
* The `Input` class represents a single MIDI input port. This object is automatically instantiated
* by the library according to the host's MIDI subsystem and does not need to be directly
* instantiated. Instead, you can access all `Input` objects by referring to the
* [`WebMidi.inputs`](WebMidi#inputs) array. You can also retrieve inputs by using methods such as
* [`WebMidi.getInputByName()`](WebMidi#getInputByName) and
* [`WebMidi.getInputById()`](WebMidi#getInputById).
*
* Note that a single MIDI device may expose several inputs and/or outputs.
*
* **Important**: the `Input` class does not directly fire channel-specific MIDI messages
* (such as [`noteon`](InputChannel#event:noteon) or
* [`controlchange`](InputChannel#event:controlchange), etc.). The [`InputChannel`](InputChannel)
* object does that. However, you can still use the
* [`Input.addListener()`](#addListener) method to listen to channel-specific events on multiple
* [`InputChannel`](InputChannel) objects at once.
*
* @fires Input#opened
* @fires Input#disconnected
* @fires Input#closed
* @fires Input#midimessage
*
* @fires Input#sysex
* @fires Input#timecode
* @fires Input#songposition
* @fires Input#songselect
* @fires Input#tunerequest
* @fires Input#clock
* @fires Input#start
* @fires Input#continue
* @fires Input#stop
* @fires Input#activesensing
* @fires Input#reset
*
* @fires Input#unknownmidimessage
*
* @extends EventEmitter
* @license Apache-2.0
*/
class Input extends EventEmitter {
/**
* Creates an `Input` object.
*
* @param {MIDIInput} midiInput [`MIDIInput`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIInput)
* object as provided by the MIDI subsystem (Web MIDI API).
*/
constructor(midiInput) {
super();
/**
* Reference to the actual MIDIInput object
* @private
*/
this._midiInput = midiInput;
/**
* @type {number}
* @private
*/
this._octaveOffset = 0;
/**
* Array containing the 16 [`InputChannel`](InputChannel) objects available for this `Input`. The
* channels are numbered 1 through 16.
*
* @type {InputChannel[]}
*/
this.channels = [];
for (let i = 1; i <= 16; i++) this.channels[i] = new InputChannel(this, i);
/**
* @type {Forwarder[]}
* @private
*/
this._forwarders = [];
// Setup listeners
this._midiInput.onstatechange = this._onStateChange.bind(this);
this._midiInput.onmidimessage = this._onMidiMessage.bind(this);
}
/**
* Destroys the `Input` by removing all listeners, emptying the [`channels`](#channels) array and
* unlinking the MIDI subsystem. This is mostly for internal use.
*
* @returns {Promise<void>}
*/
async destroy() {
this.removeListener();
this.channels.forEach(ch => ch.destroy());
this.channels = [];
this._forwarders = [];
if (this._midiInput) {
this._midiInput.onstatechange = null;
this._midiInput.onmidimessage = null;
}
await this.close();
this._midiInput = null;
}
/**
* Executed when a `"statechange"` event occurs.
*
* @param e
* @private
*/
_onStateChange(e) {
let event = {
timestamp: wm.time,
target: this,
port: this // for consistency
};
if (e.port.connection === "open") {
/**
* Event emitted when the `Input` has been opened by calling the [`open()`]{@link #open}
* method.
*
* @event Input#opened
* @type {object}
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `opened`
* @property {Input} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
*/
event.type = "opened";
this.emit("opened", event);
} else if (e.port.connection === "closed" && e.port.state === "connected") {
/**
* Event emitted when the `Input` has been closed by calling the
* [`close()`]{@link #close} method.
*
* @event Input#closed
* @type {object}
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `closed`
* @property {Input} target The object that dispatched the event.
* @property {Input} port The `Input` that triggered the event.
*/
event.type = "closed";
this.emit("closed", event);
} else if (e.port.connection === "closed" && e.port.state === "disconnected") {
/**
* Event emitted when the `Input` becomes unavailable. This event is typically fired
* when the MIDI device is unplugged.
*
* @event Input#disconnected
* @type {object}
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `disconnected`
* @property {Input} port Object with properties describing the {@link Input} that was
* disconnected. This is not the actual `Input` as it is no longer available.
* @property {Input} target The object that dispatched the event.
*/
event.type = "disconnected";
event.port = {
connection: e.port.connection,
id: e.port.id,
manufacturer: e.port.manufacturer,
name: e.port.name,
state: e.port.state,
type: e.port.type
};
this.emit("disconnected", event);
} else if (e.port.connection === "pending" && e.port.state === "disconnected") ; else {
console.warn("This statechange event was not caught: ", e.port.connection, e.port.state);
}
}
/**
* Executed when a `"midimessage"` event is received
* @param e
* @private
*/
_onMidiMessage(e) {
// Create Message object from MIDI data
const message = new Message(e.data);
/**
* Event emitted when any MIDI message is received on an `Input`.
*
* @event Input#midimessage
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `midimessage`
*
* @since 2.1
*/
const event = {
port: this,
target: this,
message: message,
timestamp: e.timeStamp,
type: "midimessage",
data: message.data,
// @deprecated (will be removed in v4)
rawData: message.data,
// @deprecated (will be removed in v4)
statusByte: message.data[0],
// @deprecated (will be removed in v4)
dataBytes: message.dataBytes // @deprecated (will be removed in v4)
};
this.emit("midimessage", event);
// Messages are forwarded to InputChannel if they are channel messages or parsed locally for
// system messages.
if (message.isSystemMessage) {
// system messages
this._parseEvent(event);
} else if (message.isChannelMessage) {
// channel messages
this.channels[message.channel]._processMidiMessageEvent(event);
}
// Forward message if forwarders have been defined
this._forwarders.forEach(forwarder => forwarder.forward(message));
}
/**
* @private
*/
_parseEvent(e) {
// Make a shallow copy of the incoming event so we can use it as the new event.
const event = Object.assign({}, e);
event.type = event.message.type || "unknownmidimessage";
// Add custom property for 'songselect'
if (event.type === "songselect") {
event.song = e.data[1] + 1; // deprecated
event.value = e.data[1];
event.rawValue = event.value;
}
// Emit event
this.emit(event.type, event);
}
/**
* Opens the input for usage. This is usually unnecessary as the port is opened automatically when
* WebMidi is enabled.
*
* @returns {Promise<Input>} The promise is fulfilled with the `Input` object.
*/
async open() {
// Explicitly opens the port for usage. This is not mandatory. When the port is not explicitly
// opened, it is implicitly opened (asynchronously) when assigning a listener to the
// `onmidimessage` property of the `MIDIInput`. We do it explicitly so that 'connected' events
// are dispatched immediately and that we are ready to listen.
try {
await this._midiInput.open();
} catch (err) {
return Promise.reject(err);
}
return Promise.resolve(this);
}
/**
* Closes the input. When an input is closed, it cannot be used to listen to MIDI messages until
* the input is opened again by calling [`Input.open()`](Input#open).
*
* **Note**: if what you want to do is stop events from being dispatched, you should use
* [`eventsSuspended`](#eventsSuspended) instead.
*
* @returns {Promise<Input>} The promise is fulfilled with the `Input` object
*/
async close() {
// We close the port. This triggers a statechange event which, in turn, will emit the 'closed'
// event.
if (!this._midiInput) return Promise.resolve(this);
try {
await this._midiInput.close();
} catch (err) {
return Promise.reject(err);
}
return Promise.resolve(this);
}
/**
* @private
* @deprecated since v3.0.0 (moved to 'Utilities' class)
*/
getChannelModeByNumber() {
if (wm.validation) {
console.warn("The 'getChannelModeByNumber()' method has been moved to the 'Utilities' class.");
}
}
/**
* Adds an event listener that will trigger a function callback when the specified event is
* dispatched. The event usually is **input-wide** but can also be **channel-specific**.
*
* Input-wide events do not target a specific MIDI channel so it makes sense to listen for them
* at the `Input` level and not at the [`InputChannel`](InputChannel) level. Channel-specific
* events target a specific channel. Usually, in this case, you would add the listener to the
* [`InputChannel`](InputChannel) object. However, as a convenience, you can also listen to
* channel-specific events directly on an `Input`. This allows you to react to a channel-specific
* event no matter which channel it actually came through.
*
* When listening for an event, you simply need to specify the event name and the function to
* execute:
*
* ```javascript
* const listener = WebMidi.inputs[0].addListener("midimessage", e => {
* console.log(e);
* });
* ```
*
* Calling the function with an input-wide event (such as
* [`"midimessage"`]{@link #event:midimessage}), will return the [`Listener`](Listener) object
* that was created.
*
* If you call the function with a channel-specific event (such as
* [`"noteon"`]{@link InputChannel#event:noteon}), it will return an array of all
* [`Listener`](Listener) objects that were created (one for each channel):
*
* ```javascript
* const listeners = WebMidi.inputs[0].addListener("noteon", someFunction);
* ```
*
* You can also specify which channels you want to add the listener to:
*
* ```javascript
* const listeners = WebMidi.inputs[0].addListener("noteon", someFunction, {channels: [1, 2, 3]});
* ```
*
* In this case, `listeners` is an array containing 3 [`Listener`](Listener) objects. The order of
* the listeners in the array follows the order the channels were specified in.
*
* Note that, when adding channel-specific listeners, it is the [`InputChannel`](InputChannel)
* instance that actually gets a listener added and not the `Input` instance. You can check that
* by calling [`InputChannel.hasListener()`](InputChannel#hasListener()).
*
* There are 8 families of events you can listen to:
*
* 1. **MIDI System Common** Events (input-wide)
*
* * [`songposition`]{@link Input#event:songposition}
* * [`songselect`]{@link Input#event:songselect}
* * [`sysex`]{@link Input#event:sysex}
* * [`timecode`]{@link Input#event:timecode}
* * [`tunerequest`]{@link Input#event:tunerequest}
*
* 2. **MIDI System Real-Time** Events (input-wide)
*
* * [`clock`]{@link Input#event:clock}
* * [`start`]{@link Input#event:start}
* * [`continue`]{@link Input#event:continue}
* * [`stop`]{@link Input#event:stop}
* * [`activesensing`]{@link Input#event:activesensing}
* * [`reset`]{@link Input#event:reset}
*
* 3. **State Change** Events (input-wide)
*
* * [`opened`]{@link Input#event:opened}
* * [`closed`]{@link Input#event:closed}
* * [`disconnected`]{@link Input#event:disconnected}
*
* 4. **Catch-All** Events (input-wide)
*
* * [`midimessage`]{@link Input#event:midimessage}
* * [`unknownmidimessage`]{@link Input#event:unknownmidimessage}
*
* 5. **Channel Voice** Events (channel-specific)
*
* * [`channelaftertouch`]{@link InputChannel#event:channelaftertouch}
* * [`controlchange`]{@link InputChannel#event:controlchange}
* * [`controlchange-controller0`]{@link InputChannel#event:controlchange-controller0}
* * [`controlchange-controller1`]{@link InputChannel#event:controlchange-controller1}
* * [`controlchange-controller2`]{@link InputChannel#event:controlchange-controller2}
* * (...)
* * [`controlchange-controller127`]{@link InputChannel#event:controlchange-controller127}
* * [`keyaftertouch`]{@link InputChannel#event:keyaftertouch}
* * [`noteoff`]{@link InputChannel#event:noteoff}
* * [`noteon`]{@link InputChannel#event:noteon}
* * [`pitchbend`]{@link InputChannel#event:pitchbend}
* * [`programchange`]{@link InputChannel#event:programchange}
*
* Note: you can listen for a specific control change message by using an event name like this:
* `controlchange-controller23`, `controlchange-controller99`, `controlchange-controller122`,
* etc.
*
* 6. **Channel Mode** Events (channel-specific)
*
* * [`allnotesoff`]{@link InputChannel#event:allnotesoff}
* * [`allsoundoff`]{@link InputChannel#event:allsoundoff}
* * [`localcontrol`]{@link InputChannel#event:localcontrol}
* * [`monomode`]{@link InputChannel#event:monomode}
* * [`omnimode`]{@link InputChannel#event:omnimode}
* * [`resetallcontrollers`]{@link InputChannel#event:resetallcontrollers}
*
* 7. **NRPN** Events (channel-specific)
*
* * [`nrpn`]{@link InputChannel#event:nrpn}
* * [`nrpn-dataentrycoarse`]{@link InputChannel#event:nrpn-dataentrycoarse}
* * [`nrpn-dataentryfine`]{@link InputChannel#event:nrpn-dataentryfine}
* * [`nrpn-dataincrement`]{@link InputChannel#event:nrpn-dataincrement}
* * [`nrpn-datadecrement`]{@link InputChannel#event:nrpn-datadecrement}
*
* 8. **RPN** Events (channel-specific)
*
* * [`rpn`]{@link InputChannel#event:rpn}
* * [`rpn-dataentrycoarse`]{@link InputChannel#event:rpn-dataentrycoarse}
* * [`rpn-dataentryfine`]{@link InputChannel#event:rpn-dataentryfine}
* * [`rpn-dataincrement`]{@link InputChannel#event:rpn-dataincrement}
* * [`rpn-datadecrement`]{@link InputChannel#event:rpn-datadecrement}
*
* @param event {string | EventEmitter.ANY_EVENT} The type of the event.
*
* @param listener {function} A callback function to execute when the specified event is detected.
* This function will receive an event parameter object. For details on this object's properties,
* check out the documentation for the various events (links above).
*
* @param {object} [options={}]
*
* @param {array} [options.arguments] An array of arguments which will be passed separately to the
* callback function. This array is stored in the [`arguments`](Listener#arguments) property of
* the [`Listener`](Listener) object and can be retrieved or modified as desired.
*
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* An integer between 1 and 16 or an array of such integers representing the MIDI channel(s) to
* listen on. If no channel is specified, all channels will be used. This parameter is ignored for
* input-wide events.
*
* @param {object} [options.context=this] The value of `this` in the callback function.
*
* @param {number} [options.duration=Infinity] The number of milliseconds before the listener
* automatically expires.
*
* @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning
* of the listeners array and thus be triggered before others.
*
* @param {number} [options.remaining=Infinity] The number of times after which the callback
* should automatically be removed.
*
* @returns {Listener|Listener[]} If the event is input-wide, a single [`Listener`](Listener)
* object is returned. If the event is channel-specific, an array of all the
* [`Listener`](Listener) objects is returned (one for each channel).
*/
addListener(event, listener, options = {}) {
if (wm.validation) {
// Legacy compatibility
if (typeof options === "function") {
let channels = listener != undefined ? [].concat(listener) : undefined; // clone
listener = options;
options = {
channels: channels
};
}
}
// Check if the event is channel-specific or input-wide
if (Enumerations.CHANNEL_EVENTS.includes(event)) {
// If no channel defined, use all.
if (options.channels === undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
let listeners = [];
Utilities.sanitizeChannels(options.channels).forEach(ch => {
listeners.push(this.channels[ch].addListener(event, listener, options));
});
return listeners;
} else {
return super.addListener(event, listener, options);
}
}
/**
* Adds a one-time event listener that will trigger a function callback when the specified event
* happens. The event can be **channel-bound** or **input-wide**. Channel-bound events are
* dispatched by [`InputChannel`]{@link InputChannel} objects and are tied to a specific MIDI
* channel while input-wide events are dispatched by the `Input` object itself and are not tied
* to a specific channel.
*
* Calling the function with an input-wide event (such as
* [`"midimessage"`]{@link #event:midimessage}), will return the [`Listener`](Listener) object
* that was created.
*
* If you call the function with a channel-specific event (such as
* [`"noteon"`]{@link InputChannel#event:noteon}), it will return an array of all
* [`Listener`](Listener) objects that were created (one for each channel):
*
* ```javascript
* const listeners = WebMidi.inputs[0].addOneTimeListener("noteon", someFunction);
* ```
*
* You can also specify which channels you want to add the listener to:
*
* ```javascript
* const listeners = WebMidi.inputs[0].addOneTimeListener("noteon", someFunction, {channels: [1, 2, 3]});
* ```
*
* In this case, the `listeners` variable contains an array of 3 [`Listener`](Listener) objects.
*
* The code above will add a listener for the `"noteon"` event and call `someFunction` when the
* event is triggered on MIDI channels `1`, `2` or `3`.
*
* Note that, when adding events to channels, it is the [`InputChannel`](InputChannel) instance
* that actually gets a listener added and not the `Input` instance.
*
* Note: if you want to add a listener to a single MIDI channel you should probably do so directly
* on the [`InputChannel`](InputChannel) object itself.
*
* There are 8 families of events you can listen to:
*
* 1. **MIDI System Common** Events (input-wide)
*
* * [`songposition`]{@link Input#event:songposition}
* * [`songselect`]{@link Input#event:songselect}
* * [`sysex`]{@link Input#event:sysex}
* * [`timecode`]{@link Input#event:timecode}
* * [`tunerequest`]{@link Input#event:tunerequest}
*
* 2. **MIDI System Real-Time** Events (input-wide)
*
* * [`clock`]{@link Input#event:clock}
* * [`start`]{@link Input#event:start}
* * [`continue`]{@link Input#event:continue}
* * [`stop`]{@link Input#event:stop}
* * [`activesensing`]{@link Input#event:activesensing}
* * [`reset`]{@link Input#event:reset}
*
* 3. **State Change** Events (input-wide)
*
* * [`opened`]{@link Input#event:opened}
* * [`closed`]{@link Input#event:closed}
* * [`disconnected`]{@link Input#event:disconnected}
*
* 4. **Catch-All** Events (input-wide)
*
* * [`midimessage`]{@link Input#event:midimessage}
* * [`unknownmidimessage`]{@link Input#event:unknownmidimessage}
*
* 5. **Channel Voice** Events (channel-specific)
*
* * [`channelaftertouch`]{@link InputChannel#event:channelaftertouch}
* * [`controlchange`]{@link InputChannel#event:controlchange}
* * [`controlchange-controller0`]{@link InputChannel#event:controlchange-controller0}
* * [`controlchange-controller1`]{@link InputChannel#event:controlchange-controller1}
* * [`controlchange-controller2`]{@link InputChannel#event:controlchange-controller2}
* * (...)
* * [`controlchange-controller127`]{@link InputChannel#event:controlchange-controller127}
* * [`keyaftertouch`]{@link InputChannel#event:keyaftertouch}
* * [`noteoff`]{@link InputChannel#event:noteoff}
* * [`noteon`]{@link InputChannel#event:noteon}
* * [`pitchbend`]{@link InputChannel#event:pitchbend}
* * [`programchange`]{@link InputChannel#event:programchange}
*
* Note: you can listen for a specific control change message by using an event name like this:
* `controlchange-controller23`, `controlchange-controller99`, `controlchange-controller122`,
* etc.
*
* 6. **Channel Mode** Events (channel-specific)
*
* * [`allnotesoff`]{@link InputChannel#event:allnotesoff}
* * [`allsoundoff`]{@link InputChannel#event:allsoundoff}
* * [`localcontrol`]{@link InputChannel#event:localcontrol}
* * [`monomode`]{@link InputChannel#event:monomode}
* * [`omnimode`]{@link InputChannel#event:omnimode}
* * [`resetallcontrollers`]{@link InputChannel#event:resetallcontrollers}
*
* 7. **NRPN** Events (channel-specific)
*
* * [`nrpn`]{@link InputChannel#event:nrpn}
* * [`nrpn-dataentrycoarse`]{@link InputChannel#event:nrpn-dataentrycoarse}
* * [`nrpn-dataentryfine`]{@link InputChannel#event:nrpn-dataentryfine}
* * [`nrpn-dataincrement`]{@link InputChannel#event:nrpn-dataincrement}
* * [`nrpn-datadecrement`]{@link InputChannel#event:nrpn-datadecrement}
*
* 8. **RPN** Events (channel-specific)
*
* * [`rpn`]{@link InputChannel#event:rpn}
* * [`rpn-dataentrycoarse`]{@link InputChannel#event:rpn-dataentrycoarse}
* * [`rpn-dataentryfine`]{@link InputChannel#event:rpn-dataentryfine}
* * [`rpn-dataincrement`]{@link InputChannel#event:rpn-dataincrement}
* * [`rpn-datadecrement`]{@link InputChannel#event:rpn-datadecrement}
*
* @param event {string} The type of the event.
*
* @param listener {function} A callback function to execute when the specified event is detected.
* This function will receive an event parameter object. For details on this object's properties,
* check out the documentation for the various events (links above).
*
* @param {object} [options={}]
*
* @param {array} [options.arguments] An array of arguments which will be passed separately to the
* callback function. This array is stored in the [`arguments`](Listener#arguments) property of
* the [`Listener`](Listener) object and can be retrieved or modified as desired.
*
* @param {number|number[]} [options.channels] An integer between 1 and 16 or an array of
* such integers representing the MIDI channel(s) to listen on. This parameter is ignored for
* input-wide events.
*
* @param {object} [options.context=this] The value of `this` in the callback function.
*
* @param {number} [options.duration=Infinity] The number of milliseconds before the listener
* automatically expires.
*
* @param {boolean} [options.prepend=false] Whether the listener should be added at the beginning
* of the listeners array and thus be triggered before others.
*
* @returns {Listener[]} An array of all [`Listener`](Listener) objects that were created.
*/
addOneTimeListener(event, listener, options = {}) {
options.remaining = 1;
return this.addListener(event, listener, options);
}
/**
* This is an alias to the [Input.addListener()]{@link Input#addListener} method.
* @since 2.0.0
* @deprecated since v3.0
* @private
*/
on(event, channel, listener, options) {
return this.addListener(event, channel, listener, options);
}
/**
* Checks if the specified event type is already defined to trigger the specified callback
* function. For channel-specific events, the function will return `true` only if all channels
* have the listener defined.
*
* @param event {string|Symbol} The type of the event.
*
* @param listener {function} The callback function to check for.
*
* @param {object} [options={}]
*
* @param {number|number[]} [options.channels] An integer between 1 and 16 or an array of such
* integers representing the MIDI channel(s) to check. This parameter is ignored for input-wide
* events.
*
* @returns {boolean} Boolean value indicating whether or not the `Input` or
* [`InputChannel`](InputChannel) already has this listener defined.
*/
hasListener(event, listener, options = {}) {
if (wm.validation) {
// Legacy compatibility
if (typeof options === "function") {
let channels = [].concat(listener); // clone
listener = options;
options = {
channels: channels
};
}
}
if (Enumerations.CHANNEL_EVENTS.includes(event)) {
// If no channel defined, use all.
if (options.channels === undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
return Utilities.sanitizeChannels(options.channels).every(ch => {
return this.channels[ch].hasListener(event, listener);
});
} else {
return super.hasListener(event, listener);
}
}
/**
* Removes the specified event listener. If no listener is specified, all listeners matching the
* specified event will be removed. If the event is channel-specific, the listener will be removed
* from all [`InputChannel`]{@link InputChannel} objects belonging to that channel. If no event is
* specified, all listeners for the `Input` as well as all listeners for all
* [`InputChannel`]{@link InputChannel} objects belonging to the `Input` will be removed.
*
* By default, channel-specific listeners will be removed from all
* [`InputChannel`]{@link InputChannel} objects unless the `options.channel` narrows it down.
*
* @param [type] {string} The type of the event.
*
* @param [listener] {function} The callback function to check for.
*
* @param {object} [options={}]
*
* @param {number|number[]} [options.channels] An integer between 1 and 16 or an array of
* such integers representing the MIDI channel(s) to match. This parameter is ignored for
* input-wide events.
*
* @param {*} [options.context] Only remove the listeners that have this exact context.
*
* @param {number} [options.remaining] Only remove the listener if it has exactly that many
* remaining times to be executed.
*/
removeListener(event, listener, options = {}) {
if (wm.validation) {
// Legacy compatibility
if (typeof options === "function") {
let channels = [].concat(listener); // clone
listener = options;
options = {
channels: channels
};
}
}
if (options.channels === undefined) options.channels = Enumerations.MIDI_CHANNEL_NUMBERS;
// If the event is not specified, remove everything (channel-specific and input-wide)!
if (event == undefined) {
Utilities.sanitizeChannels(options.channels).forEach(ch => {
if (this.channels[ch]) this.channels[ch].removeListener();
});
return super.removeListener();
}
// If the event is specified, check if it's channel-specific or input-wide.
if (Enumerations.CHANNEL_EVENTS.includes(event)) {
Utilities.sanitizeChannels(options.channels).forEach(ch => {
this.channels[ch].removeListener(event, listener, options);
});
} else {
super.removeListener(event, listener, options);
}
}
/**
* Adds a forwarder that will forward all incoming MIDI messages matching the criteria to the
* specified [`Output`](Output) destination(s). This is akin to the hardware MIDI THRU port, with
* the added benefit of being able to filter which data is forwarded.
*
* @param {Output|Output[]} output An [`Output`](Output) object, or an array of such
* objects, to forward messages to.
* @param {object} [options={}]
* @param {string|string[]} [options.types=(all messages)] A message type, or an array of such
* types (`noteon`, `controlchange`, etc.), that the message type must match in order to be
* forwarded. If this option is not specified, all types of messages will be forwarded. Valid
* messages are the ones found in either
* [`SYSTEM_MESSAGES`](Enumerations#SYSTEM_MESSAGES) or
* [`CHANNEL_MESSAGES`](Enumerations#CHANNEL_MESSAGES).
* @param {number|number[]} [options.channels=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]]
* A MIDI channel number or an array of channel numbers that the message must match in order to be
* forwarded. By default all MIDI channels are included (`1` to `16`).
*
* @returns {Forwarder} The [`Forwarder`](Forwarder) object created to handle the forwarding. This
* is useful if you wish to manipulate or remove the [`Forwarder`](Forwarder) later on.
*/
addForwarder(output, options = {}) {
let forwarder;
// Unless 'output' is a forwarder, create a new forwarder
if (output instanceof Forwarder) {
forwarder = output;
} else {
forwarder = new Forwarder(output, options);
}
this._forwarders.push(forwarder);
return forwarder;
}
/**
* Removes the specified [`Forwarder`](Forwarder) object from the input.
*
* @param {Forwarder} forwarder The [`Forwarder`](Forwarder) to remove (the
* [`Forwarder`](Forwarder) object is returned when calling `addForwarder()`.
*/
removeForwarder(forwarder) {
this._forwarders = this._forwarders.filter(item => item !== forwarder);
}
/**
* Checks whether the specified [`Forwarder`](Forwarder) object has already been attached to this
* input.
*
* @param {Forwarder} forwarder The [`Forwarder`](Forwarder) to check for (the
* [`Forwarder`](Forwarder) object is returned when calling [`addForwarder()`](#addForwarder).
* @returns {boolean}
*/
hasForwarder(forwarder) {
return this._forwarders.includes(forwarder);
}
/**
* Name of the MIDI input.
*
* @type {string}
* @readonly
*/
get name() {
return this._midiInput.name;
}
/**
* ID string of the MIDI port. The ID is host-specific. Do not expect the same ID on different
* platforms. For example, Google Chrome and the Jazz-Plugin report completely different IDs for
* the same port.
*
* @type {string}
* @readonly
*/
get id() {
return this._midiInput.id;
}
/**
* Input port's connection state: `pending`, `open` or `closed`.
*
* @type {string}
* @readonly
*/
get connection() {
return this._midiInput.connection;
}
/**
* Name of the manufacturer of the device that makes this input port available.
*
* @type {string}
* @readonly
*/
get manufacturer() {
return this._midiInput.manufacturer;
}
/**
* An integer to offset the reported octave of incoming notes. By default, middle C (MIDI note
* number 60) is placed on the 4th octave (C4).
*
* If, for example, `octaveOffset` is set to 2, MIDI note number 60 will be reported as C6. If
* `octaveOffset` is set to -1, MIDI note number 60 will be reported as C3.
*
* Note that this value is combined with the global offset value defined in the
* [`WebMidi.octaveOffset`](WebMidi#octaveOffset) property (if any).
*
* @type {number}
*
* @since 3.0
*/
get octaveOffset() {
return this._octaveOffset;
}
set octaveOffset(value) {
if (this.validation) {
value = parseInt(value);
if (isNaN(value)) throw new TypeError("The 'octaveOffset' property must be an integer.");
}
this._octaveOffset = value;
}
/**
* State of the input port: `connected` or `disconnected`.
*
* @type {string}
* @readonly
*/
get state() {
return this._midiInput.state;
}
/**
* The port type. In the case of the `Input` object, this is always: `input`.
*
* @type {string}
* @readonly
*/
get type() {
return this._midiInput.type;
}
/**
* @type {boolean}
* @private
* @deprecated since v3.0.0 (moved to 'InputChannel' class)
*/
get nrpnEventsEnabled() {
if (wm.validation) {
console.warn("The 'nrpnEventsEnabled' property has been moved to the 'InputChannel' class.");
}
return false;
}
}
// Events that do not have code below them must be placed outside the class definition (?!)
/**
* Input-wide (system) event emitted when a **system exclusive** message has been received.
* You should note that, to receive `sysex` events, you must call the
* [`WebMidi.enable()`](WebMidi#enable()) method with the `sysex` option set to `true`:
*
* ```js
* WebMidi.enable({sysex: true})
* .then(() => console.log("WebMidi has been enabled with sysex support."))
* ```
*
* @event Input#sysex
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `sysex`
*
*/
/**
* Input-wide (system) event emitted when a **time code quarter frame** message has been
* received.
*
* @event Input#timecode
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `timecode`
*
* @since 2.1
*/
/**
* Input-wide (system) event emitted when a **song position** message has been received.
*
* @event Input#songposition
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `songposition`
*
* @since 2.1
*/
/**
* Input-wide (system) event emitted when a **song select** message has been received.
*
* @event Input#songselect
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} value Song (or sequence) number to select (0-127)
* @property {string} rawValue Song (or sequence) number to select (0-127)
*
* @since 2.1
*/
/**
* Input-wide (system) event emitted when a **tune request** message has been received.
*
* @event Input#tunerequest
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `tunerequest`
*
* @since 2.1
*/
/**
* Input-wide (system) event emitted when a **timing clock** message has been received.
*
* @event Input#clock
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `clock`
*
* @since 2.1
*/
/**
* Input-wide (system) event emitted when a **start** message has been received.
*
* @event Input#start
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `start`
*
* @since 2.1
*/
/**
* Input-wide (system) event emitted when a **continue** message has been received.
*
* @event Input#continue
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `continue`
*
* @since 2.1
*/
/**
* Input-wide (system) event emitted when a **stop** message has been received.
*
* @event Input#stop
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `stop`
*
* @since 2.1
*/
/**
* Input-wide (system) event emitted when an **active sensing** message has been received.
*
* @event Input#activesensing
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `activesensing`
*
* @since 2.1
*/
/**
* Input-wide (system) event emitted when a **reset** message has been received.
*
* @event Input#reset
*
* @type {object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `reset`
*
* @since 2.1
*/
/**
* Input-wide (system) event emitted when an unknown MIDI message has been received. It could
* be, for example, one of the undefined/reserved messages.
*
* @event Input#unknownmessage
*
* @type {Object}
*
* @property {Input} port The `Input` that triggered the event.
* @property {Input} target The object that dispatched the event.
* @property {Message} message A [`Message`](Message) object containing information about the
* incoming MIDI message.
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {string} type `unknownmessage`
*
* @since 2.1
*/
/**
* The `WebMidi` object makes it easier to work with the low-level Web MIDI API. Basically, it
* simplifies sending outgoing MIDI messages and reacting to incoming MIDI messages.
*
* When using the WebMidi.js library, you should know that the `WebMidi` class has already been
* instantiated. You cannot instantiate it yourself. If you use the **IIFE** version, you should
* simply use the global object called `WebMidi`. If you use the **CJS** (CommonJS) or **ESM** (ES6
* module) version, you get an already-instantiated object when you import the module.
*
* @fires WebMidi#connected
* @fires WebMidi#disabled
* @fires WebMidi#disconnected
* @fires WebMidi#enabled
* @fires WebMidi#error
* @fires WebMidi#midiaccessgranted
* @fires WebMidi#portschanged
*
* @extends EventEmitter
* @license Apache-2.0
*/
class WebMidi extends EventEmitter {
/**
* The WebMidi class is a singleton and you cannot instantiate it directly. It has already been
* instantiated for you.
*/
constructor() {
super();
/**
* Object containing system-wide default values that can be changed to customize how the library
* works.
*
* @type {object}
*
* @property {object} defaults.note - Default values relating to note
* @property {number} defaults.note.attack - A number between 0 and 127 representing the
* default attack velocity of notes. Initial value is 64.
* @property {number} defaults.note.release - A number between 0 and 127 representing the
* default release velocity of notes. Initial value is 64.
* @property {number} defaults.note.duration - A number representing the default duration of
* notes (in seconds). Initial value is Infinity.
*/
this.defaults = {
note: {
attack: Utilities.from7bitToFloat(64),
release: Utilities.from7bitToFloat(64),
duration: Infinity
}
};
/**
* The [`MIDIAccess`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIAccess)
* instance used to talk to the lower-level Web MIDI API. This should not be used directly
* unless you know what you are doing.
*
* @type {MIDIAccess}
* @readonly
*/
this.interface = null;
/**
* Indicates whether argument validation and backwards-compatibility checks are performed
* throughout the WebMidi.js library for object methods and property setters.
*
* This is an advanced setting that should be used carefully. Setting `validation` to `false`
* improves performance but should only be done once the project has been thoroughly tested with
* `validation` turned on.
*
* @type {boolean}
*/
this.validation = true;
/**
* Array of all (Input) objects
* @type {Input[]}
* @private
*/
this._inputs = [];
/**
* Array of disconnected [`Input`](Input) objects. This is used when inputs are plugged back in
* to retain their previous state.
* @type {Input[]}
* @private
*/
this._disconnectedInputs = [];
/**
* Array of all [`Output`](Output) objects
* @type {Output[]}
* @private
*/
this._outputs = [];
/**
* Array of disconnected [`Output`](Output) objects. This is used when outputs are plugged back
* in to retain their previous state.
* @type {Output[]}
* @private
*/
this._disconnectedOutputs = [];
/**
* Array of statechange events to process. These events must be parsed synchronously so they do
* not override each other.
*
* @type {string[]}
* @private
*/
this._stateChangeQueue = [];
/**
* @type {number}
* @private
*/
this._octaveOffset = 0;
}
/**
* Checks if the Web MIDI API is available in the current environment and then tries to connect to
* the host's MIDI subsystem. This is an asynchronous operation and it causes a security prompt to
* be displayed to the user.
*
* To enable the use of MIDI system exclusive messages, the `sysex` option should be set to
* `true`. However, under some environments (e.g. Jazz-Plugin), the `sysex` option is ignored
* and system exclusive messages are always enabled. You can check the
* [`sysexEnabled`](#sysexEnabled) property to confirm.
*
* To enable access to software synthesizers available on the host, you would set the `software`
* option to `true`. However, this option is only there to future-proof the library as support for
* software synths has not yet been implemented in any browser (as of September 2021).
*
* By the way, if you call the [`enable()`](#enable) method while WebMidi.js is already enabled,
* the callback function will be executed (if any), the promise will resolve but the events
* ([`"midiaccessgranted"`](#event:midiaccessgranted), [`"connected"`](#event:connected) and
* [`"enabled"`](#event:enabled)) will not be fired.
*
* There are 3 ways to execute code after `WebMidi` has been enabled:
*
* - Pass a callback function in the `options`
* - Listen to the [`"enabled"`](#event:enabled) event
* - Wait for the promise to resolve
*
* In order, this is what happens towards the end of the enabling process:
*
* 1. [`"midiaccessgranted"`](#event:midiaccessgranted) event is triggered once the user has
* granted access to use MIDI.
* 2. [`"connected"`](#event:connected) events are triggered (for each available input and output)
* 3. [`"enabled"`](#event:enabled) event is triggered when WebMidi.js is fully ready
* 4. specified callback (if any) is executed
* 5. promise is resolved and fulfilled with the `WebMidi` object.
*
* **Important note**: starting with Chrome v77, a page using Web MIDI API must be hosted on a
* secure origin (`https://`, `localhost` or `file:///`) and the user will always be prompted to
* authorize the operation (no matter if the `sysex` option is `true` or not).
*
* ##### Example
* ```js
* // Enabling WebMidi and using the promise
* WebMidi.enable().then(() => {
* console.log("WebMidi.js has been enabled!");
* })
* ```
*
* @param [options] {object}
*
* @param [options.callback] {function} A function to execute once the operation completes. This
* function will receive an `Error` object if enabling the Web MIDI API failed.
*
* @param [options.sysex=false] {boolean} Whether to enable MIDI system exclusive messages or not.
*
* @param [options.validation=true] {boolean} Whether to enable library-wide validation of method
* arguments and setter values. This is an advanced setting that should be used carefully. Setting
* [`validation`](#validation) to `false` improves performance but should only be done once the
* project has been thoroughly tested with [`validation`](#validation) turned on.
*
* @param [options.software=false] {boolean} Whether to request access to software synthesizers on
* the host system. This is part of the spec but has not yet been implemented by most browsers as
* of April 2020.
*
* @param [options.requestMIDIAccessFunction] {function} A custom function to use to return
* the MIDIAccess object. This is useful if you want to use a polyfill for the Web MIDI API
* or if you want to use a custom implementation of the Web MIDI API - probably for testing
* purposes.
*
* @async
*
* @returns {Promise.<WebMidi>} The promise is fulfilled with the `WebMidi` object for
* chainability
*
* @throws {Error} The Web MIDI API is not supported in your environment.
* @throws {Error} Jazz-Plugin must be installed to use WebMIDIAPIShim.
*/
async enable(options = {}, legacy = false) {
this.validation = options.validation !== false;
if (this.validation) {
// Backwards-compatibility. Previous syntax was: enable(callback, sysex)
if (typeof options === "function") options = {
callback: options,
sysex: legacy
};
if (legacy) options.sysex = true;
}
// If already enabled, trigger callback and resolve promise but do not dispatch events.
if (this.enabled) {
if (typeof options.callback === "function") options.callback();
return Promise.resolve();
}
/**
* Event emitted when an error occurs trying to enable `WebMidi`
*
* @event WebMidi#error
* @type {object}
* @property {DOMHighResTimeStamp} timestamp The moment when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {WebMidi} target The object that triggered the event
* @property {string} type `error`
* @property {*} error Actual error that occurred
*/
const errorEvent = {
timestamp: this.time,
target: this,
type: "error",
error: undefined
};
/**
* Event emitted once the MIDI interface has been successfully created (which implies user has
* granted access to MIDI).
*
* @event WebMidi#midiaccessgranted
* @type {object}
* @property {DOMHighResTimeStamp} timestamp The moment when the event occurred (in milliseconds
* since the navigation start of the document).
* @property {WebMidi} target The object that triggered the event
* @property {string} type `midiaccessgranted`
*/
const midiAccessGrantedEvent = {
timestamp: this.time,
target: this,
type: "midiaccessgranted"
};
/**
* Event emitted once `WebMidi` has been fully enabled
*
* @event WebMidi#enabled
* @type {object}
* @property {DOMHighResTimeStamp} timestamp The moment when the event occurred (in milliseconds
* since the navigation start of the document).
* @property {WebMidi} target The object that triggered the event
* @property {string} type `"enabled"`
*/
const enabledEvent = {
timestamp: this.time,
target: this,
type: "enabled"
};
// Request MIDI access (this is where the prompt will appear)
try {
if (typeof options.requestMIDIAccessFunction === "function") {
this.interface = await options.requestMIDIAccessFunction({
sysex: options.sysex,
software: options.software
});
} else {
this.interface = await navigator.requestMIDIAccess({
sysex: options.sysex,
software: options.software
});
}
} catch (err) {
errorEvent.error = err;
this.emit("error", errorEvent);
if (typeof options.callback === "function") options.callback(err);
return Promise.reject(err);
}
// Now that the Web MIDI API interface has been created, we trigger the 'midiaccessgranted'
// event. This allows the developer an occasion to assign listeners on 'connected' events.
this.emit("midiaccessgranted", midiAccessGrantedEvent);
// We setup the state change listener before creating the ports so that it properly catches the
// the ports' `connected` events
this.interface.onstatechange = this._onInterfaceStateChange.bind(this);
// Update inputs and outputs (this is where `Input` and `Output` objects are created).
try {
await this._updateInputsAndOutputs();
} catch (err) {
errorEvent.error = err;
this.emit("error", errorEvent);
if (typeof options.callback === "function") options.callback(err);
return Promise.reject(err);
}
// If we make it here, the ports have been successfully created, so we trigger the 'enabled'
// event.
this.emit("enabled", enabledEvent);
// Execute the callback (if any) and resolve the promise with 'this' (for chainability)
if (typeof options.callback === "function") options.callback();
return Promise.resolve(this);
}
/**
* Completely disables **WebMidi.js** by unlinking the MIDI subsystem's interface and closing all
* [`Input`](Input) and [`Output`](Output) objects that may have been opened. This also means that
* listeners added to [`Input`](Input) objects, [`Output`](Output) objects or to `WebMidi` itself
* are also destroyed.
*
* @async
* @returns {Promise<Array>}
*
* @throws {Error} The Web MIDI API is not supported by your environment.
*
* @since 2.0.0
*/
async disable() {
// This needs to be done right away to prevent racing conditions in listeners while the inputs
// are being destroyed.
if (this.interface) this.interface.onstatechange = undefined;
return this._destroyInputsAndOutputs().then(() => {
if (navigator && typeof navigator.close === "function") navigator.close(); // jzz
this.interface = null; // also resets enabled, sysexEnabled
/**
* Event emitted once `WebMidi` has been successfully disabled.
*
* @event WebMidi#disabled
* @type {object}
* @property {DOMHighResTimeStamp} timestamp The moment when the event occurred (in
* milliseconds since the navigation start of the document).
* @property {WebMidi} target The object that triggered the event
* @property {string} type `"disabled"`
*/
let event = {
timestamp: this.time,
target: this,
type: "disabled"
};
// Finally, trigger the 'disabled' event and then remove all listeners.
this.emit("disabled", event);
this.removeListener();
});
}
/**
* Returns the [`Input`](Input) object that matches the specified ID string or `false` if no
* matching input is found. As per the Web MIDI API specification, IDs are strings (not integers).
*
* Please note that IDs change from one host to another. For example, Chrome does not use the same
* kind of IDs as Jazz-Plugin.
*
* @param id {string} The ID string of the input. IDs can be viewed by looking at the
* [`WebMidi.inputs`](WebMidi#inputs) array. Even though they sometimes look like integers, IDs
* are strings.
* @param [options] {object}
* @param [options.disconnected] {boolean} Whether to retrieve a disconnected input
*
* @returns {Input} An [`Input`](Input) object matching the specified ID string or `undefined`
* if no matching input can be found.
*
* @throws {Error} WebMidi is not enabled.
*
* @since 2.0.0
*/
getInputById(id, options = {
disconnected: false
}) {
if (this.validation) {
if (!this.enabled) throw new Error("WebMidi is not enabled.");
if (!id) return;
}
if (options.disconnected) {
for (let i = 0; i < this._disconnectedInputs.length; i++) {
if (this._disconnectedInputs[i].id === id.toString()) return this._disconnectedInputs[i];
}
} else {
for (let i = 0; i < this.inputs.length; i++) {
if (this.inputs[i].id === id.toString()) return this.inputs[i];
}
}
}
/**
* Returns the first [`Input`](Input) object whose name **contains** the specified string. Note
* that the port names change from one environment to another. For example, Chrome does not report
* input names in the same way as the Jazz-Plugin does.
*
* @param name {string} The non-empty string to look for within the name of MIDI inputs (such as
* those visible in the [inputs](WebMidi#inputs) array).
*
* @returns {Input} The [`Input`](Input) that was found or `undefined` if no input contained the
* specified name.
* @param [options] {object}
* @param [options.disconnected] {boolean} Whether to retrieve a disconnected input
*
* @throws {Error} WebMidi is not enabled.
*
* @since 2.0.0
*/
getInputByName(name, options = {
disconnected: false
}) {
if (this.validation) {
if (!this.enabled) throw new Error("WebMidi is not enabled.");
if (!name) return;
name = name.toString();
}
if (options.disconnected) {
for (let i = 0; i < this._disconnectedInputs.length; i++) {
if (~this._disconnectedInputs[i].name.indexOf(name)) return this._disconnectedInputs[i];
}
} else {
for (let i = 0; i < this.inputs.length; i++) {
if (~this.inputs[i].name.indexOf(name)) return this.inputs[i];
}
}
}
/**
* Returns the first [`Output`](Output) object whose name **contains** the specified string. Note
* that the port names change from one environment to another. For example, Chrome does not report
* input names in the same way as the Jazz-Plugin does.
*
* @param name {string} The non-empty string to look for within the name of MIDI inputs (such as
* those visible in the [`outputs`](#outputs) array).
* @param [options] {object}
* @param [options.disconnected] {boolean} Whether to retrieve a disconnected output
*
* @returns {Output} The [`Output`](Output) that was found or `undefined` if no output matched
* the specified name.
*
* @throws {Error} WebMidi is not enabled.
*
* @since 2.0.0
*/
getOutputByName(name, options = {
disconnected: false
}) {
if (this.validation) {
if (!this.enabled) throw new Error("WebMidi is not enabled.");
if (!name) return;
name = name.toString();
}
if (options.disconnected) {
for (let i = 0; i < this._disconnectedOutputs.length; i++) {
if (~this._disconnectedOutputs[i].name.indexOf(name)) return this._disconnectedOutputs[i];
}
} else {
for (let i = 0; i < this.outputs.length; i++) {
if (~this.outputs[i].name.indexOf(name)) return this.outputs[i];
}
}
}
/**
* Returns the [`Output`](Output) object that matches the specified ID string or `false` if no
* matching output is found. As per the Web MIDI API specification, IDs are strings (not
* integers).
*
* Please note that IDs change from one host to another. For example, Chrome does not use the same
* kind of IDs as Jazz-Plugin.
*
* @param id {string} The ID string of the port. IDs can be viewed by looking at the
* [`WebMidi.outputs`](WebMidi#outputs) array.
* @param [options] {object}
* @param [options.disconnected] {boolean} Whether to retrieve a disconnected output
*
* @returns {Output} An [`Output`](Output) object matching the specified ID string. If no
* matching output can be found, the method returns `undefined`.
*
* @throws {Error} WebMidi is not enabled.
*
* @since 2.0.0
*/
getOutputById(id, options = {
disconnected: false
}) {
if (this.validation) {
if (!this.enabled) throw new Error("WebMidi is not enabled.");
if (!id) return;
}
if (options.disconnected) {
for (let i = 0; i < this._disconnectedOutputs.length; i++) {
if (this._disconnectedOutputs[i].id === id.toString()) return this._disconnectedOutputs[i];
}
} else {
for (let i = 0; i < this.outputs.length; i++) {
if (this.outputs[i].id === id.toString()) return this.outputs[i];
}
}
}
/**
* @private
* @deprecated since version 3.0.0, use Utilities.toNoteNumber() instead.
*/
noteNameToNumber(name) {
if (this.validation) {
console.warn("The noteNameToNumber() method is deprecated. Use " + "Utilities.toNoteNumber() instead.");
}
return Utilities.toNoteNumber(name, this.octaveOffset);
}
/**
* @private
* @deprecated since 3.0.0, use Utilities.getNoteDetails() instead.
*/
getOctave(number) {
if (this.validation) {
console.warn("The getOctave()is deprecated. Use Utilities.getNoteDetails() instead");
number = parseInt(number);
}
if (!isNaN(number) && number >= 0 && number <= 127) {
return Utilities.getNoteDetails(Utilities.offsetNumber(number, this.octaveOffset)).octave;
} else {
return false;
}
}
/**
* @private
* @deprecated since 3.0.0, use Utilities.sanitizeChannels() instead.
*/
sanitizeChannels(channel) {
if (this.validation) {
console.warn("The sanitizeChannels() method has been moved to the utilities class.");
}
return Utilities.sanitizeChannels(channel);
}
/**
* @private
* @deprecated since version 3.0.0, use Utilities.sanitizeChannels() instead.
*/
toMIDIChannels(channel) {
if (this.validation) {
console.warn("The toMIDIChannels() method has been deprecated. Use Utilities.sanitizeChannels() instead.");
}
return Utilities.sanitizeChannels(channel);
}
/**
* @private
* @deprecated since version 3.0.0, use Utilities.guessNoteNumber() instead.
*/
guessNoteNumber(input) {
if (this.validation) {
console.warn("The guessNoteNumber() method has been deprecated. Use Utilities.guessNoteNumber() instead.");
}
return Utilities.guessNoteNumber(input, this.octaveOffset);
}
/**
* @private
* @deprecated since version 3.0.0, use Utilities.buildNoteArray() instead.
*/
getValidNoteArray(notes, options = {}) {
if (this.validation) {
console.warn("The getValidNoteArray() method has been moved to the Utilities.buildNoteArray()");
}
return Utilities.buildNoteArray(notes, options);
}
/**
* @private
* @deprecated since version 3.0.0, use Utilities.toTimestamp() instead.
*/
convertToTimestamp(time) {
if (this.validation) {
console.warn("The convertToTimestamp() method has been moved to Utilities.toTimestamp().");
}
return Utilities.toTimestamp(time);
}
/**
* @return {Promise<void>}
* @private
*/
async _destroyInputsAndOutputs() {
let promises = [];
this.inputs.forEach(input => promises.push(input.destroy()));
this.outputs.forEach(output => promises.push(output.destroy()));
return Promise.all(promises).then(() => {
this._inputs = [];
this._outputs = [];
});
}
/**
* @private
*/
_onInterfaceStateChange(e) {
this._updateInputsAndOutputs();
/**
* Event emitted when an [`Input`](Input) or [`Output`](Output) port is connected or
* disconnected. This event is typically fired whenever a MIDI device is plugged in or
* unplugged. Please note that it may fire several times if a device possesses multiple inputs
* and/or outputs (which is often the case).
*
* @event WebMidi#portschanged
* @type {object}
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred
* (in milliseconds since the navigation start of the document).
* @property {string} type `portschanged`
* @property {WebMidi} target The object to which the listener was originally added (`WebMidi`)
* @property {Input|Output} port The [`Input`](Input) or [`Output`](Output) object that
* triggered the event.
*
* @since 3.0.2
*/
/**
* Event emitted when an [`Input`](Input) or [`Output`](Output) becomes available. This event is
* typically fired whenever a MIDI device is plugged in. Please note that it may fire several
* times if a device possesses multiple inputs and/or outputs (which is often the case).
*
* @event WebMidi#connected
* @type {object}
* @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred
* (in milliseconds since the navigation start of the document).
* @property {string} type `connected`
* @property {WebMidi} target The object to which the listener was originally added (`WebMidi`)
* @property {Input|Output} port The [`Input`](Input) or [`Output`](Output) object that
* triggered the event.
*/
/**
* Event emitted when an [`Input`](Input) or [`Output`](Output) becomes unavailable. This event
* is typically fired whenever a MIDI device is unplugged. Please note that it may fire several
* times if a device possesses multiple inputs and/or outputs (which is often the case).
*
* @event WebMidi#disconnected
* @type {object}
* @property {DOMHighResTimeStamp} timestamp The moment when the event occurred (in milliseconds
* since the navigation start of the document).
* @property {string} type `disconnected`
* @property {WebMidi} target The object to which the listener was originally added (`WebMidi`)
* @property {Input|Output} port The [`Input`](Input) or [`Output`](Output) object that
* triggered the event.
*/
let event = {
timestamp: e.timeStamp,
type: e.port.state,
target: this
};
// We check if "connection" is "open" because connected events are also triggered with
// "connection=closed"
if (e.port.state === "connected" && e.port.connection === "open") {
if (e.port.type === "output") {
event.port = this.getOutputById(e.port.id);
} else if (e.port.type === "input") {
event.port = this.getInputById(e.port.id);
}
// Emit "connected" event
this.emit(e.port.state, event);
// Make a shallow copy of the event so we can use it for the "portschanged" event
const portsChangedEvent = Object.assign({}, event);
portsChangedEvent.type = "portschanged";
this.emit(portsChangedEvent.type, portsChangedEvent);
// We check if "connection" is "pending" because we do not always get the "closed" event
} else if (e.port.state === "disconnected" && e.port.connection === "pending") {
if (e.port.type === "input") {
event.port = this.getInputById(e.port.id, {
disconnected: true
});
} else if (e.port.type === "output") {
event.port = this.getOutputById(e.port.id, {
disconnected: true
});
}
// Emit "disconnected" event
this.emit(e.port.state, event);
// Make a shallow copy of the event so we can use it for the "portschanged" event
const portsChangedEvent = Object.assign({}, event);
portsChangedEvent.type = "portschanged";
this.emit(portsChangedEvent.type, portsChangedEvent);
}
}
/**
* @private
*/
async _updateInputsAndOutputs() {
return Promise.all([this._updateInputs(), this._updateOutputs()]);
}
/**
* @private
*/
async _updateInputs() {
// We must check for the existence of this.interface because it might have been closed via
// WebMidi.disable().
if (!this.interface) return;
// Check for items to remove from the existing array (because they are no longer being reported
// by the MIDI back-end).
for (let i = this._inputs.length - 1; i >= 0; i--) {
const current = this._inputs[i];
const inputs = Array.from(this.interface.inputs.values());
if (!inputs.find(input => input === current._midiInput)) {
// Instead of destroying removed inputs, we stash them in case they come back (which is the
// case when the computer goes to sleep and is later brought back online).
this._disconnectedInputs.push(current);
this._inputs.splice(i, 1);
}
}
// Array to hold pending promises from trying to open all input ports
let promises = [];
// Add new inputs (if not already present)
this.interface.inputs.forEach(nInput => {
// Check if the input is currently absent from the 'inputs' array.
if (!this._inputs.find(input => input._midiInput === nInput)) {
// If the input has previously been stashed away, reuse it. If not, create a new one.
let input = this._disconnectedInputs.find(input => input._midiInput === nInput);
if (!input) input = new Input(nInput);
this._inputs.push(input);
promises.push(input.open());
}
});
// Return a promise that resolves when all promises have resolved
return Promise.all(promises);
}
/**
* @private
*/
async _updateOutputs() {
// We must check for the existence of this.interface because it might have been closed via
// WebMidi.disable().
if (!this.interface) return;
// Check for items to remove from the existing array (because they are no longer being reported
// by the MIDI back-end).
for (let i = this._outputs.length - 1; i >= 0; i--) {
const current = this._outputs[i];
const outputs = Array.from(this.interface.outputs.values());
if (!outputs.find(output => output === current._midiOutput)) {
// Instead of destroying removed inputs, we stash them in case they come back (which is the
// case when the computer goes to sleep and is later brought back online).
this._disconnectedOutputs.push(current);
this._outputs.splice(i, 1);
}
}
// Array to hold pending promises from trying to open all output ports
let promises = [];
// Add new outputs (if not already present)
this.interface.outputs.forEach(nOutput => {
// Check if the output is currently absent from the 'outputs' array.
if (!this._outputs.find(output => output._midiOutput === nOutput)) {
// If the output has previously been stashed away, reuse it. If not, create a new one.
let output = this._disconnectedOutputs.find(output => output._midiOutput === nOutput);
if (!output) output = new Output(nOutput);
this._outputs.push(output);
promises.push(output.open());
}
});
// Return a promise that resolves when all sub-promises have resolved
return Promise.all(promises);
}
// injectPluginMarkup(parent) {
//
// // Silently ignore on Node.js
// if (Utilities.isNode) return;
//
// // Default to <body> if no parent is specified
// if (!(parent instanceof Element) && !(parent instanceof HTMLDocument)) {
// parent = document.body;
// }
//
// // IE10 needs this:
// // <meta http-equiv="X-UA-Compatible" content="requiresActiveX=true"/>
//
// // Create markup and add to parent
// const obj = document.createElement("object");
// obj.classid = "CLSID:1ACE1618-1C7D-4561-AEE1-34842AA85E90"; // IE
// if (!obj.isJazz) obj.type = "audio/x-jazz"; // Standards-compliant
// obj.style.visibility = "hidden";
// obj.style.width = obj.style.height = "0px";
// parent.appendChild(obj);
//
// }
/**
* Indicates whether access to the host's MIDI subsystem is active or not.
*
* @readonly
* @type {boolean}
*/
get enabled() {
return this.interface !== null;
}
/**
* An array of all currently available MIDI inputs.
*
* @readonly
* @type {Input[]}
*/
get inputs() {
return this._inputs;
}
/**
* @private
* @deprecated
*/
get isNode() {
if (this.validation) {
console.warn("WebMidi.isNode has been deprecated. Use Utilities.isNode instead.");
}
return Utilities.isNode;
}
/**
* @private
* @deprecated
*/
get isBrowser() {
if (this.validation) {
console.warn("WebMidi.isBrowser has been deprecated. Use Utilities.isBrowser instead.");
}
return Utilities.isBrowser;
}
/**
* An integer to offset the octave of notes received from external devices or sent to external
* devices.
*
* When a MIDI message comes in on an input channel the reported note name will be offset. For
* example, if the `octaveOffset` is set to `-1` and a [`"noteon"`](InputChannel#event:noteon)
* message with MIDI number 60 comes in, the note will be reported as C3 (instead of C4).
*
* By the same token, when [`OutputChannel.playNote()`](OutputChannel#playNote) is called, the
* MIDI note number being sent will be offset. If `octaveOffset` is set to `-1`, the MIDI note
* number sent will be 72 (instead of 60).
*
* @type {number}
*
* @since 2.1
*/
get octaveOffset() {
return this._octaveOffset;
}
set octaveOffset(value) {
if (this.validation) {
value = parseInt(value);
if (isNaN(value)) throw new TypeError("The 'octaveOffset' property must be an integer.");
}
this._octaveOffset = value;
}
/**
* An array of all currently available MIDI outputs as [`Output`](Output) objects.
*
* @readonly
* @type {Output[]}
*/
get outputs() {
return this._outputs;
}
/**
* Indicates whether the environment provides support for the Web MIDI API or not.
*
* **Note**: in environments that do not offer built-in MIDI support, this will report `true` if
* the
* [`navigator.requestMIDIAccess`](https://developer.mozilla.org/en-US/docs/Web/API/MIDIAccess)
* function is available. For example, if you have installed WebMIDIAPIShim.js but no plugin, this
* property will be `true` even though actual support might not be there.
*
* @readonly
* @type {boolean}
*/
get supported() {
return typeof navigator !== "undefined" && !!navigator.requestMIDIAccess;
}
/**
* Indicates whether MIDI system exclusive messages have been activated when WebMidi.js was
* enabled via the [`enable()`](#enable) method.
*
* @readonly
* @type boolean
*/
get sysexEnabled() {
return !!(this.interface && this.interface.sysexEnabled);
}
/**
* The elapsed time, in milliseconds, since the time
* [origin](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#The_time_origin).
* Said simply, it is the number of milliseconds that passed since the page was loaded. Being a
* floating-point number, it has sub-millisecond accuracy. According to the
* [documentation](https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp), the
* time should be accurate to 5 µs (microseconds). However, due to various constraints, the
* browser might only be accurate to one millisecond.
*
* Note: `WebMidi.time` is simply an alias to `performance.now()`.
*
* @type {DOMHighResTimeStamp}
* @readonly
*/
get time() {
return performance.now();
}
/**
* The version of the library as a [semver](https://semver.org/) string.
*
* @readonly
* @type string
*/
get version() {
return "3.1.12";
}
/**
* The flavour of the library. Can be one of:
*
* * `esm`: ECMAScript Module
* * `cjs`: CommonJS Module
* * `iife`: Immediately-Invoked Function Expression
*
* @readonly
* @type string
* @since 3.0.25
*/
get flavour() {
return "iife"; // will be replaced during bundling by the correct identifier
}
/**
* @private
* @deprecated since 3.0.0. Use Enumerations.CHANNEL_EVENTS instead.
*/
get CHANNEL_EVENTS() {
if (this.validation) {
console.warn("The CHANNEL_EVENTS enum has been moved to Enumerations.CHANNEL_EVENTS.");
}
return Enumerations.CHANNEL_EVENTS;
}
/**
* @private
* @deprecated since 3.0.0. Use Enumerations.SYSTEM_MESSAGES instead.
*/
get MIDI_SYSTEM_MESSAGES() {
if (this.validation) {
console.warn("The MIDI_SYSTEM_MESSAGES enum has been moved to " + "Enumerations.SYSTEM_MESSAGES.");
}
return Enumerations.SYSTEM_MESSAGES;
}
/**
* @private
* @deprecated since 3.0.0. Use Enumerations.CHANNEL_MODE_MESSAGES instead
*/
get MIDI_CHANNEL_MODE_MESSAGES() {
if (this.validation) {
console.warn("The MIDI_CHANNEL_MODE_MESSAGES enum has been moved to " + "Enumerations.CHANNEL_MODE_MESSAGES.");
}
return Enumerations.CHANNEL_MODE_MESSAGES;
}
/**
* @private
* @deprecated since 3.0.0. Use Enumerations.CONTROL_CHANGE_MESSAGES instead.
*/
get MIDI_CONTROL_CHANGE_MESSAGES() {
if (this.validation) {
console.warn("The MIDI_CONTROL_CHANGE_MESSAGES enum has been replaced by the " + "Enumerations.CONTROL_CHANGE_MESSAGES array.");
}
return Enumerations.MIDI_CONTROL_CHANGE_MESSAGES;
}
/**
* @deprecated since 3.0.0. Use Enumerations.REGISTERED_PARAMETERS instead.
* @private
*/
get MIDI_REGISTERED_PARAMETER() {
if (this.validation) {
console.warn("The MIDI_REGISTERED_PARAMETER enum has been moved to " + "Enumerations.REGISTERED_PARAMETERS.");
}
return Enumerations.REGISTERED_PARAMETERS;
}
/**
* @deprecated since 3.0.0.
* @private
*/
get NOTES() {
if (this.validation) {
console.warn("The NOTES enum has been deprecated.");
}
return ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
}
}
// Export singleton instance of WebMidi class. The 'constructor' is nulled so that it cannot be used
// to instantiate a new WebMidi object or extend it. However, it is not freezed so it remains
// extensible (properties can be added at will).
const wm = new WebMidi();
wm.constructor = null;
exports.Enumerations = Enumerations;
exports.Forwarder = Forwarder;
exports.Input = Input;
exports.InputChannel = InputChannel;
exports.Message = Message;
exports.Note = Note;
exports.Output = Output;
exports.OutputChannel = OutputChannel;
exports.Utilities = Utilities;
exports.WebMidi = wm;
}(this.window = this.window || {}));