Skip to main content

Slide Trinkey

A USB stick with a slide potentiometer, a capacitive touch pad and two NeoPixels. It plugs straight into a USB-A port. George's CircuitPython firmware makes it a class-compliant MIDI device: no driver, no library on either end. One slider, one touch pad, that's it, which makes it a good first device.

MakerAdafruit, with George's firmware
USB name"Slide Trinkey M0"
Ports1 in, 1 out
SendsCC 1 for the slider, note 60 for the touch pad, channel 1.
ReceivesAny note on. The pixels take that note's colour for a second.
Buyadafruit.com/product/5021
Firmwarefirmware/trinkey-midi, CircuitPython

What it sends​

ControlMessageNumbersNotes
SliderCCCC 1, 0 to 1270 at the left, 127 at the right. Sent when it moves, and once at startup.
Touch padNote on, note offNote 60, velocity 127On when touched, off on release.
USB name"Slide Trinkey M0"The drive, when it mounts, is SLIDEPY.

What it receives​

MessageEffect
Any note onBoth pixels take a colour from the note number for one second. m.noteOn(1, 60, 127) from a sketch.

Everything is on channel 1. The board sends one CC 1 with the slider's position at startup, so a sketch starts in sync. Flashing and the pin names are in the trinkey-midi README.

Start here​

There is no dedicated starter yet. The generic one draws every note and CC it hears, so move the slider and watch CC 1 move. Then use the helper below.

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

This is the generic starter. It draws every note and CC the stick sends. 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 SlideTrinkey(this)Connects to the first device whose name contains "Slide Trinkey". (this, "name") to change it.
slide.value0 to 1, 0 at the left.
slide.rawThe CC value 0 to 127, or -1 before the first message.
slide.touched, slide.justTouched(), slide.justReleased()The touch pad. The "just" ones are true for one frame.
slide.pixel(note)Sends a note on and off to the board. The pixels show that note's colour for a second.
void sliderChanged(float value), void touchPressed(), void touchReleased()Define any in your sketch and the helper calls it.

Stand-ins with nothing plugged in: LEFT and RIGHT move the slider, T touches.

From the helpers' README​

Thin helpers over the CircuitPython firmware in trinkeys/ (its README covers flashing). One file holds both classes.

SlideTrinkey slide = new SlideTrinkey(this); // matches "Slide Trinkey"; (this, "name") to change it
RotaryTrinkey knob = new RotaryTrinkey(this); // matches "Rotary Trinkey"
slide.value; // 0..1, 0 = left, 1 = right
slide.raw; // the CC value 0..127, -1 until the first message (the board sends one at startup)
slide.touched; slide.justTouched(); slide.justReleased();
slide.pixel(note); // Note On + Note Off to the board: pixel colour from the note number, one second
knob.value; knob.raw; // absolute position 0..1 / 0..127
knob.delta; // clicks since the last frame: + clockwise, - counter-clockwise, 0 when still
knob.pressed; knob.justPressed(); knob.justReleased();
knob.touched; knob.justTouched(); knob.justTouchReleased(); knob.pixel(note);

Callbacks: sliderChanged(float value), knobTurned(int delta), knobPressed(), knobReleased(), touchPressed(), touchReleased(). Both boards use the touch names; with both plugged in, read slide.touched / knob.touched. Stand-ins: Slide = LEFT/RIGHT move, T touches; Rotary = LEFT/RIGHT one click per press, ENTER presses, T touches.

Message map (channel 1, from trinkeys/README.md):

BoardSendsListens
Slide Trinkey M0CC 1 slider 0..127 (0 left, 127 right); note 60 touch pad (Note On / Off)any Note On: pixel colour from the note number, one second
Rotary Trinkey M0CC 2 absolute 0..127; CC 3 relative, 1 = one click clockwise, 127 = one click counter-clockwise (two's complement, so 2 = two clicks); note 61 knob press; note 62 touch padsame

delta is the sum of the relative clicks since the previous frame, so a fast spin reads as 2 or 3.

Python: SlideTrinkey(this), RotaryTrinkey(this), slide.value, slide.raw, slide.touched, slide.just_touched(), slide.just_released(), slide.pixel(note), knob.value, knob.raw, knob.delta, knob.pressed, knob.just_pressed(), knob.just_released(), knob.touched, knob.just_touched(), knob.pixel(note); callbacks slider_changed(value), knob_turned(delta), knob_pressed(), knob_released(), touch_pressed(), touch_released(); module-level relative(v). p5: new SlideTrinkey(), new RotaryTrinkey() with the Java names; RotaryTrinkey.relative(v).

Try first​

  • One number on screen, huge. Then a colour. Then a speed.
  • A horizontal position. The slider is a crossfader.
  • Touch to send a note back and watch the pixels answer.

More on the ideas page.

Without the helper​

One CC and one note, so the raw path is HelloMidi: it opens every input and prints every message. Read the three bytes, no helpers first.