A groovebox built on a Raspberry Pi Pico 2 W running CircuitPython. It has a live looper, a step sequencer, an arpeggiator and a synth with 16 instruments. You play it on a 4x4 grid of light-up mechanical keys and drive it from ten function keys and a 4-character display.
I designed the instrument and built the hardware; the code was written by Claude Code, an AI coding agent, working from plans we developed together. How this was built has the details.
- Looper: 8 layers sharing one loop length. Record free-form or synced to the sequencer (with a count-in and snap to the 1/16 grid), overdub, mute, double the loop length or mirror its first half.
- Step sequencer: 8 drum tracks × 16 steps, running alongside the looper so the two stay in step.
- Instruments: a 16-sound drum kit (house and DnB kicks, hats, snare, clap, toms, percussion, ride, crash) and 15 melodic voices: bass, reese, acid, stab, pluck, hoover, pad, jungle sub, organ, chord stab, marimba, bell, chip lead, supersaw and laser. Each layer gets its own copy of its instrument, so sound edits never leak between layers.
- Sound editing: waveform, ADSR, filter, LFO, ring mod, detune, pitch envelope and glide for the melodic voices; pitch, length, tone and snap for drums.
- Key and scale: every melodic layer plays the same key and scale (pentatonics through Dorian, Phrygian and harmonic minor), so layers always stay in tune. Changing either re-voices loops that are already recorded.
- Arpeggiator: per layer, with up, down, up-down, random and as-played modes, three rates and four gate lengths. What it plays is recorded as ordinary notes.
- Channel view: all 16 channels (8 loop layers, 8 sequencer tracks) on the pads at once, for selecting or live-muting.
- AUTO: an arranger that mutes and unmutes channels on phrase boundaries, with occasional breakdowns and drops.
- 16 save slots for whole grooves, stored as JSON on the Pico's drive.
- Lights: each colour means the same thing everywhere (red recording, yellow your playing, blue content, grey muted, white the playhead).
- Psst: try holding MENU while it starts up.
docs/HARDWARE.md has the parts list, wiring, layout calibration and troubleshooting. In short: a Pico 2 W on a proto board, an Adafruit NeoKey 5x6 snapped into a 4x4 and a 2x5, a UDA1334A I2S DAC and an HT16K33 quad alphanumeric display, all powered from USB, behind a front panel.
- Install CircuitPython 10.x: download the UF2 for the Pico 2 W, hold BOOTSEL while plugging the Pico in, and drag the UF2 onto the drive that appears. It reboots as
CIRCUITPY. - Add the libraries: from the CircuitPython 10.x library bundle, copy
adafruit_ht16k33/,adafruit_bus_device/andneopixel.mpyintoCIRCUITPY/lib/. - Copy the code:
code.py,boot.pyandconfig.py, and thecore/,gbox/,echo/andtools/folders, go in the root ofCIRCUITPY. Don't copytests/ordocs/. - Reset the Pico. A rainbow wave and a scrolling
GROOVEBOXmean it's working. If nothing lights, open the serial console; the startup error is printed there.
Updating the code later: boot.py makes the drive writable by the groovebox (so it can save grooves), which makes it read-only to your computer. To copy new files over, press BOOTSEL during startup to boot into safe mode, where boot.py doesn't run.
If your wiring differs from mine, change the pins and layout tables in config.py. docs/HARDWARE.md explains how to find the right values with tools/io_test.py.
docs/BUTTONS.md is the full manual. A first groove:
- Press LOOP/SEQ for SEQ mode, tap some pads to set kick steps, and press PLAY/STOP.
- Press VIEW, then tap a pad on the bottom two rows to choose another track, and add steps to it.
- Press LOOP/SEQ for LOOP mode, then RECORD. It counts in to the next bar; play some drums, and press RECORD again to finish on a bar line.
- Press VIEW and tap a pad on the top two rows to pick an empty layer (layer 2 is a bass), then record a bassline over it.
- Press MENU, scroll to
AUTwith UP / DOWN, and press MENU to let the groove arrange itself. RECORD closes the menu. UP / DOWN set the selected channel's volume.
The code is in packages, imported through them (from core import clock); only the startup files and config.py sit at the top.
| File | Does |
|---|---|
boot.py |
Makes the drive writable by the code, for groove saves |
code.py |
Startup: picks which program to run |
config.py |
Pins, key and pixel layout tables, tuning and sizes |
gbox/ |
The groovebox |
groovebox.py |
The main loop: scans keys, runs the clock, routes events to the modes, menu and views, drives the lights |
looper.py |
The 8-layer looper: recording, overdub, sync to the sequencer, playback |
sequencer.py |
The 8-track × 16-step sequencer |
arp.py |
The arpeggiator, an input effect in front of the looper |
arranger.py |
AUTO, the mute arranger |
menu.py |
The settings menu: sounds, instruments, ARP, BPM, key and scale, loop length, save and load |
groove.py |
Groove files on the drive (SAVE / LOAD) |
pad_views.py |
What the pads show |
startup.py |
The startup animation |
core/ |
The hardware and the sound, shared |
synth_engine.py |
The synthio voices: triggering, per-channel instruments, sound parameters, volumes |
sound_presets.py |
The drum kit and the melodic instruments |
synth_params.py |
The editable sound parameters, their ranges and display names |
scales.py |
The keys and scales |
palette.py |
The colours |
display.py, pixels.py |
The 4-character display and the key NeoPixels |
hw.py, keymap.py |
Key scanning, and the lookups built from config's layout tables |
clock.py, event_types.py |
The shared timebase and event names |
tools/ |
Diagnostics |
io_test.py |
Hardware test and layout calibration (enabled from code.py) |
timing_probe.py |
Main-loop timing and heap reports (config.TIMING_PROBE) |
CircuitPython can't refill the audio buffer during a garbage collection. If a collection takes longer than the buffer lasts, you hear a click. So the code avoids creating garbage in anything that runs every pass, and keeps long-lived data in flat arrays rather than long lists: a list holding more than about 64 objects can overflow the collector's mark stack and make it re-scan the whole heap, doubling every collection. tests/test_alloc_patterns.py checks for patterns that caused clicks before. Bear this in mind when adding features.
The tests run on a desktop, with no hardware. tests/fakes.py stands in for the CircuitPython modules, and tests/harness.py runs the real gbox/groovebox.py main loop against scripted key presses. They need only the Python standard library (3.11 is what I use). From the repo root:
python -m unittest discover -s tests
For editor autocompletion and type checking against the CircuitPython APIs, install the stubs into a virtual environment:
python -m venv .venv
.venv/Scripts/activate # Windows; on macOS/Linux: source .venv/bin/activate
pip install -r requirements-dev.txt
I designed the instrument and built the hardware. Claude Code, Anthropic's AI coding agent, wrote the code. Each feature started as a written plan that we went back and forth on, covering how it should work, how it should feel to play and where it fits on the keys. Then it was built in stages, and I tested each stage on the device before the next. Plenty of designs changed or were dropped once I'd played them.
A notable problem was audio clicks. They turned out to be garbage collection: CircuitPython can't refill the audio buffer while it collects, so any collection that runs long is audible. Claude Code did most of the investigation with some thought and input from me. It built timing_probe.py to measure main-loop and collection times on the device, and traced the clicks to two causes. First, the main loop was making garbage on every pass: one innocent-looking x in (A, B) test made 128 bytes of it per pass. Second, long lists of recorded notes made every collection about twice as slow. The fixes are the allocation-free main loop and flat arrays described under the one rule.
What made it possible to keep adding features was the test suite, which Claude Code built. tests/harness.py runs the real gbox/groovebox.py main loop on a desktop against fake keys, lights, display and audio, with scripted key presses and deterministic fake time. New features came with test scenarios of their own, and the whole suite (398 tests, about 5 seconds) ran before changes went onto the Pico. That's how the arpeggiator, key and scale, and AUTO went in without breaking the looper and sequencer underneath them. The suite can't hear the audio or measure timing on the real hardware, so testing on the device still had the last word.
- USB power only. The NeoPixel power arrangement relies on USB's 5 V. Battery power would need a rethink.
- The sequencer is one bar long, and its 8 tracks always play the kit's first 8 sounds.
- Each loop layer holds 256 notes, about 16 bars of 16th-note arpeggio.
- Arpeggiator settings and AUTO aren't saved with grooves (the notes an arpeggiator recorded are).
- SAVE and LOAD stop playback. File work while audio plays caused glitches, so it happens in silence.
- Committing a very long arpeggiator take can cause a brief pause in the audio.
- The 7-note scales can clash when two layers land on neighbouring notes. The pentatonics never do.
- Updating the code needs a safe-mode boot, because the drive is read-only to the computer during normal use.
- No MIDI and no Wi-Fi, even though the Pico 2 W has the radio.
- A chord progression lane: a key or chord per bar, so loops follow a progression.
- More arpeggiator: an octave range, a note-repeat mode for drums, and saving its settings with grooves.
- A choice of sound for each sequencer track.
- Longer sequencer patterns, or chaining patterns.
- USB MIDI: clock sync with other gear, and notes in and out.
- Making long arpeggiator takes commit without the pause.
0 comments
log in to comment.