Skip to main content

Hooks for Web MIDI API

Three hooks, one on top of the next. useMIDIAccess asks the browser for permission, and the other two listen to whatever it hands back.

All of them follow the devices: a keyboard plugged in after permission was granted starts working on its own, with nothing to rebuild.

useMIDIAccess​

Source

Requests MIDI access in the browser. The first argument chooses whether to ask on mount; pass false to ask later, from a button the user presses.

const { request, midiAccess, error, inputs } = useMIDIAccess(false)

return (
<>
{midiAccess ? null : <button onClick={() => request()}>Connect MIDI</button>}
{error && <p>{error}</p>}
<ul>
{inputs.map((input) => (
<li key={input.id}>{input.name}</li>
))}
</ul>
</>
)

inputs is the list of connected devices, and it changes as they come and go, so a device list needs no statechange listener of its own.

error is one of the strings 'NOT_SUPPORTED' (the browser has no Web MIDI API), 'PERMISSION_DENIED' (the user said no — asking again is worthwhile) or 'UNAVAILABLE' (anything else). Its type, MIDIAccessError from @tremolo-ui/dom, catches a misspelt comparison. A failed request leaves any access already granted in place.

Pass { sysex: true } to request for system exclusive messages. Browsers treat that as a separate, more sensitive permission, so ask for it only when the app actually reads or sends sysex.

useMIDIInput​

Source

Handles the channel voice messages of every connected input. Handlers are given as an object, and every one of them is given the channel last, as 0–15 (MIDI channels are written 1–16 on hardware, so add one before showing it to anyone).

useMIDIInput(midiAccess, {
onNoteOnEvent: (note, velocity, channel) => play(note, velocity / 127),
onNoteOffEvent: (note, channel) => stop(note),
onControlChangeEvent: (controller, value) => {
if (controller === 1) setModulation(value / 127)
},
// 0-16383 centred at 8192, as -1 to 1
onPitchBendEvent: (value) => setBend(normalizePitchBend(value)),
})
handlerarguments
onNoteOnEventnote, velocity (1–127), channel
onNoteOffEventnote, channel
onControlChangeEventcontroller, value (0–127), channel
onProgramChangeEventprogram, channel
onPitchBendEventvalue (0–16383), channel
onAftertouchEventnote, pressure, channel
onChannelPressureEventpressure, channel

A note on with a velocity of 0 is how most devices say note off, so it arrives as onNoteOffEvent.

The pitch bend is the raw 14-bit value, which is not symmetric around its centre. normalizePitchBend from @tremolo-ui/functions turns it into -1 to 1 with both ends reached, and PITCH_BEND_CENTER (8192) is the wheel at rest.

Writing the handlers inline is fine: they are read fresh on every event, so a re-render never detaches anything.

System messages — clock, sysex, and the rest of 0xf0–0xff — carry no channel and are not decoded here. Use useMIDIMessage for those.

useMIDIMessage​

Source

The raw midimessage events, for when you need more detail than useMIDIInput gives.

useMIDIMessage(midiAccess, (event) => {
console.log([...event.data])
})