Skip to main content

The three bytes, no helpers

The helpers are convenient, but they're also a layer between you and the thing I actually want you to see: three numbers arriving. So here's the same idea with no helper tab at all. HelloMidi lists every MIDI device, opens every input, prints each message as [status, data1, data2], and draws a circle sized by the third byte and coloured by the second. It's under fifty lines in each language.

Plug in anything, press things, and watch the numbers. That's MIDI.

Open it​

In p5.js, open starters/raw/p5/hello-midi/ and click once. In Python Mode it's starters/raw/python/hello_midi/hello_midi.pyde, in Java mode starters/raw/java/HelloMidi/HelloMidi.pde. One file each, nothing else in the folder. No device? Each key you press is a fake note on.

The whole sketch​

Loads nothing but p5, so it also works double-clicked from a folder or pasted into the web editor as is. The helper-based starters live next to it at /examples. The same requestMIDIAccess walk, with more words, is in George's CSS-Tricks article Dip Your Toes Into Hardware With WebMIDI.

Click inside the sketch to connect MIDI. No device? The keys named in the code stand in.

sketch.js
// HelloMidi: raw Web MIDI in p5.js, no helper. One call, navigator.requestMIDIAccess(), then every input's
// onmidimessage hands you the raw bytes. A connection line, a device picker, the last 12 messages, and a circle
// whose size is data2 and colour is data1. Click or press a key once to connect (browsers want a gesture first).
// No device: keys are fake notes.
//
// A MIDI message is three bytes. status = kind + channel (0x90 = note on, channel 1; 0xB2 = control change,
// channel 3). data1 = which note or knob. data2 = how hard, or the knob's value.
let access = null, picker, note = "click or press a key to connect MIDI", log = [], status = -1, data1 = 0, data2 = 0;
const TYPES = { 0x80: "noteOff", 0x90: "noteOn", 0xa0: "polyTouch", 0xb0: "cc", 0xc0: "program", 0xd0: "aftertouch", 0xe0: "pitchBend" };

function setup() {
const cnv = createCanvas(900, 520);
textFont("monospace"); textSize(12); colorMode(HSB, 127, 100, 100); noStroke(); // hue 0..127: a note number IS a hue
picker = createSelect(); picker.option("All inputs"); // the device picker, above the canvas
cnv.elt.parentNode.insertBefore(picker.elt, cnv.elt);
for (const ev of ["pointerdown", "keydown"]) window.addEventListener(ev, connect, { once: true }); // first gesture anywhere
}

function connect() {
if (access) return;
navigator.requestMIDIAccess().then((a) => { access = a; a.onstatechange = listen; listen(); }) // statechange: devices come and go
.catch((e) => { note = "no Web MIDI here (Chrome, Edge or Opera needed); keys still work"; console.log(e); });
}
function listen() { // every input, and the picker rebuilt
const names = [];
for (const input of access.inputs.values()) { input.onmidimessage = (e) => onMidi(e.data, input.name); names.push(input.name); }
const keep = picker.value(); picker.elt.innerHTML = ""; picker.option("All inputs");
for (const n of names) picker.option(n); if (names.includes(keep)) picker.selected(keep);
note = names.length ? `listening to ${names.length} input${names.length > 1 ? "s" : ""}: ${names.join(", ")}` : "no MIDI inputs found; keys still work";
}

function onMidi(b, device = "keyboard") { // b[0] status, b[1] data1, b[2] data2
if (b.length < 3) return; // 1- and 2-byte messages and SysEx: ignored here
if (picker.value() !== "All inputs" && device !== picker.value()) return;
[status, data1, data2] = b;
const type = TYPES[status & 0xf0] || "status", ch = status < 0xf0 ? "ch" + String((status & 0x0f) + 1).padStart(2) : " ";
log.push(`${(millis() / 1000).toFixed(2).padStart(7)} ${device.slice(0, 18).padEnd(18)} ${ch} ${type.padEnd(10)} ${String(data1).padStart(3)} ${String(data2).padStart(3)} [${status}, ${data1}, ${data2}]`);
if (log.length > 12) log.shift();
}

function draw() {
background(0, 0, 10);
fill(0, 0, 90);
text(note, 16, 22); // the connection line
for (let i = 0; i < log.length; i++) text(log[i], 16, 50 + i * 16); // newest at the bottom
if (status < 0) return;
fill(data1, 80, 100); // colour = data1 (the note or controller number)
circle(width / 2, 400, 20 + data2 * 2); // size = data2 (velocity or value)
}

function keyPressed() { onMidi([0x90, key.charCodeAt(0) % 128, 100]); } // stand-in: a note on

What to notice​

p5 gives you a Uint8Array, already 0 to 255. Java bytes are signed, so 200 comes out as -56 until you mask it with & 0xFF.

Messages arrive in an event in the browser, or on another thread in Java. Store the bytes and draw from them in draw(). Never draw inside the callback. You'll get away with it for a while, and then you won't.

Python Mode reaches the JDK's MIDI classes through their interfaces, because the Java module system hides them from Jython. That is the six-line jcall at the top of the Python sketch. Copy it.

Web MIDI wants a click before it hands out access, and SysEx is a second, separate permission. Only the Launchpad needs that one.

Raw or helpers?​

Every device starter has a raw version too, under sixty lines each, decoding its own two or three messages inline. Each device page links to its own at the bottom. Here's how I'd pick between the two, from the starters' README:

Start raw to see what is happening. HelloMidi logs each message as a status byte (what kind of message, which channel) and two data bytes. Plug anything in and watch the numbers. A sketch that needs two or three messages can decode them inline in a dozen lines.

Switch to ../../midi-helpers/ when the plumbing crowds out the idea. They do what these sketches do by hand or skip: frame-synced justPressed(), callbacks by name, a config for a custom PipSqueak, a learned layout for an odd Midi Fighter, RGB and text on the Launchpad, telling the accelerometer burst from touch pads, keyboard stand-ins, click-to-connect in p5.

What the raw sketches show that the helpers hide:

  • Java bytes are signed. b[1] & 0xFF turns -56 back into 200. p5's Uint8Array is already 0..255.
  • Messages arrive on another thread (Java) or in an event (p5). Store the bytes, draw in draw().
  • Python Mode calls the JDK's MIDI classes through their interfaces (jcall in each sketch); the Java module system hides those classes from Jython. Six lines, copied into each sketch.
  • Web MIDI needs a click first, and SysEx is a separate permission.
  • The Launchpad is a different device until you send F0 00 20 29 02 0D 0E 01 F7 (programmer mode). Send ...0E 00 F7 when you are done or it stays dark until replugged. The sketches do that in dispose(), stop() and on pagehide.