Skip to main content

Circuit Playground

Adafruit's round board, with an accelerometer, light, sound and temperature sensors, eight touch pads, ten NeoPixels, two buttons and a slide switch. George's firmware turns it into a MIDI controller with one sensor on duty at a time, depending on the mode. The firmware is from a 2019 Web MIDI workshop and hasn't changed since.

Source of truth

The firmware is public: github.com/georgemandis/circuit-playground-midi-multi-tool. This page comes from reading that .ino. Where they disagree, the code wins.

MakerAdafruit, with George's firmware
USB namedepends on how it was flashed. "Circuit Playground Express" on the ones on the table. The Explorer prints it.
Ports1 in, 1 out
SendsChannel 2. Notes for touch, random and accelerometer modes, CC 1 for light, sound and temperature.
ReceivesNotes. Mode 8 plays them on the speaker, mode 10 mixes them into a pixel colour.
Buyadafruit.com/product/3333
Firmwaregithub.com/georgemandis/circuit-playground-midi-multi-tool

Pick a mode first​

To change or view the current mode your Circuit Playground is in, toggle the slide switch so that one or more NeoPixel LEDs light up and stay on:

Circuit Playground slide switch

With the slide switch toggled you can change modes by pressing the left and right buttons on the board. The number of lit pixels is the mode number, one pixel for mode 1 up to all ten for mode 10, and a pitch plays on each press:

Circuit Playground buttons

Toggle the switch back and the mode starts.

It's one mode at a time, so a sketch that wants both light and touch needs two boards. The buttons and the switch only pick the mode. They never send MIDI, so if the Explorer shows nothing when you press a button, that's expected.

The ten modes​

Everything the board sends is on channel 2: status 0x91 for notes, 0xB1 for control changes.

Modes that send MIDI​

ModeWhat it does
1Cap touch. Each of the eight pads is a note: note on with velocity 127 when touched, note off on release. Note number is 1 + the pad's pin (table below). The pixels flash a random colour on each touch, and the buttons nudge the touch sensitivity.
2Light. CC 1 with the light level scaled to 0..127, once a second.
3Sound. CC 1 with the sound level scaled to 0..127, once a second.
4Temperature. CC 1 with degrees Celsius as the value, once a second.
5Random notes. A note on with a random note and random velocity, every 50 ms or so. The buttons speed it up and slow it down.
6Accelerometer. Three note ons in a row every 200 ms, so five times a second: x, y, then z, each as note = m/s² + 20, velocity 127. 20 is zero g, 30 is about one g. Flat on a table reads roughly 20, 20, 30.
7Tap. A tap on the board flashes all ten pixels a random colour and prints to the serial port. No MIDI. The firmware README says it sends a message; the code that would is commented out. See below.

Modes that receive MIDI​

ModeWhat it does
8Speaker. Plays any incoming note on over the built-in speaker, at the note's pitch, for 50 ms.
10Colour mixer. A note on on channel 1 sets red, channel 2 sets green, channel 3 sets blue, each to note + velocity (0 to 254), on all ten pixels at once.

Neither​

ModeWhat it does
9Random tones on the speaker and random pixel flashes. No MIDI in or out. For funsies.

No mode does both.

Touch pads, helper index to firmware pin to note:

touch(i)01234567
pin3201126910
note43121371011

About mode 7​

The firmware README says mode 7 "converts the accelerometer tap detection to a MIDI message". The helpers' README says it sends nothing. The code: setup() arms the tap detector and attaches an interrupt. In mode 7 the interrupt prints the tap to serial, sets a flag and flashes all ten pixels. The part of mode 7 that would send noteOn(2, 1, 127) sits under "Accelerometer detect tap: TBD" and is commented out. So the README overstates it. A tap is felt and shown on the board and no MIDI leaves it. A hardware check is still pending. For a buzzer or a tap, use a touch pad in mode 1.

Start here​

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

