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
- Status Byte (
event.data[0]): Defines the message type (e.g., Note On0x90, Note Off0x80, Control Change0xB0) and the target channel (0–15). - Data Byte 1 (
event.data[1]): Identifies the specific parameter, such as the MIDI note number (0–127) or CC controller number. - 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 of0is 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.