Skip to main content

Anything else

The Trinkeys, the NeoTrellis, a keyboard you brought from home, a drum pad, a foot pedal. If the operating system sees it as a MIDI device, the generic helper can read it and the Explorer will show you what it sends.

MakerAnyone
USB nameWhatever the Explorer prints at the top.
PortsWhatever it has. Most controllers are 1 in, 1 out.
SendsNotes and CCs. The Explorer tells you which numbers.
ReceivesSometimes. Try the Explorer's send panel on anything with lights.

Find out what it sends​

  1. Plug it in and open the Explorer.
  2. Press, turn, slide or tilt everything once. Write down the note and CC numbers.
  3. Note the device name at the top. That is the substring you give the helper.

On the table​

DeviceWhat it isWhat it sends
Slide TrinkeyAdafruit USB stick with a slider, a touch pad and two pixels. Shows up as "Slide Trinkey M0".Slider is CC 1, 0 at the left to 127 at the right. Touch pad is note 60. Send it a note on and the pixels take that note's colour for a second. Firmware in firmware/trinkey-midi/.
Rotary TrinkeyA click knob with a push switch, a touch pad and one pixel. Shows up as "Rotary Trinkey M0".Knob is CC 2 as an absolute 0..127 position and CC 3 as relative clicks (1 clockwise, 127 counter-clockwise). Pressing the knob is note 61, the touch pad note 62. Same colour trick on incoming notes.
NeoTrellis M4Adafruit's 4×8 grid of RGB-lit buttons with an accelerometerProbably notes for the buttons and CCs for tilt. Check the Explorer. It is a grid, so the Midi Fighter ideas transfer.
YoursAnythingSame drill. Explorer first.

Start here​

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

Code: starters/p5/anything-else/sketch.js. Paste it into the p5.js web editor with the helper script tag, or download the folder and double-click index.html.

sketch.js
// AnyMidi starter: the Explorer's raw log as a sketch. A connection line, a device picker, the last 12 messages
// (time, device, channel, type, number, value, raw bytes), then note circles and CC bars underneath to play with.
// Click once to connect MIDI. No device: letters are notes, drag the mouse for a knob.
let m, picker, known = "", log = [];
const pop = new Array(128).fill(0), knob = new Array(128).fill(0);
const TYPES = { 0x80: "noteOff", 0x90: "noteOn", 0xa0: "polyTouch", 0xb0: "cc", 0xc0: "program", 0xd0: "aftertouch", 0xe0: "pitchBend" };

function setup() {
const cnv = createCanvas(900, 560);
textFont("monospace"); textSize(12); noStroke();
picker = createSelect(); picker.option("All inputs"); // the device picker, above the canvas
cnv.elt.parentNode.insertBefore(picker.elt, cnv.elt);
m = new AnyMidi(); // no name: every input
m.connectOnClick(() => {}); // the connection line below replaces the helper's hint
}