Code: starters/p5/circuit-playground/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
// Circuit Playground starter: the eight touch pads light a ring, tilt rolls a ball, the light sensor sets
// the sky. The firmware sends ONE sensor at a time: flip the slide switch and use the two buttons to pick a
// mode (see HARDWARE-CHECKLIST.md). Click once to connect MIDI. No board: keys 1-8 are the pads, the mouse tilts.
let cpx;

function setup() {
createCanvas(800, 600);
cpx = new CircuitPlayground(); // change this: new CircuitPlayground({ name: "..." }) if it isn't found
cpx.connectOnClick(); // connects on the first click and shows a hint until then
}

function draw() {
let tiltX = cpx.accel.x, tiltY = cpx.accel.y; // -1..1, one g = 1 (accelerometer mode)
if (!cpx.connected()) { // mouse stand-in
tiltX = mouseX * 2 / width - 1;
tiltY = mouseY * 2 / height - 1;
}
background(lerpColor(color(10, 10, 40), color(255, 230, 120), cpx.light)); // change this: light is 0..1
translate(width / 2, height / 2);
for (let i = 0; i < 8; i++) { // the eight capacitive pads, as a ring
const a = TWO_PI * i / 8;
fill(cpx.touch(i) ? color(255, 80, 120) : color(70, 70, 90)); // change this: a colour per pad?
circle(cos(a) * 220, sin(a) * 220, cpx.touch(i) ? 90 : 50);
}
fill(255);
circle(tiltX * 200, tiltY * 200, 60 + cpx.sound * 100); // change this: the ball; sound is 0..1
}

The helper​

new CircuitPlayground(this)Connects by USB-name substring. The board's USB name depends on how it was flashed. The Explorer prints it.
cpx.touch(i), cpx.touchJustPressed(i), cpx.anyTouch()The eight pads, index 0 to 7 as in the table above. Mode 1.
cpx.light, cpx.sound (0 to 1), cpx.temperature (°C), cpx.sensor (raw CC 1)All three follow the same CC 1. Which one means anything depends on the mode the board is in.
cpx.accel.x, .y, .z-1 to 1, one g is 1. Mode 6.
cpx.mode"touch", "accel", "sensor", "notes" or empty: the kind of traffic last recognised.
cpx.pixels(r, g, b), cpx.clear()All ten pixels one colour, for a board in mode 10.
void touchPressed(int pad), void touchReleased(int pad), void accelChanged()Define any in your sketch and the helper calls it. The generic noteOn and controlChange work too.

From the helpers' README​

This differs from the original API contract. The firmware at github.com/georgemandis/circuit-playground-midi-multi-tool (main, 2026-10-02) sends one sensor at a time, chosen by its mode, and never sends the buttons or the slide switch (they pick the mode). So there is no buttonA, buttonB or slideSwitch, and light, sound and temperature all come from the same CC 1. Only the mode the board is in tells them apart. The helper exposes what arrives, plus mode, the kind of traffic it last recognised.

CircuitPlayground cpx = new CircuitPlayground(this); // or (this, "name")
cpx.touch(i); cpx.touchJustPressed(i); cpx.touchJustReleased(i); cpx.anyTouch(); // i = 0..7 (table below)
cpx.accel.x; cpx.accel.y; cpx.accel.z; // -1..1, one g = 1, clamped. accel.rawX etc. are the notes
cpx.light; cpx.sound; // 0..1 (CC 1 / 127; both follow CC 1)
cpx.temperature; // whole degrees C (the raw CC 1 value)
cpx.sensor; // raw CC 1 value 0..127, -1 if never
cpx.mode; // "touch", "accel", "sensor", "notes" or "" (nothing yet)
cpx.pixels(r, g, b); cpx.pixel(i, r, g, b); cpx.clear(); // all ten NeoPixels, one colour (mode 10)
cpx.padForPin(pin); cpx.padForNote(note); cpx.noteForPad(i); cpx.accelFromNote(note);

Callbacks: touchPressed(int pad), touchReleased(int pad), accelChanged(). Stand-in: keys 1..8 are the touch pads.

