JavaScript Web MIDI Table

PieceWhat it doesField note
requestMIDIAccess()The entryGesture-gated promise to a live MIDIAccess - inputs and outputs Maps that repopulate under you
{sysex: true}The second grantSystem-exclusive needs its own scarier prompt - patch dumps and firmware live here; ask only when true
statechangeThe hot-plug lawHubs re-enumerate, sleep drops ports - rescan inputs/outputs or your cached port plays to nobody
midimessage bytesThe anatomy[status, d1, d2]: 0x90 = note-on CHANNEL 1 - the low nibble is the channel, not decoration
note-on/off pairingThe hang riskNote-off must match note AND channel - mismatched pairs hang notes forever; CC 123 is the panic button
send(msg, when)The schedulerOptional timestamp fires at OS-level precision without JS awake - the web sequencer primitive
timeStampThe recording truthDOMHighResTimeStamp relative to page load - when it HAPPENED, not when the callback ran
browser matrixThe supportChromium family solid, Safari late, Firefox absent - feature-detect plus an honest fallback workflow
Reference: the MDN Web MIDI API. The browser joins thirty years of music hardware: requestMIDIAccess hands you live input/output maps, MIDI messages are small byte arrays on the 1983 wire protocol, and none of it is audio - the events travel, the synth makes the sound.
Bottom line: the API is permissioned twice - the basic grant for notes and knobs, a separate scarier sysex grant for anything vendor-specific - and it is a live handle: statechange is not optional because hot-plug is the norm, not the exception. The byte-level honesty: status bytes carry the channel in their low nibble, note-off must match note-on's channel or notes hang forever, and all-notes-off on the hidden/error paths is the production hygiene nobody mentions until a synth screams at 2am.
Related tools: the Web Audio table (the sound half of the web music stack), the timers table (the jitter you must NOT build sequencers on), the performance timing table (the clock the timestamps speak), and the streams table (the byte discipline MIDI shares with serial).

Web MIDI connects the browser to thirty years of music hardware: keyboards, controllers, drum pads, synths, interfaces. navigator.requestMIDIAccess() resolves to a MIDIAccess object holding two maps - inputs (things that send you notes and knobs) and outputs (things you send messages to) - and from there MIDI is the same wire protocol the 1983 spec defined: small byte arrays, fast, device-to-device, no audio in the stream (MIDI carries events, not sound - the synth makes the noise).

Bottom line: the API is permissioned twice. The basic grant (notes, knobs, transport) needs a user gesture and may show a prompt; the SYSTEM EXCLUSIVE grant - sysex, needed for patch dumps, firmware updates and vendor-specific control - is a second, scarier permission you request explicitly with { sysex: true }. And access is a live handle: devices hot-plug constantly, so statechange is not optional - your inputs/outputs maps re-populate under you and code that cached a port reference at startup plays to nobody after the hub re-enumerates.

The honest part: Web MIDI is Chromium-family territory (Chrome, Edge, Opera; Safari finally shipped it in 2026-era releases, Firefox still lacks it), which means feature-detect and a graceful keyboard-mouse fallback for everything except actual MIDI hardware. The second honesty: MIDI messages are three bytes and unforgiving - status byte (0x90 = note on channel 1), data byte, data byte, with running-status and channel offsets as the two conventions that make byte-level bugs invisible until a note hangs forever because your note-off never matched the channel of your note-on.

How to use

  1. Request access inside a gesture: button.onclick = async () => { const access = await navigator.requestMIDIAccess(); } - add { sysex: true } only when you truly send system-exclusive data; the extra prompt costs trust, and browsers treat it as a distinct, higher-stakes grant.
  2. Treat inputs/outputs as live maps: access.inputs and access.outputs are Map-like, keyed by port id - enumerate at startup AND on statechange (access.onstatechange = e => rescan()), because hubs re-enumerate, sleep/wake drops ports, and controllers power themselves off mid-set.
  3. Listen at the port level: input.onmidimessage = (m) => { const [status, d1, d2] = m.data; ... } - the message carries a Uint8Array and m.timeStamp (ms relative to page load, not wall clock). Split status into command (high nibble) and channel (low nibble): 0x90 is note-on ONLY on channel 1.
  4. Send with the same discipline: const out = access.outputs.get(portId); out.send([0x90, 60, 100]); - note-off is 0x80 (or note-on with velocity 0, which some hardware prefers); send() takes an optional timestamp for scheduled messages, which is how you build a sequencer without setInterval jitter.
  5. Design for the unsupported branch: 'requestMIDIAccess' in navigator gates everything - on Firefox and older Safari, show the manual workflow (file import/export of MIDI files, virtual-port instructions) rather than a dead page; the Web MIDI ecosystem's own fallbacks are the pattern to copy.

Frequently asked questions

What is sysex and why does it need its own permission?

System Exclusive messages are the vendor-specific escape hatch of the MIDI spec: raw byte streams that mean whatever the manufacturer says they mean - patch dumps, firmware updates, deep editor control. The browser gates them separately because sysex is where MIDI becomes arbitrary device control: a firmware upload is a sysex stream, and the permission model treats 'this page may send arbitrary bytes to your hardware' as a different risk class from 'this page may hear your keyboard'. Request it only when the feature genuinely needs it ({ sysex: true } at request time), explain WHY in your UI before the prompt, and expect the second dialog - shipping both prompts in one first-touch flow is the standard way to lose half your users at the door.

Why do my notes hang when I stop the page mid-performance?

Because note-on is a state, not an event: a key-down message (0x9n) tells the synth to START a note, and only the matching note-off (0x8n, same note, same channel) stops it. If your page dies - tab close, exception, suspend - between the two, the hardware keeps sounding forever. The production hygiene: send all-notes-off (CC 123, or 0xBn, 123, 0) on visibilitychange/hidden and on any error path, wrap your port writers in try/finally so the release always runs, and remember the channel byte is part of the match - a note-off on channel 0 does not silence a note-on on channel 1, which is exactly the bug you wrote when you stripped the channel nibble.

How do timestamps and scheduling work in Web MIDI?

Every message carries a DOMHighResTimeStamp (m.timeStamp, milliseconds since page load) for received events, and send() accepts an optional timestamp parameter for outbound scheduling - the OS-level MIDI stack queues and fires it without JavaScript awake. That parameter is the whole trick behind web sequencers: schedule a bar of note events 100ms ahead, let the MIDI layer fire them on hardware clock discipline, and never build timing on setTimeout (its jitter is audible). For received data, timeStamp tells you when the event ACTUALLY happened relative to page time - needed for recording, where 'when the callback ran' is the wrong answer by tens of milliseconds.

Is Web MIDI ready for production use in 2026?

For Chromium-family web apps: yes - it has shipped stable for years, powers real web DAWs, and the hot-plug/sysex model is settled. The gap is the browser matrix: Firefox has no Web MIDI (its implementation stalled on the permission design), Safari shipped it late, and iOS support lags further behind, so any production page needs the feature-detect fallback and an honest 'works best in Chrome' note. The other production reality is hardware variance: MPE controllers, standalone sequencers and cheap USB-MIDI cables all behave differently at the byte level, so test against the oldest, cheapest controller you can find - the flaky 20-dollar cable is your real compatibility target, not the flagship workstation.

Related tools