Text Editor
Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.
Alternatively, you can use any text editor that saves simple text files
Download the Project Bundle
Your project will use a specific set of CircuitPython libraries, and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.
Plug your Kee Boar board into your computer with a known good USB cable with both data and power wires. It should show up as a thumb drive in your File Explorer or Finder (depending on your operating system) named CIRCUITPY.
Drag the contents of the uncompressed bundle directory onto your Key Boar board CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.
Upload the Code and Libraries to the KB RP2040
You should have the following files on your CIRCUITPY drive:
- lib folder
- code.py
# SPDX-FileCopyrightText: 2023 John Park for Adafruit # # SPDX-License-Identifier: MIT # Cyber Cat MIDI Keyboard conversion for Meowsic Cat Piano # Functions: # --28 keys # --left five toe buttons: patches # --right five toe buttons: picking CC number for ice cream cone control # --volume arrows: octave up/down # --tempo arrows: pitchbend up/down # --on switch: reset # --nose button: midi panic # --record button: ice cream cone CC enable/disable (led indicator) # --play button: start stop arp or sequence in soft synth via cc 16 0/127 # --treble clef button: hold notes (use nose to turn off all notes) # --face button: momentary CC 0/127 on CC number 17 import keypad import board import busio import supervisor import digitalio from adafruit_simplemath import map_range from adafruit_msa3xx import MSA311 import usb_midi import adafruit_midi from adafruit_midi.note_on import NoteOn from adafruit_midi.note_off import NoteOff from adafruit_midi.control_change import ControlChange from adafruit_midi.program_change import ProgramChange from adafruit_midi.pitch_bend import PitchBend supervisor.runtime.autoreload = True # set False to prevent unwanted restarts due to OS weirdness ledpin = digitalio.DigitalInOut(board.A3) ledpin.direction = digitalio.Direction.OUTPUT ledpin.value = True i2c = board.STEMMA_I2C() msa = MSA311(i2c) key_matrix = keypad.KeyMatrix( column_pins=(board.D2, board.D3, board.D4, board.D5, board.D6, board.D7, board.D8, board.D9), row_pins=(board.D10, board.MOSI, board.MISO, board.CLK, board.A0, board.A1) ) midi_uart = busio.UART(board.TX, None, baudrate=31250, timeout=0.001) midi_usb_channel = 1 midi_usb = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=midi_usb_channel-1) midi_serial_channel = 1 midi_serial = adafruit_midi.MIDI(midi_out=midi_uart, out_channel=midi_serial_channel-1) octave = 4 note_offset = 9 # first note on keyboard is an A, first key in keypad matrix is 0 def send_note_on(note, octv): note = ((note+note_offset)+(12*octv)) midi_usb.send(NoteOn(note, 120)) midi_serial.send(NoteOn(note, 120)) def send_note_off(note, octv): note = ((note+note_offset)+(12*octv)) midi_usb.send(NoteOff(note, 0)) midi_serial.send(NoteOff(note, 0)) def send_cc(number, val): midi_usb.send(ControlChange(number, val)) midi_serial.send(ControlChange(number, val)) def send_pc(bank, folder, patch): send_cc(0, bank) send_cc(32, folder) midi_usb.send(ProgramChange(patch)) midi_serial.send(ProgramChange(patch)) def send_bend(bend_start, bend_val, rate, bend_dir): b = bend_start if bend_dir == 0: while b > bend_val + rate: print(b) b = b - rate midi_usb.send(PitchBend(b)) midi_serial.send(PitchBend(b)) if bend_dir == 1: while b < bend_val - rate: print(b) b = b + rate midi_usb.send(PitchBend(b)) midi_serial.send(PitchBend(b)) def send_midi_panic(): for x in range(128): midi_usb.send(NoteOff(x, 0)) midi_serial.send(NoteOff(x, 0)) # key ranges piano_keys = range(0, 28) # 'range()' excludes last value, so add one patch_toes = list(range(28, 33)) cc_toes = list(range(35, 40)) clef_button = 33 nose_button = 47 face_button = 34 record_button = 44 play_button = 45 vol_down_button = 43 vol_up_button = 42 tempo_down_button = 41 tempo_up_button = 40 # patch assigments patch_list = ( (0, 0, 0), # bank 0, folder 0, patch 0 (1, 0, 0), (1, 0, 1), (2, 0, 0), (3, 0, 0), ) pb_max = 16383 # bend up value pb_default = 8192 # bend center value pb_min = 0 # bend down value pb_change_rate = 100 # interval for pitch bend, lower number is slower pb_return_rate = 100 # interval for pitch bend release # accelerometer filtering variables slop = 0.2 # threshold for accelerometer send filter_percent = 0.5 # ranges from 0.0 to 1.0 accel_data_y = msa.acceleration[1] last_accel_data_y = msa.acceleration[1] # midi cc variables cc_enable = True cc_numbers = (1, 43, 44, 14, 15) # mod wheel, filter cutoff, resonance, user, user cc_current = 0 cc_play = 16 cc_face_number = 17 started = False # state of arp/seq play note_hold = False print("Cyber Cat MIDI Keyboard") while True: if cc_enable: new_data_y = msa.acceleration[1] accel_data_y = ((new_data_y * filter_percent) + (1-filter_percent) * accel_data_y) # smooth if abs(accel_data_y - last_accel_data_y) > slop: modulation = int(map_range(accel_data_y, 9, -9, 0, 127)) send_cc(cc_numbers[cc_current], modulation) last_accel_data_y = accel_data_y event = key_matrix.events.get() if event: if event.pressed: key = event.key_number # Note keys if key in piano_keys: send_note_on(key, octave) # Volume buttons if key is vol_down_button: octave = min(max((octave - 1), 0), 7) if key is vol_up_button: octave = min(max((octave + 1), 0), 7) # Tempo buttons if key is tempo_down_button: send_bend(pb_default, pb_min, pb_change_rate, 0) if key is tempo_up_button: send_bend(pb_default, pb_max, pb_change_rate, 1) # Patch buttons (left cat toes) if key in patch_toes: pc_key = patch_toes.index(key) # remove offset for patch list indexing send_pc(patch_list[pc_key][0], patch_list[pc_key][1], patch_list[pc_key][2]) # cc buttons (right cat toes) if key in cc_toes: cc_current = cc_toes.index(key) # remove offset for cc list indexing # Play key -- use MIDI learn to have arp/seq start or stop with this if key is play_button: if not started: send_cc(cc_play, 127) # map to seq/arp on/off Synth One, e.g. started = True else: send_cc(cc_play, 0) started = False # Record key -- enable icecream cone if key is record_button: if cc_enable is True: cc_enable = False ledpin.value = False elif cc_enable is False: send_cc(cc_numbers[cc_current], 0) # zero it cc_enable = True ledpin.value = True # Clef if key is clef_button: # hold note_hold = not note_hold # Face if key is face_button: # momentary cc send_cc(cc_face_number, 127) # Nose if key is nose_button: send_midi_panic() # all notes off if event.released: key = event.key_number if key in piano_keys: if not note_hold: send_note_off(key, octave) if note_hold: pass if key is face_button: # momentary cc release send_cc(cc_face_number, 0) if key is tempo_down_button: send_bend(pb_min, pb_default, pb_return_rate, 1) if key is tempo_up_button: send_bend(pb_max, pb_default, pb_return_rate, 0)
How It Works
The main functions of the code are to turn key and button presses into MIDI messages, and to read the accelerometer and turn it's values into MIDI CC messages.
Libraries
First, we import libraries. keypad
allows us to read the key/button matrix, board
gives us pin definitions, busio
is used for I2C, digitalio
for LED, simplemath
map_range
is used to turn accelerometer values into useable CC values, msa311
is the accelerometer board, and the remaining libraries are for MIDI
.
import keypad import board import busio import supervisor import digitalio from adafruit_simplemath import map_range from adafruit_msa3xx import MSA311 import usb_midi import adafruit_midi from adafruit_midi.note_on import NoteOn from adafruit_midi.note_off import NoteOff from adafruit_midi.control_change import ControlChange from adafruit_midi.program_change import ProgramChange from adafruit_midi.pitch_bend import PitchBend
ledpin = digitalio.DigitalInOut(board.A3) ledpin.direction = digitalio.Direction.OUTPUT ledpin.value = True
i2c = board.STEMMA_I2C() msa = MSA311(i2c)
key_matrix = keypad.KeyMatrix( column_pins=(board.D2, board.D3, board.D4, board.D5, board.D6, board.D7, board.D8, board.D9), row_pins=(board.D10, board.MOSI, board.MISO, board.CLK, board.A0, board.A1) )
You'll set things up so octaves can shifted with one of the arrow button pairs. The octave
variable will keep track of this, and the note_offset
accounts for the lowest key (0 in the matrix) being an A (9 for the lowest MIDI A).
octave = 4 note_offset = 9
MIDI
MIDI is set up on both the serial UART and over USB. You can change which channels to use here if you like.
midi_uart = busio.UART(board.TX, None, baudrate=31250) midi_usb_channel = 1 midi_usb = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=midi_usb_channel-1) midi_serial_channel = 1 midi_serial = adafruit_midi.MIDI(midi_out=midi_uart, out_channel=midi_serial_channel-1)
These functions are set up to send all of the required MIDI messages:
def send_note_on(note, octv): note = ((note+note_offset)+(12*octv)) midi_usb.send(NoteOn(note, 120)) midi_serial.send(NoteOn(note, 120)) def send_note_off(note, octv): note = ((note+note_offset)+(12*octv)) midi_usb.send(NoteOff(note, 0)) midi_serial.send(NoteOff(note, 0)) def send_cc(number, val): midi_usb.send(ControlChange(number, val)) midi_serial.send(ControlChange(number, val)) def send_pc(bank, folder, patch): send_cc(0, bank) send_cc(32, folder) midi_usb.send(ProgramChange(patch)) midi_serial.send(ProgramChange(patch)) def send_bend(bend_start, bend_val, rate, bend_dir): b = bend_start if bend_dir == 0: while b > bend_val + rate: print(b) b = b - rate midi_usb.send(PitchBend(b)) midi_serial.send(PitchBend(b)) if bend_dir == 1: while b < bend_val - rate: print(b) b = b + rate midi_usb.send(PitchBend(b)) midi_serial.send(PitchBend(b)) def send_midi_panic(): for x in range(128): midi_usb.send(NoteOff(x, 0)) midi_serial.send(NoteOff(x, 0))
Key/Button Assignments
All of the keys and buttons will show up as a key matrix event when pressed or released, these are their correlations to the physical buttons:
piano_keys = range(0, 28) # 'range()' excludes last value, so add one patch_toes = list(range(28, 33)) cc_toes = list(range(35, 40)) clef_button = 33 nose_button = 47 face_button = 34 record_button = 44 play_button = 45 vol_down_button = 43 vol_up_button = 42 tempo_down_button = 41 tempo_up_button = 40
patch_list = ( (0, 0, 0), # bank 0, folder 0, patch 0 (1, 0, 0), (1, 0, 1), (2, 0, 0), (3, 0, 0), )
Pitch Bend Setup
These variables set the maximum, default, and minimum values for pitch bend messages, as well as the increment size to sweep through them.
pb_max = 16383 # bend up value pb_default = 8192 # bend center value pb_min = 0 # bend down value pb_change_rate = 100 # interval for pitch bend, lower number is slower pb_return_rate = 100 # interval for pitch bend release
Accelerometer Filtering
These values are used to filter the raw accelerometer readings into something more useable.
# accelerometer filtering variables slop = 0.2 # threshold for accelerometer send filter_percent = 0.5 # ranges from 0.0 to 1.0 accel_data_y = msa.acceleration[1] last_accel_data_y = msa.acceleration[1]
MIDI CC Variables
These variables are used to track the ice cream cone CC enable state, set the CC numbers used by the right paw toes, play button, and face button.
The started
and note_hold
variables track state of the arpeggiator/sequencer enable switch (on the software/hardware synth, there is not a built in arpeggiator in the code), and the state of the note_hold
clef button.
cc_enable = True cc_numbers = (1, 43, 44, 14, 15) # mod wheel, filter cutoff, resonance, user, user cc_current = 0 cc_play = 16 cc_face_number = 17 started = False # state of arp/seq play note_hold = False
The main loop checks for accelerometer changes and button events.
If the accelerometer is enabled, it will send CC messages on the chosen number.
while True: if cc_enable: new_data_y = msa.acceleration[1] accel_data_y = ((new_data_y * filter_percent) + (1-filter_percent) * accel_data_y) # smooth if abs(accel_data_y - last_accel_data_y) > slop: modulation = int(map_range(accel_data_y, 9, -9, 0, 127)) send_cc(cc_numbers[cc_current], modulation) last_accel_data_y = accel_data_y
Key Matrix Events
This line checks to see if any keys or buttons have been pressed or released:
event = key_matrix.events.get()
If pressed or released, they all run their corresponding functions as defined earlier:
if event: if event.pressed: key = event.key_number # Note keys if key in piano_keys: send_note_on(key, octave) # Volume buttons if key is vol_down_button: octave = min(max((octave - 1), 0), 7) if key is vol_up_button: octave = min(max((octave + 1), 0), 7) # Tempo buttons if key is tempo_down_button: send_bend(pb_default, pb_min, pb_change_rate, 0) if key is tempo_up_button: send_bend(pb_default, pb_max, pb_change_rate, 1) # Patch buttons (left cat toes) if key in patch_toes: pc_key = patch_toes.index(key) # remove offset for patch list indexing send_pc(patch_list[pc_key][0], patch_list[pc_key][1], patch_list[pc_key][2]) # cc buttons (right cat toes) if key in cc_toes: cc_current = cc_toes.index(key) # remove offset for cc list indexing # Play key -- use MIDI learn to have arp/seq start or stop with this if key is play_button: if not started: send_cc(cc_play, 127) # map to seq/arp on/off Synth One, e.g. started = True else: send_cc(cc_play, 0) started = False # Record key -- enable icecream cone if key is record_button: if cc_enable is True: cc_enable = False ledpin.value = False elif cc_enable is False: send_cc(cc_numbers[cc_current], 0) # zero it cc_enable = True ledpin.value = True # Clef if key is clef_button: # hold note_hold = not note_hold # Face if key is face_button: # momentary cc send_cc(cc_face_number, 127) # Nose if key is nose_button: send_midi_panic() # all notes off if event.released: key = event.key_number if key in piano_keys: if not note_hold: send_note_off(key, octave) if note_hold: pass if key is face_button: # momentary cc release send_cc(cc_face_number, 0) if key is tempo_down_button: send_bend(pb_min, pb_default, pb_return_rate, 1) if key is tempo_up_button: send_bend(pb_max, pb_default, pb_return_rate, 0)
Page last edited January 22, 2025
Text editor powered by tinymce.