What the firmware sends. All output is on status 0x91, channel 2 (the sketch calls noteOn(1, ...) with a 0-based channel). Mode numbers are the firmware README's. The NeoPixel ring shows mode − 1 pixels while the slide switch is on; left and right buttons change it.

ModeSendsHelper fields
1 cap touchNote On vel 127 on touch, Note Off on release; note = 1 + pintouch(i), callbacks
2 lightCC 1 = light / 1023 · 127, once a secondlight, sensor
3 soundCC 1 = sound / 1023 · 127, once a secondsound, sensor
4 temperatureCC 1 = °C as an integer, once a secondtemperature, sensor
5 random notesNote On, random note and velocity, every ~50 ms (buttons change the rate)mode == "notes"; use noteOn() or AnyMidi
6 accelerometerthree Note Ons in a burst every 200 ms: X, Y, Z as round(m/s² + 20), vel 127accel
7 tapnothing over MIDI (pixels flash locally)-
8 speakerplays any incoming Note Onpixels() will also beep it
9 random tonesnothing over MIDI-
10 colour mixerlistens: Note On on channel 1 / 2 / 3 sets red / green / blue = note + velocity (0..254), all pixelspixels(), clear()

Touch pads, index → firmware pin → note:

touch(i)01234567
pin3201126910
note43121371011

Touch and accelerometer are both Note Ons on the same channel. Three Note Ons within 60 ms count as one accelerometer reading (touch never does that); any Note Off means touch. In accelerometer mode a note that collides with a pad note (board on its side gives about note 10) is not reported as a touch. pixel(i, ...) sets all ten pixels because the firmware has no per-pixel message.

Python: CircuitPlayground(this, name="circuit playground"), cpx.touch(i), cpx.touch_just_pressed(i), cpx.accel.x, cpx.light, cpx.sound, cpx.temperature, cpx.sensor, cpx.mode, cpx.pixels(r, g, b), cpx.clear(); callbacks touch_pressed(pad), touch_released(pad), accel_changed().

Try first​

  • Mode 2: a sketch whose background follows the room light. Cover the sensor with your hand.
  • Mode 3: clap to spawn a shape. The value is loudness.
  • Mode 6: a bouncy ball that rolls the way you tilt the board.
  • Mode 1: eight touch pads, eight notes. Wire a fruit to one with a clip lead and play it.
  • Mode 10: send notes at it from the Explorer's send panel and mix a colour on the pixels.

More on the ideas page.

Projects for this board​

The projects from the 2019 workshop, which used these boards with Web MIDI. They are being rebuilt in ports/ and land here as they finish.

Colour mixer, mode 10​

In mode 10 a sketch mixes colours on the onboard NeoPixels by sending note on messages to channels 1, 2 and 3 for red, green and blue. The device listens for status bytes 144, 145 and 146, note ons for MIDI channels 1 to 3.

Because we're limited to two 7-bit bytes instead of a single 8-bit value there's a curious "bug" with this approach. Can you identify it? Can you fix it?

sketch.js
// Colour Mixer (George's 2019 workshop project, firmware mode 10), p5.js. Three sliders mix a colour on screen and
// the board's ten NeoPixels follow in real time. Uses midi-helpers.js to connect; the colour messages are sent raw
// below so you can see the bytes. Click once to connect MIDI.
//
// Mode 10: a Note On on channel 1 sets red, channel 2 green, channel 3 blue, and the value is note + velocity.
// From 2019: "Because we're limited to two 7-bit messages instead of a single 8-bit message there's a curious 'bug'
// with this approach... Can you identify it? Can you fix it?" A MIDI data byte is 0..127, so one byte only reaches
// half brightness. The fix splits the 0..255 value across note and velocity: 200 = 127 + 73. Even fixed, 127 + 127 =
// 254: pure white is one step out of reach, the other half of the bug. B flips bug / fix, W is white.
// Mouse: drag the sliders. Keys: B bug/fix, W white, K black. Works without a board (nothing to light).
let cpx, rgb = [200, 80, 40], fixed = true, dragging = -1;
const NAMES = ["red", "green", "blue"];

