MAKE MIDI SOUND CONtroL THROUGH THE BROWSER: MIDI.js

Sources: https://www.keithmcmillen.com/blog/making-music-in-the-browser-web-midi-api/

https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API

 

Local Download: C:\Users\rodol\Documents\Arduino\ESP32 – 2024

 

Application example by Keith McMillen’s tutorial: Making Music in the Browser – Web MIDI API

This is  4 parts tutorial rgadually leading to the implementation of a functionnal 5graphic pads and/or 5 touch keyboards letters q w e r t y, triggering MIDI syntax and e.g. notes sounds directly from the browser to you MISI-appliant applications in Windows or Mac.

“With the latest version of Google Chrome, MIDI is enabled by default from JUne 2015th. This opens up a huge variety of possibilities for not only art and music in the browser, but also allows any hardware that uses MIDI as its communication platform to control and be controlled by your browser. “

We now have MIDI support in something that everyone with a computer, tablet, or smartphone has: the web browser.

  • Works on anything with a web browser that can run a Web-MIDI app and use local MIDI hardware.
  • Works with your existing MIDI setup. If your MIDI gear is connected to your computer, tablet or phone (by a cable or even wirelessly) that same connection will connect your MIDI gear to your Web-MIDI enabled browser.
  • Updates are automatic. No need to install new versions, the latest version is always available at the website URL.
  • Accessible anywhere. Apps and data in “the Cloud” are available anywhere you have an internet connection.
  • Connects you and your (live) music to other people and communities.

    “In this introductory article to my Making Music in the Web Browser series, I’m going to take you through the basics of setting up your MIDI keyboard / controller to interface with your browser. In upcoming articles I will show you how to use the Web Audio API to build simple playback and manipulation applications all the way to building a variety of synthesizers in your browser.”

However MIDI API:

  • Does not directly support Standard MIDI Files, although a Standard MIDI File player can be built on top of the Web MIDI API.
  • Is not intended to semantically capture patches or controller assignments, as General MIDI does, although General MIDI can easily be utilized through the Web MIDI API.

 

Check Browser version compliance and MIDI Access

  Checking if the browser supports the WebMIDI API by checking for the existence of the method:
requestMIDIAccess()

If the browser supports Web MIDI, therequest MIDIAccess() method returns a ‘MIDI Access’ object, which contains all of our connected device’s info, which we’ll be exploring soon. This ‘MIDI Access’ object gets passed to the onMIDISuccess() function for us to use. If the browser doesn’t support Web MIDI, then the onMIDIFailure() function gets called, ending all of the fun you could have had. *You may have noticed that System Exclusive messages, or ‘sysex’, are also supported if set to ‘true’, but that’s for another article.

 

// request MIDI access
if (navigator.requestMIDIAccess) {
    navigator.requestMIDIAccess({
        sysex: false // this defaults to 'false' and we won't be covering sysex in this article. 
    }).then(onMIDISuccess, onMIDIFailure);
} else {
    alert("No MIDI support in your browser.");
}

// midi functions
function onMIDISuccess(midiAccess) {
    // when we get a succesful response, run this code
    console.log('MIDI Access Object', midiAccess);
}

