Text Editor

Adafruit recommends using the Mu editor for using your CircuitPython code with the Pico. You can get more info in this guide.

Alternatively, you can use any text editor that saves files.

Download the Project Bundle

Your project will use a specific set of CircuitPython libraries and the code.py file. In order to get the libraries you need, click on the Download Project Bundle link below, and uncompress the .zip file.

Drag the contents of the uncompressed bundle directory onto your board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.

# SPDX-FileCopyrightText: 2021 John Park for Adafruit Industries
# SPDX-License-Identifier: MIT

# Pico RP2040 Mechanical MIDI Modal Keyboard
# 7x3 mech keyboard
# Each key sends MIDI NoteOn / NoteOff message over USB
# Can be any scale/mode
# Key combo sends MIDI panic (see bottom section of code)

import time
import board
from digitalio import DigitalInOut, Direction, Pull
import usb_midi
import adafruit_midi
from adafruit_midi.note_on import NoteOn
from adafruit_midi.note_off import NoteOff
from adafruit_debouncer import Debouncer

print("---Pico MIDI Modal Mech Keyboard---")

MIDI_CHANNEL = 1  # pick your MIDI channel here

midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=MIDI_CHANNEL-1)

def send_midi_panic():
    print("All MIDI notes off")
    for x in range(128):
        midi.send(NoteOff(x, 0))

led = DigitalInOut(board.LED)
led.direction = Direction.OUTPUT
led.value = True

num_keys = 21

# list of pins to use (skipping GP15 on Pico because it's funky)
pins = (
    board.GP0,
    board.GP1,
    board.GP2,
    board.GP3,
    board.GP4,
    board.GP5,
    board.GP6,
    board.GP7,
    board.GP8,
    board.GP9,
    board.GP10,
    board.GP11,
    board.GP12,
    board.GP13,
    board.GP14,
    board.GP16,
    board.GP17,
    board.GP18,
    board.GP19,
    board.GP20,
    board.GP21,
)


keys = []
for pin in pins:
    tmp_pin = DigitalInOut(pin)
    tmp_pin.pull = Pull.UP
    keys.append(Debouncer(tmp_pin))

root_notes = (48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59)  # used during config
note_numbers = (48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
                60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
                72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83)
note_names = ("C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
              "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
              "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",)
scale_root = root_notes[0]  # default if nothing is picked
root_picked = False  # state of root selection
mode_picked = False  # state of mode selection
mode_choice = 0


#  ----- User selection of the root note ----- #
print("Pick the root using top twelve keys, then press bottom right key to enter:")
print(". . . . . . .")
print(". . . . . o o")
print("o o o o o o .")

while not root_picked:
    for i in range(12):
        keys[i].update()
        if keys[i].fell:
            scale_root = root_notes[i]
            midi.send(NoteOn(root_notes[i], 120))
            print("Root is", note_names[i])
        if keys[i].rose:
            midi.send(NoteOff(root_notes[i], 0))
    keys[20].update()

    if keys[20].rose:
        root_picked = True
        print("Root picked.\n")

#  lists of mode intervals relative to root
major = ( 0, 2, 4, 5, 7, 9, 11 )
minor = ( 0, 2, 3, 5, 7, 8, 10 )
dorian = ( 0, 2, 3, 5, 7, 9, 10 )
phrygian = ( 0, 1, 3, 5, 7, 8, 10 )
lydian = (0 , 2, 4, 6, 7, 9, 11 )
mixolydian = ( 0, 2, 4, 5, 7, 9, 10)
locrian = ( 0, 1, 3, 5, 6, 8, 10)

modes = []
modes.append(major)
modes.append(minor)
modes.append(dorian)
modes.append(phrygian)
modes.append(lydian)
modes.append(mixolydian)
modes.append(locrian)

mode_names = ("Major/Ionian",
              "Minor/Aeolian",
              "Dorian",
              "Phrygian",
              "Lydian",
              "Mixolydian",
              "Locrian")