function setup() {
createCanvas(600, 420);
textSize(16);
cpx = new CircuitPlayground();
cpx.connectOnClick();
}

function send() { // one Note On per colour: channel 1 red, 2 green, 3 blue
for (let i = 0; i < 3; i++) {
const v = rgb[i], note = min(127, v);
if (fixed) cpx.core.noteOn(i + 1, note, v - note); // split: note + velocity = v (max 254)
else cpx.core.noteOn(i + 1, note, 0); // naive: one 7-bit byte, tops out at 127
}
}
const boardValue = (v) => (fixed ? min(254, v) : min(127, v)); // what the board actually shows

function draw() {
background(30);
noStroke();
fill(rgb[0], rgb[1], rgb[2]); rect(20, 20, 270, 100, 12);
fill(boardValue(rgb[0]), boardValue(rgb[1]), boardValue(rgb[2])); rect(310, 20, 270, 100, 12);
fill(220);
text(`on screen: rgb(${rgb.join(", ")})`, 20, 140);
text(`on the board: rgb(${rgb.map(boardValue).join(", ")}) ${fixed ? "fix: note + velocity" : "BUG: one 7-bit byte"}`, 310, 140);
for (let i = 0; i < 3; i++) {
const y = 190 + i * 70, note = min(127, rgb[i]), vel = fixed ? rgb[i] - note : 0;
fill(60); rect(60, y - 4, 510, 8, 4);
fill([color(255, 80, 80), color(80, 255, 80), color(80, 120, 255)][i]);
circle(60 + rgb[i] * 2, y, 28);
fill(220);
text(`${NAMES[i]} ${rgb[i]}`, 60, y - 16);
text(`[${0x90 + i}, ${note}, ${vel}]`, 420, y - 16); // the bytes that just went out
}
text("drag sliders B bug/fix W white K black" + (cpx.connected() ? "" : " (no board connected)"), 20, height - 40);
}

function mousePressed() { for (let i = 0; i < 3; i++) if (abs(mouseY - (190 + i * 70)) < 20) dragging = i; mouseDragged(); }
function mouseDragged() { if (dragging >= 0) { rgb[dragging] = constrain(floor((mouseX - 60) / 2), 0, 255); send(); } }
function mouseReleased() { dragging = -1; }
function keyPressed() {
if (key === "b") fixed = !fixed;
else if (key === "w") rgb = [255, 255, 255];
else if (key === "k") rgb = [0, 0, 0];
send();
}

Simple synthesizer, mode 8​

The sketch is the keyboard, the board's speaker is the voice. Every note on the sketch sends, the board plays.

sketch.js
// Simple Synth (2019 workshop project, firmware mode 8), p5.js. An on-screen keyboard that plays the board's speaker:
// in mode 8 the board plays any Note On it receives for 50 ms. Uses midi-helpers.js to connect; click once first.
// Click the keys, or type: a s d f g h j k l are the white keys from C, w e t y u the black ones. Z / X shift
// octaves. Each beep is only 50 ms, so a held key is re-sent every 60 ms to sound continuous.
// Without a board the keys still light up; there is just nothing to hear.
const WHITE = "asdfghjkl", BLACK = "wetyu";
const WHITE_OFF = [0, 2, 4, 5, 7, 9, 11, 12, 14]; // semitones above C for the nine white keys
const BLACK_OFF = [1, 3, 6, 8, 10]; // and the five black keys
const BLACK_POS = [0, 1, 3, 4, 5]; // which white key each black key sits after
let cpx, base = 60, held = -1, lastSent = 0; // base: middle C

function setup() {
createCanvas(540, 300);
textSize(14);
cpx = new CircuitPlayground();
cpx.connectOnClick();
}

function play(note) { held = note; lastSent = 0; }

