JavaScript Web MIDI API
Apprenez la Web MIDI API JavaScript : accès, énumération des entrées/sorties, lecture de MIDIMessageEvent, envoi de messages Note On/Off, gestion du branchement à chaud et exigences de contexte sécurisé.
La Web MIDI API permet à une page web de communiquer directement avec du matériel MIDI — claviers, pads de batterie, surfaces de contrôle et synthétiseurs — sans plugin ni application native. Vous pouvez lire ce qu'un musicien joue en temps réel et renvoyer des notes et des changements de contrôle vers un module sonore.
Ce guide couvre l'ensemble du processus : demander l'accès, énumérer les ports d'entrée et de sortie, lire les messages avec MIDIMessageEvent, envoyer des messages Note On/Off et Control Change, gérer les périphériques branchés ou débranchés pendant que la page est ouverte, ainsi que les règles de contexte sécurisé et de permission que l'API impose.
Ce qu'est réellement le MIDI
Le MIDI (Musical Instrument Digital Interface) ne transporte pas d'audio. Il transporte de petits événements — « cette touche a été enfoncée avec cette force », « ce bouton a été déplacé à cette valeur ». Le navigateur expose ces événements sous forme d'octets bruts ; les transformer en son est le rôle d'un synthétiseur, qu'il s'agisse de matériel externe ou de votre propre code (par exemple, la Web Audio API).
Un message de canal standard se compose de trois octets :
| Octet | Nom | Signification |
|---|---|---|
| 1 | Status | Type de message (quartet de poids fort) + canal 0–15 (quartet de poids faible) |
| 2 | Data 1 | Numéro de note (0–127), ou numéro de contrôleur |
| 3 | Data 2 | Vélocité (0–127), ou valeur du contrôleur |
Octets de statut courants (canal 1, où le quartet de canal vaut 0) :
0x90— Note On (une vélocité0est traitée comme Note Off)0x80— Note Off0xB0— Control Change (pédale de sustain, modulation, volume, …)0xE0— Pitch Bend
Le numéro de note 60 correspond au Do médian ; la vélocité 0–127 indique la force avec laquelle la touche a été frappée.
Demander l'accès
Tout commence par navigator.requestMIDIAccess(). Cette méthode retourne une Promise qui se résout en un objet MIDIAccess contenant les ports disponibles. Le navigateur peut demander à l'utilisateur une autorisation lors de la première utilisation.
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);
}
}Contexte sécurisé et permissions
La Web MIDI API ne fonctionne que dans un contexte sécurisé — les pages servies via https:// (ou http://localhost pendant le développement). Appeler requestMIDIAccess() sur une page non sécurisée rejette la promesse.
L'accès est également contrôlé par la Permissions Policy et une invite de permission utilisateur. Si l'utilisateur la refuse (ou si un en-tête Permissions-Policy: midi=() bloque la fonctionnalité), la promesse est rejetée — c'est pourquoi l'appel est enveloppé dans try/catch. Demander sysex: true requiert un niveau de privilège supérieur et affiche une invite distincte, car les messages SysEx peuvent reprogrammer un périphérique — ne le demandez que si nécessaire.
Énumérer les entrées et les sorties
MIDIAccess expose deux collections de type Map — inputs (périphériques qui envoient des données au navigateur) et outputs (périphériques vers lesquels le navigateur peut envoyer des données). Les deux sont des MIDIInputMap / MIDIOutputMap, que vous itérez comme une Map, indexée par un id de port stable.
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}`);
}
}Chaque port dispose de métadonnées utiles : id, name, manufacturer, type ("input" ou "output"), state ("connected" / "disconnected"), et connection ("open", "closed" ou "pending").
Lire les entrées MIDI
Attachez un gestionnaire onmidimessage à un port d'entrée. Chaque événement est un MIDIMessageEvent dont la propriété data est un Uint8Array des octets bruts (voir Tableaux binaires pour comprendre le fonctionnement des tableaux typés). Il s'agit du même modèle de callback que vous utilisez ailleurs avec les événements 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));
}
}Masquer l'octet de statut avec & 0xf0 et & 0x0f sépare le type de message du canal, de sorte qu'un seul gestionnaire fonctionne quel que soit le canal MIDI parmi les 16 sur lequel un périphérique émet.
Envoyer des sorties MIDI
Pour contrôler du matériel ou des logiciels externes, récupérez un port de sortie et appelez output.send(data), où data est un tableau (ou Uint8Array) d'octets.
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() accepte un horodatage optionnel (un DOMHighResTimeStamp provenant de performance.now()). Planifier le Note Off dans le futur est plus fiable que setTimeout, car la synchronisation est gérée par le sous-système MIDI plutôt que par la boucle d'événements JavaScript. Envoyer 0 sans horodatage signifie « maintenant ».
Éviter les notes bloquées
Le bug le plus courant est une note bloquée — un Note On sans Note Off correspondant, laissant le son jouer indéfiniment. Associez-les toujours : gardez une trace des notes actives et envoyez Note Off lorsque la touche est relâchée ou que la page se décharge.
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();
}Gérer le branchement à chaud
Les périphériques MIDI USB se branchent et se débranchent pendant que la page est ouverte. Écoutez l'événement statechange sur l'objet MIDIAccess afin de pouvoir attacher des gestionnaires aux nouvelles entrées connectées et mettre à jour votre interface lorsque quelque chose est débranché.
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
}
};
}Compatibilité navigateur et bonnes pratiques
- Détectez la fonctionnalité avec
if (navigator.requestMIDIAccess)avant d'appeler — Safari n'a ajouté le support que récemment, et certains environnements le désactivent. - Servez via HTTPS (ou
localhost) ; l'exigence de contexte sécurisé n'est pas optionnelle. - Demandez
sysex: trueuniquement si nécessaire, car cela déclenche une invite plus stricte. - Associez toujours Note On / Note Off et mettez toutes les notes en silence lors de
pagehide/beforeunload. - Utilisez les horodatages de
send()pour une synchronisation précise plutôt quesetTimeout. - Pour le son généré par le navigateur (plutôt que du matériel externe), combinez les entrées MIDI avec la Web Audio API.
Résumé
La Web MIDI API offre aux applications web un canal direct et à faible latence vers le matériel musical. Demandez un objet MIDIAccess avec navigator.requestMIDIAccess(), itérez ses inputs et outputs, lisez les événements entrants depuis MIDIMessageEvent.data, et envoyez des messages de trois octets avec port.send(). Respectez les règles de contexte sécurisé et de permission, gérez le branchement à chaud des périphériques via statechange, et nettoyez toujours les notes pour éviter le classique bug de note bloquée. À partir de là, vous pouvez créer des claviers virtuels, des contrôleurs MIDI et des instruments jouant directement dans le navigateur.