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
SourceRequests 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
SourceHandles 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)),
})
| handler | arguments |
|---|---|
onNoteOnEvent | note, velocity (1–127), channel |
onNoteOffEvent | note, channel |
onControlChangeEvent | controller, value (0–127), channel |
onProgramChangeEvent | program, channel |
onPitchBendEvent | value (0–16383), channel |
onAftertouchEvent | note, pressure, channel |
onChannelPressureEvent | pressure, 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
SourceThe raw midimessage events, for when you need more detail than
useMIDIInput gives.
useMIDIMessage(midiAccess, (event) => {
console.log([...event.data])
})