(function(scope) { "use strict"; /** * The `WebMidi` object makes it easier to work with the Web MIDI API. Basically, it simplifies * two things: sending outgoing MIDI messages and reacting to incoming MIDI messages. * * Sending MIDI messages is done via an `Output` object. All available outputs can be accessed in * the `WebMidi.outputs` array. There is one `Output` object for each output port available on * your system. Similarly, reacting to MIDI messages as they are coming in is simply a matter of * adding a listener to an `Input` object. Similarly, all inputs can be found in the * `WebMidi.inputs` array. * * Please note that a single hardware device might create more than one input and/or output ports. * * #### Sending messages * * To send MIDI messages, you simply need to call the desired method (`playNote()`, * `sendPitchBend()`, `stopNote()`, etc.) from an `Output` object and pass in the appropriate * parameters. All the native MIDI communication will be handled for you. The only additional * thing that needs to be done is to first enable `WebMidi`. Here is an example: * * WebMidi.enable(function(err) { * if (err) console.log("An error occurred", err); * WebMidi.outputs[0].playNote("C3"); * }); * * The code above, calls the `WebMidi.enable()` method. Upon success, this method executes the * callback function specified as a parameter. In this case, the callback calls the `playnote()` * function to play a 3rd octave C on the first available output port. * * #### Receiving messages * * Receiving messages is just as easy. You simply have to set a callback function to be triggered * when a specific MIDI message is received. For example, here"s how to listen for pitch bend * events on the first input port: * * WebMidi.enable(function(err) { * if (err) console.log("An error occurred", err); * * WebMidi.inputs[0].addListener("pitchbend", "all", function(e) { * console.log("Pitch value: " + e.value); * }); * * }); * * As you can see, this library is much easier to use than the native Web MIDI API. No need to * manually craft or decode binary MIDI messages anymore! * * @class WebMidi * @static * * @throws Error WebMidi is a singleton, it cannot be instantiated directly. * * @todo Switch away from yuidoc (deprecated) to be able to serve doc over https * @todo Yuidoc does not allow multiple exceptions (@throws) for a single method ?! * */ function WebMidi() { // Singleton. Prevent instantiation through WebMidi.__proto__.constructor() if (WebMidi.prototype._singleton) { throw new Error("WebMidi is a singleton, it cannot be instantiated directly."); } WebMidi.prototype._singleton = this; // MIDI inputs and outputs this._inputs = []; this._outputs = []; // Object to hold all user-defined handlers for interface-wide events (connected, disconnected, // etc.) this._userHandlers = {}; // Array of statechange events to process. These events must be parsed synchronously so they do // not override each other. this._stateChangeQueue = []; // Indicates whether we are currently processing a statechange event (in which case new events // are to be queued). this._processingStateChange = false; // Events triggered at the interface level (WebMidi) this._midiInterfaceEvents = ["connected", "disconnected"]; // the current nrpns being constructed, by channel this._nrpnBuffer = [[],[],[],[], [],[],[],[], [],[],[],[], [],[],[],[]]; // Enable/Disable NRPN event dispatch this._nrpnEventsEnabled = true; // NRPN message types this._nrpnTypes = ["entry", "increment", "decrement"]; // Notes and semitones for note guessing this._notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; this._semitones = {C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 }; // Define some "static" properties Object.defineProperties(this, { /** * [read-only] List of valid MIDI system messages and matching hexadecimal values. * * Note: values 249 and 253 are actually dispatched by the Web MIDI API but the MIDI 1.0 does * not say what they are used for. About those values, it only states: undefined (reserved) * * @property MIDI_SYSTEM_MESSAGES * @type Object * @static * * @since 2.0.0 */ MIDI_SYSTEM_MESSAGES: { value: { // System common messages sysex: 0xF0, // 240 timecode: 0xF1, // 241 songposition: 0xF2, // 242 songselect: 0xF3, // 243 tuningrequest: 0xF6, // 246 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 }, writable: false, enumerable: true, configurable: false }, /** * [read-only] An object containing properties for each MIDI channel messages and their * associated hexadecimal value. * * @property MIDI_CHANNEL_MESSAGES * @type Object * @static * * @since 2.0.0 */ MIDI_CHANNEL_MESSAGES: { value: { noteoff: 0x8, // 8 noteon: 0x9, // 9 keyaftertouch: 0xA, // 10 controlchange: 0xB, // 11 channelmode: 0xB, // 11 nrpn: 0xB, // 11 programchange: 0xC, // 12 channelaftertouch: 0xD, // 13 pitchbend: 0xE // 14 }, writable: false, enumerable: true, configurable: false }, /** * [read-only] An object containing properties for each registered parameters and their * associated pair of hexadecimal values. MIDI registered parameters extend the original list * of control change messages (a.k.a. CC messages). Currently, there are only a limited number * of them. * * @property MIDI_REGISTERED_PARAMETER * @type Object * @static * * @since 2.0.0 */ MIDI_REGISTERED_PARAMETER: { value: { 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] }, writable: false, enumerable: true, configurable: false }, /** * [read-only] An object containing properties for each MIDI control change messages (a.k.a. * CC messages) and their associated hexadecimal value. * * @property MIDI_CONTROL_CHANGE_MESSAGES * @type Object * @static * * @since 2.0.0 */ MIDI_CONTROL_CHANGE_MESSAGES: { value: { bankselectcoarse: 0, modulationwheelcoarse: 1, breathcontrollercoarse: 2, footcontrollercoarse: 4, portamentotimecoarse: 5, dataentrycoarse: 6, volumecoarse: 7, balancecoarse: 8, pancoarse: 10, expressioncoarse: 11, effectcontrol1coarse: 12, effectcontrol2coarse: 13, generalpurposeslider1: 16, generalpurposeslider2: 17, generalpurposeslider3: 18, generalpurposeslider4: 19, bankselectfine: 32, modulationwheelfine: 33, breathcontrollerfine: 34, footcontrollerfine: 36, portamentotimefine: 37, dataentryfine: 38, volumefine: 39, balancefine: 40, panfine: 42, expressionfine: 43, effectcontrol1fine: 44, effectcontrol2fine: 45, 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, reverblevel: 91, tremololevel: 92, choruslevel: 93, celestelevel: 94, phaserlevel: 95, databuttonincrement: 96, databuttondecrement: 97, nonregisteredparametercoarse: 98, nonregisteredparameterfine: 99, registeredparametercoarse: 100, registeredparameterfine: 101 }, writable: false, enumerable: true, configurable: false }, /** * [read-only] An object containing properties for MIDI control change messages * that make up NRPN messages * * @property MIDI_NRPN_MESSAGES * @type Object * @static * * @since 2.0.0 */ MIDI_NRPN_MESSAGES: { value: { entrymsb: 6, entrylsb: 38, increment: 96, decrement: 97, paramlsb: 98, parammsb: 99, nullactiveparameter: 127 }, writable: false, enumerable: true, configurable: false }, /** * [read-only] List of MIDI channel mode messages as defined in the official MIDI * specification. * * @property MIDI_CHANNEL_MODE_MESSAGES * @type Object * @static * * @since 2.0.0 */ MIDI_CHANNEL_MODE_MESSAGES: { value: { allsoundoff: 120, resetallcontrollers: 121, localcontrol: 122, allnotesoff: 123, omnimodeoff: 124, omnimodeon: 125, monomodeon: 126, polymodeon: 127 }, writable: false, enumerable: true, configurable: false }, /** * An integer to offset the octave both in inbound and outbound messages. 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. * * @property octaveOffset * @type Number * @static * * @since 2.1 */ octaveOffset: { value: 0, writable: true, enumerable: true, configurable: false } }); // Define getters/setters Object.defineProperties(this, { /** * [read-only] Indicates whether the environment supports 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` function is available. For example, if you have installed * WebMIDIAPIShim but no plugin, this property will be true even though actual support might * not be there. * * @property supported * @type Boolean * @static */ supported: { enumerable: true, get: function() { return "requestMIDIAccess" in navigator; } }, /** * [read-only] Indicates whether the interface to the host"s MIDI subsystem is currently * enabled. * * @property enabled * @type Boolean * @static */ enabled: { enumerable: true, get: function() { return this.interface !== undefined; }.bind(this) }, /** * [read-only] An array of all currently available MIDI input ports. * * @property inputs * @type {Array} * @static */ inputs: { enumerable: true, get: function() { return this._inputs; }.bind(this) }, /** * [read-only] An array of all currently available MIDI output ports. * * @property outputs * @type {Array} * @static */ outputs: { enumerable: true, get: function() { return this._outputs; }.bind(this) }, /** * [read-only] Indicates whether the interface to the host"s MIDI subsystem is currently * active. * * @property sysexEnabled * @type Boolean * @static */ sysexEnabled: { enumerable: true, get: function() { return !!(this.interface && this.interface.sysexEnabled); }.bind(this) }, /** * [read-only] Indicates whether WebMidi should dispatch Non-Registered * Parameter Number events (which are generally groups of CC messages) * If correct sequences of CC messages are received, NRPN events will * fire. The first out of order NRPN CC will fall through the collector * logic and all CC messages buffered will be discarded as incomplete. * * @private * * @property nrpnEventsEnabled * @type Boolean * @static */ nrpnEventsEnabled: { enumerable: true, get: function() { return !!(this._nrpnEventsEnabled); }.bind(this), set: function(enabled) { this._nrpnEventsEnabled = enabled; return this._nrpnEventsEnabled; } }, /** * [read-only] NRPN message types * * @property nrpnTypes * @type Array * @static */ nrpnTypes: { enumerable: true, get: function() { return this._nrpnTypes; }.bind(this) }, /** * [read-only] Current MIDI performance time in milliseconds. This can be used to queue events * in the future. * * @property time * @type DOMHighResTimeStamp * @static */ time: { enumerable: true, get: function() { return performance.now(); } } }); } // WebMidi is a singleton so we instantiate it ourselves and keep it in a var for internal // reference. var wm = new WebMidi(); /** * Checks if the Web MIDI API is available and then tries to connect to the host's MIDI subsystem. * This is an asynchronous operation. When it's done, the specified handler callback will be * executed. If an error occurred, the callback function will receive an `Error` object as its * sole parameter. * * To enable the use of system exclusive messages, the `sysex` parameter should be set to true. * However, under some environments (e.g. Jazz-Plugin), the sysex parameter is ignored and sysex * is always enabled. * * Warning: starting with Chrome v77, the 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 `sysex` is requested or not). * * @method enable * @static * * @param [callback] {Function} A function to execute upon success. This function will receive an * `Error` object upon failure to enable the Web MIDI API. * @param [sysex=false] {Boolean} Whether to enable MIDI system exclusive messages or not. * * @throws Error The Web MIDI API is not supported by your browser. * @throws Error Jazz-Plugin must be installed to use WebMIDIAPIShim. */ WebMidi.prototype.enable = function(callback, sysex) { // Why are you not using a Promise-based API for the enable() method? // // Short answer: because of IE. // // Long answer: // // IE 11 and below still do not support promises. Therefore, WebMIDIAPIShim has to implement a // simple Promise look-alike object to handle the call to requestMIDIAccess(). This look-alike // is not a fully-fledged Promise object. It does not support using catch() for example. This // means that, to provide a real Promise-based interface for the enable() method, we would need // to add a dependency in the form of a Promise polyfill. So, to keep things simpler, we will // stick to the good old callback based enable() function. if (this.enabled) return; if ( !this.supported) { if (typeof callback === "function") { callback( new Error("The Web MIDI API is not supported by your browser.") ); } return; } navigator.requestMIDIAccess({sysex: sysex}).then( function(midiAccess) { var events = [], promises = [], promiseTimeout; this.interface = midiAccess; this._resetInterfaceUserHandlers(); // We setup a temporary `statechange` handler that will catch all events triggered while we // setup. Those events will be re-triggered after calling the user"s callback. This will // allow the user to listen to "connected" events which can be very convenient. this.interface.onstatechange = function (e) { events.push(e); }; // Here we manually open the inputs and outputs. Usually, this is optional. When the ports // are not explicitely opened, they will be opened automatically (and asynchonously) by // setting a listener on `midimessage` (MIDIInput) or calling `send()` (MIDIOutput). // However, we do not want that here. We want to be sure that "connected" events will be // available in the user"s callback. So, what we do is open all input and output ports and // wait until all promises are resolved. Then, we re-trigger the events after the user"s // callback has been executed. This seems like the most sensible and practical way. var inputs = midiAccess.inputs.values(); for (var input = inputs.next(); input && !input.done; input = inputs.next()) { promises.push(input.value.open()); } var outputs = midiAccess.outputs.values(); for (var output = outputs.next(); output && !output.done; output = outputs.next()) { promises.push(output.value.open()); } // Since this library might be used in environments without support for promises (such as // Jazz-Midi) or in environments that are not properly opening the ports (such as Web MIDI // Browser), we fall back to a timer-based approach if the promise-based approach fails. function onPortsOpen() { clearTimeout(promiseTimeout); this._updateInputsAndOutputs(); this.interface.onstatechange = this._onInterfaceStateChange.bind(this); // We execute the callback and then re-trigger the statechange events. if (typeof callback === "function") { callback.call(this); } events.forEach(function (event) { this._onInterfaceStateChange(event); }.bind(this)); } promiseTimeout = setTimeout(onPortsOpen.bind(this), 200); if (Promise) { Promise .all(promises) .catch(function(err) { console.warn(err); }) .then(onPortsOpen.bind(this)); } // When MIDI access is requested, all input and output ports have their "state" set to // "connected". However, the value of their "connection" property is "closed". // // A `MIDIInput` becomes `open` when you explicitely call its `open()` method or when you // assign a listener to its `onmidimessage` property. A `MIDIOutput` becomes `open` when you // use the `send()` method or when you can explicitely call its `open()` method. // // Calling `_updateInputsAndOutputs()` attaches listeners to all inputs. As per the spec, // this triggers a `statechange` event on MIDIAccess. }.bind(this), function (err) { if (typeof callback === "function") { callback.call(this, err); } }.bind(this) ); }; /** * Completely disables `WebMidi` by unlinking the MIDI subsystem's interface and destroying all * `Input` and `Output` objects that may be available. This also means that any listener(s) that * may have been defined on `WebMidi` or any `Input` objects will be destroyed. * * @method disable * @static * * @since 2.0.0 */ WebMidi.prototype.disable = function() { if ( !this.supported ) { throw new Error("The Web MIDI API is not supported by your browser."); } if (this.enabled) { this.removeListener(); this.inputs.forEach(function (input) { input.removeListener(); }); } if (this.interface) this.interface.onstatechange = undefined; this.interface = undefined; // also resets enabled, sysexEnabled, nrpnEventsEnabled this._inputs = []; this._outputs = []; this._nrpnEventsEnabled = true; this._resetInterfaceUserHandlers(); }; /** * Adds an event listener on the `WebMidi` object that will trigger a function callback when the * specified event happens. * * WebMidi must be enabled before adding event listeners. * * Currently, only one event is being dispatched by the `WebMidi` object: * * * {{#crossLink "WebMidi/statechange:event"}}statechange{{/crossLink}} * * @method addListener * @static * @chainable * * @param type {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). * * @throws {Error} WebMidi must be enabled before adding event listeners. * @throws {TypeError} The specified event type is not supported. * @throws {TypeError} The "listener" parameter must be a function. * * @return {WebMidi} Returns the `WebMidi` object so methods can be chained. */ WebMidi.prototype.addListener = function(type, listener) { if (!this.enabled) { throw new Error("WebMidi must be enabled before adding event listeners."); } if (typeof listener !== "function") { throw new TypeError("The 'listener' parameter must be a function."); } if (this._midiInterfaceEvents.indexOf(type) >= 0) { this._userHandlers[type].push(listener); } else { throw new TypeError("The specified event type is not supported."); } return this; }; /** * Checks if the specified event type is already defined to trigger the specified listener * function. * * @method hasListener * @static * * @param {String} type The type of the event. * @param {Function} listener The callback function to check for. * * @throws {Error} WebMidi must be enabled before checking event listeners. * @throws {TypeError} The "listener" parameter must be a function. * @throws {TypeError} The specified event type is not supported. * * @return {Boolean} Boolean value indicating whether or not a callback is already defined for * this event type. */ WebMidi.prototype.hasListener = function(type, listener) { if (!this.enabled) { throw new Error("WebMidi must be enabled before checking event listeners."); } if (typeof listener !== "function") { throw new TypeError("The 'listener' parameter must be a function."); } if (this._midiInterfaceEvents.indexOf(type) >= 0) { for (var o = 0; o < this._userHandlers[type].length; o++) { if (this._userHandlers[type][o] === listener) { return true; } } } else { throw new TypeError("The specified event type is not supported."); } return false; }; /** * Removes the specified listener(s). If the `listener` parameter is left undefined, all listeners * for the specified `type` will be removed. If both the `listener` and the `type` parameters are * omitted, all listeners attached to the `WebMidi` object will be removed. * * @method removeListener * @static * @chainable * * @param {String} [type] The type of the event. * @param {Function} [listener] The callback function to check for. * * @throws {Error} WebMidi must be enabled before removing event listeners. * @throws {TypeError} The "listener" parameter must be a function. * @throws {TypeError} The specified event type is not supported. * * @return {WebMidi} The `WebMidi` object for easy method chaining. */ WebMidi.prototype.removeListener = function(type, listener) { if (!this.enabled) { throw new Error("WebMidi must be enabled before removing event listeners."); } if (listener !== undefined && typeof listener !== "function") { throw new TypeError("The 'listener' parameter must be a function."); } if (this._midiInterfaceEvents.indexOf(type) >= 0) { if (listener) { for (var o = 0; o < this._userHandlers[type].length; o++) { if (this._userHandlers[type][o] === listener) { this._userHandlers[type].splice(o, 1); } } } else { this._userHandlers[type] = []; } } else if (type === undefined) { this._resetInterfaceUserHandlers(); } else { throw new TypeError("The specified event type is not supported."); } return this; }; /** * Returns a sanitized array of valid MIDI channel numbers (1-16). The parameter should be one of * the following: * * * a single integer * * an array of integers * * the special value `"all"` * * the special value `"none"` * * Passing `"all"` or `undefined` as a parameter to this function results in all channels being * returned (1-16). Passing `"none"` results in no channel being returned (as an empty array). * * Note: parameters that cannot successfully be parsed to integers between 1 and 16 are silently * ignored. * * @method toMIDIChannels * @static * * @param [channel="all"] {Number|Array|"all"|"none"} * @returns {Array} An array of 0 or more valid MIDI channel numbers */ WebMidi.prototype.toMIDIChannels = function(channel) { var channels; if (channel === "all" || channel === undefined) { channels = ["all"]; } else if (channel === "none") { channels = []; return channels; } else 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 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; } return channels .map(function(ch) { return parseInt(ch); }) .filter(function(ch) { return (ch >= 1 && ch <= 16); }); }; /** * * Returns the `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. * * @method getInputById * @static * * @param id {String} The ID string of the port. IDs can be viewed by looking at the * `WebMidi.inputs` array. * * @returns {Input|false} A MIDIInput port matching the specified ID string. If no matching port * can be found, the method returns `false`. * * @throws Error WebMidi is not enabled. * * @since 2.0.0 */ WebMidi.prototype.getInputById = function(id) { if (!this.enabled) throw new Error("WebMidi is not enabled."); id = String(id); for (var i = 0; i < this.inputs.length; i++) { if (this.inputs[i].id === id) return this.inputs[i]; } return false; }; /** * Returns the `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. * * @method getOutputById * @static * * @param id {String} The ID string of the port. IDs can be viewed by looking at the * `WebMidi.outputs` array. * * @returns {Output|false} A MIDIOutput port matching the specified ID string. If no matching port * can be found, the method returns `false`. * * @throws Error WebMidi is not enabled. * * @since 2.0.0 */ WebMidi.prototype.getOutputById = function(id) { if (!this.enabled) throw new Error("WebMidi is not enabled."); id = String(id); for (var i = 0; i < this.outputs.length; i++) { if (this.outputs[i].id === id) return this.outputs[i]; } return false; }; /** * Returns the first MIDI `Input` whose name *contains* the specified string. * * Please note that the port names change from one host to another. For example, Chrome does * not report port names in the same way as the Jazz-Plugin does. * * @method getInputByName * @static * * @param name {String} The name of a MIDI input port such as those visible in the * `WebMidi.inputs` array. * * @returns {Input|False} The `Input` that was found or `false` if no input matched the specified * name. * * @throws Error WebMidi is not enabled. * @throws TypeError The name must be a string. * * @since 2.0.0 */ WebMidi.prototype.getInputByName = function(name) { if (!this.enabled) { throw new Error("WebMidi is not enabled."); } for (var i = 0; i < this.inputs.length; i++) { if (~this.inputs[i].name.indexOf(name)) { return this.inputs[i]; } } return false; }; /** * Returns the octave number for the specified MIDI note number (0-127). By default, the value is * based on middle C (note number 60) being placed on the 4th octave (C4). However, by using the * octaveOffset property, you can offset the result as much * as you want. * * @method getOctave * @static * * @param number {Number} An integer representing a valid MIDI note number (between 0 and 127). * * @returns {Number} The octave (as a signed integer) or `undefined`. * * @since 2.0.0-rc.6 */ WebMidi.prototype.getOctave = function(number) { if (number != null && number >= 0 && number <= 127) { return Math.floor(Math.floor(number) / 12 - 1) + Math.floor(wm.octaveOffset); } }; /** * Returns the first MIDI `Output` that matches the specified name. * * Please note that the port names change from one host to another. For example, Chrome does * not report port names in the same way as the Jazz-Plugin does. * * @method getOutputByName * @static * * @param name {String} The name of a MIDI output port such as those visible in the * `WebMidi.outputs` array. * * @returns {Output|False} The `Output` that was found or `false` if no output matched the * specified name. * * @throws Error WebMidi is not enabled. * * @since 2.0.0 */ WebMidi.prototype.getOutputByName = function(name) { if (!this.enabled) { throw new Error("WebMidi is not enabled."); } for (var i = 0; i < this.outputs.length; i++) { if (~this.outputs[i].name.indexOf(name)) { return this.outputs[i]; } } return false; }; /** * Returns a valid MIDI note number (0-127) given the specified input. The input usually is a note * name (C3, F#4, D-2, G8, etc.). If an integer between 0 and 127, it will simply be returned as * is. * * @method guessNoteNumber * @static * * @param input {Number|String} A string to extract the note number from. An integer can also be * used, in which case it will simply be returned (if between 0 and 127). * @throws {Error} Invalid input value * @returns {Number} A valid MIDI note number (0-127). */ WebMidi.prototype.guessNoteNumber = function(input) { var output = false; if (input && input.toFixed && input >= 0 && input <= 127) { // uint output = Math.round(input); } else if (parseInt(input) >= 0 && parseInt(input) <= 127) { // uint as string output = parseInt(input); } else if (typeof input === "string" || input instanceof String) { // string output = this.noteNameToNumber(input); } if (output === false) throw new Error("Invalid input value (" + input + ")."); return output; }; /** * Returns a MIDI note number matching the note name passed in the form of a string parameter. The * note name must include the octave number. The name can also optionally include a sharp (#), * a double sharp (##), a flat (b) or a double flat (bb) symbol: C5, G4, D#-1, F0, Gb7, Eb-1, * Abb4, B##6, etc. * * Note that, in converting note names to numbers, C4 is considered to be middle C (MIDI note * number 60) as per the scientific pitch notation standard. * * Also note that the resulting note number is offset by the `octaveOffset` value (if not zero). * For example, if you pass in "C4" and the `octaveOffset` value is 2 the resulting MIDI note * number will be 36. * * @method noteNameToNumber * @static * * @param name {String} The name of the note in the form of a letter, followed by an optional "#", * "##", "b" or "bb" followed by the octave number. * * @throws {RangeError} Invalid note name. * @throws {RangeError} Invalid note name or note outside valid range. * @return {Number} The MIDI note number (between 0 and 127) */ WebMidi.prototype.noteNameToNumber = function(name) { if (typeof name !== "string") name = ""; var matches = name.match(/([CDEFGAB])(#{0,2}|b{0,2})(-?\d+)/i); if(!matches) throw new RangeError("Invalid note name."); var semitones = wm._semitones[matches[1].toUpperCase()]; var octave = parseInt(matches[3]); var result = ((octave + 1 - Math.floor(wm.octaveOffset)) * 12) + semitones; if (matches[2].toLowerCase().indexOf("b") > -1) { result -= matches[2].length; } else if (matches[2].toLowerCase().indexOf("#") > -1) { result += matches[2].length; } if (result < 0 || result > 127) { throw new RangeError("Invalid note name or note outside valid range."); } return result; }; /** * @method _updateInputsAndOutputs * @static * @protected */ WebMidi.prototype._updateInputsAndOutputs = function() { this._updateInputs(); this._updateOutputs(); }; /** * @method _updateInputs * @static * @protected */ WebMidi.prototype._updateInputs = function() { // Check for items to remove from the existing array (because they are no longer being reported // by the MIDI back-end). for (var i = 0; i < this._inputs.length; i++) { var remove = true; var updated = this.interface.inputs.values(); for (var input = updated.next(); input && !input.done; input = updated.next()) { if (this._inputs[i]._midiInput === input.value) { remove = false; break; } } if (remove) { this._inputs.splice(i, 1); } } // Check for items to add in the existing inputs array because they just appeared in the MIDI // back-end inputs list. We must check for the existence of this.interface because it might // have been closed via WebMidi.disable(). this.interface && this.interface.inputs.forEach(function (nInput) { var add = true; for (var j = 0; j < this._inputs.length; j++) { if (this._inputs[j]._midiInput === nInput) { add = false; } } if (add) { this._inputs.push( new Input(nInput) ); } }.bind(this)); }; /** * @method _updateOutputs * @static * @protected */ WebMidi.prototype._updateOutputs = function() { // Check for items to remove from the existing array (because they are no longer being reported // by the MIDI back-end). for (var i = 0; i < this._outputs.length; i++) { var remove = true; var updated = this.interface.outputs.values(); for (var output = updated.next(); output && !output.done; output = updated.next()) { if (this._outputs[i]._midiOutput === output.value) { remove = false; break; } } if (remove) { this._outputs.splice(i, 1); } } // Check for items to add in the existing inputs array because they just appeared in the MIDI // back-end outputs list. We must check for the existence of this.interface because it might // have been closed via WebMidi.disable(). this.interface && this.interface.outputs.forEach(function (nOutput) { var add = true; for (var j = 0; j < this._outputs.length; j++) { if (this._outputs[j]._midiOutput === nOutput) { add = false; } } if (add) { this._outputs.push( new Output(nOutput) ); } }.bind(this)); }; /** * @method _onInterfaceStateChange * @static * @protected */ WebMidi.prototype._onInterfaceStateChange = function(e) { this._updateInputsAndOutputs(); /** * Event emitted when a MIDI port 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 input/output ports. * * @event connected * @param {Object} event * @param {Number} event.timestamp The timestamp when the event occurred (in milliseconds since * the epoch). * @param {String} event.type The type of event that occurred. * @param {String} event.port The actual `Input` or `Output` object associated to the event. */ /** * Event emitted when a MIDI port 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 input/output ports. * * @event disconnected * @param {Object} event * @param {Number} event.timestamp The timestamp when the event occurred (in milliseconds since * the epoch). * @param {String} event.type The type of event that occurred. * @param {String} event.port An generic object containing details about the port that triggered * the event. */ var event = { timestamp: e.timeStamp, type: e.port.state }; if (this.interface && e.port.state === "connected") { 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); } } else { event.port = { connection: "closed", id: e.port.id, manufacturer: e.port.manufacturer, name: e.port.name, state: e.port.state, type: e.port.type }; } this._userHandlers[e.port.state].forEach(function (handler) { handler(event); }); }; /** * @method _resetInterfaceUserHandlers * @static * @protected */ WebMidi.prototype._resetInterfaceUserHandlers = function() { for (var i = 0; i < this._midiInterfaceEvents.length; i++) { this._userHandlers[this._midiInterfaceEvents[i]] = []; } }; /** * The `Input` object represents a MIDI input port on the host system. This object is created by * the MIDI subsystem and cannot be instantiated directly. * * You will find all available `Input` objects in the `WebMidi.inputs` array. * * @class Input * @param {MIDIInput} midiInput `MIDIInput` object */ function Input(midiInput) { var that = this; // User-defined handlers list this._userHandlers = { channel: {}, system: {} }; // Reference to the actual MIDIInput object this._midiInput = midiInput; Object.defineProperties(this, { /** * [read-only] Status of the MIDI port"s connection (`pending`, `open` or `closed`) * * @property connection * @type String */ connection: { enumerable: true, get: function () { return that._midiInput.connection; } }, /** * [read-only] 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. * * @property id * @type String */ id: { enumerable: true, get: function () { return that._midiInput.id; } }, /** * [read-only] Name of the manufacturer of the device that makes this port available. * * @property manufacturer * @type String */ manufacturer: { enumerable: true, get: function () { return that._midiInput.manufacturer; } }, /** * [read-only] Name of the MIDI port * * @property name * @type String */ name: { enumerable: true, get: function () { return that._midiInput.name; } }, /** * [read-only] State of the MIDI port (`connected` or `disconnected`) * * @property state * @type String */ state: { enumerable: true, get: function () { return that._midiInput.state; } }, /** * [read-only] Type of the MIDI port (`input`) * * @property type * @type String */ type: { enumerable: true, get: function () { return that._midiInput.type; } } }); this._initializeUserHandlers(); this._midiInput.onmidimessage = this._onMidiMessage.bind(this); } /** * Adds an event listener to the `Input` that will trigger a function callback when the specified * event happens. The events that are dispatched can be channel-specific or Input-wide. * * Here is a list of events that are dispatched by `Input` objects and that can be listened to. * * Channel-specific MIDI events: * * * {{#crossLink "Input/noteoff:event"}}noteoff{{/crossLink}} * * {{#crossLink "Input/noteon:event"}}noteon{{/crossLink}} * * {{#crossLink "Input/keyaftertouch:event"}}keyaftertouch{{/crossLink}} * * {{#crossLink "Input/controlchange:event"}}controlchange{{/crossLink}} * * {{#crossLink "Input/channelmode:event"}}channelmode{{/crossLink}} * * {{#crossLink "Input/programchange:event"}}programchange{{/crossLink}} * * {{#crossLink "Input/channelaftertouch:event"}}channelaftertouch{{/crossLink}} * * {{#crossLink "Input/pitchbend:event"}}pitchbend{{/crossLink}} * * Input-wide MIDI events: * * * {{#crossLink "Input/sysex:event"}}sysex{{/crossLink}} * * {{#crossLink "Input/timecode:event"}}timecode{{/crossLink}} * * {{#crossLink "Input/songposition:event"}}songposition{{/crossLink}} * * {{#crossLink "Input/songselect:event"}}songselect{{/crossLink}} * * {{#crossLink "Input/tuningrequest:event"}}tuningrequest{{/crossLink}} * * {{#crossLink "Input/clock:event"}}clock{{/crossLink}} * * {{#crossLink "Input/start:event"}}start{{/crossLink}} * * {{#crossLink "Input/continue:event"}}continue{{/crossLink}} * * {{#crossLink "Input/stop:event"}}stop{{/crossLink}} * * {{#crossLink "Input/activesensing:event"}}activesensing{{/crossLink}} * * {{#crossLink "Input/reset:event"}}reset{{/crossLink}} * * {{#crossLink "Input/midimessage:event"}}midimessage{{/crossLink}} * * {{#crossLink "Input/unknownsystemmessage:event"}}unknownsystemmessage{{/crossLink}} * * For device-wide events, the `channel` parameter will be silently ignored. You can simply use * `undefined` in that case. * * If you want to view all incoming MIDI traffic, you can listen to the input-wide `midimessage` * event. This event is dispatched for every single message that is received on that input. * * @method addListener * @chainable * * @param type {String} The type of the event. * * @param channel {Number|Array|String} The MIDI channel to listen on (integer between 1 and 16). * You can also specify an array of channel numbers or the value "all" (or leave it undefined for * input-wide events). * * @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). * * @throws {RangeError} The "channel" parameter is invalid. * @throws {TypeError} The "listener" parameter must be a function. * @throws {TypeError} The specified event type is not supported. * * @return {WebMidi} Returns the `WebMidi` object so methods can be chained. */ Input.prototype.addListener = function(type, channel, listener) { var that = this; if (channel === undefined) { channel = "all"; } if (!Array.isArray(channel)) { channel = [channel]; } // Check if channel entries are valid channel.forEach(function(item){ if (item !== "all" && !(item >= 1 && item <= 16)) { throw new RangeError( "The 'channel' parameter is invalid." ); } }); if (typeof listener !== "function") { throw new TypeError("The 'listener' parameter must be a function."); } if (wm.MIDI_SYSTEM_MESSAGES[type] !== undefined) { if (!this._userHandlers.system[type]) this._userHandlers.system[type] = []; this._userHandlers.system[type].push(listener); } else if (wm.MIDI_CHANNEL_MESSAGES[type] !== undefined) { // If "all" is present anywhere in the channel array, use all 16 channels if (channel.indexOf("all") > -1) { channel = []; for (var j = 1; j <= 16; j++) { channel.push(j); } } if (!this._userHandlers.channel[type]) { this._userHandlers.channel[type] = []; } // Push all channel listeners in the array channel.forEach(function(ch){ if (!that._userHandlers.channel[type][ch]) { that._userHandlers.channel[type][ch] = []; } that._userHandlers.channel[type][ch].push(listener); }); } else { throw new TypeError("The specified event type is not supported."); } return this; }; /** * This is an alias to the {{#crossLink "Input/addListener"}}Input.addListener(){{/crossLink}} * function. * * @method on * @since 2.0.0 */ Input.prototype.on = Input.prototype.addListener; /** * Checks if the specified event type is already defined to trigger the listener function on the * specified channel(s). If more than one channel is specified, the function will return `true` * only if all channels have the listener defined. * * For device-wide events (`sysex`, `start`, etc.), the `channel` parameter is silently ignored. * We suggest you use `undefined` in such cases. * * @method hasListener * * @param type {String} The type of the event. * @param channel {Number|Array|String} The MIDI channel to check on (between 1 and 16). You * can also specify an array of channel numbers or the string "all". * @param listener {Function} The callback function to check for. * * @throws {TypeError} The "listener" parameter must be a function. * * @return {Boolean} Boolean value indicating whether or not the channel(s) already have this * listener defined. */ Input.prototype.hasListener = function(type, channel, listener) { var that = this; if (typeof listener !== "function") { throw new TypeError("The 'listener' parameter must be a function."); } if (channel === undefined) { channel = "all"; } if (channel.constructor !== Array) { channel = [channel]; } if (wm.MIDI_SYSTEM_MESSAGES[type] !== undefined) { for (var o = 0; o < this._userHandlers.system[type].length; o++) { if (this._userHandlers.system[type][o] === listener) { return true; } } } else if (wm.MIDI_CHANNEL_MESSAGES[type] !== undefined) { // If "all" is present anywhere in the channel array, use all 16 channels if (channel.indexOf("all") > -1) { channel = []; for (var j = 1; j <= 16; j++) { channel.push(j); } } if (!this._userHandlers.channel[type]) { return false; } // Go through all specified channels return channel.every(function(chNum) { var listeners = that._userHandlers.channel[type][chNum]; return listeners && listeners.indexOf(listener) > -1; }); } return false; }; /** * Removes the specified listener from the specified channel(s). If the `listener` parameter is * left undefined, all listeners for the specified `type` will be removed from all channels. If * the `channel` is also omitted, all listeners of the specified type will be removed from all * channels. If no parameters are defined, all listeners attached to any channel of the `Input` * will be removed. * * For device-wide events (`sysex`, `start`, etc.), the `channel` parameter is silently ignored. * You can use `undefined` in such cases. * * @method removeListener * @chainable * * @param [type] {String} The type of the event. * @param [channel] {Number|String|Array} The MIDI channel(s) to check on. It can be a uint * (between 1 and 16) an array of channel numbers or the special value "all". * @param [listener] {Function} The callback function to check for. * * @throws {TypeError} The specified event type is not supported. * @throws {TypeError} The "listener" parameter must be a function.. * * @return {Input} The `Input` object for easy method chaining. */ Input.prototype.removeListener = function(type, channel, listener) { var that = this; if (listener !== undefined && typeof listener !== "function") { throw new TypeError("The 'listener' parameter must be a function."); } if (channel === undefined) { channel = "all"; } if (channel.constructor !== Array) { channel = [channel]; } if (wm.MIDI_SYSTEM_MESSAGES[type] !== undefined) { if (listener === undefined) { this._userHandlers.system[type] = []; } else { for (var o = 0; o < this._userHandlers.system[type].length; o++) { if (this._userHandlers.system[type][o] === listener) { this._userHandlers.system[type].splice(o, 1); } } } } else if (wm.MIDI_CHANNEL_MESSAGES[type] !== undefined) { // If "all" is present anywhere in the channel array, use all 16 channels if (channel.indexOf("all") > -1) { channel = []; for (var j = 1; j <= 16; j++) { channel.push(j); } } if (!this._userHandlers.channel[type]) { return this; } // Go through all specified channels channel.forEach(function(chNum) { var listeners = that._userHandlers.channel[type][chNum]; if (!listeners) { return; } if (listener === undefined) { that._userHandlers.channel[type][chNum] = []; } else { for (var l = 0; l < listeners.length; l++) { if (listeners[l] === listener) { listeners.splice(l, 1); } } } }); } else if (type === undefined) { this._initializeUserHandlers(); } else { throw new TypeError("The specified event type is not supported."); } return this; }; /** * @method _initializeUserHandlers * @protected */ Input.prototype._initializeUserHandlers = function() { for (var prop1 in wm.MIDI_CHANNEL_MESSAGES) { if (Object.prototype.hasOwnProperty.call(wm.MIDI_CHANNEL_MESSAGES, prop1)) { this._userHandlers.channel[prop1] = {}; } } for (var prop2 in wm.MIDI_SYSTEM_MESSAGES) { if (Object.prototype.hasOwnProperty.call(wm.MIDI_SYSTEM_MESSAGES, prop2)) { this._userHandlers.system[prop2] = []; } } }; /** * @method _onMidiMessage * @protected */ Input.prototype._onMidiMessage = function(e) { // Execute "midimessage" listeners (if any) if (this._userHandlers.system["midimessage"].length > 0) { var event = { target: this, data: e.data, timestamp: e.timeStamp, type: "midimessage" }; /** * Event emitted when a MIDI message is received. This should be used primarily for debugging * purposes. * * @event midimessage * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {uint} event.timestamp The timestamp when the event occurred (in milliseconds since * the [Unix Epoch](https://en.wikipedia.org/wiki/Unix_time)). * @param {String} event.type The type of event that occurred. * @since 2.1 */ this._userHandlers.system["midimessage"].forEach( function(callback) { callback(event); } ); } if (e.data[0] < 240) { // channel-specific message this._parseChannelEvent(e); this._parseNrpnEvent(e); } else if (e.data[0] <= 255) { // system message this._parseSystemEvent(e); } }; /** * Parses channel events and constructs NRPN message parts in valid sequences. * Keeps a separate NRPN buffer for each channel. * Emits an event after it receives the final CC parts msb 127 lsb 127. * If a message is incomplete and other messages are received before * the final 127 bytes, the incomplete message is cleared. * @method _parseNrpnEvent * @param e Event * @protected */ Input.prototype._parseNrpnEvent = function(e) { var command = e.data[0] >> 4; var channelBufferIndex = (e.data[0] & 0xf); // use this for index of channel in _nrpnBuffer var channel = channelBufferIndex + 1; var data1, data2; if (e.data.length > 1) { data1 = e.data[1]; data2 = e.data.length > 2 ? e.data[2] : undefined; } // nrpn disabled if(!wm.nrpnEventsEnabled) { return; } // nrpn enabled, message not valid for nrpn if( !( command === wm.MIDI_CHANNEL_MESSAGES.controlchange && ( (data1 >= wm.MIDI_NRPN_MESSAGES.increment && data1 <= wm.MIDI_NRPN_MESSAGES.parammsb) || data1 === wm.MIDI_NRPN_MESSAGES.entrymsb || data1 === wm.MIDI_NRPN_MESSAGES.entrylsb ) ) ) { return; } // set up a CC event to parse as NRPN part var ccEvent = { target: this, type: "controlchange", data: e.data, timestamp: e.timeStamp, channel: channel, controller: { number: data1, name: this.getCcNameByNumber(data1) }, value: data2 }; if( // if we get a starting MSB(CC99 - 0-126) vs an end MSB(CC99 - 127) // destroy inclomplete NRPN and begin building again ccEvent.controller.number === wm.MIDI_NRPN_MESSAGES.parammsb && ccEvent.value != wm.MIDI_NRPN_MESSAGES.nullactiveparameter ) { wm._nrpnBuffer[channelBufferIndex] = []; wm._nrpnBuffer[channelBufferIndex][0] = ccEvent; } else if( // add the param LSB wm._nrpnBuffer[channelBufferIndex].length === 1 && ccEvent.controller.number === wm.MIDI_NRPN_MESSAGES.paramlsb ) { wm._nrpnBuffer[channelBufferIndex].push(ccEvent); } else if( // add data inc/dec or value MSB for 14bit wm._nrpnBuffer[channelBufferIndex].length === 2 && (ccEvent.controller.number === wm.MIDI_NRPN_MESSAGES.increment || ccEvent.controller.number === wm.MIDI_NRPN_MESSAGES.decrement || ccEvent.controller.number === wm.MIDI_NRPN_MESSAGES.entrymsb) ) { wm._nrpnBuffer[channelBufferIndex].push(ccEvent); } else if( // if we have a value MSB, only add an LSB to pair with that wm._nrpnBuffer[channelBufferIndex].length === 3 && wm._nrpnBuffer[channelBufferIndex][2].number === wm.MIDI_NRPN_MESSAGES.entrymsb && ccEvent.controller.number === wm.MIDI_NRPN_MESSAGES.entrylsb ) { wm._nrpnBuffer[channelBufferIndex].push(ccEvent); } else if( // add an end MSB(CC99 - 127) wm._nrpnBuffer[channelBufferIndex].length >= 3 && wm._nrpnBuffer[channelBufferIndex].length <= 4 && ccEvent.controller.number === wm.MIDI_NRPN_MESSAGES.parammsb && ccEvent.value === wm.MIDI_NRPN_MESSAGES.nullactiveparameter ) { wm._nrpnBuffer[channelBufferIndex].push(ccEvent); } else if( // add an end LSB(CC99 - 127) wm._nrpnBuffer[channelBufferIndex].length >= 4 && wm._nrpnBuffer[channelBufferIndex].length <= 5 && ccEvent.controller.number === wm.MIDI_NRPN_MESSAGES.paramlsb && ccEvent.value === wm.MIDI_NRPN_MESSAGES.nullactiveparameter ) { wm._nrpnBuffer[channelBufferIndex].push(ccEvent); // now we have a full inc or dec NRPN message, lets create that event! var rawData = []; wm._nrpnBuffer[channelBufferIndex].forEach(function(ev) { rawData.push(ev.data); }); var nrpnNumber = (wm._nrpnBuffer[channelBufferIndex][0].value<<7) | (wm._nrpnBuffer[channelBufferIndex][1].value); var nrpnValue = wm._nrpnBuffer[channelBufferIndex][2].value; if(wm._nrpnBuffer[channelBufferIndex].length === 6) { nrpnValue = (wm._nrpnBuffer[channelBufferIndex][2].value<<7) | (wm._nrpnBuffer[channelBufferIndex][3].value); } var nrpnControllerType = ""; switch (wm._nrpnBuffer[channelBufferIndex][2].controller.number) { case wm.MIDI_NRPN_MESSAGES.entrymsb: nrpnControllerType = wm._nrpnTypes[0]; break; case wm.MIDI_NRPN_MESSAGES.increment: nrpnControllerType = wm._nrpnTypes[1]; break; case wm.MIDI_NRPN_MESSAGES.decrement: nrpnControllerType = wm._nrpnTypes[2]; break; default: throw new Error("The NPRN type was unidentifiable."); } /** * Event emitted when a valid NRPN message sequence has been received on a specific device and * channel. * * @private * * @event nrpn * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Array} event.data The raw MIDI message as arrays of 8 bit values( Uint8Array ). * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {uint} event.channel The channel where the event occurred (between 1 and 16). * @param {String} event.type The type of event that occurred. * @param {Object} event.controller * @param {uint} event.controller.number The number of the NRPN. * @param {String} event.controller.name The usual name or function of the controller. * @param {uint} event.value The value received (between 0 and 65535). */ var nrpnEvent = { timestamp: ccEvent.timestamp, channel: ccEvent.channel, type: "nrpn", data: rawData, controller: { number: nrpnNumber, type: nrpnControllerType, name: "Non-Registered Parameter " + nrpnNumber }, value: nrpnValue }; // now we are done building an NRPN, so clear the NRPN buffer for this channel wm._nrpnBuffer[channelBufferIndex] = []; // If some callbacks have been defined for this event, on that device and channel, execute // them. if ( this._userHandlers.channel[nrpnEvent.type] && this._userHandlers.channel[nrpnEvent.type][nrpnEvent.channel] ) { this._userHandlers.channel[nrpnEvent.type][nrpnEvent.channel].forEach( function(callback) { callback(nrpnEvent); } ); } } else { // something didn't match, clear the incomplete NRPN message by wm._nrpnBuffer[channelBufferIndex] = []; } }; /** * @method _parseChannelEvent * @param e Event * @protected */ Input.prototype._parseChannelEvent = function(e) { var command = e.data[0] >> 4; var channel = (e.data[0] & 0xf) + 1; var data1, data2; if (e.data.length > 1) { data1 = e.data[1]; data2 = e.data.length > 2 ? e.data[2] : undefined; } // Returned event var event = { target: this, data: e.data, timestamp: e.timeStamp, channel: channel }; if ( command === wm.MIDI_CHANNEL_MESSAGES.noteoff || (command === wm.MIDI_CHANNEL_MESSAGES.noteon && data2 === 0) ) { /** * Event emitted when a note off MIDI message has been received on a specific device and * channel. * * @event noteoff * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {uint} event.channel The channel where the event occurred (between 1 and 16). * @param {String} event.type The type of event that occurred. * @param {Object} event.note * @param {uint} event.note.number The MIDI note number. * @param {String} event.note.name The usual note name (C, C#, D, D#, etc.). * @param {uint} event.note.octave The octave (between -2 and 8). * @param {Number} event.velocity The release velocity (between 0 and 1). * @param {Number} event.rawVelocity The attack velocity expressed as a 7-bit integer (between * 0 and 127). */ event.type = "noteoff"; event.note = { number: data1, name: wm._notes[data1 % 12], octave: wm.getOctave(data1) }; event.velocity = data2 / 127; event.rawVelocity = data2; } else if (command === wm.MIDI_CHANNEL_MESSAGES.noteon) { /** * Event emitted when a note on MIDI message has been received on a specific device and * channel. * * @event noteon * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {uint} event.channel The channel where the event occurred (between 1 and 16). * @param {String} event.type The type of event that occurred. * @param {Object} event.note * @param {uint} event.note.number The MIDI note number. * @param {String} event.note.name The usual note name (C, C#, D, D#, etc.). * @param {uint} event.note.octave The octave (between -2 and 8). * @param {Number} event.velocity The attack velocity (between 0 and 1). * @param {Number} event.rawVelocity The attack velocity expressed as a 7-bit integer (between * 0 and 127). */ event.type = "noteon"; event.note = { number: data1, name: wm._notes[data1 % 12], octave: wm.getOctave(data1) }; event.velocity = data2 / 127; event.rawVelocity = data2; } else if (command === wm.MIDI_CHANNEL_MESSAGES.keyaftertouch) { /** * Event emitted when a key-specific aftertouch MIDI message has been received on a specific * device and channel. * * @event keyaftertouch * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {uint} event.channel The channel where the event occurred (between 1 and 16). * @param {String} event.type The type of event that occurred. * @param {Object} event.note * @param {uint} event.note.number The MIDI note number. * @param {String} event.note.name The usual note name (C, C#, D, D#, etc.). * @param {uint} event.note.octave The octave (between -2 and 8). * @param {Number} event.value The aftertouch amount (between 0 and 1). */ event.type = "keyaftertouch"; event.note = { number: data1, name: wm._notes[data1 % 12], octave: wm.getOctave(data1) }; event.value = data2 / 127; } else if ( command === wm.MIDI_CHANNEL_MESSAGES.controlchange && data1 >= 0 && data1 <= 119 ) { /** * Event emitted when a control change MIDI message has been received on a specific device and * channel. * * @event controlchange * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {uint} event.channel The channel where the event occurred (between 1 and 16). * @param {String} event.type The type of event that occurred. * @param {Object} event.controller * @param {uint} event.controller.number The number of the controller. * @param {String} event.controller.name The usual name or function of the controller. * @param {uint} event.value The value received (between 0 and 127). */ event.type = "controlchange"; event.controller = { number: data1, name: this.getCcNameByNumber(data1) }; event.value = data2; } else if ( command === wm.MIDI_CHANNEL_MESSAGES.channelmode && data1 >= 120 && data1 <= 127 ) { /** * Event emitted when a channel mode MIDI message has been received on a specific device and * channel. * * @event channelmode * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {uint} event.channel The channel where the event occurred (between 1 and 16). * @param {String} event.type The type of event that occurred. * @param {Object} event.controller * @param {uint} event.controller.number The number of the controller. * @param {String} event.controller.name The usual name or function of the controller. * @param {uint} event.value The value received (between 0 and 127). */ event.type = "channelmode"; event.controller = { number: data1, name: this.getChannelModeByNumber(data1) }; event.value = data2; } else if (command === wm.MIDI_CHANNEL_MESSAGES.programchange) { /** * Event emitted when a program change MIDI message has been received on a specific device and * channel. * * @event programchange * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {uint} event.channel The channel where the event occurred (between 1 and 16). * @param {String} event.type The type of event that occurred. * @param {uint} event.value The value received (between 0 and 127). */ event.type = "programchange"; event.value = data1; } else if (command === wm.MIDI_CHANNEL_MESSAGES.channelaftertouch) { /** * Event emitted when a channel-wide aftertouch MIDI message has been received on a specific * device and channel. * * @event channelaftertouch * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {uint} event.channel The channel where the event occurred (between 1 and 16). * @param {String} event.type The type of event that occurred. * @param {Number} event.value The aftertouch value received (between 0 and 1). */ event.type = "channelaftertouch"; event.value = data1 / 127; } else if (command === wm.MIDI_CHANNEL_MESSAGES.pitchbend) { /** * Event emitted when a pitch bend MIDI message has been received on a specific device and * channel. * * @event pitchbend * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {uint} event.channel The channel where the event occurred (between 1 and 16). * @param {String} event.type The type of event that occurred. * @param {Number} event.value The pitch bend value received (between -1 and 1). */ event.type = "pitchbend"; event.value = ((data2 << 7) + data1 - 8192) / 8192; } else { event.type = "unknownchannelmessage"; } // If some callbacks have been defined for this event, on that device and channel, execute them. if ( this._userHandlers.channel[event.type] && this._userHandlers.channel[event.type][channel] ) { this._userHandlers.channel[event.type][channel].forEach( function(callback) { callback(event); } ); } }; /** * Returns the name of a control change message matching the specified number. If no match is * found, the function returns `undefined`. * * @method getCcNameByNumber * * @param number {Number} The number of the control change message. * @returns {String|undefined} The matching control change name or `undefined`. * * @throws RangeError The control change number must be between 0 and 119. * * @since 2.0.0 */ Input.prototype.getCcNameByNumber = function(number) { number = Math.floor(number); if ( !(number >= 0 && number <= 119) ) { throw new RangeError("The control change number must be between 0 and 119."); } for (var cc in wm.MIDI_CONTROL_CHANGE_MESSAGES) { if ( Object.prototype.hasOwnProperty.call(wm.MIDI_CONTROL_CHANGE_MESSAGES, cc) && number === wm.MIDI_CONTROL_CHANGE_MESSAGES[cc] ) { return cc; } } return undefined; }; /** * Returns the channel mode name matching the specified number. If no match is found, the function * returns `undefined`. * * @method getChannelModeByNumber * * @param number {Number} The number of the channel mode message. * @returns {String|undefined} The matching channel mode message"s name or `undefined`; * * @throws RangeError The channel mode number must be between 120 and 127. * * @since 2.0.0 */ Input.prototype.getChannelModeByNumber = function(number) { number = Math.floor(number); if ( !(number >= 120 && status <= 127) ) { throw new RangeError("The control change number must be between 120 and 127."); } for (var cm in wm.MIDI_CHANNEL_MODE_MESSAGES) { if ( Object.prototype.hasOwnProperty.call(wm.MIDI_CHANNEL_MODE_MESSAGES, cm) && number === wm.MIDI_CHANNEL_MODE_MESSAGES[cm] ) { return cm; } } }; /** * @method _parseSystemEvent * @protected */ Input.prototype._parseSystemEvent = function(e) { var command = e.data[0]; // Returned event var event = { target: this, data: e.data, timestamp: e.timeStamp }; if (command === wm.MIDI_SYSTEM_MESSAGES.sysex) { /** * Event emitted when a system exclusive MIDI message has been received. You should note that, * to receive `sysex` events, you must call the `WebMidi.enable()` method with a second * parameter set to `true`: * * WebMidi.enable(function(err) { * * if (err) { * console.log("WebMidi could not be enabled."); * } * * var input = WebMidi.inputs[0]; * * input.addListener("sysex", "all", function (e) { * console.log(e); * }); * * }, true); * * @event sysex * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. * */ event.type = "sysex"; } else if (command === wm.MIDI_SYSTEM_MESSAGES.timecode) { /** * Event emitted when a system MIDI time code quarter frame message has been received. * * @event timecode * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. */ event.type = "timecode"; } else if (command === wm.MIDI_SYSTEM_MESSAGES.songposition) { /** * Event emitted when a system song position pointer MIDI message has been received. * * @event songposition * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. */ event.type = "songposition"; } else if (command === wm.MIDI_SYSTEM_MESSAGES.songselect) { /** * Event emitted when a system song select MIDI message has been received. * * @event songselect * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. * @param {String} event.song Song (or sequence) number to select. */ event.type = "songselect"; event.song = e.data[1]; } else if (command === wm.MIDI_SYSTEM_MESSAGES.tuningrequest) { /** * Event emitted when a system tune request MIDI message has been received. * * @event tuningrequest * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit * values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. */ event.type = "tuningrequest"; } else if (command === wm.MIDI_SYSTEM_MESSAGES.clock) { /** * Event emitted when a system timing clock MIDI message has been received. * * @event clock * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit * values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. */ event.type = "clock"; } else if (command === wm.MIDI_SYSTEM_MESSAGES.start) { /** * Event emitted when a system start MIDI message has been received. * * @event start * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit * values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. */ event.type = "start"; } else if (command === wm.MIDI_SYSTEM_MESSAGES.continue) { /** * Event emitted when a system continue MIDI message has been received. * * @event continue * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit * values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. */ event.type = "continue"; } else if (command === wm.MIDI_SYSTEM_MESSAGES.stop) { /** * Event emitted when a system stop MIDI message has been received. * * @event stop * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit * values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. */ event.type = "stop"; } else if (command === wm.MIDI_SYSTEM_MESSAGES.activesensing) { /** * Event emitted when a system active sensing MIDI message has been received. * * @event activesensing * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. */ event.type = "activesensing"; } else if (command === wm.MIDI_SYSTEM_MESSAGES.reset) { /** * Event emitted when a system reset MIDI message has been received. * * @event reset * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. */ event.type = "reset"; } else { /** * Event emitted when an unknown system MIDI message has been received. It could be, for * example, one of the undefined/reserved messages. * * @event unknownsystemmessage * * @param {Object} event * @param {Input} event.target The `Input` that triggered the event. * @param {Uint8Array} event.data The raw MIDI message as an array of 8 bit values. * @param {Number} event.timestamp The time when the event occurred (in milliseconds) * @param {String} event.type The type of event that occurred. */ event.type = "unknownsystemmessage"; } // If some callbacks have been defined for this event, execute them. if (this._userHandlers.system[event.type]) { this._userHandlers.system[event.type].forEach( function(callback) { callback(event); } ); } }; /** * The `Output` object represents a MIDI output port on the host system. This object is created by * the MIDI subsystem and cannot be instantiated directly. * * You will find all available `Output` objects in the `WebMidi.outputs` array. * * @class Output * @param {MIDIOutput} midiOutput Actual `MIDIOutput` object as defined by the MIDI subsystem */ function Output(midiOutput) { var that = this; this._midiOutput = midiOutput; Object.defineProperties(this, { /** * [read-only] Status of the MIDI port"s connection * * @property connection * @type String */ connection: { enumerable: true, get: function () { return that._midiOutput.connection; } }, /** * [read-only] ID string of the MIDI port * * @property id * @type String */ id: { enumerable: true, get: function () { return that._midiOutput.id; } }, /** * [read-only] Manufacturer of the device to which this port belongs * * @property manufacturer * @type String */ manufacturer: { enumerable: true, get: function () { return that._midiOutput.manufacturer; } }, /** * [read-only] Name of the MIDI port * * @property name * @type String */ name: { enumerable: true, get: function () { return that._midiOutput.name; } }, /** * [read-only] State of the MIDI port * * @property state * @type String */ state: { enumerable: true, get: function () { return that._midiOutput.state; } }, /** * [read-only] Type of the MIDI port (`output`) * * @property type * @type String */ type: { enumerable: true, get: function () { return that._midiOutput.type; } } }); } /** * Sends a MIDI message on the MIDI output port, at the scheduled timestamp. * * Unless, you are familiar with the details of the MIDI message format, you should not use this * method directly. Instead, use one of the simpler helper methods: `playNote()`, `stopNote()`, * `sendControlChange()`, `sendSystemMessage()`, etc. * * @method send * @chainable * * @param status {Number} The MIDI status byte of the message (128-255). * * @param [data=[]] {Array} An array of uints for the message. The number of data bytes varies * depending on the status byte. It is perfectly legal to send no data for some message types (use * undefined or an empty array in this case). Each byte must be between 0 and 255. * * @param [timestamp=0] {DOMHighResTimeStamp} The timestamp at which to send the message. You can * use `WebMidi.time` to retrieve the current timestamp. To send immediately, leave blank or use * 0. * * @throws {RangeError} The status byte must be an integer between 128 (0x80) and 255 (0xFF). * @throws {RangeError} Data bytes must be integers between 0 (0x00) and 255 (0x7F). * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.send = function(status, data, timestamp) { if ( !(status >= 128 && status <= 255) ) { throw new RangeError("The status byte must be an integer between 128 (0x80) and 255 (0xFF)."); } if (data === undefined) data = []; if ( !Array.isArray(data) ) data = [data]; var message = []; data.forEach(function(item){ var parsed = Math.floor(item); // mandatory because of "null" if (parsed >= 0 && parsed <= 255) { message.push(parsed); } else { throw new RangeError("Data bytes must be integers between 0 (0x00) and 255 (0xFF)."); } }); this._midiOutput.send([status].concat(message), parseFloat(timestamp) || 0); return this; }; /** * Sends a MIDI *system exclusive* (sysex) message. The generated message will automatically be * prepended with the *sysex* byte (0xF0) and terminated with the *end of sysex* byte (0xF7). * * To use the `sendSysex()` method, system exclusive message support must have been enabled. To * do so, you must pass `true` as the second parameter to `WebMidi.enable()`: * * WebMidi.enable(function (err) { * if (err) { * console.warn(err); * } else { * console.log("Sysex is enabled!"); * } * }, true); * * Note that, depending on browser, version and platform, it may be necessary to serve the page * over HTTPS to enable sysex support. * * #### Examples * * If you want to send a sysex message to a Korg device connected to the first output, you would * use the following code: * * WebMidi.outputs[0].sendSysex(0x42, [0x1, 0x2, 0x3, 0x4, 0x5]); * * The parameters can be specified using any number notation (decimal, hex, binary, etc.). * Therefore, the code below is equivalent to the code above: * * WebMidi.outputs[0].sendSysex(66, [1, 2, 3, 4, 5]); * * The above code sends the byte values 1, 2, 3, 4 and 5 to Korg devices (hex 42 is the same as * decimal 66). * * 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: * * 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. * * @method sendSysex * @chainable * * @param manufacturer {Number|Array} An unsigned integer or an array of three unsigned integers * between 0 and 127 that identify the targeted manufacturer. The *MIDI Manufacturers Association* * maintains a full list of * [Manufacturer ID Numbers](https://www.midi.org/specifications-old/item/manufacturer-id-numbers) * . * * @param [data=[]] {Array} An array of uints between 0 and 127. This is the data you wish to * transfer. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @throw Sysex message support must first be activated. * @throw The data bytes of a sysex message must be integers between 0 (0x00) and 127 (0x7F). * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendSysex = function(manufacturer, data, options) { if (!wm.sysexEnabled) { throw new Error("Sysex message support must first be activated."); } options = options || {}; manufacturer = [].concat(manufacturer); data.forEach(function(item){ if (item < 0 || item > 127) { throw new RangeError( "The data bytes of a sysex message must be integers between 0 (0x00) and 127 (0x7F)." ); } }); data = manufacturer.concat(data, wm.MIDI_SYSTEM_MESSAGES.sysexend); this.send(wm.MIDI_SYSTEM_MESSAGES.sysex, data, this._parseTimeParameter(options.time)); 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. * * @method sendTimecodeQuarterFrame * @chainable * * @param value {Number} The quarter frame message content (integer between 0 and 127). * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendTimecodeQuarterFrame = function(value, options) { options = options || {}; this.send(wm.MIDI_SYSTEM_MESSAGES.timecode, value, this._parseTimeParameter(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. * * @method sendSongPosition * @chainable * * @param [value=0] {Number} The MIDI beat to cue to (int between 0 and 16383). * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendSongPosition = function(value, options) { value = Math.floor(value) || 0; options = options || {}; var msb = (value >> 7) & 0x7F; var lsb = value & 0x7F; this.send( wm.MIDI_SYSTEM_MESSAGES.songposition, [msb, lsb], this._parseTimeParameter(options.time) ); return this; }; /** * Sends a *Song Select* MIDI message. Beware that some devices will display position 0 as * position 1 for user-friendlyness. * * @method sendSongSelect * @chainable * * @param value {Number} The number of the song to select (integer between 0 and 127). * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @throws The song number must be between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendSongSelect = function(value, options) { value = Math.floor(value); options = options || {}; if ( !(value >= 0 && value <= 127) ) { throw new RangeError("The song number must be between 0 and 127."); } this.send(wm.MIDI_SYSTEM_MESSAGES.songselect, [value], this._parseTimeParameter(options.time)); return this; }; /** * Sends a *MIDI tuning request* real-time message. * * Note: there is currently a bug in Chrome"s MIDI implementation. If you try to use this * function, Chrome will actually throw a "Message is incomplete" error. The bug is * [scheduled to be fixed](https://bugs.chromium.org/p/chromium/issues/detail?id=610116). * * @method sendTuningRequest * @chainable * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendTuningRequest = function(options) { options = options || {}; this.send( wm.MIDI_SYSTEM_MESSAGES.tuningrequest, undefined, this._parseTimeParameter(options.time) ); return this; }; /** * Sends a *MIDI Clock* real-time message. According to the standard, there are 24 MIDI Clocks * for every quarter note. * * @method sendClock * @chainable * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendClock = function(options) { options = options || {}; this.send(wm.MIDI_SYSTEM_MESSAGES.clock, undefined, this._parseTimeParameter(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()` function. * * @method sendStart * @chainable * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendStart = function(options) { options = options || {}; this.send(wm.MIDI_SYSTEM_MESSAGES.start, undefined, this._parseTimeParameter(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()` function. * * @method sendContinue * @chainable * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @return {WebMidi} Returns the `WebMidi` object so methods can be chained. */ Output.prototype.sendContinue = function(options) { options = options || {}; this.send(wm.MIDI_SYSTEM_MESSAGES.continue, undefined, this._parseTimeParameter(options.time)); return this; }; /** * Sends a *Stop* real-time message. This tells the device connected to this port to stop playback * immediately (or at the scheduled time). * * @method sendStop * @chainable * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendStop = function(options) { options = options || {}; this.send(wm.MIDI_SYSTEM_MESSAGES.stop, undefined, this._parseTimeParameter(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 should be sent every 300 ms if there was * no other activity on the MIDI port. * * @method sendActiveSensing * @chainable * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendActiveSensing = function(options) { options = options || {}; this.send( wm.MIDI_SYSTEM_MESSAGES.activesensing, [], this._parseTimeParameter(options.time) ); return this; }; /** * Sends *Reset* real-time message. This tells the device connected to this port that is should * reset itself to a default state. * * @method sendReset * @chainable * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendReset = function(options) { options = options || {}; this.send(wm.MIDI_SYSTEM_MESSAGES.reset, undefined, this._parseTimeParameter(options.time)); return this; }; /** * Sends a MIDI **note off** message to the specified channel(s) for a single note or multiple * simultaneous notes (chord). You can delay the execution of the **note off** command by using * the `time` property of the `options` parameter (in milliseconds). * * @method stopNote * @chainable * * @param note {Number|Array|String} The note(s) you wish to stop. The notes can be specified in * one of three ways. The first way is by using the MIDI note number (an integer between `0` and * `127`). The second way is by using the note name followed by the octave (C3, G#4, F-1, Db7). * The octave range should be between -2 and 8. The lowest note is C-2 (MIDI note number 0) and * the highest note is G8 (MIDI note number 127). It is also possible to specify an array of note * numbers and/or names. The final way is to use the special value `all` to send an "allnotesoff" * channel message. * * @param [channel=all] {Number|Array|String} The MIDI channel number (between `1` and `16`) or an * array of channel numbers. If the special value `all` is used (default), the message will be * sent to all 16 channels. * * @param {Object} [options={}] * * @param {Boolean} [options.rawVelocity=false] Controls whether the release velocity is set using * an integer between `0` and `127` (`true`) or a decimal number between `0` and `1` (`false`, * default). * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @param {Number} [options.velocity=0.5] The velocity at which to release the note (between `0` * and `1`). If the `rawVelocity` option is `true`, the value should be specified as an integer * between `0` and `127`. An invalid velocity value will silently trigger the default of `0.5`. * Note that when the first parameter to `stopNote()` is `all`, the release velocity is silently * ignored. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.stopNote = function(note, channel, options) { if (note === "all") { return this.sendChannelMode("allnotesoff", 0, channel, options); } var nVelocity = 64; options = options || {}; if (options.rawVelocity) { if (!isNaN(options.velocity) && options.velocity >= 0 && options.velocity <= 127) { nVelocity = options.velocity; } } else { if (!isNaN(options.velocity) && options.velocity >= 0 && options.velocity <= 1) { nVelocity = options.velocity * 127; } } // Send note off messages this._convertNoteToArray(note).forEach(function(item) { wm.toMIDIChannels(channel).forEach(function(ch) { this.send( (wm.MIDI_CHANNEL_MESSAGES.noteoff << 4) + (ch - 1), [item, Math.round(nVelocity)], this._parseTimeParameter(options.time) ); }.bind(this)); }.bind(this)); return this; }; /** * Requests the playback of a single note or multiple notes on the specified channel(s). You can * delay the execution of the **note on** command by using the `time` property of the `options` * parameter (milliseconds). * * If no duration is specified in the `options`, the note will play until a matching **note off** * is sent. If a duration is specified, a **note off** will be automatically sent after said * duration. * * Note: As per the MIDI standard, a **note on** event with a velocity of `0` is considered to be * a **note off**. * * @method playNote * @chainable * * @param note {Number|String|Array} The note(s) you wish to play. The notes can be specified in * one of two ways. The first way is by using the MIDI note number (an integer between 0 and 127). * The second way is by using the note name followed by the octave (C3, G#4, F-1, Db7). The octave * range should be between -2 and 8. The lowest note is C-2 (MIDI note number 0) and the highest * note is G8 (MIDI note number 127). It is also possible to specify an array of note numbers * and/or names. * * @param [channel=all] {Number|Array|String} The MIDI channel number (between `1` and `16`) or an * array of channel numbers. If the special value **all** is used (default), the message will be * sent to all 16 channels. * * @param {Object} [options={}] * * @param {Number} [options.duration=undefined] The number of milliseconds (integer) to wait * before sending a matching **note off** event. If left undefined, only a **note on** message is * sent. * * @param {Boolean} [options.rawVelocity=false] Controls whether the attack and release velocities * are set using integers between `0` and `127` (`true`) or a decimal number between `0` and `1` * (`false`, default). * * @param {Number} [options.release=0.5] The velocity at which to release the note (between `0` * and `1`). If the `rawVelocity` option is `true`, the value should be specified as an integer * between `0` and `127`. 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 {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @param {Number} [options.velocity=0.5] The velocity at which to play the note (between `0` and * `1`). If the `rawVelocity` option is `true`, the value should be specified as an integer * between `0` and `127`. An invalid velocity value will silently trigger the default of `0.5`. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.playNote = function(note, channel, options) { var time, nVelocity = 64; options = options || {}; if (options.rawVelocity) { if (!isNaN(options.velocity) && options.velocity >= 0 && options.velocity <= 127) { nVelocity = options.velocity; } } else { if (!isNaN(options.velocity) && options.velocity >= 0 && options.velocity <= 1) { nVelocity = options.velocity * 127; } } time = this._parseTimeParameter(options.time); // Send note on messages this._convertNoteToArray(note).forEach(function(item) { wm.toMIDIChannels(channel).forEach(function(ch) { this.send( (wm.MIDI_CHANNEL_MESSAGES.noteon << 4) + (ch - 1), [item, Math.round(nVelocity)], time ); }.bind(this)); }.bind(this)); // Send note off messages (only if a valid duration has been defined) if (!isNaN(options.duration)) { if (options.duration <= 0) { options.duration = 0; } var nRelease = 64; if (options.rawVelocity) { if (!isNaN(options.release) && options.release >= 0 && options.release <= 127) { nRelease = options.release; } } else { if (!isNaN(options.release) && options.release >= 0 && options.release <= 1) { nRelease = options.release * 127; } } this._convertNoteToArray(note).forEach(function(item) { wm.toMIDIChannels(channel).forEach(function(ch) { this.send( (wm.MIDI_CHANNEL_MESSAGES.noteoff << 4) + (ch - 1), [item, Math.round(nRelease)], (time || wm.time) + options.duration ); }.bind(this)); }.bind(this)); } return this; }; /** * 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 * {{#crossLink "WebMidi/sendChannelAftertouch:method"}}sendChannelAftertouch(){{/crossLink}}. * * @method sendKeyAftertouch * @chainable * * @param note {Number|String|Array} The note for which you are sending an aftertouch value. The * notes can be specified in one of two ways. The first way is by using the MIDI note number (an * integer between 0 and 127). The second way is by using the note name followed by the octave * (C3, G#4, F-1, Db7). The octave range should be between -2 and 8. The lowest note is C-2 (MIDI * note number 0) and the highest note is G8 (MIDI note number 127). It is also possible to use * an array of note names and/or numbers. * * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Number} [pressure=0.5] The pressure level to send (between 0 and 1). * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @throws {RangeError} The channel must be between 1 and 16. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendKeyAftertouch = function(note, channel, pressure, options) { var that = this; options = options || {}; if (channel < 1 || channel > 16) { throw new RangeError("The channel must be between 1 and 16."); } if (isNaN(pressure) || pressure < 0 || pressure > 1) { pressure = 0.5; } var nPressure = Math.round(pressure * 127); this._convertNoteToArray(note).forEach(function(item) { wm.toMIDIChannels(channel).forEach(function(ch) { that.send( (wm.MIDI_CHANNEL_MESSAGES.keyaftertouch << 4) + (ch - 1), [item, nPressure], that._parseTimeParameter(options.time) ); }); }); return this; }; /** * Sends a MIDI `control change` message (a.k.a. CC message) to the specified channel(s) at the * scheduled time. The control change message to send can be specified numerically or by using one * of the following common names: * * * `bankselectcoarse` (#0) * * `modulationwheelcoarse` (#1) * * `breathcontrollercoarse` (#2) * * `footcontrollercoarse` (#4) * * `portamentotimecoarse` (#5) * * `dataentrycoarse` (#6) * * `volumecoarse` (#7) * * `balancecoarse` (#8) * * `pancoarse` (#10) * * `expressioncoarse` (#11) * * `effectcontrol1coarse` (#12) * * `effectcontrol2coarse` (#13) * * `generalpurposeslider1` (#16) * * `generalpurposeslider2` (#17) * * `generalpurposeslider3` (#18) * * `generalpurposeslider4` (#19) * * `bankselectfine` (#32) * * `modulationwheelfine` (#33) * * `breathcontrollerfine` (#34) * * `footcontrollerfine` (#36) * * `portamentotimefine` (#37) * * `dataentryfine` (#38) * * `volumefine` (#39) * * `balancefine` (#40) * * `panfine` (#42) * * `expressionfine` (#43) * * `effectcontrol1fine` (#44) * * `effectcontrol2fine` (#45) * * `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) * * `reverblevel` (#91) * * `tremololevel` (#92) * * `choruslevel` (#93) * * `celestelevel` (#94) * * `phaserlevel` (#95) * * `databuttonincrement` (#96) * * `databuttondecrement` (#97) * * `nonregisteredparametercoarse` (#98) * * `nonregisteredparameterfine` (#99) * * `registeredparametercoarse` (#100) * * `registeredparameterfine` (#101) * * Note: as you can see above, not all control change message have a matching common name. This * does not mean you cannot use the others. It simply means you will need to use their number * instead of their name. * * @method sendControlChange * @chainable * * @param controller {Number|String} The MIDI controller number (0-119) or name. * * @param [value=0] {Number} The value to send (0-127). * * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @throws {RangeError} Controller numbers must be between 0 and 119. * @throws {RangeError} Value must be between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendControlChange = function(controller, value, channel, options) { options = options || {}; if (typeof controller === "string") { controller = wm.MIDI_CONTROL_CHANGE_MESSAGES[controller]; if (controller === undefined) throw new TypeError("Invalid controller name."); } else { controller = Math.floor(controller); if ( !(controller >= 0 && controller <= 119) ) { throw new RangeError("Controller numbers must be between 0 and 119."); } } value = Math.floor(value) || 0; if ( !(value >= 0 && value <= 127) ) { throw new RangeError("Controller value must be between 0 and 127."); } wm.toMIDIChannels(channel).forEach(function(ch) { this.send( (wm.MIDI_CHANNEL_MESSAGES.controlchange << 4) + (ch - 1), [controller, value], this._parseTimeParameter(options.time) ); }.bind(this)); return this; }; /** * Selects a MIDI registered parameter so it is affected by data entry, data increment and data * decrement messages. * * @method _selectRegisteredParameter * @protected * * @param parameter {Array} A two-position array specifying the two control bytes (0x65, 0x64) * that identify the registered parameter. * @param channel * @param time * * @returns {Output} */ Output.prototype._selectRegisteredParameter = function(parameter, channel, time) { var that = this; parameter[0] = Math.floor(parameter[0]); if ( !(parameter[0] >= 0 && parameter[0] <= 127) ) { throw new RangeError("The control65 value must be between 0 and 127"); } parameter[1] = Math.floor(parameter[1]); if ( !(parameter[1] >= 0 && parameter[1] <= 127) ) { throw new RangeError("The control64 value must be between 0 and 127"); } wm.toMIDIChannels(channel).forEach(function() { that.sendControlChange(0x65, parameter[0], channel, {time: time}); that.sendControlChange(0x64, parameter[1], channel, {time: time}); }); return this; }; /** * Selects a MIDI non-registered parameter so it is affected by data entry, data increment and * data decrement messages. * * @method _selectNonRegisteredParameter * @protected * * @param parameter {Array} A two-position array specifying the two control bytes (0x63, 0x62) * that identify the registered parameter. * @param channel * @param time * * @returns {Output} */ Output.prototype._selectNonRegisteredParameter = function(parameter, channel, time) { var that = this; 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"); } wm.toMIDIChannels(channel).forEach(function() { that.sendControlChange(0x63, parameter[0], channel, {time: time}); that.sendControlChange(0x62, parameter[1], channel, {time: time}); }); return this; }; /** * Sets the value of the currently selected MIDI registered parameter. * * @method _setCurrentRegisteredParameter * @protected * * @param data {int|Array} * @param channel * @param time * * @returns {Output} */ Output.prototype._setCurrentRegisteredParameter = function(data, channel, time) { var that = this; data = [].concat(data); data[0] = Math.floor(data[0]); if ( !(data[0] >= 0 && data[0] <= 127) ) { throw new RangeError("The msb value must be between 0 and 127"); } wm.toMIDIChannels(channel).forEach(function() { that.sendControlChange(0x06, data[0], channel, {time: time}); }); data[1] = Math.floor(data[1]); if(data[1] >= 0 && data[1] <= 127) { wm.toMIDIChannels(channel).forEach(function() { that.sendControlChange(0x26, data[1], channel, {time: time}); }); } 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 * `_setCurrentRegisteredParameter()`. * * @method _deselectRegisteredParameter * @protected * * @param channel * @param time * * @returns {Output} */ Output.prototype._deselectRegisteredParameter = function(channel, time) { var that = this; wm.toMIDIChannels(channel).forEach(function() { that.sendControlChange(0x65, 0x7F, channel, {time: time}); that.sendControlChange(0x64, 0x7F, channel, {time: time}); }); return this; }; /** * Sets the specified MIDI registered parameter to the desired value. The value is defined with * up to two bytes of data that each can go from 0 to 127. * * >Unless you are very familiar with the MIDI standard you probably should favour one of the * >simpler to use functions such as: `setPitchbendRange()`, `setModulationRange()`, * >`setMasterTuning()`, etc. * * MIDI registered parameters extend the original list of control change messages. Currently, * there are only a limited number of them. Here are the original registered parameters with the * identifier that can be used as the first parameter of 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` * * Note that the **Tuning Program** and **Tuning Bank** parameters are part of the *MIDI Tuning * Standard*, which is not widely implemented. * * Another set of extra parameters have been later added for 3D sound controllers. They are: * * * 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` * * @method setRegisteredParameter * @chainable * * @param parameter {String|Array} 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 [data=[]] {Number|Array} An single integer or an array of integers with a maximum length * of 2 specifying the desired data. * * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.setRegisteredParameter = function(parameter, data, channel, options) { var that = this; options = options || {}; if ( !Array.isArray(parameter) ) { if ( !wm.MIDI_REGISTERED_PARAMETER[parameter]) { throw new Error("The specified parameter is not available."); } parameter = wm.MIDI_REGISTERED_PARAMETER[parameter]; } wm.toMIDIChannels(channel).forEach(function() { that._selectRegisteredParameter(parameter, channel, options.time); that._setCurrentRegisteredParameter(data, channel, options.time); that._deselectRegisteredParameter(channel, options.time); }); return this; }; /** * Sets a non-registered parameter 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 an 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: * * WebMidi.outputs[0].setNonRegisteredParameter([1, 8], 123); * * Obviously, you 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: * * WebMidi.outputs[0].setNonRegisteredParameter([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: * * WebMidi.outputs[0].setNonRegisteredParameter([2, 63], [0, 10]); * * For further implementation details, refer to the manufacturer"s documentation. * * @method setNonRegisteredParameter * @chainable * * @param parameter {Array} A two-position array specifying the two control bytes (0x63, * 0x62) that identify the non-registered parameter. * * @param [data=[]] {Number|Array} An integer or an array of integers with a length of 1 or 2 * specifying the desired data. * * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @returns {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.setNonRegisteredParameter = function(parameter, data, channel, options) { var that = this; options = options || {}; if ( !(parameter[0] >= 0 && parameter[0] <= 127) || !(parameter[1] >= 0 && parameter[1] <= 127) ) { throw new Error( "Position 0 and 1 of the 2-position parameter array must both be between 0 and 127." ); } data = [].concat(data); wm.toMIDIChannels(channel).forEach(function() { that._selectNonRegisteredParameter(parameter, channel, options.time); that._setCurrentRegisteredParameter(data, channel, options.time); that._deselectRegisteredParameter(channel, options.time); }); return this; }; /** * Increments the specified MIDI registered parameter by 1. * * >Unless you are very familiar with the MIDI standard you probably should favour one of the * >simpler to use functions such as: `setPitchbendRange()`, `setModulationRange()`, * >`setMasterTuning()`, etc. * * 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` * * @method incrementRegisteredParameter * @chainable * * @param parameter {String|Array} 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 [channel=all] {uint|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @throws Error The specified parameter is not available. * * @returns {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.incrementRegisteredParameter = function(parameter, channel, options) { var that = this; options = options || {}; if ( !Array.isArray(parameter) ) { if ( !wm.MIDI_REGISTERED_PARAMETER[parameter]) { throw new Error("The specified parameter is not available."); } parameter = wm.MIDI_REGISTERED_PARAMETER[parameter]; } wm.toMIDIChannels(channel).forEach(function() { that._selectRegisteredParameter(parameter, channel, options.time); that.sendControlChange(0x60, 0, channel, {time: options.time}); that._deselectRegisteredParameter(channel, options.time); }); return this; }; /** * Decrements the specified MIDI registered parameter by 1. * * >Unless you are very familiar with the MIDI standard you probably should favour one of the * >simpler to use functions such as: `setPitchbendRange()`, `setModulationRange()`, * >`setMasterTuning()`, etc. * * 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` * * @method decrementRegisteredParameter * @chainable * * @param parameter {String|Array} 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 [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @throws TypeError The specified parameter is not available. * * @returns {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.decrementRegisteredParameter = function(parameter, channel, options) { options = options || {}; if ( !Array.isArray(parameter) ) { if ( !wm.MIDI_REGISTERED_PARAMETER[parameter]) { throw new TypeError("The specified parameter is not available."); } parameter = wm.MIDI_REGISTERED_PARAMETER[parameter]; } wm.toMIDIChannels(channel).forEach(function() { this._selectRegisteredParameter(parameter, channel, options.time); this.sendControlChange(0x61, 0, channel, {time: options.time}); this._deselectRegisteredParameter(channel, options.time); }.bind(this)); 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 can be specified with the * `semitones` parameter, the `cents` parameter or by specifying both parameters at the same time. * * @method setPitchBendRange * @chainable * * @param [semitones=0] {Number} The desired adjustment value in semitones (integer between * 0-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 [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @throws {RangeError} The semitones value must be between 0 and 127. * @throws {RangeError} The cents value must be between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.setPitchBendRange = function(semitones, cents, channel, options) { var that = this; options = options || {}; semitones = Math.floor(semitones) || 0; if ( !(semitones >= 0 && semitones <= 127) ) { throw new RangeError("The semitones value must be between 0 and 127"); } cents = Math.floor(cents) || 0; if ( !(cents >= 0 && cents <= 127) ) { throw new RangeError("The cents value must be between 0 and 127"); } wm.toMIDIChannels(channel).forEach(function() { that.setRegisteredParameter( "pitchbendrange", [semitones, cents], channel, {time: options.time} ); }); 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. * * @method setModulationRange * @chainable * * @param [semitones=0] {Number} The desired adjustment value in semitones (integer between * 0-127). * * @param [cents=0] {Number} The desired adjustment value in cents (0-127). * * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @throws {RangeError} The semitones value must be between 0 and 127. * @throws {RangeError} The cents value must be between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.setModulationRange = function(semitones, cents, channel, options) { var that = this; options = options || {}; semitones = Math.floor(semitones) || 0; if ( !(semitones >= 0 && semitones <= 127) ) { throw new RangeError("The semitones value must be between 0 and 127"); } cents = Math.floor(cents) || 0; if ( !(cents >= 0 && cents <= 127) ) { throw new RangeError("The cents value must be between 0 and 127"); } wm.toMIDIChannels(channel).forEach(function() { that.setRegisteredParameter( "modulationrange", [semitones, cents], channel, {time: options.time} ); }); return this; }; /** * 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. For those familiar with the MIDI protocol, this function actually generates * >**Master Coarse Tuning** and **Master Fine Tuning** RPN messages. * * @method setMasterTuning * @chainable * * @param [value=0.0] {Number} The desired decimal adjustment value in semitones (-65 < x < 64) * * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent 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. */ Output.prototype.setMasterTuning = function(value, channel, options) { var that = this; options = options || {}; value = parseFloat(value) || 0.0; if (value <= -65 || value >= 64) { throw new RangeError( "The value must be a decimal number larger than -65 and smaller than 64." ); } var coarse = Math.floor(value) + 64; var fine = value - Math.floor(value); // Calculate MSB and LSB for fine adjustment (14bit resolution) fine = Math.round((fine + 1) / 2 * 16383); var msb = (fine >> 7) & 0x7F; var lsb = fine & 0x7F; wm.toMIDIChannels(channel).forEach(function() { that.setRegisteredParameter("channelcoarsetuning", coarse, channel, {time: options.time}); that.setRegisteredParameter("channelfinetuning", [msb, lsb], channel, {time: options.time}); }); 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. * * @method setTuningProgram * @chainable * * @param value {Number} The desired tuning program (0-127). * * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent 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. */ Output.prototype.setTuningProgram = function(value, channel, options) { var that = this; options = options || {}; value = Math.floor(value); if ( !(value >= 0 && value <= 127) ) { throw new RangeError("The program value must be between 0 and 127"); } wm.toMIDIChannels(channel).forEach(function() { that.setRegisteredParameter("tuningprogram", value, channel, {time: options.time}); }); 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. * * @method setTuningBank * @chainable * * @param value {Number} The desired tuning bank (0-127). * * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent 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. */ Output.prototype.setTuningBank = function(value, channel, options) { var that = this; options = options || {}; value = Math.floor(value) || 0; if ( !(value >= 0 && value <= 127) ) { throw new RangeError("The bank value must be between 0 and 127"); } wm.toMIDIChannels(channel).forEach(function() { that.setRegisteredParameter("tuningbank", value, channel, {time: options.time}); }); return this; }; /** * 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: * * * `allsoundoff` (#120) * * `resetallcontrollers` (#121) * * `localcontrol` (#122) * * `allnotesoff` (#123) * * `omnimodeoff` (#124) * * `omnimodeon` (#125) * * `monomodeon` (#126) * * `polymodeon` (#127) * * 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. * * @method sendChannelMode * @chainable * * @param command {Number|String} The numerical identifier of the channel mode message (integer * between 120-127) or its name as a string. * @param [value=0] {Number} The value to send (integer between 0-127). * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * @param {Object} [options={}] * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent 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. * */ Output.prototype.sendChannelMode = function(command, value, channel, options) { options = options || {}; if (typeof command === "string") { command = wm.MIDI_CHANNEL_MODE_MESSAGES[command]; if (!command) { throw new TypeError("Invalid channel mode message name."); } } else { command = Math.floor(command); if ( !(command >= 120 && command <= 127) ) { throw new RangeError("Channel mode numerical identifiers must be between 120 and 127."); } } value = Math.floor(value) || 0; if (value < 0 || value > 127) { throw new RangeError("Value must be an integer between 0 and 127."); } wm.toMIDIChannels(channel).forEach(function(ch) { this.send( (wm.MIDI_CHANNEL_MESSAGES.channelmode << 4) + (ch - 1), [command, value], this._parseTimeParameter(options.time) ); }.bind(this)); return this; }; /** * Sends a MIDI `program change` message to the specified channel(s) at the scheduled time. * * @method sendProgramChange * @chainable * * @param program {Number} The MIDI patch (program) number (0-127) * * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @throws {RangeError} Program numbers must be between 0 and 127. * * @return {Output} Returns the `Output` object so methods can be chained. * */ Output.prototype.sendProgramChange = function(program, channel, options) { var that = this; options = options || {}; program = Math.floor(program); if (isNaN(program) || program < 0 || program > 127) { throw new RangeError("Program numbers must be between 0 and 127."); } wm.toMIDIChannels(channel).forEach(function(ch) { that.send( (wm.MIDI_CHANNEL_MESSAGES.programchange << 4) + (ch - 1), [program], that._parseTimeParameter(options.time) ); }); return this; }; /** * Sends a MIDI `channel aftertouch` message to the specified channel(s). For key-specific * aftertouch, you should instead use `sendKeyAftertouch()`. * * @method sendChannelAftertouch * @chainable * * @param [pressure=0.5] {Number} The pressure level (between 0 and 1). An invalid pressure value * will silently trigger the default behaviour. * * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or * an array of channel numbers. If the special value "all" is used, the message will be sent to * all 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendChannelAftertouch = function(pressure, channel, options) { var that = this; options = options || {}; pressure = parseFloat(pressure); if (isNaN(pressure) || pressure < 0 || pressure > 1) { pressure = 0.5; } var nPressure = Math.round(pressure * 127); wm.toMIDIChannels(channel).forEach(function(ch) { that.send( (wm.MIDI_CHANNEL_MESSAGES.channelaftertouch << 4) + (ch - 1), [nPressure], that._parseTimeParameter(options.time) ); }); return this; }; /** * Sends a MIDI `pitch bend` message to the specified channel(s) at the scheduled time. * * @method sendPitchBend * @chainable * * @param bend {Number} The intensity level of the bend (between -1 and 1). A value of zero means * no bend. * * @param [channel=all] {Number|Array|String} The MIDI channel number (between 1 and 16) or an * array of channel numbers. If the special value "all" is used, the message will be sent to all * 16 channels. * * @param {Object} [options={}] * * @param {DOMHighResTimeStamp|String} [options.time=undefined] This value can be one of two * things. If the value is a string starting with the + sign and followed by a number, the request * will be delayed by the specified number (in milliseconds). Otherwise, the value is considered a * timestamp and the request will be scheduled at that timestamp. The `DOMHighResTimeStamp` value * is relative to the navigation start of the document. To retrieve the current time, you can use * `WebMidi.time`. If `time` is not present or is set to a time in the past, the request is to be * sent as soon as possible. * * @throws {RangeError} Pitch bend value must be between -1 and 1. * * @return {Output} Returns the `Output` object so methods can be chained. */ Output.prototype.sendPitchBend = function(bend, channel, options) { var that = this; options = options || {}; if (isNaN(bend) || bend < -1 || bend > 1) { throw new RangeError("Pitch bend value must be between -1 and 1."); } var nLevel = Math.round((bend + 1) / 2 * 16383); var msb = (nLevel >> 7) & 0x7F; var lsb = nLevel & 0x7F; wm.toMIDIChannels(channel).forEach(function(ch) { that.send( (wm.MIDI_CHANNEL_MESSAGES.pitchbend << 4) + (ch - 1), [lsb, msb], that._parseTimeParameter(options.time) ); }); return this; }; /** * Returns a 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 value will be the sum of the current timestamp plus that number. Otherwise, the * value will be returned as is. * * If the calculated return value is 0, less than zero or an otherwise invalid value, `undefined` * will be returned. * * @method _parseTimeParameter * @param [time] {Number|String} * @return DOMHighResTimeStamp * @protected */ Output.prototype._parseTimeParameter = function(time) { var value, parsed = parseFloat(time); if (typeof time === "string" && time.substring(0, 1) === "+") { if (parsed && parsed > 0) value = wm.time + parsed; } else { if (parsed > wm.time) value = parsed; } return value; }; /** * Converts an input value (which can be a uint, a string or an array of the previous two) to an * array of MIDI note numbers. * * @method _convertNoteToArray * @param [note] {Number|Array|String} * @returns {Array} * @protected */ Output.prototype._convertNoteToArray = function(note) { var notes = []; if ( !Array.isArray(note) ) { note = [note]; } note.forEach(function(item) { notes.push(wm.guessNoteNumber(item)); }); return notes; }; // Check if RequireJS/AMD is used. If it is, use it to define our module instead of // polluting the global space. if ( typeof define === "function" && typeof define.amd === "object") { define([], function () { return wm; }); } else if (typeof module !== "undefined" && module.exports) { module.exports = wm; } else { if (!scope.WebMidi) { scope.WebMidi = wm; } } }(this));