DEV Community

loading...
Cover image for Connecting the musical world to the Web using the Web MIDI API

Connecting the musical world to the Web using the Web MIDI API

unjavascripter profile image Diego Coy ・4 min read

Web MIDI

MIDI is a protocol that electronic musical instruments, controllers and computers (and now browsers) use to communicate with each other.

Web MIDI API supports the MIDI protocol and has been available since Chrome 43. It should be made clear that this API is not related with the playback of the MIDI/SMF files. It allows us instead to handle MIDI devices's input and output interfaces to send and receive messages through our web applications.

The messages

A MIDI message typically comprises a command or action that specifies what the interaction represents: pressing or releasing a key. Then we have the value of the note, and finally we have the velocity which represents how hard did you press a key, this can be interpreted as a higher volume the harder the key is pressed.

These three values are received by the browser as an array:

  // command, note, velocity
  [144, 119, 127]
  • 144 is the command, this one specifically means "note on". After you release the key, a new message will be fired with a command value of 128, this is the "note off" command.
  • 119 is the note number
  • 127 is the speed, it ranges from 0 to 127

Web MIDI API

m2m

I'll be creating an app that can make two different MIDI devices interact with each other and the browser.

Getting MIDI access

Here I'm creating a class that will call an async init function that will first check if navigator.requestMIDIAccess() can be executed, if not, will call it a day by invoking onMIDIFailure to log a message to the user to let them know their browser can't handle Web MIDI. If it is however supported, a MIDIAccess object will be passed to the onMIDISuccess success callback.

class WebMidiDemo {
  constructor() {
    this.init();
  }
  async init() {
    try {
      const midiAccess = await navigator.requestMIDIAccess();
      this.onMIDISuccess(midiAccess);
    }
    catch (err) {
      this.onMIDIFailure(err);
    }
  }
  // ...
  onMIDIFailure(err) {
    console.log(`Failed to get MIDI access - ${err}`);
  }
  // ...

Listing MIDI devices

The first thing I'm doing in this specific example is storing the detected MIDI input and output devices list for later use.

After that I'm using a separate class I created to handle the creation of a couple of <select> elements so the user can pick the devices. I'm passing a callback function, onMidiInputPortSelected and onMidiOutputPortSelected for each to handle the onchange event of the <select>. If you're curious about .bind(this), take a look at this article.

onMIDISuccess(midiAccess) {
  this.inputDevicesList = midiAccess.inputs;
  this.outputDevicesList = midiAccess.outputs;

  // uiStuff is a custom class I created to handle UI Stuff, it has NOTHING to do with the Web MIDI API. I just wanted to keep things separate
  uiStuff.createSelect(midiAccess.inputs.values(), this.onMidiInputPortSelected.bind(this), '<Select an Input device>');
  uiStuff.createSelect(midiAccess.outputs.values(), this.onMidiOutputPortSelected.bind(this), '<Select an Output device>');
}

Attaching callbacks

Notice how I close any previous selected device port, however it's possible to skip this step and have multiple ports open at the same time.

Now it's time to use the previously stored devicesList. Since it is Map like, I can use Map's get method to query it and bring the MIDI device with the ID that matches the selection.

After creating a reference for both the selected input and output device, I can attach a callback to the onmidimessage, which we will cover next.

onMidiInputPortSelected(selection) {
  if (this.selectedInputDevice) {
    this.selectedInputDevice.close();
  }

  const selectedInputDeviceId = selection.target.value;

  this.selectedInputDevice = this.inputDevicesList.get(selectedInputDeviceId);
  this.selectedInputDevice.onmidimessage = this.handleMIDIMessage.bind(this);
  console.log('Input port', selectedInputDeviceId);
}

onMidiOutputPortSelected(selection) {
  if (this.selectedOutputDevice) {
    this.selectedOutputDevice.close();
  }

  const selectedOutputDeviceId = selection.target.value;

  this.selectedOutputDevice = this.outputDevicesList.get(selectedOutputDeviceId);
  console.log('Output port', selectedOutputDeviceId);
}

Receiving and Sending messages

Every time there's an incoming MIDI message from the selected input device, the handleMIDIMessage is executed. The event is a MIDIMessageEvent, it has a data property which is a Unit8Array containing the command, note, and velocity.

With the power of destructuring I extract the array values as individual variables and then:

  1. Log the message properties to the console
  2. Verify that the incoming message is a note on command
  3. Change the body's background color, because I couldn't think of anything cooler than that ¯\(ツ)
  4. Send back that same command and note to the selected output device
handleMIDIMessage(event) {
  const [command, note, velocity] = event.data;
  console.log([command, note, velocity]);

  if(command === 144) {
    // a method to change the body's background color on each key press, not related to Web MIDI
    uiStuff.changeBackgroundColor(note);

    if(this.selectedOutputDevice) {
      this.selectedOutputDevice.send([command, note, 10]);
    }

  }
}

You may have noticed that I'm passing a static 10 as the velocity for every message. It turns out that when you send a 144 command and a 10 as the value for the velocity to the MIDI Launchpad that I have, it makes the key associated with that note light red. Different values will trigger different colors, but those are defined by the vendor, so your device may differ.

The code

Check out the repo https://github.com/UnJavaScripter/web-midi-basics

You will notice that I used TypeScript, if you use it I recommend you install @types/webmidi. Afterwards you can use it by calling WebMidi.**thing**.

If for some reason you don't like TypeScript, that's Okay too. I made TypeScript compile to ES2017 so you can check the JavaScript code without any weird additions.

The demo

Here: https://unjavascripter-web-midi-basics.glitch.me/

Browser Support

You can use the Web MIDI API in Chromium based browsers: https://caniuse.com/#search=web%20midi%20api

Things to read

You should definitely go over the W3C draft for the Web MIDI API https://webaudio.github.io/web-midi-api/

Discussion (0)

Forem Open with the Forem app