API Docs for: 2.5.1
Show:

File: src/webmidi.js

(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 I do not know what
       * they are used for. They are not part of the online
       * [MIDI 1.0 spec](http://www.midi.org/techspecs/midimessages.php).
       *
       * @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.
       *
       * @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 that may
   * have been defined on `Input` or `Output` 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.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
   * <a href="#property_octaveOffset">octaveOffset</a> 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/nrpn:event"}}nrpn{{/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 (wm.MIDI_CHANNEL_MESSAGES.hasOwnProperty(prop1)) {
        this._userHandlers.channel[prop1] = {};
      }
    }

    for (var prop2 in wm.MIDI_SYSTEM_MESSAGES) {
      if (wm.MIDI_SYSTEM_MESSAGES.hasOwnProperty(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.
       *
       * @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 (
        wm.MIDI_CONTROL_CHANGE_MESSAGES.hasOwnProperty(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 (
        wm.MIDI_CHANNEL_MODE_MESSAGES.hasOwnProperty(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 state
       * @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.
   *
   * Details on the format of MIDI messages are available in the
   * <a href="http://www.midi.org/techspecs/midimessages.php">summary of MIDI messages</a> of the
   * MIDI Manufacturers Association.
   *
   * @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/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.
   *
   * To view a list of all available `control change` messages, please consult "Table 3 - Control
   * Change Messages" from the [MIDI Messages](
   * https://www.midi.org/specifications/item/table-3-control-change-messages-data-bytes-2)
   * specification.
   *
   * @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. For more specific MIDI usage
   * information, check out [RP-18](http://dev.midi.org/techspecs/rp18.php) regarding the usage of
   * increment and decrement controllers.
   *
   * >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. For more specific MIDI usage
   * information, check out [RP-18](http://dev.midi.org/techspecs/rp18.php) regarding the usage of
   * increment and decrement controllers.
   *
   * >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));