function onMIDIFailure(e) {
    // when we get a failed response, run this code
    console.log("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + e);
}

TO DO
MIDI inputs setup

Now that we have an active MIDI connection and our device info, we need to do something with it. Click on the ‘Result’ tab above and open up Chrome Developer Tools (Console), you will see our MIDIAccess object, which shows inputs, outputs and sysex status.

What we’re after in this object is ‘inputs’, specifically their values, which we need to loop over.
Let’s add this piece of code to our onMIDISuccess()function:

midi = midiAccess; // MIDI data, inputs, outputs and sysex status

var inputs = midi.inputs.values();

//loop over all available inputs and listen yo any MIDI inputs

for ( var input = inputs.next(); input && !input.done; inut = inputs,next())  {

     // each time there is a MIDI messag call the onMIDIMessage function

    input.value.onmidimessage = onMIDIMessage;

}

In the body of the for loop you will see that every time we send a message, our onMIDIMessage() function gets called.

 

Next, let’s add add some code to our onMIDIMessage()function to log our incoming key data:

 

data = message.data; //this gives us our [command/channel, note, velocity] data.

console.log(“MIDI data”, data ); // MIDI data [144, 63, 73]

Press a key on your MIDI keyboard and you will see a series of messages.

Clicking on the triangle will show you all of the info you get on every key-press.

As cool as that is, it’s still not very usable. What we want to get at is the ‘data’ property.

 

 

 

var midi, data;
// request MIDI access
if (navigator.requestMIDIAccess) {
    navigator.requestMIDIAccess({
        sysex: false
    }).then(onMIDISuccess, onMIDIFailure);
} else {
    alert("No MIDI support in your browser.");
}

// midi functions
function onMIDISuccess(midiAccess) {
    // when we get a succesful response, run this code
    midi = midiAccess; // this is our raw MIDI data, inputs, outputs, and sysex status

    var inputs = midi.inputs.values();
    // loop over all available inputs and listen for any MIDI input
    for (var input = inputs.next(); input && !input.done; input = inputs.next()) {
        // each time there is a midi message call the onMIDIMessage function
        input.value.onmidimessage = onMIDIMessage;
    }
}

function onMIDIFailure(error) {
    // when we get a failed response, run this code
    console.log("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + error);
}

function onMIDIMessage(message) {
    data = message.data; // this gives us our [command/channel, note, velocity] data.
    console.log('MIDI data', data); // MIDI data [144, 63, 73]
}

 

 

On MIDI message management

 

end44  Now we need to build out ouronMIDIMessage()function so that we can use the incoming data.
Let’s add some variables to more easily use our data:

data = event.data;

cmd = data[0] >> 4,

channel =  data[0] & 0xf,

type =  data[0] & 0xf0,  // channel agnostic message type, by Phil Burk

note =  data[1],

velocity=  data[2],

//with pressure and title off

// note off : 128, cmd:8

// note on : 144, cmd:9

// pressure: 176, cmd:11

//bend: 224, cmd:14

 

 

Also, we should add a switch statement to control what happens when we press and release a key:

switch(type){

    case 144: // noteOn message

      noteOn(note, velocity);

      break;

  case 128: // noteOff message

     noteOff(note, velocity);

    break;

}

You can also easily convert MIDI note data to pitch using the Web Audio API and a function like this:

freqfrommidi

Our switch statement uses the first part of our MIDI data :[144, 63, 73], 144 to choose what we’ll do next. 144 is typically a note-on message, whereas 128 is a note-off message.

This first byte typically represents the type of MIDI Message, like on, off, pitch-bend, etc… We will also check for velocity just to make sure we are getting a noteOff message when we expect one.

Now, when you press a key you will see that you have noteOn and NoteOff messages, all you need to do is pass that key data into a function to do something with it.

Let’s addnoteOn()andnoteOff()functions, which calls aplayer()which plays samples:

 

function noteOn(midiNote, velocity){

    player(midiNote, velocity);

}

function noteOff(midiNote, velocity){

    player(midiNote, velocity);

}

 

 

Simple MIDI samples player

As proposed by Miller McMillen at the end of this tutorial for a basic working appication:

“just to give you a taste of using Web Audio with Web MIDI, i’ve created a simple sample player that responds to MIDI, qwerty keyboard [q,w,e,r,t], and mouse click below. Basically, I’m loading in sounds and controlling playback using MIDI note value and velocity. I’m also randomizing sample playback speed on each keypress for variety’s sake. All of the sound i’m using are from Matt Hettich’s Eurorack post ALM Dinky’s Taiko module: Matt’s Monthly Eurorack Dig: ALM Dinky’s Taiko.
Try and dissect what the code does from my comments, i’ll explain it all in future posts on the Web Audio API. Also, the samples here are only triggered by MIDI notes 60 – 64.

Looking forward to getting our hands dirty with Web Audio and Web MIDI!

Some final words of inspiration from MIDI.org:

The Web MIDI API connects your MIDI gear directly to your browser.
Your browser connects you to the rest of the world.

You can download the source code for the lesson, with sounds here.

 

 

Click on the ‘Result’ tab to see it in action.

JAVASCRIPT

var log = console.log.bind(console),
    keyData = document.getElementById('key_data'),
    midi;
var AudioContext;
var context;
var btnBox = document.getElementById('content'),
    btn = document.getElementsByClassName('button');
var data, cmd, channel, type, note, velocity;

try {
	AudioContext = window.AudioContext || window.webkitAudioContext; // for ios/safari
	context = new AudioContext();
}
catch(e) {
  alert('Web Audio API is not supported in this browser');
}
// request MIDI access
if (navigator.requestMIDIAccess) {
    navigator.requestMIDIAccess({
        sysex: false
    }).then(onMIDISuccess, onMIDIFailure);
} else {
    alert("No MIDI support in your browser.");
}

// add event listeners
document.addEventListener('keydown', keyController);
document.addEventListener('keyup', keyController);
for (var i = 0; i < btn.length; i++) {
    btn[i].addEventListener('mousedown', clickPlayOn);
    btn[i].addEventListener('mouseup', clickPlayOff);
}
// prepare audio files
for (var i = 0; i < btn.length; i++) {
    addAudioProperties(btn[i]);
}
// this maps the MIDI key value (60 - 64) to our samples
var sampleMap = {
    key60: 1,
    key61: 2,
    key62: 3,
    key63: 4,
    key64: 5
};
// user interaction, mouse click
function clickPlayOn(e) {
    e.target.classList.add('active');
    e.target.play();
}

function clickPlayOff(e) {
    e.target.classList.remove('active');
}
// qwerty keyboard controls. [q,w,e,r,t]
function keyController(e) {
    if (e.type == "keydown") {
        switch (e.keyCode) {
            case 81:
                btn[0].classList.add('active');
                btn[0].play();
                break;
            case 87:
                btn[1].classList.add('active');
                btn[1].play();
                break;
            case 69:
                btn[2].classList.add('active');
                btn[2].play();
                break;
            case 82:
                btn[3].classList.add('active');
                btn[3].play();
                break;
            case 84:
                btn[4].classList.add('active');
                btn[4].play();
                break;
            default:
                //console.log(e);
        }
    } else if (e.type == "keyup") {
        switch (e.keyCode) {
            case 81:
                btn[0].classList.remove('active');
                break;
            case 87:
                btn[1].classList.remove('active');
                break;
            case 69:
                btn[2].classList.remove('active');
                break;
            case 82:
                btn[3].classList.remove('active');
                break;
            case 84:
                btn[4].classList.remove('active');
                break;
            default:
                //console.log(e.keyCode);
        }
    }
}

// midi functions
function onMIDISuccess(midiAccess) {
    midi = midiAccess;
    var inputs = midi.inputs.values();
    // loop through all inputs
    for (var input = inputs.next(); input && !input.done; input = inputs.next()) {
        // listen for midi messages
        input.value.onmidimessage = onMIDIMessage;
        // this just lists our inputs in the console
        listInputs(input);
    }
    // listen for connect/disconnect message
    midi.onstatechange = onStateChange;
}

function onMIDIMessage(event) {
    data = event.data,
    cmd = data[0] >> 4,
    channel = data[0] & 0xf,
    type = data[0] & 0xf0, // channel agnostic message type. Thanks, Phil Burk.
    note = data[1],
    velocity = data[2];
    // with pressure and tilt off
    // note off: 128, cmd: 8 
    // note on: 144, cmd: 9
    // pressure / tilt on
    // pressure: 176, cmd 11: 
    // bend: 224, cmd: 14

    switch (type) {
        case 144: // noteOn message 
             noteOn(note, velocity);
             break;
        case 128: // noteOff message 
            noteOff(note, velocity);
            break;
    }

    //console.log('data', data, 'cmd', cmd, 'channel', channel);
    logger(keyData, 'key data', data);
}

function onStateChange(event) {
    var port = event.port,
        state = port.state,
        name = port.name,
        type = port.type;
    if (type == "input") console.log("name", name, "port", port, "state", state);
}

function listInputs(inputs) {
    var input = inputs.value;
    log("Input port : [ type:'" + input.type + "' id: '" + input.id +
        "' manufacturer: '" + input.manufacturer + "' name: '" + input.name +
        "' version: '" + input.version + "']");
}

function noteOn(midiNote, velocity) {
  if (audioContext.state !== 'running') audioContext.resume();
    player(midiNote, velocity);
}

function noteOff(midiNote, velocity) {
    player(midiNote, velocity);
}

function player(note, velocity) {
    var sample = sampleMap['key' + note];
    if (sample) {
        if (type == (0x80 & 0xf0) || velocity == 0) { //QuNexus always returns 144
            btn[sample - 1].classList.remove('active');
            return;
        }
        btn[sample - 1].classList.add('active');
        btn[sample - 1].play(velocity);
    }
}

function onMIDIFailure(e) {
    log("No access to MIDI devices or your browser doesn't support WebMIDI API. Please use WebMIDIAPIShim " + e);
}

// audio functions
// We'll go over these in detail in future posts
function loadAudio(object, url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';
    request.onload = function () {
        context.decodeAudioData(request.response, function (buffer) {
            object.buffer = buffer;
        });
    }
    request.send();
}

function addAudioProperties(object) {
    object.name = object.id;
    object.source = object.dataset.sound;
    loadAudio(object, object.source);
    object.play = function (volume) {
        var s = context.createBufferSource();
        var g = context.createGain();
        var v;
        s.buffer = object.buffer;
        s.playbackRate.value = randomRange(0.5, 2);
        if (volume) {
            v = rangeMap(volume, 1, 127, 0.2, 2);
            s.connect(g);
            g.gain.value = v * v;
            g.connect(context.destination);
        } else {
            s.connect(context.destination);
        }

        s.start();
        object.s = s;
    }
}

// utility functions
function randomRange(min, max) {
    return Math.random() * (max + min) + min;
}

function rangeMap(x, a1, a2, b1, b2) {
    return ((x - a1) / (a2 - a1)) * (b2 - b1) + b1;
}

function frequencyFromNoteNumber(note) {
    return 440 * Math.pow(2, (note - 69) / 12);
}

function logger(container, label, data) {
    messages = label + " [channel: " + (data[0] & 0xf) + ", cmd: " + (data[0] >> 4) + ", type: " + (data[0] & 0xf0) + " , note: " + data[1] + " , velocity: " + data[2] + "]";
    container.textContent = messages;
}

 

HTML

<div id="content">
		<div class="button" data-key="q" data-sound="https://files.keithmcmillen.com/blog/mmitb/dinky-kick.mp3"></div>
		<div class="button" data-key="w" data-sound="https://files.keithmcmillen.com/blog/mmitb/dinky-snare.mp3"></div>
		<div class="button" data-key="e" data-sound="https://files.keithmcmillen.com/blog/mmitb/dinky-hat-2.mp3"></div>
		<div class="button" data-key="r" data-sound="https://files.keithmcmillen.com/blog/mmitb/dinky-cym.mp3"></div>
		<div class="button" data-key="t" data-sound="https://files.keithmcmillen.com/blog/mmitb/dinky-cym-noise.mp3"></div>
</div>
<div id="device_info">
    <div id="key_data"></div>
</div>

CSS

*:before, *:after {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}
h4 {
    margin: 0 0 5px 0;
}
p {
    margin: 0 0 10px 0;
}
#content, #device_info {
    max-width: 800px;
    /*height: 125px;*/
    margin: 0 auto;
    padding: 10px 0;
    font-family: sans-serif;
    font-size: 12px;
    line-height: 12px;
    letter-spacing: 1.5px;
}
#content, #key_data {
    margin-top: 0px;
    text-align: center;
}
#inputs, #outputs {
    display: inline-block;
    width: 49%;
    margin-top: 10px;
    vertical-align: top;
}
#outputs {
    text-align: right;
}
.info {
    padding: 20px;
    border-radius: 3px;
    box-shadow: inset 0 0 10px #ccc;
    background-color: rgba(233, 233, 233, 0.25);
}
.small {
    border-bottom: 1px solid #ccc;
    margin-left: 10px;
}
p:not(.small) {
    text-transform: uppercase;
    font-weight: 800;
}
.button {
    display: inline-block;
    width: 100px;
    height: 100px;
    margin: 10px;
    background-color: #00adef;
    border-radius: 10px;
    opacity: 1;
    cursor: pointer;
    border: 2px solid white;
    transition: all 0.2s;
}
.button.active {
    background-color: #9a6aad;
    opacity: 0.25;
    box-shadow: inset 0px 0px 30px orange;
    border: 2px solid rgba(100, 100, 100, 0.3);
    animation: shake .2s ease-in-out;
}
@keyframes shake {
    0% {
        transform: translateX(0);
        transform: translateY(0);
        transform: scale(1, 1);
    }
    20% {
        transform: translateX(-10px);
        transform: translateY(-100px);
        transform: scale(0.5, 0.75);
    }
    40% {
        transform: translateX(10px);
        transform: translateY(0px);
        transform: scale(1.5, 2);
    }
    60% {
        transform: translateX(-10px);
        transform: translateY(-50px);
        transform: scale(0.5, 0.75);
    }
    80% {
        transform: translateX(10px);
        transform: translateY(50px);
        transform: scale(1.3, 2);
    }
    100% {
        transform: translateX(0);
        transform: translateY(0);
    }
}