function draw() {
if (held >= 0 && millis() - lastSent >= 60) { cpx.core.noteOn(1, held, 100); lastSent = millis(); } // re-trigger the 50 ms beep
background(30);
for (let i = 0; i < 9; i++) { // white keys
fill(held === base + WHITE_OFF[i] ? color(255, 200, 60) : 240);
stroke(30); rect(i * 60, 60, 60, 220);
fill(80); text(WHITE[i], i * 60 + 25, 265);
}
for (let i = 0; i < 5; i++) { // black keys
fill(held === base + BLACK_OFF[i] ? color(255, 200, 60) : 20);
rect(BLACK_POS[i] * 60 + 40, 60, 40, 130);
fill(200); text(BLACK[i], BLACK_POS[i] * 60 + 54, 180);
}
noStroke(); fill(220);
text(`octave ${base / 12 - 1} Z / X octave down / up ${cpx.connected() ? "playing on the board" : "no board: keys light but stay silent"}${held >= 0 ? " note " + held : ""}`, 10, 30);
}

function noteAt(x, y) { // which key is under the mouse
for (let i = 0; i < 5; i++) if (y < 190 && x >= BLACK_POS[i] * 60 + 40 && x < BLACK_POS[i] * 60 + 80) return base + BLACK_OFF[i];
return y >= 60 ? base + WHITE_OFF[constrain(floor(x / 60), 0, 8)] : -1;
}
function mousePressed() { play(noteAt(mouseX, mouseY)); }
function mouseDragged() { held = noteAt(mouseX, mouseY); }
function mouseReleased() { held = -1; }

function keyPressed() {
const w = WHITE.indexOf(key), b = BLACK.indexOf(key);
if (w >= 0) play(base + WHITE_OFF[w]);
else if (b >= 0) play(base + BLACK_OFF[b]);
else if (key === "z") base = max(24, base - 12);
else if (key === "x") base = min(96, base + 12);
}
function keyReleased() { held = -1; }

Quiz buzzer, mode 1​

Boards as buzzers: who buzzed first? In 2019 this used mode 7, the tap. That sends nothing over MIDI, so the buzzer is a touch pad in mode 1. Slap the pad.

sketch.js
// Quiz Buzzer (2019 workshop project), p5.js. Several Circuit Playgrounds, one per team; the first to send a message
// wins, the others are locked out until R resets. Uses firmware mode 1 (touch a pad to buzz): the 2019 notes say
// mode 7 "tap", but in the firmware that send is commented out ("Accelerometer detect tap: TBD"), so mode 7 sends
// nothing. Any note on channel 2 counts, so the pads do not need to be wired to anything.
// No helper here: the helper opens one device and this needs every board, so it asks Web MIDI for every input whose
// name contains "circuit playground" and numbers them as teams. Click once to connect. No boards: keys 1-4 buzz.
const COLORS = ["#ff5d73", "#ffd166", "#06d6a0", "#4cc9f0", "#c77dff", "#ff9f1c"];
const boards = [];
let winner = -1, wonAt = 0, asked = false;

function setup() { createCanvas(800, 500); textAlign(CENTER, CENTER); }

function mousePressed() {
if (asked) return;
asked = true;
navigator.requestMIDIAccess().then((access) => {
for (const input of access.inputs.values()) {
console.log("input:", input.name);
if (!input.name.toLowerCase().includes("circuit playground")) continue; // change this for other devices
const team = boards.length; // this board's team number
input.onmidimessage = (e) => { if (e.data.length === 3 && (e.data[0] & 0xf0) === 0x90 && e.data[2] > 0) buzz(team); }; // any Note On
boards.push(input.name);
}
console.log(boards.length + " board(s) connected");
}).catch((e) => console.log("no Web MIDI here (Chrome, Edge or Opera needed):", e.message));
}

function buzz(team) { // first one in wins
if (winner >= 0) return;
winner = team;
wonAt = millis();
}

