Anything else
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.
Find out what it sends
- Plug it in and open the Explorer.
- Press, turn, slide or tilt everything once. Write down the note and CC numbers.
- Note the device name at the top. That is the substring you give the helper.
On the table
| Device | What it is | What it sends |
|---|---|---|
| Slide Trinkey | Adafruit 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 trinkeys/. |
| Rotary Trinkey | A 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 M4 | Adafruit's 4×8 grid of RGB-lit buttons with an accelerometer | Probably notes for the buttons and CCs for tilt. Check the Explorer. It is a grid, so the Midi Fighter ideas transfer. |
| Yours | Anything | Same drill. Explorer first. |
Start here
- Java
- Python Mode
- p5.js
Open starters/java/AnyMidiStarter/AnyMidiStarter.pde. It draws every note and CC it sees.
// AnyMidi starter: works with whatever MIDI device you plug in. Notes pop a circle, knobs and
// sliders (control changes) move bars. No device: letters are notes, drag the mouse for a knob.
AnyMidi m;
float[] pop = new float[128]; // one per note number, shrinks every frame
int[] knob = new int[128]; // last value of each control change number
void setup() {
size(900, 400);
m = new AnyMidi(this); // change this: new AnyMidi(this, "part of the device name")
m.connect(); // prints the MIDI devices it found; with no name it takes the first input
}
void draw() {
background(15);
noStroke();
for (int n = 0; n < 128; n++) {
float x = map(n, 0, 127, 10, width - 10);
fill(90, 200, 255);
rect(x - 2, height - 10, 4, -knob[n] * 2); // change this: bars for CC values 0..127
if (pop[n] > 0) {
fill(255, 120, 80, pop[n] * 2);
circle(x, height / 2, pop[n]); // change this: what a note looks like
pop[n] -= 2;
}
}
}
// The helper calls these whenever a message arrives. Channel is 1..16; note / number / value are 0..127.
void noteOn(int channel, int note, int velocity) { pop[note] = 40 + velocity; } // change this
void controlChange(int channel, int number, int value) { knob[number] = value; } // change this
// Stand-ins so the sketch does something without a device.
void keyPressed() { if (key != CODED) noteOn(1, 48 + key % 36, 100); }
void mouseDragged() { controlChange(1, 1 + mouseY * 8 / height, int(map(mouseX, 0, width, 0, 127))); }
Open starters/python/anymidi_starter/anymidi_starter.pyde.
# AnyMidi starter: works with whatever MIDI device you plug in. Notes pop a circle, knobs and
# sliders (control changes) move bars. No device: letters are notes, drag the mouse for a knob.
from __future__ import division, print_function
from anymidi import AnyMidi
m = None
pop = [0] * 128 # one per note number, shrinks every frame
knob = [0] * 128 # last value of each control change number
def setup():
global m
size(900, 400)
m = AnyMidi(this) # change this: AnyMidi(this, "part of the device name")
m.connect() # prints the MIDI devices it found; with no name it takes the first input
def draw():
m.update() # once per frame, so the callbacks below fire
background(15)
noStroke()
for n in range(128):
x = 10 + n * (width - 20) / 127
fill(90, 200, 255)
rect(x - 2, height - 10, 4, -knob[n] * 2) # change this: bars for CC values 0..127
if pop[n] > 0:
fill(255, 120, 80, pop[n] * 2)
circle(x, height / 2, pop[n]) # change this: what a note looks like
pop[n] -= 2
# The helper calls these whenever a message arrives. Channel is 1..16; note / number / value are 0..127.
def note_on(channel, note, velocity): pop[note] = 40 + velocity # change this
def control_change(channel, number, value): knob[number] = value # change this
# Stand-ins so the sketch does something without a device.
def keyPressed():
if key != CODED: note_on(1, 48 + ord(key) % 36, 100)
def mouseDragged(): control_change(1, 1 + mouseY * 8 // height, mouseX * 127 // width)
def stop(): m.close() # closes the MIDI port when the sketch closes
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.
// AnyMidi starter: works with whatever MIDI device you plug in. Notes pop a circle, knobs and sliders
// (control changes) move bars. Click once to connect MIDI. No device: letters are notes, drag the mouse for a knob.
let m;
const pop = new Array(128).fill(0); // one per note number, shrinks every frame
const knob = new Array(128).fill(0); // last value of each control change number
function setup() {
createCanvas(900, 400);
m = new AnyMidi(); // change this: new AnyMidi({ name: "part of the device name" })
m.connectOnClick(); // connects on the first click and shows a hint until then
}
function draw() {
background(15);
noStroke();
for (let n = 0; n < 128; n++) {
const x = map(n, 0, 127, 10, width - 10);
fill(90, 200, 255);
rect(x - 2, height - 10, 4, -knob[n] * 2); // change this: bars for CC values 0..127
if (pop[n] > 0) {
fill(255, 120, 80, pop[n] * 2);
circle(x, height / 2, pop[n]); // change this: what a note looks like
pop[n] -= 2;
}
}
}
// The helper calls these whenever a message arrives. Channel is 1..16; note / number / value are 0..127.
function noteOn(channel, note, velocity) { pop[note] = 40 + velocity; } // change this
function controlChange(channel, number, value) { knob[number] = value; } // change this
// Stand-ins so the sketch does something without a 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.last | The 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 and whatever people bring.
AnyMidi m = new AnyMidi(this, "name substring"); // or new AnyMidi(this): first input (and first output)
m.note(n); m.cc(n); // last value seen, 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.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);
Callbacks: the three generic ones, plus pitchBend(int channel, int value) (-8192..8191) and
midiMessage(int status, int data1, int data2) for anything else. m.last is a MidiMsg with
status, type, channel, data1, data2, millis, sysex and a readable toString(). The Explorer prints it.
Python: AnyMidi(this, name=""), m.note(n), m.cc(n), m.down(n), m.last_note, m.last_cc,
m.pitch_bend, m.count, m.last, m.send, m.note_on, m.note_off, m.control_change,
m.send_pitch_bend, m.sysex.
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://pcd2026.mand.is/midi-helpers/p5/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). 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().
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)wherecis 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.pagehiderestores Live mode.Launchpad.toColor(c),Launchpad.xyToNoteand 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.note(n),m.cc(n),m.down(n),m.lastNote,m.lastCC,m.pitchBend,m.count,m.last,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.