Skip to main content

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​

HelloMidi.pde
// 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

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] & 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.