function draw() {
const n = max(boards.length, 4);
background(winner < 0 ? color(20, 24, 32) : color(COLORS[winner % COLORS.length]));
for (let i = 0; i < n; i++) { // one box per team
const w = (width - 40) / n, x = 20 + i * w, c = color(winner === i ? 255 : COLORS[i % COLORS.length]);
c.setAlpha(winner < 0 || winner === i ? 255 : 90);
fill(c);
rect(x + 8, 320, w - 16, 140, 16);
fill(winner === i ? 0 : 255); textSize(28);
text("team " + (i + 1), x + w / 2, 390);
textSize(12); text(i < boards.length ? "board " + (i + 1) : "key " + (i + 1), x + w / 2, 430);
}
fill(255); textSize(winner < 0 ? 48 : 96);
text(winner < 0 ? (asked ? "ready... buzz in!" : "click to connect MIDI") : `team ${winner + 1}!`, width / 2, 160);
textSize(16);
text(winner < 0 ? `${boards.length} board(s) connected keys 1-4 buzz too` : `locked out R to reset (${((millis() - wonAt) / 1000).toFixed(1)} s)`, width / 2, 260);
}

function keyPressed() {
if (key === "r") winner = -1;
if (key >= "1" && key <= "4") buzz(+key - 1);
}

Morse code translator, mode 8 or mode 1​

Two directions. Type a phrase and the board beeps it as Morse in mode 8. Or tap it on a touch pad in mode 1 and the sketch decodes the dots and dashes. A Morse table: this gist.

