7. 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 Java mode open starters/raw/java/HelloMidi/HelloMidi.pde. In Python Mode it's
starters/raw/python/hello_midi/hello_midi.pyde. One tab each, nothing else in the folder. No device?
Each key you press is a fake note on.
The whole sketch
- Java
- Python Mode
- p5.js
// HelloMidi: raw MIDI in Processing, no helper library. Lists every MIDI device, opens every input,
// prints each message as [status, data1, data2] and draws a circle whose size is data2 and colour is data1.
// No device: keys are fake notes. Everything here is javax.sound.midi, which ships with Java.
//
// A MIDI message is three bytes. status = what kind + which channel (0x90 = note on, channel 1;
// 0xB2 = control change, channel 3). data1 = which note or which knob. data2 = how hard, or the knob's value.
import javax.sound.midi.*;
int status = -1, data1 = 0, data2 = 0, count = 0; // the last three bytes seen (written by the MIDI thread, read by draw())
void setup() {
size(600, 400);
colorMode(HSB, 127, 100, 100); // hue runs 0..127, so a note or controller number IS a hue
textSize(16);
for (MidiDevice.Info info : MidiSystem.getMidiDeviceInfo()) try {
MidiDevice dev = MidiSystem.getMidiDevice(info);
boolean isInput = dev.getMaxTransmitters() != 0; // a "transmitter" sends to us, so that is an input port
println((isInput ? "input: " : "output: ") + info.getName());
if (!isInput || dev instanceof Sequencer) continue; // skip outputs and Java's own built-in sequencer
dev.open();
dev.getTransmitter().setReceiver(new Receiver() { // Java calls send() on its own thread for every message
public void send(MidiMessage m, long time) { onMidi(m.getMessage()); }
public void close() { }
});
} catch (MidiUnavailableException e) { println(" could not open " + info.getName() + " (in use by another program?)"); }
}
void onMidi(byte[] b) { // b[0] status, b[1] data1, b[2] data2. Java bytes are signed, so & 0xFF
if (b.length < 3) return; // one- and two-byte messages (clock, program change) and SysEx: ignored here
status = b[0] & 0xFF;
data1 = b[1] & 0xFF;
data2 = b[2] & 0xFF;
count++;
println("[" + status + ", " + data1 + ", " + data2 + "] type 0x" + hex(status & 0xF0, 2) + " channel " + ((status & 0x0F) + 1));
}
void draw() {
background(0, 0, 10);
fill(0, 0, 90);
if (status < 0) { text("waiting for MIDI... or press keys", 20, 30); return; }
text("[" + status + ", " + data1 + ", " + data2 + "] " + count + " messages", 20, 30);
fill(data1, 80, 100); // colour = data1 (the note or controller number)
circle(width / 2, height / 2, 20 + data2 * 3); // size = data2 (velocity or value)
}
void keyPressed() { onMidi(new byte[] { (byte) 0x90, (byte) (key % 128), (byte) 100 }); } // stand-in: a note on
# HelloMidi: raw MIDI in Python Mode, no helper. Lists every MIDI device, opens every input, prints each
# message as [status, data1, data2] and draws a circle whose size is data2 and colour is data1.
# No device: keys are fake notes. Java's own MIDI classes do the work (Python Mode is Jython,
# so it can use them directly) with one wrinkle: the JDK hides its MIDI device classes, so methods are called
# through the interface's Method object (jcall below) instead of dev.open(). Everything else is plain.
#
# A MIDI message is three bytes. status = what kind + which channel (0x90 = note on, channel 1;
# 0xB2 = control change, channel 3). data1 = which note or which knob. data2 = how hard, or the knob's value.
from __future__ import division, print_function
# Python Mode runs Jython with respectJavaAccessibility off; Java 17 then refuses the
# private-member reflection on javax.sound.midi. Turn it back on before the import.
from org.python.core import Options as _JyOptions
_JyOptions.respectJavaAccessibility = True
from javax.sound.midi import MidiSystem, MidiDevice, Transmitter, Receiver, Sequencer
S = {"status": -1, "data1": 0, "data2": 0, "count": 0} # the last three bytes seen (written by the MIDI thread)
def jcall(iface, obj, name, *args):
"""Call obj.name(*args) through the interface iface (Jython cannot call the hidden class directly)."""
for m in iface.getMethods():
if m.getName() == name and m.getParameterCount() == len(args):
return m.invoke(obj, list(args))
class Listener(Receiver): # Java calls send() on its own thread for every message
def send(self, msg, time):
b = msg.getMessage() # Java bytes are signed: & 0xFF makes them 0..255
if len(b) >= 3: on_midi(b[0] & 0xFF, b[1] & 0xFF, b[2] & 0xFF) # 1- and 2-byte messages and SysEx: ignored
def close(self): pass
def on_midi(status, data1, data2):
S.update(status=status, data1=data1, data2=data2, count=S["count"] + 1)
print("[%d, %d, %d] type 0x%X channel %d" % (status, data1, data2, status & 0xF0, (status & 0x0F) + 1))
def setup():
size(600, 400)
colorMode(HSB, 127, 100, 100) # hue runs 0..127, so a note or controller number IS a hue
textSize(16)
for info in MidiSystem.getMidiDeviceInfo():
dev = MidiSystem.getMidiDevice(info)
is_input = jcall(MidiDevice, dev, "getMaxTransmitters") != 0 # a "transmitter" sends to us = an input port
print(("input: " if is_input else "output: ") + info.getName())
if not is_input or isinstance(dev, Sequencer): continue # skip outputs and Java's own sequencer
try:
jcall(MidiDevice, dev, "open")
jcall(Transmitter, jcall(MidiDevice, dev, "getTransmitter"), "setReceiver", Listener())
except Exception as e:
print(" could not open (in use by another program?): %s" % e)
def draw():
background(0, 0, 10)
fill(0, 0, 90)
if S["status"] < 0: text("waiting for MIDI... or press keys", 20, 30); return
text("[%d, %d, %d] %d messages" % (S["status"], S["data1"], S["data2"], S["count"]), 20, 30)
fill(S["data1"], 80, 100) # colour = data1 (the note or controller number)
circle(width / 2, height / 2, 20 + S["data2"] * 3) # size = data2 (velocity or value)
def keyPressed(): on_midi(0x90, 60 if key == CODED else ord(key[0]) % 128, 100) # stand-in: a note on
Loads nothing but p5, so it also works double-clicked from a folder or pasted into the web editor as is.
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.
// HelloMidi: raw Web MIDI in p5.js, no helper library. Click once (browsers want a gesture before MIDI),
// then it lists every MIDI port, listens to all inputs, logs each message as [status, data1, data2] and draws
// a circle whose size is data2 and colour is data1. No device: keys are fake notes.
//
// A MIDI message is three bytes. status = what kind + which channel (0x90 = note on, channel 1;
// 0xB2 = control change, channel 3). data1 = which note or which knob. data2 = how hard, or the knob's value.
let status = -1, data1 = 0, data2 = 0, count = 0, asked = false, note = "click to connect MIDI... or press keys";
function setup() {
createCanvas(600, 400);
colorMode(HSB, 127, 100, 100); // hue runs 0..127, so a note or controller number IS a hue
textSize(16);
}
function mousePressed() { // the whole Web MIDI API is one call: requestMIDIAccess()
if (asked) return;
asked = true;
navigator.requestMIDIAccess().then((access) => {
for (const port of access.outputs.values()) console.log("output:", port.name);
let n = 0;
for (const input of access.inputs.values()) {
console.log("input:", input.name);
input.onmidimessage = (e) => onMidi(e.data); // e.data is a Uint8Array of the raw bytes
n++;
}
note = n ? `listening to ${n} input${n > 1 ? "s" : ""}` : "no MIDI inputs found... press keys";
}).catch((e) => { note = "no Web MIDI here (Chrome, Edge or Opera needed)... press keys"; console.log(e); });
}
function onMidi(b) { // b[0] status, b[1] data1, b[2] data2
if (b.length < 3) return; // one- and two-byte messages (clock, program change) and SysEx: ignored here
[status, data1, data2] = b;
count++;
console.log(`[${status}, ${data1}, ${data2}] type 0x${(status & 0xf0).toString(16)} channel ${(status & 0x0f) + 1}`);
}
function draw() {
background(0, 0, 10);
fill(0, 0, 90);
text(note, 20, height - 20);
if (status < 0) return;
text(`[${status}, ${data1}, ${data2}] ${count} messages`, 20, 30);
fill(data1, 80, 100); // colour = data1 (the note or controller number)
circle(width / 2, height / 2, 20 + data2 * 3); // size = data2 (velocity or value)
}
function keyPressed() { onMidi([0x90, key.charCodeAt(0) % 128, 100]); } // stand-in: a note on
What to notice
Java bytes are signed. 200 comes out as -56 until you mask it with & 0xFF. p5 gives you a
Uint8Array, already 0 to 255.
Messages arrive on another thread in Java, or in an event in the browser. 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 shows a status byte (what kind of message, which channel), then 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] & 0xFFturns -56 back into 200. p5'sUint8Arrayis 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 (
jcallin 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 F7when you are done or it stays dark until replugged. The sketches do that indispose(),stop()and onpagehide.