W3docs

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 :

OctetNomSignification
1StatusType de message (quartet de poids fort) + canal 0–15 (quartet de poids faible)
2Data 1Numéro de note (0127), ou numéro de contrôleur
3Data 2Vélocité (0127), ou valeur du contrôleur

Octets de statut courants (canal 1, où le quartet de canal vaut 0) :

  • 0x90Note On (une vélocité 0 est traitée comme Note Off)
  • 0x80Note Off
  • 0xB0Control Change (pédale de sustain, modulation, volume, …)
  • 0xE0Pitch Bend

Le numéro de note 60 correspond au Do médian ; la vélocité 0127 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 Mapinputs (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: true uniquement 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 que setTimeout.
  • 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.

Entraînement

Pratique
Quelles sont les capacités de la JavaScript Web MIDI API ?
Quelles sont les capacités de la JavaScript Web MIDI API ?
Was this page helpful?