sketch.js
// Morse Code (2019 workshop project, firmware mode 8), p5.js. Type a phrase and press Enter: the board beeps it in
// Morse through its speaker while the screen shows the phrase, the code and a blinking indicator (small white dot,
// long green dash) like the 2019 page. In mode 8 the board plays any Note On for 50 ms, so a dot is one Note On and a
// dash is Note Ons every 50 ms for three units. Uses midi-helpers.js; click once to connect.
// Keys: letters, digits and space type the phrase; Enter sends; Backspace edits. Without a board it blinks silently.
const ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
const CODE = [".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.",
"...", "-", "..-", "...-", ".--", "-..-", "-.--", "--..", "-----", ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----."];
const UNIT = 70; // ms per Morse unit: dot 1, dash 3, gap 1, letter gap 3, word gap 7
let cpx, typed = "sos", phrase = "", morse = "", steps = [], startAt = -1, lastBeep = 0; // steps: { at, len, kind } kind 0 gap, 1 dot, 2 dash

function setup() {
createCanvas(800, 400);
textAlign(CENTER, CENTER);
cpx = new CircuitPlayground();
cpx.connectOnClick();
}

function start() { // translate the phrase into timed steps
phrase = typed.toLowerCase().trim(); morse = ""; steps = [];
let t = 0;
for (const c of phrase) {
if (c === " ") { t += 7; morse += " "; continue; }
const k = ALPHABET.indexOf(c);
if (k < 0) continue;
for (const s of CODE[k]) { steps.push({ at: t * UNIT, len: s === "." ? 1 : 3, kind: s === "." ? 1 : 2 }); t += (s === "." ? 1 : 3) + 1; }
morse += CODE[k] + " ";
t += 2;
}
steps.push({ at: t * UNIT, len: 1, kind: 0 }); // a final silent step so the display knows it ended
startAt = millis();
}

function draw() {
let kind = 0;
if (startAt >= 0) {
const now = millis() - startAt;
for (const s of steps) if (now >= s.at && now < s.at + s.len * UNIT) kind = s.kind;
if (kind > 0 && millis() - lastBeep >= 50) { cpx.core.noteOn(1, 72, 100); lastBeep = millis(); } // keep the 50 ms beep going
if (now > steps[steps.length - 1].at + UNIT) startAt = -1;
}
background(20, 24, 32);
fill(kind === 1 ? color(255) : kind === 2 ? color(60, 255, 120) : color(70, 30, 70)); // the indicator
circle(width / 2, 110, kind === 1 ? 40 : kind === 2 ? 130 : 70);
fill(240); textSize(28);
text(startAt >= 0 ? phrase : typed + "_", width / 2, 230);
fill(150); textSize(22);
text(startAt >= 0 ? morse : "type a phrase, press Enter", width / 2, 280);
textSize(14);
text((cpx.connected() ? "beeping on the board (mode 8)" : "no board: silent blink") + ` unit ${UNIT} ms`, width / 2, height - 44);
}

function keyPressed() {
if (keyCode === ENTER || keyCode === RETURN) start();
else if (keyCode === BACKSPACE) typed = typed.slice(0, -1);
else if (key.length === 1 && (ALPHABET.includes(key.toLowerCase()) || key === " ")) typed += key;
return false; // keep space and backspace from scrolling the page
}

Fruit piano, mode 1​

Mode 1, alligator clips and fruit (or anything conductive). Each pad is a note. The sketch plays it or draws it.

sketch.js
// Fruit Piano (2019 workshop project, firmware mode 1) and its Tactile Flashcards variant, p5.js. Alligator clips run
// from the board's eight touch pads to fruit; touching a fruit plays a tone (Web Audio) and splashes a colour.
// F makes every pad a word instead (flashcards), L cycles the language. Uses midi-helpers.js; click once to connect
// (the same click unlocks the browser's audio). No board: keys 1-8 are the pads.
const NOTES = [60, 62, 64, 65, 67, 69, 71, 72]; // C major scale, one note per pad
const WORDS = [["Apple", "Pomme", "Manzana"], ["Lime", "Citron vert", "Lima"], ["Lemon", "Citron", "Limón"], ["Orange", "Orange", "Naranja"],
["Pear", "Poire", "Pera"], ["Banana", "Banane", "Plátano"], ["Grape", "Raisin", "Uva"], ["Tomato", "Tomate", "Tomate"]];
const LANGS = ["English", "Français", "Español"];
const splash = new Array(8).fill(0), voices = {};
let cpx, audio = null, lang = 0, lastPad = -1, flashcards = false;

function setup() {
createCanvas(800, 500);
colorMode(HSB, 360, 100, 100, 100);
textAlign(CENTER, CENTER);
cpx = new CircuitPlayground();
cpx.connectOnClick();
}

function touchPressed(pad) { // the helper calls these (keys 1-8 too, with no board)
splash[pad] = 1;
lastPad = pad;
tone(NOTES[pad], true);
}
function touchReleased(pad) { tone(NOTES[pad], false); }

function tone(note, on) { // one oscillator per note, started and stopped like a key
if (!audio) return;
if (on && !voices[note]) {
const o = audio.createOscillator(), g = audio.createGain();
o.frequency.value = 440 * Math.pow(2, (note - 69) / 12); o.type = "triangle";
g.gain.value = 0.2; o.connect(g).connect(audio.destination); o.start();
voices[note] = { o, g };
} else if (!on && voices[note]) {
const { o, g } = voices[note]; delete voices[note];
g.gain.setTargetAtTime(0, audio.currentTime, 0.05); o.stop(audio.currentTime + 0.3);
}
}

function draw() {
background(0, 0, 12);
for (let i = 0; i < 8; i++) { // one circle per pad, pads 0..7 going round the board
const x = 100 + i * 85, hue = i * 45, on = cpx.touch(i);
if (splash[i] > 0) { fill(hue, 70, 100, splash[i] * 60); circle(x, 250, 80 + (1 - splash[i]) * 300); splash[i] -= 0.02; }
fill(hue, 80, on ? 100 : 45);
circle(x, 250, on ? 110 : 70);
fill(0, 0, 100); textSize(16);
text(flashcards ? WORDS[i][lang] : "pad " + (i + 1), x, 250);
}
if (flashcards && lastPad >= 0) { textSize(72); fill(lastPad * 45, 70, 100); text(WORDS[lastPad][lang], width / 2, 90); }
fill(0, 0, 70); textSize(14);
text((flashcards ? `flashcards: ${LANGS[lang]} L next language` : "fruit piano") + " F toggle " + (cpx.connected() ? "touch the fruit" : "no board: keys 1-8"), width / 2, height - 44);
}

function mousePressed() { if (!audio) audio = new (window.AudioContext || window.webkitAudioContext)(); if (audio.state === "suspended") audio.resume(); }
function keyPressed() {
mousePressed(); // a key press is a gesture too: unlock audio
if (key === "f") flashcards = !flashcards;
if (key === "l") lang = (lang + 1) % LANGS.length;
}

Thermometer, mode 4​

Mode 4 sends degrees Celsius once a second. A very local weather app. An hour of readings is a graph.

sketch.js
// Thermometer (2019 workshop project, firmware mode 4), p5.js. The board sends whole degrees Celsius once a second
// as CC 1 on channel 2; the helper exposes it as cpx.temperature. A big live reading and a 60-second history graph.
// Uses midi-helpers.js; click once to connect. Keys: F toggles Celsius / Fahrenheit. No board: up / down arrows
// move a fake temperature so the graph works.
const history = new Array(60).fill(0); // one reading per second, newest last
let cpx, samples = 0, lastSample = 0, fake = 22, fahrenheit = false;

function setup() {
createCanvas(800, 500);
textAlign(CENTER, CENTER);
cpx = new CircuitPlayground();
cpx.connectOnClick();
}

const reading = () => (cpx.connected() ? cpx.temperature : fake);
const shown = (c) => (fahrenheit ? c * 9 / 5 + 32 : c);
const waiting = () => cpx.connected() && cpx.sensor < 0; // connected, but no CC 1 yet (wrong mode?)

function draw() {
if (millis() - lastSample >= 1000 && !waiting()) { // once a second, push a sample
lastSample = millis();
history.shift(); history.push(reading());
samples = min(60, samples + 1);
}
background(20, 24, 32);
fill(240); textSize(120);
text(waiting() ? "..." : shown(reading()).toFixed(1) + (fahrenheit ? " °F" : " °C"), width / 2, 120);
textSize(14); fill(150);
text(waiting() ? "board connected, waiting for a reading: is it in mode 4?" : cpx.connected() ? "live from the board, one reading a second" : "no board: up / down arrows fake a temperature", width / 2, 210);
const recent = history.slice(60 - samples); // graph of the last minute, auto-scaled
let lo = recent.length ? min(recent) : 0, hi = recent.length ? max(recent) : 1;
if (hi - lo < 2) { lo -= 1; hi += 1; }
stroke(60); line(60, 260, 60, 460); line(60, 460, 760, 460);
noFill(); stroke(255, 160, 60); strokeWeight(3);
beginShape();
for (let i = 60 - samples; i < 60; i++) vertex(map(i, 0, 59, 60, 760), map(history[i], lo, hi, 450, 270));
endShape();
strokeWeight(1); noStroke(); fill(150); textAlign(RIGHT, CENTER);
if (recent.length) { text(shown(hi).toFixed(1), 55, 270); text(shown(lo).toFixed(1), 55, 450); }
textAlign(CENTER, CENTER);
text("last 60 seconds F: " + (fahrenheit ? "show Celsius" : "show Fahrenheit"), width / 2, 480);
}

function keyPressed() {
if (key === "f") fahrenheit = !fahrenheit;
if (keyCode === UP_ARROW) fake += 0.5;
if (keyCode === DOWN_ARROW) fake -= 0.5;
}

Tactile flashcards, mode 1​

Same wiring as the fruit piano, different payoff. Instead of a note, touching an object shows you its name, and a key cycles the language. It lives inside the fruit piano sketch above: run it, press F, and every pad becomes a word. L switches language. The word list is at the top of the sketch, so swap in whatever is on the table.

Without the helper​

Channel 2 notes and CC 1 decoded inline, including the accelerometer burst. Under sixty lines, same stand-ins. Read the three bytes, no helpers first.