JavaScript Web MIDI API
Aprenda a Web MIDI API do JavaScript: solicite acesso, enumere entradas e saídas, leia dados de MIDIMessageEvent, envie mensagens Note On/Off, gerencie hot-plugging de dispositivos e cumpra os requisitos de contexto seguro.
A Web MIDI API permite que uma página web se comunique diretamente com hardware MIDI — teclados, pad de bateria, superfícies de controle e sintetizadores — sem nenhum plugin ou aplicativo nativo. Você pode ler o que um músico toca em tempo real e enviar notas e mudanças de controle de volta para um módulo de som.
Este guia cobre todo o fluxo: solicitar acesso, enumerar portas de entrada e saída, ler mensagens com MIDIMessageEvent, enviar mensagens Note On/Off e Control Change, lidar com dispositivos conectados ou removidos enquanto a página está aberta, e as regras de contexto seguro e permissão que a API impõe.
O que é MIDI de fato
MIDI (Musical Instrument Digital Interface) não transporta áudio. Ele transporta pequenos eventos — "esta tecla foi pressionada com esta intensidade", "este botão girou para este valor". O navegador expõe esses eventos como bytes brutos; transformá-los em som é responsabilidade de um sintetizador, seja hardware externo ou seu próprio código (por exemplo, a Web Audio API).
Uma mensagem de canal padrão possui três bytes:
| Byte | Nome | Significado |
|---|---|---|
| 1 | Status | Tipo de mensagem (nibble alto) + canal 0–15 (nibble baixo) |
| 2 | Dado 1 | Número da nota (0–127) ou número do controlador |
| 3 | Dado 2 | Velocidade (0–127) ou valor do controlador |
Bytes de status comuns (canal 1, onde o nibble de canal é 0):
0x90— Note On (velocidade0é tratada como Note Off)0x80— Note Off0xB0— Control Change (pedal de sustain, modulação, volume, …)0xE0— Pitch Bend
O número de nota 60 é o Dó central; a velocidade 0–127 descreve com que força a tecla foi pressionada.
Solicitando acesso
Tudo começa com navigator.requestMIDIAccess(). Ele retorna uma Promise que resolve para um objeto MIDIAccess contendo as portas disponíveis. O navegador pode solicitar permissão ao usuário no primeiro uso.
async function initMIDI() {
// Feature-detect before calling — support is not universal.
if (!navigator.requestMIDIAccess) {
console.warn('Web MIDI API is not supported in this browser.');
return;
}
try {
// Pass { sysex: true } only if you genuinely need System Exclusive messages —
// it triggers a stricter, separate permission prompt.
const midiAccess = await navigator.requestMIDIAccess({ sysex: false });
console.log('MIDI access granted', midiAccess);
return midiAccess;
} catch (err) {
console.error('Could not access MIDI devices:', err);
}
}Contexto seguro e permissões
A Web MIDI API funciona apenas em um contexto seguro — páginas servidas via https:// (ou http://localhost durante o desenvolvimento). Chamar requestMIDIAccess() em uma página insegura rejeita a promise.
O acesso também é controlado pela Permissions Policy e por um prompt de permissão do usuário. Se o usuário negar (ou um cabeçalho Permissions-Policy: midi=() bloquear o recurso), a promise é rejeitada — é por isso que a chamada está envolvida em try/catch. Solicitar sysex: true pede um nível de privilégio mais alto e exibe um prompt separado, pois mensagens SysEx podem reprogramar um dispositivo — portanto, solicite apenas quando necessário.
Enumerando entradas e saídas
MIDIAccess expõe duas coleções semelhantes a Map — inputs (dispositivos que enviam dados ao navegador) e outputs (dispositivos para os quais o navegador pode enviar dados). Ambas são MIDIInputMap / MIDIOutputMap, então você as itera como um Map, com chave pelo id estável da porta.
function listPorts(midiAccess) {
console.log('Inputs:');
for (const input of midiAccess.inputs.values()) {
console.log(` ${input.name} (${input.manufacturer}) — ${input.state}`);
}
console.log('Outputs:');
for (const output of midiAccess.outputs.values()) {
console.log(` ${output.name} (${output.manufacturer}) — ${output.state}`);
}
}Cada porta possui metadados úteis: id, name, manufacturer, type ("input" ou "output"), state ("connected" / "disconnected") e connection ("open", "closed" ou "pending").
Lendo entradas MIDI
Anexe um manipulador onmidimessage a uma porta de entrada. Cada evento é um MIDIMessageEvent cuja propriedade data é um Uint8Array dos bytes brutos (veja Arrays binários para entender como arrays tipados funcionam). Este é o mesmo padrão de callback que você usa em outros lugares com eventos JavaScript.
function startListening(midiAccess) {
midiAccess.inputs.forEach((input) => {
input.onmidimessage = onMIDIMessage;
});
}
function onMIDIMessage(event) {
// event.data is a Uint8Array; channel messages are usually 3 bytes.
const [status, data1, data2] = event.data;
const command = status & 0xf0; // high nibble = message type
const channel = status & 0x0f; // low nibble = channel 0–15
switch (command) {
case 0x90: // Note On
if (data2 > 0) {
console.log(`Note On — note ${data1}, velocity ${data2}, ch ${channel}`);
} else {
console.log(`Note Off — note ${data1} (velocity 0)`);
}
break;
case 0x80: // Note Off
console.log(`Note Off — note ${data1}, ch ${channel}`);
break;
case 0xb0: // Control Change
console.log(`Control Change — controller ${data1}, value ${data2}`);
break;
default:
console.log('Other message:', Array.from(event.data));
}
}Mascarar o byte de status com & 0xf0 e & 0x0f separa o tipo de mensagem do canal, para que um único manipulador funcione independentemente de qual dos 16 canais MIDI o dispositivo transmite.
Enviando saída MIDI
Para controlar hardware ou software externo, pegue uma porta de saída e chame output.send(data), onde data é um array (ou Uint8Array) de bytes.
function sendNote(midiAccess) {
const output = midiAccess.outputs.values().next().value; // first available port
if (!output) {
console.log('No MIDI outputs available.');
return;
}
output.send([0x90, 60, 100]); // Note On: Middle C, velocity 100, channel 1
output.send([0x80, 60, 0], performance.now() + 500); // Note Off scheduled 500 ms later
}send() aceita um timestamp opcional (um DOMHighResTimeStamp de performance.now()). Agendar o Note Off no futuro é mais confiável do que setTimeout, porque o timing é gerenciado pelo subsistema MIDI em vez do loop de eventos JavaScript. Enviar 0 sem timestamp significa "agora mesmo."
Evitando notas presas
O bug mais comum é uma nota presa — um Note On sem o Note Off correspondente, deixando o som tocando indefinidamente. Sempre os emparelhe: rastreie quais notas estão ativas e envie Note Off quando a tecla for liberada ou a página for descarregada.
const activeNotes = new Set();
function noteOn(output, note, velocity = 100) {
output.send([0x90, note, velocity]);
activeNotes.add(note);
}
function noteOff(output, note) {
output.send([0x80, note, 0]);
activeNotes.delete(note);
}
// Panic: silence everything (e.g. on window 'pagehide')
function allNotesOff(output) {
for (const note of activeNotes) output.send([0x80, note, 0]);
activeNotes.clear();
}Lidando com hot-plugging
Dispositivos MIDI USB são conectados e desconectados enquanto a página está aberta. Ouça o evento statechange no objeto MIDIAccess para poder anexar manipuladores a novas entradas conectadas e atualizar sua interface quando algo for desconectado.
async function setupMIDI() {
const midiAccess = await navigator.requestMIDIAccess();
function attachInputHandlers() {
midiAccess.inputs.forEach((input) => {
input.onmidimessage = onMIDIMessage;
});
}
attachInputHandlers();
midiAccess.onstatechange = (event) => {
const port = event.port;
console.log(`${port.type} "${port.name}" is now ${port.state}`);
if (port.type === 'input' && port.state === 'connected') {
attachInputHandlers(); // wire up the device that just appeared
}
};
}Suporte a navegadores e boas práticas
- Detecte o recurso com
if (navigator.requestMIDIAccess)antes de chamar — o Safari adicionou suporte relativamente recentemente e alguns ambientes o desativam. - Sirva via HTTPS (ou
localhost); o requisito de contexto seguro não é opcional. - Solicite
sysex: trueapenas quando necessário, pois isso aciona um prompt mais restritivo. - Sempre emparelhe Note On / Note Off e silencie todas as notas em
pagehide/beforeunload. - Use timestamps em
send()para timing preciso em vez desetTimeout. - Para som gerado pelo navegador (em vez de hardware externo), combine a entrada MIDI com a Web Audio API.
Resumo
A Web MIDI API oferece aos aplicativos web um canal direto e de baixa latência para hardware musical. Solicite um objeto MIDIAccess com navigator.requestMIDIAccess(), itere seus inputs e outputs, leia eventos recebidos de MIDIMessageEvent.data e envie mensagens de três bytes com port.send(). Respeite as regras de contexto seguro e permissão, gerencie o hot-plugging de dispositivos via statechange e sempre limpe as notas para evitar o clássico bug de nota presa. A partir daí, você pode criar teclados virtuais, controladores MIDI e instrumentos que tocam direto no navegador.