Build a Custom MIDI Controller: Arcade Buttons, Potentiometers, and Raspberry Pi Pico as a USB MIDI Device
A custom MIDI controller is a genuinely practical build for anyone doing music production, live performance, or just wanting physical knobs and buttons instead of clicking a mouse in a DAW — and it's a great showcase project for combining this site's electronics, laser cutting, and 3D printing content, since the enclosure and control layout are as much a part of the build as the wiring. This project uses a Raspberry Pi Pico running CircuitPython to present itself as a class-compliant USB MIDI device — meaning it just works when plugged into a computer, with no drivers and no vendor software required — reading a grid of arcade buttons and a bank of potentiometers and translating them into MIDI note and control-change messages any DAW or MIDI-compatible software understands.
Why the Pico for This
The Raspberry Pi Pico (and Pico W, if you want to add wireless features later) is an unusually good fit for MIDI controller projects: it's cheap, has plenty of GPIO for a real button and potentiometer grid, its RP2040 chip has enough ADC channels for multiple analog controls without external multiplexing on a modest build, and CircuitPython's built-in usb_midi and adafruit_midi libraries handle the USB MIDI class-compliance and message formatting for you — you're not implementing the USB MIDI spec from scratch, just reading pins and calling library functions to send notes and control changes.
ComponentRole Raspberry Pi PicoReads all inputs, runs CircuitPython, presents as a USB MIDI device to the host computer Arcade-style momentary pushbuttonsTrigger MIDI notes — velocity-sensitive if you add a way to read press dynamics, simple on/off otherwise Linear potentiometers or rotary potsContinuous controllers (CC) — filter cutoff, volume, effect sends, whatever your software maps them to 74HC165 shift register(s) (optional, for larger button grids)Expands available digital inputs beyond the Pico's native GPIO count without needing a pin per button Laser-cut or 3D-printed enclosureHouses and labels the control layout — this is where this site's laser and 3D printing content directly appliesReading Buttons: Direct GPIO vs. Shift Registers
For a small controller (8-16 buttons), wiring each button directly to its own GPIO pin with an internal pull-up resistor (configured in software, no external resistor needed) is the simplest approach and keeps the code straightforward. For a larger grid — 32 buttons or more — you'll run out of usable GPIO pins quickly, which is where a 74HC165 parallel-in/serial-out shift register earns its place: it reads up to 8 button inputs and reports them over just 3-4 shared pins (clock, latch, data), and multiple 74HC165s can be daisy-chained to read dozens of buttons from the same handful of Pico pins. This is the same shift-register approach covered in this site's guide on shift registers and I/O expanders for Arduino and ESP32, applied here to inputs instead of outputs.
Reading Potentiometers
Each potentiometer wired as a simple voltage divider (outer legs to 3.3V and ground, wiper to an ADC-capable Pico pin) reads directly via CircuitPython's analogio module. The RP2040's ADC has some inherent noise, so raw readings jitter slightly even with a pot held perfectly still — smooth this with a simple moving average or by only sending a new MIDI CC message when the reading changes by more than a small threshold since the last sent value, which also reduces unnecessary MIDI message traffic. For more than 3-4 pots, an external analog multiplexer (like a CD4051) lets you read many potentiometers through a single ADC pin, similar in spirit to how the shift register expands digital inputs.
Sending MIDI
With adafruit_midi installed on the Pico's CircuitPython environment, sending a note or control change is a few lines of code — read a button's state, and on a press transition (not held, just the moment it changes from unpressed to pressed) send a NoteOn message with your chosen note number and velocity, then send NoteOff on release. For potentiometers, map the raw ADC reading (typically 0-65535 on RP2040) to the MIDI CC range of 0-127, and send a ControlChange message only when the value changes meaningfully. Assign a distinct note number to each button and a distinct CC number to each potentiometer, and keep a simple map (in a comment or config table) of which physical control sends which MIDI number — you'll need this reference when mapping controls inside your DAW or MIDI software later, and losing track of it means re-deriving the mapping from scratch.
MIDI MessageUsed ForTriggered By Note On / Note OffTriggering samples, drum hits, or software instrument notesButton press / release transitions Control Change (CC)Continuous parameters — volume, filter cutoff, pan, effect sendsPotentiometer position, sent on meaningful change Program ChangeSwitching presets or patches in receiving softwareOptional — a dedicated button or button combo, if your build includes oneBuilding the Enclosure
This is where the build stops being purely electronics and becomes a genuine cross-discipline project. A laser-cut layered enclosure — a base layer for mounting the Pico and wiring, a middle spacer layer sized to your button and pot bodies, and a top panel with precisely cut holes for each control — gives a clean, labeled result and is a natural application of this site's parametric box generation and kerf compensation guides, since your hole sizes need to account for kerf to fit components snugly rather than loosely. A 3D-printed enclosure is the better choice if your layout has more complex curves or you want integrated standoffs and cable routing channels molded into the design rather than assembled from flat layers — either approach works well, and a hybrid (laser-cut top panel over a 3D-printed base) is common and combines the crisp, accurate control cutouts a laser gives you with the mounting flexibility a printed base provides.
Testing and Debugging
- Before wiring the full grid, test one button and one potentiometer on a breadboard with minimal code to confirm your MIDI messages actually arrive correctly in your target software — most DAWs have a MIDI monitor or learn mode that shows incoming messages, which is the fastest way to confirm note numbers and CC numbers match what you intended.
- A button that "double-triggers" (sends multiple Note On messages from a single physical press) is almost always a debouncing issue — mechanical switches bounce for a few milliseconds when pressed, and without debouncing in code (a simple time-based check ignoring state changes within a short window after the last one) a single press can register as several rapid presses.
- If potentiometer readings jump erratically rather than smoothly, check for a poor ground connection or an unshielded long wire run picking up noise — this is the same class of problem covered in this site's guide on EMI and noise suppression for maker electronics, just at a smaller and lower-stakes scale.
Once the core input-to-MIDI pipeline works, the enclosure and control layout become the real creative space — an 8x8 velocity grid for a sample pad controller, a bank of faders and knobs for a mixing surface, or a hybrid layout built around exactly the workflow you actually use in your own software, all running on the same Pico and CircuitPython foundation established here.
Related Guides
- CircuitPython for Makers: Getting Started on ESP32 and Raspberry Pi Pico
- Build a Standalone BadUSB Device with a Raspberry Pi Pico (No Flipper Required)
- Build a USB-MIDI Controller with Arduino: Buttons, Encoders, and Faders
- Raspberry Pi Pico vs Arduino: Choosing the Right Board for Your Project
- Building a Voron 2.4: Frame Assembly, Wiring, and Klipper Commissioning
- Build a Meshtastic Off-Grid Mesh Messaging Node with ESP32 and LoRa
- Build a Mist Coolant System for CNC Metal Machining
- Build a DIY Bench Signal/Function Generator with the AD9833: Waveform Design, Buffering, and Calibration