Web MIDI API Synthesizer Input and Output in JavaScript

The Web MIDI API enables web applications to interact directly with hardware and software synthesizers by sending and receiving standardized MIDI (Musical Instrument Digital Interface) messages. This article explores how JavaScript initializes MIDI access, parses real-time input events like note triggers and knob adjustments from physical controllers, and formats outgoing byte arrays to drive external sound modules and synthesizers.

Requesting MIDI Access

To interact with connected MIDI hardware, a web application must first request permission from the browser using navigator.requestMIDIAccess(). This asynchronous method returns a MIDIAccess object containing collections of available input and output ports.

navigator.requestMIDIAccess({ sysex: false })
  .then(onMIDISuccess, onMIDIFailure);

function onMIDISuccess(midiAccess) {
  console.log("MIDI Access Granted", midiAccess);
  listenToInputs(midiAccess);
  sendToOutputs(midiAccess);
}

function onMIDIFailure(error) {
  console.error("MIDI Access Failed:", error);
}

The optional sysex parameter allows the application to send and receive System Exclusive messages when set to true, which requires explicit user permission.


Handling Synthesizer Input Messages

Incoming MIDI data is captured by attaching event listeners to the MIDIInput interfaces found inside midiAccess.inputs. When a user presses a key or turns a knob on a MIDI controller, the browser triggers a midimessage event containing a raw Uint8Array of data bytes.

function listenToInputs(midiAccess) {
  for (const input of midiAccess.inputs.values()) {
    input.onmidimessage = handleMIDIMessage;
  }
}

function handleMIDIMessage(event) {
  const [status, data1, data2] = event.data;
  const command = status >> 4;
  const channel = status & 0xf;

  switch (command) {
    case 0x9: // Note On
      if (data2 > 0) {
        console.log(`Note On: Key ${data1} on Channel ${channel} with Velocity ${data2}`);
      } else {
        console.log(`Note Off: Key ${data1} on Channel ${channel}`);
      }
      break;
    case 0x8: // Note Off
      console.log(`Note Off: Key ${data1} on Channel ${channel}`);
      break;
    case 0xB: // Control Change (CC)
      console.log(`CC Message: Controller ${data1} Value ${data2}`);
      break;
  }
}

Understanding Input Bytes

  1. Status Byte (event.data[0]): Defines the message type (e.g., Note On 0x90, Note Off 0x80, Control Change 0xB0) and the target channel (0–15).
  2. Data Byte 1 (event.data[1]): Identifies the specific parameter, such as the MIDI note number (0–127) or CC controller number.
  3. Data Byte 2 (event.data[2]): Represents the value, such as velocity (key strike intensity, 0–127) or controller position. Note that a “Note On” message with a velocity of 0 is conventionally treated as a “Note Off”.

Sending Synthesizer Output Messages

To control an external synthesizer or sound engine from JavaScript, outgoing messages are transmitted using the send() method on a MIDIOutput instance. Messages are formatted as standard three-byte arrays.

function sendToOutputs(midiAccess) {
  const outputs = Array.from(midiAccess.outputs.values());
  if (outputs.length === 0) return;

  const output = outputs[0]; // Select the first connected device

  // Play Middle C (MIDI note 60) with velocity 127 on Channel 1
  const NOTE_ON = 0x90;
  const NOTE_OFF = 0x80;
  const noteNumber = 60;
  const velocity = 127;

  // Trigger Note On immediately
  output.send([NOTE_ON, noteNumber, velocity]);

  // Schedule Note Off after 1000 milliseconds
  const releaseTime = window.performance.now() + 1000.0;
  output.send([NOTE_OFF, noteNumber, 0], releaseTime);
}

The output.send() method accepts an optional second argument representing a high-resolution timestamp based on performance.now(). This enables accurate, drift-free musical scheduling directly from the browser runtime.


Device Connection State Changes

Synthesizers can be connected or disconnected while the application is active. The MIDIAccess object provides a statechange event to detect hardware changes dynamically:

midiAccess.onstatechange = function(event) {
  console.log(`Device: ${event.port.name}, State: ${event.port.state}, Connection: ${event.port.connection}`);
};

This listener ensures that newly connected synthesizers are automatically wired up to input handlers and made available as valid output targets without requiring a page reload.