const wanted = (device) => picker.value() === "All inputs" || device === picker.value();
function logMsg(status, d1, d2, device, value) { // one line per message, newest last
if (!wanted(device)) return;
const type = TYPES[status & 0xf0] || "status " + 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(d1).padStart(3)} ${String(value).padStart(5)} [${status}, ${d1}, ${d2}]`);
if (log.length > 12) log.shift();
}
function raw(status, d1, d2, device) { // the bytes as received; rebuilt for the keyboard stand-in
return device === "keyboard" || !m.last ? [status, d1, d2] : [m.last.status, m.last.data1, m.last.data2];
}

function draw() {
if (m.core.access && !m.core.access.onstatechange) m.core.access.onstatechange = () => m.connect(); // devices come and go: reopen
const inputs = m.devices(); // repopulate the picker when the list changes
if (inputs.join("|") !== known) {
known = inputs.join("|"); const keep = picker.value(); picker.elt.innerHTML = "";
picker.option("All inputs"); for (const n of inputs) picker.option(n); if (inputs.includes(keep)) picker.selected(keep);
}
background(15); fill(200);
text(m.connected() ? m.status : m.status.startsWith("no MIDI") ? "no MIDI inputs found" : m.status, 16, 22); // the connection line
for (let i = 0; i < log.length; i++) text(log[i], 16, 50 + i * 16);
for (let n = 0; n < 128; n++) { // the playground: change this part
const x = map(n, 0, 127, 10, width - 10);
fill(90, 200, 255); rect(x - 2, height - 10, 4, -knob[n] * 1.5); // change this: bars for CC values 0..127
if (pop[n] > 0) { fill(255, 120, 80, pop[n] * 2); circle(x, 370, pop[n]); pop[n] -= 2; } // change this: what a note looks like
}
}

// The helper calls these for every message, with the device name last. Channel is 1..16.
function noteOn(ch, n, v, dev = "keyboard") { logMsg(...raw(0x90 | (ch - 1), n, v, dev), dev, v); if (wanted(dev)) pop[n] = 40 + v; } // change this
function noteOff(ch, n, v, dev = "keyboard") { logMsg(...raw(0x80 | (ch - 1), n, v, dev), dev, v); }
function controlChange(ch, cc, val, dev = "keyboard") { logMsg(...raw(0xb0 | (ch - 1), cc, val, dev), dev, val); if (wanted(dev)) knob[cc] = val; } // change this
function pitchBend(ch, value, dev = "keyboard") { const v = value + 8192; logMsg(...raw(0xe0 | (ch - 1), v & 0x7f, v >> 7, dev), dev, value); }
function midiMessage(status, d1, d2, dev = "keyboard") { logMsg(status, d1, d2, dev, d2); } // everything else: program, aftertouch, clock...

// Stand-ins, so the sketch does something with no device.
function keyPressed() { if (key.length === 1) noteOn(1, 48 + key.charCodeAt(0) % 36, 100); }
function mouseDragged() { controlChange(1, 1 + floor(mouseY * 8 / height), floor(map(mouseX, 0, width, 0, 127))); }

The helper​

new AnyMidi(this), new AnyMidi(this, "name substring")The first input on the system, or the first whose name contains the substring.
m.note(n), m.cc(n), m.down(n)The last value seen for that note or controller, 0..127, or -1 if it has never arrived. Whether a note is held.
m.lastNote, m.lastCC, m.lastCCValue, m.lastThe most recent thing that happened, for sketches that react to whatever arrives.
m.send(status, d1, d2), m.noteOn(ch, n, vel), m.controlChange(ch, cc, val)Output, for devices with lights.
void noteOn(…), void noteOff(…), void controlChange(…), void midiMessage(…)Define any in your sketch and the helper calls it with channel, number and value.

From the helpers' README​

For the NeoTrellis, whatever people bring, and a desk with four devices on it.

AnyMidi m = new AnyMidi(this); // every input, first output
AnyMidi m = new AnyMidi(this, "name substring"); // one input and one output
m.connect(); m.status(); m.devices(); // "listening to 4 inputs: PipSqueak, ..." / the input names
m.note(n); m.cc(n); // last value seen on any input, 0..127, -1 if never. A note's value is its velocity, 0 after Note Off
m.down(n); // note held?
m.lastNote; m.lastVelocity; m.lastCC; m.lastCCValue; m.lastChannel; m.lastDevice; m.pitchBend; m.pressure; m.count; m.last
m.send(status, d1, d2); m.noteOn(ch, n, vel); m.noteOff(ch, n); m.controlChange(ch, cc, val);
m.programChange(ch, p); m.pitchBend(ch, value); m.sysex(bytes);

With no name it listens to every input at once, so moving any device on the desk shows up. status() reads listening to N inputs: names, or connected to X with a name. Sending goes to the first output, or the named one.

Callbacks: the three generic ones, plus pitchBend(int channel, int value) (-8192..8191) and midiMessage(int status, int data1, int data2) for anything else. Every one of them also accepts a trailing String device, so void noteOn(int channel, int note, int velocity, String device) tells you which input it was. Define whichever arity you want. m.last is a MidiMsg with status, type, channel, data1, data2, millis, sysex, device and a readable toString(). The Explorer prints it.

Python: AnyMidi(this) or AnyMidi(this, "name"), m.status, m.devices(), m.note(n), m.cc(n), m.down(n), m.last_note, m.last_cc, m.last_device, m.pitch_bend, m.count, m.last (with .device), m.send, m.note_on, m.note_off, m.control_change, m.send_pitch_bend, m.sysex. Callbacks take the trailing device if you declare it: def note_on(channel, note, velocity, device).

The p5.js helpers​

The same helpers for the browser: one script per device, or the bundle midi-helpers/p5/midi-helpers.js, served from this site.

The same API for the browser, as plain classic scripts: no modules, no bundler, no dependencies. Each device script defines one global constructor and carries its own copy of the Web MIDI plumbing (MidiCore), guarded so it is defined once however many scripts you load. midi-helpers.js has all of them. Works from file://, GitHub Pages and the workshop host. Sources are in p5/src/; p5/build.sh concatenates them into the committed scripts.

<script src="https://cdn.jsdelivr.net/npm/p5@1/lib/p5.min.js"></script>
<script src="midi-helpers.js"></script> <!-- or pipsqueak.js alone -->
<script>
const stick = new PipSqueak(); // never touches hardware until connect()
function setup() { createCanvas(600, 600); stick.connectOnClick(); }
function draw() {
background(20);
circle(300 + stick.x * 250, 300 - stick.y * 250, stick.pressed ? 60 : 30);
}
function stickPressed() { console.log("click"); }
</script>

In the p5.js web editor (editor.p5js.org, in Chrome, Edge or Opera): open index.html in the sketch files panel and add, after the p5 tag,

<script src="https://morethanmusic.dev/mtm-midi-helpers/latest/midi-helpers.js"></script>

Write the sketch as above. Press Play, click the canvas once. The browser asks for MIDI permission the first time and the helper connects. The editor's preview iframe allows Web MIDI: PreviewFrame.jsx on the develop branch lists midi in the iframe's allow attribute, with allow-same-origin and allow-scripts in its sandbox. A real click-through is still on the hardware checklist. If anything is off, use the editor's full-screen preview or download the sketch and open it in Chrome.

Connecting. Web MIDI wants a user gesture, so every helper has connectOnClick(). It waits for the first click, touch or key anywhere on the page, calls connect(), and keeps a status string in helper.status (click to connect MIDI, connecting..., connected to X, no MIDI device matching 'pipsqueak', Web MIDI unavailable: ...). On p5 1.x it also registers a post hook that draws that status bottom-left until a device connects. On p5 2.x, or for your own look, call helper.drawHint() at the end of draw() or pass a function: stick.connectOnClick((status) => text(status, 10, 20)). await stick.connect() works too, from mousePressed() say, and resolves to true or false. Not found never throws. The constructor never touches hardware.

Frame sync. Each helper runs its own requestAnimationFrame loop, so pressed and justPressed() change at most once per frame, between draws. Set helper.autoUpdate = false and call helper.update() yourself if you need control (instance mode, tests).

Callbacks are global functions: noteOn(channel, note, velocity), noteOff(...), controlChange(channel, number, value) from every helper; stickPressed() / stickReleased(); padPressed(index) / padReleased(index) / bankChanged(bank) (Midi Fighter); padPressed(x, y) / padReleased(x, y) / buttonPressed(id) / buttonReleased(id) (Launchpad); touchPressed(pad) / touchReleased(pad) / accelChanged(); pitchBend(channel, value) / midiMessage(status, d1, d2) (AnyMidi). The generic ones get the device name as one more argument: function noteOn(channel, note, velocity, device). JavaScript cannot overload padPressed, so a sketch with both grid devices passes { callbacks: {...} } to one of them. Any helper takes { callbacks: { padPressed(i) {...} } } to skip the global lookup (instance mode).

Keyboard stand-ins work while nothing is connected: arrows + SPACE (PipSqueak), 1234 qwer asdf zxcv (Midi Fighter), 1..8 (Circuit Playground touch pads), arrows / T / Enter (Trinkeys).

Per device, the surface is the Java one with JS idioms. Constructors take an options object or a name string: new PipSqueak({ name, x: {...}, y: {...}, button: {...}, deadzone, smoothing }) (same JSON shape as everywhere else), new MidiFighter({ name, channel, mode, map }), new Launchpad({ name }), new CircuitPlayground({ name }), new AnyMidi("name") / new AnyMidi() (every input).

  • stick.x, stick.y, stick.angle (null in the deadzone), stick.magnitude, stick.pressed, stick.justPressed(), stick.justReleased(), stick.recenter(), stick.flash(), stick.send(status, d1, d2). On the constructor: PipSqueak.normalize(rawX, rawY, config), PipSqueak.normalizeAxis, PipSqueak.mergeConfig.
  • mf.pressed(i), mf.justPressed(i), mf.justReleased(i), mf.bank (0..3), mf.mode, mf.led(i, on, bank), mf.leds([...]) or a 16-bit mask, mf.clear(), mf.setMap(json); MidiFighter.noteForIndex(i, mode, bank), MidiFighter.indexForNote(note, mode).
  • pad.pressed(x, y), pad.justPressed(x, y), pad.buttonPressed(id), pad.set(x, y, c) where c is a p5 color, "#ff0080", [r, g, b], a palette index 0..127, or a name ("red", "cyan", ...), pad.setRGB, pad.flash, pad.pulse, pad.button(id, c), pad.clear(), pad.text(str, c, speed, loop), pad.stopText(), pad.close(). The Launchpad asks for SysEx permission (programmer mode, RGB). Refused, it still connects and snaps RGB colours to the nearest palette entry. pagehide restores Live mode. Launchpad.toColor(c), Launchpad.xyToNote and the rest are exposed.
  • cpx.touch(i), cpx.accel.x/y/z, cpx.light, cpx.sound, cpx.temperature, cpx.sensor, cpx.mode, cpx.pixels(r, g, b), cpx.clear().
  • m.status, m.devices(), m.note(n), m.cc(n), m.down(n), m.lastNote, m.lastCC, m.lastDevice, m.pitchBend, m.count, m.last (with .device), m.send, m.noteOn, m.noteOff, m.controlChange, m.programChange, m.sendPitchBend, m.sysex.
  • slide.value, slide.raw, slide.touched, slide.justTouched(), slide.pixel(note); knob.value, knob.raw, knob.delta, knob.pressed, knob.justPressed(), knob.touched, knob.pixel(note).

The p5 ES modules in grid-controllers/ and pipsqueak/ are untouched; these scripts port their protocol logic. Differences: Midi Fighter banks are 0..3 here (1..4 there), setRGB takes 0..255 (0..127 there), and there is no event emitter. Use the global callbacks or poll in draw().

Try first​

  • One CC, one number on screen, huge. Then a colour, then a speed.
  • A piano keyboard as 61 buttons. Each note a column, velocity its height.
  • Two controls as x and y.

More on the ideas page.

Without the helper​

For an unknown device the raw path is HelloMidi: it opens every input and prints every message, which is most of what AnyMidi does. The three bytes, no helpers.