JavaScript Web MIDI API
Aprende la Web MIDI API de JavaScript: solicitar acceso, enumerar entradas y salidas, leer datos de MIDIMessageEvent, enviar mensajes Note On/Off, manejar la conexión en caliente de dispositivos y los requisitos de contexto seguro.
La Web MIDI API permite que una página web se comunique directamente con hardware MIDI — teclados, pads de batería, superficies de control y sintetizadores — sin ningún complemento ni aplicación nativa. Puedes leer lo que toca un músico en tiempo real y enviar notas y cambios de control de vuelta a un módulo de sonido.
Esta guía cubre todo el flujo: solicitar acceso, enumerar puertos de entrada y salida, leer mensajes con MIDIMessageEvent, enviar mensajes Note On/Off y Control Change, manejar dispositivos que se conectan o desconectan mientras la página está abierta, y las reglas de contexto seguro y permisos que impone la API.
Qué es realmente MIDI
MIDI (Musical Instrument Digital Interface) no transporta audio. Transporta pequeños eventos — "esta tecla se presionó con esta fuerza," "esta perilla se movió a este valor." El navegador expone esos eventos como bytes sin procesar; convertirlos en sonido es tarea de un sintetizador, ya sea hardware externo o tu propio código (por ejemplo, la Web Audio API).
Un mensaje de canal estándar es de tres bytes:
| Byte | Nombre | Significado |
|---|---|---|
| 1 | Estado | Tipo de mensaje (nibble alto) + canal 0–15 (nibble bajo) |
| 2 | Dato 1 | Número de nota (0–127), o número de controlador |
| 3 | Dato 2 | Velocidad (0–127), o valor del controlador |
Bytes de estado comunes (canal 1, donde el nibble de canal es 0):
0x90— Note On (la velocidad0se trata como Note Off)0x80— Note Off0xB0— Control Change (pedal de sustain, modulación, volumen, …)0xE0— Pitch Bend
El número de nota 60 es el Do central; la velocidad 0–127 describe con qué fuerza se presionó la tecla.
Solicitar acceso
Todo comienza con navigator.requestMIDIAccess(). Devuelve una Promise que se resuelve en un objeto MIDIAccess que contiene los puertos disponibles. El navegador puede solicitar permiso al usuario la primera vez que se use.
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 y permisos
La Web MIDI API solo funciona en un contexto seguro — páginas servidas sobre https:// (o http://localhost durante el desarrollo). Llamar a requestMIDIAccess() en una página no segura rechaza la promesa.
El acceso también está controlado por la Permissions Policy y una solicitud de permiso al usuario. Si el usuario lo deniega (o un encabezado Permissions-Policy: midi=() bloquea la función), la promesa se rechaza — por eso la llamada está envuelta en try/catch. Solicitar sysex: true pide un nivel de privilegio más alto y solicita permiso por separado, porque los mensajes SysEx pueden reprogramar un dispositivo, así que solicítalo solo cuando sea necesario.
Enumerar entradas y salidas
MIDIAccess expone dos colecciones similares a Map — inputs (dispositivos que envían datos al navegador) y outputs (dispositivos a los que el navegador puede enviar datos). Ambas son MIDIInputMap / MIDIOutputMap, así que se iteran como un Map, con clave en el id estable del puerto.
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 puerto tiene metadatos útiles: id, name, manufacturer, type ("input" o "output"), state ("connected" / "disconnected"), y connection ("open", "closed", o "pending").
Leer entrada MIDI
Adjunta un controlador onmidimessage a un puerto de entrada. Cada evento es un MIDIMessageEvent cuya propiedad data es un Uint8Array de los bytes sin procesar (consulta Arrays binarios para ver cómo funcionan los arrays tipados). Este es el mismo patrón de callback que usas en otros lugares con 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));
}
}Enmascarar el byte de estado con & 0xf0 y & 0x0f separa el tipo de mensaje del canal, de modo que un solo controlador funciona independientemente de cuál de los 16 canales MIDI transmite el dispositivo.
Enviar salida MIDI
Para controlar hardware o software externo, toma un puerto de salida y llama a output.send(data), donde data es un array (o 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() acepta un timestamp opcional (un DOMHighResTimeStamp de performance.now()). Programar el Note Off en el futuro es más confiable que setTimeout, porque el tiempo es manejado por el subsistema MIDI en lugar del bucle de eventos de JavaScript. Enviar 0 sin timestamp significa "ahora mismo."
Evitar notas atascadas
El error más común es una nota atascada — un Note On sin su correspondiente Note Off, dejando el sonido sonando para siempre. Siempre emparéjalos: rastrea qué notas están activas y envía Note Off cuando se suelte la tecla o la página se descargue.
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();
}Manejar la conexión en caliente
Los dispositivos MIDI USB se conectan y desconectan mientras la página está abierta. Escucha statechange en el objeto MIDIAccess para poder adjuntar controladores a las entradas recién conectadas y actualizar tu UI cuando se desconecte algo.
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
}
};
}Compatibilidad con navegadores y mejores prácticas
- Detecta la función con
if (navigator.requestMIDIAccess)antes de llamar — Safari agregó soporte relativamente hace poco, y algunos entornos lo deshabilitan. - Sirve sobre HTTPS (o
localhost); el requisito de contexto seguro no es opcional. - Solicita
sysex: truesolo cuando sea necesario, ya que activa una solicitud más estricta. - Siempre empareja Note On / Note Off y silencia todas las notas en
pagehide/beforeunload. - Usa timestamps de
send()para temporización precisa en lugar desetTimeout. - Para sonido generado por el navegador (en lugar de hardware externo), combina la entrada MIDI con la Web Audio API.
Resumen
La Web MIDI API le da a las aplicaciones web un canal directo y de baja latencia hacia el hardware musical. Solicita un objeto MIDIAccess con navigator.requestMIDIAccess(), itera sus inputs y outputs, lee los eventos entrantes desde MIDIMessageEvent.data, y envía mensajes de tres bytes con port.send(). Ten en cuenta las reglas de contexto seguro y permisos, maneja la conexión en caliente de dispositivos mediante statechange, y siempre limpia las notas para evitar el clásico error de nota atascada. A partir de ahí puedes construir teclados virtuales, controladores MIDI e instrumentos que suenen directamente en el navegador.