Map Web MIDI API Messages to Tone.js

This article explains how to connect physical MIDI keyboards or controllers to your web browser and use them to play sounds. You will learn how to request MIDI access using the Web MIDI API, parse incoming MIDI messages, and map those messages to trigger notes in Tone.js using a polyphonic synthesizer.

Step 1: Initialize Tone.js

First, ensure Tone.js is loaded in your project. You need to create an instrument and start the audio context. Web browsers require a user interaction (like clicking a button) to start audio.

// Create a polyphonic synthesizer and connect it to the master output
const synth = new Tone.PolySynth(Tone.Synth).toDestination();

// Start the audio context on user interaction
document.getElementById('start-btn').addEventListener('click', async () => {
  await Tone.start();
  console.log("Audio context active");
  initMIDI();
});

Step 2: Request Web MIDI Access

The Web MIDI API is accessed via navigator.requestMIDIAccess(). This method prompts the user for permission to access their MIDI devices.

function initMIDI() {
  if (navigator.requestMIDIAccess) {
    navigator.requestMIDIAccess()
      .then(onMIDISuccess, onMIDIFailure);
  } else {
    console.warn("Web MIDI API is not supported in this browser.");
  }
}

function onMIDISuccess(midiAccess) {
  // Loop through all available MIDI input devices
  for (let input of midiAccess.inputs.values()) {
    input.onmidimessage = getMIDIMessage;
  }
}

function onMIDIFailure() {
  console.error("Could not access your MIDI devices.");
}

Step 3: Parse MIDI Messages and Trigger Tone.js

MIDI messages are sent as a Uint8Array containing three bytes: 1. Status Byte: Identifies the message type (e.g., Note On, Note Off) and the MIDI channel. 2. Data Byte 1: The MIDI note number (0–127). 3. Data Byte 2: The velocity (0–127), representing how hard the key was struck.

We filter these bytes to identify “Note On” and “Note Off” events, convert the MIDI note number into a musical frequency using Tone.Frequency, and trigger the synthesizer.

function getMIDIMessage(message) {
  const [status, noteNumber, velocity] = message.data;
  
  // Mask the status byte to determine the command type (independent of MIDI channel)
  const command = status & 0xf0; 

  // Convert MIDI note number to a pitch name (e.g., 60 to "C4")
  const noteName = Tone.Frequency(noteNumber, "midi").toNote();

  // Command 144 (0x90) represents Note On
  if (command === 144 && velocity > 0) {
    // Convert 0-127 velocity to a 0-1 range for Tone.js
    const normalizedVelocity = velocity / 127;
    synth.triggerAttack(noteName, Tone.now(), normalizedVelocity);
  } 
  // Command 128 (0x80) represents Note Off. 
  // Note On with a velocity of 0 is also treated as Note Off.
  else if (command === 128 || (command === 144 && velocity === 0)) {
    synth.triggerRelease(noteName);
  }
}