Raspberry Pi Pico
USB MIDI MPR121 Play-Doh Drums

The main idea with this set of experiments was to see how easy it would be to make custom instruments using USB MIDI with CircuitPython. Adafruit's CircuitPython manages USB differently to MicroPython and the USB MIDI library allows us to send MIDI messages over USB. These can be picked up by any device or PC program that can accept MIDI input over USB.

The photograph shows the 'drum kit' I made using Play-Doh for the drums and an MPR121 for capactive touch sensing.

Pico Circuit

In order to hear the MIDI notes on your PC, you will need to install some software to your PC. I used Virtual Midi Piano Keyboard to view/hear the MIDI playing. You can use other software if you wish. This program is free and, importantly, allows you to choose channels and instruments using MIDI messages.

If you are using VMPK then, once installed, you will need to make sure that you set up the software to receive MIDI input. Go to the Edit menu and choose MIDI Connections. Complete as shown below,

VMPK MIDI settings

You need the following libraries for this project. Download the mpy library for these and copy to your lib folder.

Testing MIDI Playback

Before building any kind of circuit, it is worth testing out the things that don't depend on that circuit. In this case, MIDI playback can be tested with just a Pico connected to your PC. The following program plays a quick scale for the instruments for Channel 1 of General MIDI.

import usb_midi
import adafruit_midi

from time import sleep
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn
from adafruit_midi.program_change import ProgramChange

midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)

cmaj4 = [60, 62, 64, 65, 67, 69, 71, 72]

for instrument in range(128):
    midi.send(ProgramChange(instrument))
    for note in cmaj4:
        midi.send(NoteOn(note, 120))
        sleep(0.2)
        midi.send(NoteOff(note, 120))
        sleep(0.1)

If you got some sound here and the instruments changed, then you're in business. Notice that the channel numbers are actually zero-indexed although you will generally see them referred to by the number one above. The same is true of the instruments. You can find the list of instrument names on the Wikipedia page for General MIDI.

Now for the drums. Drums work differently to other instruments. The drums are all notes, rather than instruments and are all on Channel 10. They are notes 35 to 81.

import usb_midi
import adafruit_midi

from time import sleep
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn


midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=9
)

for note in range(35, 82):
    midi.send(NoteOn(note, 120))
    sleep(0.5)
    midi.send(NoteOff(note, 120))
    sleep(0.1)

You can send multiple messages at once to play chords. When we want to do that, we just send a list. In this program, you can see how we can use note names in our NoteOn and NoteOff messages,

import usb_midi
import adafruit_midi

from time import sleep
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn


midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)

c = [NoteOn("C4", 120), NoteOn("E4", 120), NoteOn("G4", 120)]
d = [NoteOn("D4", 120), NoteOn("F#4", 120), NoteOn("A4", 120)]
e = [NoteOn("E4", 120), NoteOn("G#4", 120), NoteOn("B4", 120)]

coff = [NoteOff("C4", 120), NoteOff("E4", 120), NoteOff("G4", 120)]
doff = [NoteOff("D4", 120), NoteOff("F#4", 120), NoteOff("A4", 120)]
eoff = [NoteOff("E4", 120), NoteOff("G#4", 120), NoteOff("B4", 120)]

on = [c, d, e]
off = [coff, doff, eoff]

for i in range(4):
    for j in range(3):
        midi.send(on[j])
        sleep(0.5)
        midi.send(off[j])
        sleep(0.1)

And now for the drums. Without the Play-Doh, this is the circuit I have used,

Pico Circuit

Here is the program,

from time import sleep
import board
import usb_midi
import adafruit_midi
import adafruit_mpr121
import busio

from adafruit_midi.control_change import ControlChange
from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn
from adafruit_midi.program_change import ProgramChange

def btn_changes(current, last, n): 
    return [((last >> i & 1)<<1) + (current >> i & 1) for i in range(n)] 

midi = adafruit_midi.MIDI(
    midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=9
)

i2c = busio.I2C(board.GP5, board.GP4)
mpr121 = adafruit_mpr121.MPR121(i2c)

# crash cymbal
# ride cymbal
# snare
# hi-hat
# bass
# hi tom
# mid tom
# low tom
drums = [49, 38, 51, 44, 35, 50, 47, 41]

last = 0
while True:
    touched = mpr121.touched()
    if touched != last:
        states = btn_changes(touched, last, 8)
        press = [i for i,e in enumerate(states) if e==1] 
        release = [i for i,e in enumerate(states) if e==2]
        msgs = []
        for p in press:
            msgs.append(NoteOn(drums[p], 120))
        for r in release:
            msgs.append(NoteOff(drums[p], 120))
        midi.send(msgs)            
    last = touched

There is a little bit of latency here but if, like me, you aren't much of a drummer, that isn't going to cause too much concern.