intervals = list(mixolydian)  # intervals for Mixolydian by default

print("Pick the mode with top seven keys, then press bottom right key to enter:")
print(". . . . . . .")
print("o o o o o o o")
print("o o o o o o .")

while not mode_picked:
    for i in range(7):
        keys[i].update()
        if keys[i].fell:
            mode_choice = i
            print(mode_names[mode_choice], "mode")
            for j in range(7):
                intervals[j] = modes[i][j]
            # play the scale
            for k in range(7):
                midi.send(NoteOn(scale_root+intervals[k], 120))
                note_index = note_numbers.index(scale_root+intervals[k])
                print(note_names[note_index])
                time.sleep(0.15)
                midi.send(NoteOff(scale_root+intervals[k], 0))
                time.sleep(0.15)
            midi.send(NoteOn(scale_root+12, 120))
            note_index = note_numbers.index(scale_root+12)
            print(note_names[note_index], "\n")
            time.sleep(0.15)
            midi.send(NoteOff(scale_root+12, 0))
            time.sleep(0.15)

    keys[20].update()
    if keys[20].rose:
        print(mode_names[mode_choice], "mode picked.\n")
        mode_picked = True

scale = []  # create the base scale
for i in range(7):
    scale.append(scale_root + intervals[i])

midi_notes = []  # build the list with three octaves
for k in range(7):
    midi_notes.append(scale[k]+24)
for l in range(7):
    midi_notes.append(scale[l]+12)
for m in range(7):
    midi_notes.append(scale[m])

led.value = False
print("Ready, set, play!")


while True:

    for i in range(num_keys):
        keys[i].update()
        if keys[i].fell:
            try:
                midi.send(NoteOn(midi_notes[i], 120))
                note_index = note_numbers.index(midi_notes[i])
                print("MIDI NoteOn:", note_names[note_index])
            except ValueError:  # deals w six key limit
                pass

        if keys[i].rose:
            try:
                midi.send(NoteOff(midi_notes[i], 0))
                note_index = note_numbers.index(midi_notes[i])
                print("MIDI NoteOff:", note_names[note_index])
            except ValueError:
                pass

    #  Key combo for MIDI panic
    # . o o o o o .
    # o o o . o o o
    # . o o o o o .

    if (not keys[0].value and
            not keys[6].value
            and not keys[10].value
            and not keys[14].value
            and not keys[20].value):
        send_midi_panic()
        time.sleep(1)

Use the Modal MIDI Keyboard

To test the keyboard, plug it into your computer and launch this handy Chrome browser MIDI Monitor web app to check that it is working.

Make Some Sound

MIDI note messages are fun to look at, but even better when the make some sound! Use a software synthesizer that accepts MIDI messages (pretty much all of them do!).

Here are some examples of free, open source synths for Linux, Windows, and mac os:  

Launch your software synth and select the Pico CircuitPython keyboard as your MIDI source.

This video shows how root and mode selection work on startup. You can start over again at any time by pressing the reset button.

Root Note Selection

On startup, you can press each of the first 12 keys starting from the upper left corner of the keyboard to preview/select your scale root note.

Press the bottom right key to enter/commit the most recently previewed note.

Mode Selection

Once your root note is picked, the Modal MIDI keyboard goes into mode selection configuration. Press each of the keys on the top row of the keyboard to preview each mode:

  • Major/Ionian
  • Minor/Aeolian
  • Dorian
  • Phrygian
  • Lydian
  • Mixolydian
  • Locrian

You'll hear each note of the selected mode play. Once you like your choice, press the lower right key to enter.

Now, you can start playing all three octaves!

This guide was first published on Jul 07, 2021. It was last updated on Jun 24, 2021.

This page (Code the Modal MIDI Controller) was last updated on Sep 11, 2023.

Text editor powered by tinymce.