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.
Drag the contents of the uncompressed bundle directory onto your KeeBoar board's 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
After downloading the Project Bundle, plug your KB2040 into the computer USB port. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the KB2040's CIRCUITPY drive.
- lib folder
- code.py
# SPDX-FileCopyrightText: 2022 John Park for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# Drum Trigger Sequencer 2040
# Based on code by Tod Kurt @todbot https://github.com/todbot/picostepseq
# Uses General MIDI drum notes on channel 10
# Range is note 35/B0 - 81/A4, but classic 808 set is defined here
import time
from adafruit_ticks import ticks_ms, ticks_diff, ticks_add
import board
from digitalio import DigitalInOut, Pull
import keypad
import adafruit_aw9523
import usb_midi
from adafruit_seesaw import seesaw, rotaryio, digitalio
from adafruit_debouncer import Debouncer
from adafruit_ht16k33 import segments
# define I2C
i2c = board.STEMMA_I2C()
num_steps = 16 # number of steps/switches
num_drums = 11 # primary 808 drums used here, but you can use however many you like
# Beat timing assumes 4/4 time signature, e.g. 4 beats per measure, 1/4 note gets the beat
bpm = 120 # default BPM
beat_time = 60/bpm # time length of a single beat
beat_millis = beat_time * 1000 # time length of single beat in milliseconds
steps_per_beat = 4 # subdivide beats down to to 16th notes
steps_millis = beat_millis / steps_per_beat # time length of a beat subdivision, e.g. 1/16th note
step_counter = 0 # goes from 0 to length of sequence - 1
sequence_length = 16 # how many notes stored in a sequence
curr_drum = 0
playing = False
# Setup button
start_button_in = DigitalInOut(board.A2)
start_button_in.pull = Pull.UP
start_button = Debouncer(start_button_in)
# Setup switches
switch_pins = (
board.TX, board.RX, board.D2, board.D3,
board.D4, board.D5, board.D6, board.D7,
board.D8, board.D9, board.D10, board.MOSI,
board.MISO, board.SCK, board.A0, board.A1
)
switches = keypad.Keys(switch_pins, value_when_pressed=False, pull=True)
# Setup LEDs
leds = adafruit_aw9523.AW9523(i2c, address=0x5B) # both jumperes soldered on board
for led in range(num_steps): # turn them off
leds.set_constant_current(led, 0)
leds.LED_modes = 0xFFFF # constant current mode
leds.directions = 0xFFFF # output
# Values for LED brightness 0-255
offled = 0
dimled = 2
midled = 20
highled = 150
for led in range(num_steps): # dramatic boot up light sequence
leds.set_constant_current(led, dimled)
time.sleep(0.05)
time.sleep(0.5)
#
# STEMMA QT Rotary encoder setup
rotary_seesaw = seesaw.Seesaw(i2c, addr=0x36) # default address is 0x36
encoder = rotaryio.IncrementalEncoder(rotary_seesaw)
last_encoder_pos = 0
rotary_seesaw.pin_mode(24, rotary_seesaw.INPUT_PULLUP) # setup the button pin
knobbutton_in = digitalio.DigitalIO(rotary_seesaw, 24) # use seesaw digitalio
knobbutton = Debouncer(knobbutton_in) # create debouncer object for button
encoder_pos = -encoder.position
# MIDI setup
midi = usb_midi.ports[1]
drum_names = [
"Bass", "Snar", "LTom", "MTom", "HTom",
"Clav", "Clap", "Cowb", "Cymb", "OHat", "CHat"
]
drum_notes = [36, 38, 41, 43, 45, 37, 39, 56, 49, 46, 42] # general midi drum notes matched to 808
# default starting sequence needs to match number of drums in num_drums
sequence = [
[ 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 ], # bass drum
[ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 ], # snare
[ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 ], # low tom
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0 ], # mid tom
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 ], # high tom
[ 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], # rimshot/claves
[ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0 ], # handclap/maracas
[ 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0 ], # cowbell
[ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], # cymbal
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0 ], # hihat open
[ 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0 ] # hihat closed
]
def play_drum(note):
midi_msg_on = bytearray([0x99, note, 120]) # 0x90 is noteon ch 1, 0x99 is noteon ch 10
midi_msg_off = bytearray([0x89, note, 0])
midi.write(midi_msg_on)
midi.write(midi_msg_off)
def light_steps(step, state):
if state:
leds.set_constant_current(step, midled)
else:
leds.set_constant_current(step, offled)
def light_beat(step):
leds.set_constant_current(step, highled)
def edit_mode_toggle():
# pylint: disable=global-statement
global edit_mode
# pylint: disable=used-before-assignment
edit_mode = (edit_mode + 1) % num_modes
display.fill(0)
if edit_mode == 0:
display.print(bpm)
elif edit_mode == 1:
display.print(drum_names[curr_drum])
def print_sequence():
print("sequence = [ ")
for k in range(num_drums):
print(" [" + ",".join('1' if e else '0' for e in sequence[k]) + "], #", drum_names[k])
print("]")
# set the leds
for j in range(sequence_length):
light_steps(j, sequence[curr_drum][j])
display = segments.Seg14x4(i2c, address=(0x71))
display.brightness = 0.3
display.fill(0)
display.show()
display.print(bpm)
display.show()
edit_mode = 0 # 0=bpm, 1=voices
num_modes = 2
print("Drum Trigger 2040")
display.fill(0)
display.show()
display.marquee("Drum", 0.05, loop=False)
time.sleep(0.5)
display.marquee("Trigger", 0.075, loop=False)
time.sleep(0.5)
display.marquee("2040", 0.05, loop=False)
time.sleep(1)
display.marquee("BPM", 0.05, loop=False)
time.sleep(0.75)
display.marquee(str(bpm), 0.1, loop=False)
while True:
start_button.update()
if start_button.fell: # pushed encoder button plays/stops transport
if playing is True:
print_sequence()
playing = not playing
step_counter = 0
last_step = int(ticks_add(ticks_ms(), -steps_millis))
print("*** Play:", playing)
if playing:
now = ticks_ms()
diff = ticks_diff(now, last_step)
if diff >= steps_millis:
late_time = ticks_diff(int(diff), int(steps_millis))
last_step = ticks_add(now, - late_time//2)
light_beat(step_counter) # brighten current step
for i in range(num_drums):
if sequence[i][step_counter]: # if there's a 1 at the step for the seq, play it
play_drum(drum_notes[i])
light_steps(step_counter, sequence[curr_drum][step_counter]) # return led to step value
step_counter = (step_counter + 1) % sequence_length
encoder_pos = -encoder.position # only check encoder while playing between steps
knobbutton.update()
if knobbutton.fell:
edit_mode_toggle()
else: # check the encoder all the time when not playing
encoder_pos = -encoder.position
knobbutton.update()
if knobbutton.fell: # change edit mode, refresh display
edit_mode_toggle()
# switches add or remove steps
switch = switches.events.get()
if switch:
if switch.pressed:
i = switch.key_number
sequence[curr_drum][i] = not sequence[curr_drum][i] # toggle step
light_steps(i, sequence[curr_drum][i]) # toggle light
if encoder_pos != last_encoder_pos:
encoder_delta = encoder_pos - last_encoder_pos
if edit_mode == 0:
bpm = bpm + encoder_delta # or (encoder_delta * 5)
bpm = min(max(bpm, 10), 400)
beat_time = 60/bpm # time length of a single beat
beat_millis = beat_time * 1000
steps_millis = beat_millis / steps_per_beat
display.fill(0)
display.print(bpm)
if edit_mode == 1:
curr_drum = (curr_drum + encoder_delta) % num_drums
# quickly set the step leds
for i in range(sequence_length):
light_steps(i, sequence[curr_drum][i])
display.print(drum_names[curr_drum])
last_encoder_pos = encoder_pos
How it Works
Libraries
The Drum Trigger Sequencer 2040 code first imports a number of libraries used for counting accurate time, reading switches, lighting LEDs using the AW9523 driver board, sending MIDI messages, writing to the display, and using the rotary encoder via SeeSaw.
import time from adafruit_ticks import ticks_ms, ticks_diff, ticks_add import board from digitalio import DigitalInOut, Pull import keypad import adafruit_aw9523 import usb_midi from adafruit_seesaw import seesaw, rotaryio, digitalio from adafruit_debouncer import Debouncer from adafruit_ht16k33 import segments
Step and Time Setup
After setting up the I2C bus, the code defines a number of variables related to steps (the sixteen divisions of the four beat measure), drum tracks (a.k.a. "drum voices"), tempo (bpm), and variables for state, such as the step_counter, sequence_length and playing state.
# define I2C i2c = board.STEMMA_I2C() num_steps = 16 # number of steps/switches num_drums = 11 # primary 808 drums used here, but you can use however many you like # Beat timing assumes 4/4 time signature, e.g. 4 beats per measure, 1/4 note gets the beat bpm = 120 # default BPM beat_time = 60/bpm # time length of a single beat beat_millis = beat_time * 1000 # time length of single beat in milliseconds steps_per_beat = 4 # subdivide beats down to to 16th notes steps_millis = beat_millis / steps_per_beat # time length of a beat subdivision, e.g. 1/16th note step_counter = 0 # goes from 0 to length of sequence - 1 sequence_length = 16 # how many notes stored in a sequence curr_drum = 0 playing = False # Setup button start_button_in = DigitalInOut(board.A2) start_button_in.pull = Pull.UP start_button = Debouncer(start_button_in)
Switch Setup
The next section of code sets up the switches using the keypad library and the switch LEDs using the AW9523 library in constant current mode.
# Setup switches
switch_pins = (
board.TX, board.RX, board.D2, board.D3,
board.D4, board.D5, board.D6, board.D7,
board.D8, board.D9, board.D10, board.MOSI,
board.MISO, board.SCK, board.A0, board.A1
)
switches = keypad.Keys(switch_pins, value_when_pressed=False, pull=True)
# Setup LEDs
leds = adafruit_aw9523.AW9523(i2c, address=0x5B) # both jumperes soldered on board
for led in range(num_steps): # turn them off
leds.set_constant_current(led, 0)
leds.LED_modes = 0xFFFF # constant current mode
leds.directions = 0xFFFF # output
# Values for LED brightness 0-255
offled = 0
dimled = 2
midled = 20
highled = 150
for led in range(num_steps): # dramatic boot up light sequence
leds.set_constant_current(led, dimled)
time.sleep(0.05)
time.sleep(0.5)
Rotary Encoder Setup
Then, the rotary encoder knob and push button are set up using the seesaw library.
# STEMMA QT Rotary encoder setup rotary_seesaw = seesaw.Seesaw(i2c, addr=0x36) # default address is 0x36 encoder = rotaryio.IncrementalEncoder(rotary_seesaw) last_encoder_pos = 0 rotary_seesaw.pin_mode(24, rotary_seesaw.INPUT_PULLUP) # setup the button pin knobbutton_in = digitalio.DigitalIO(rotary_seesaw, 24) # use seesaw digitalio knobbutton = Debouncer(knobbutton_in) # create debouncer object for button encoder_pos = -encoder.position
MIDI, Drum, Pattern Setup
MIDI is set up over usb_midi.
The drum_names list is used to store the strings that are displayed on the 14-segment LED backpacks.
The related drum_notes list defines which General MIDI note numbers correlate to each drum track (e.g., Note 36 is Bass Drum, Note 42 is a Closed Hi-Hat).
The sequence list stores the default 16-step pattern for each of the drum tracks. A 1 means there is a trigger at that step, a 0 means there is not. So, [ 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 ] would trigger a drum on every quarter note of the measure (a four-on-the-floor kick drum pattern).
# MIDI setup
midi = usb_midi.ports[1]
drum_names = [
"Bass", "Snar", "LTom", "MTom", "HTom",
"Clav", "Clap", "Cowb", "Cymb", "OHat", "CHat"
]
drum_notes = [36, 38, 41, 43, 45, 37, 39, 56, 49, 46, 42] # general midi drum notes matched to 808
# default starting sequence needs to match number of drums in num_drums
sequence = [
[ 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 ], # bass drum
[ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 ], # snare
[ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 ], # low tom
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0 ], # mid tom
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 ], # high tom
[ 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], # rimshot/claves
[ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0 ], # handclap/maracas
[ 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0 ], # cowbell
[ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], # cymbal
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0 ], # hihat open
[ 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0 ] # hihat closed
]
Functions
A number of functions are created as convenient reusable code sections:
play_drum()
The play_drum(note) function is called to send a MIDI NoteOn and NoteOff message for a given note number. These are sent as a bytearrays for optimal speed.
def play_drum(note):
midi_msg_on = bytearray([0x99, note, 120]) # 0x90 is noteon ch 1, 0x99 is noteon ch 10
midi_msg_off = bytearray([0x89, note, 0])
midi.write(midi_msg_on)
midi.write(midi_msg_off)
light_steps() & light_beat()
The light_steps(step, state) function is called to toggle an LED on or off when a step switch is pressed.
The light_beat(step) is called once per 16th note when the sequencer is playing in order to show where the current step is in the pattern -- think of it sort of like the bouncing ball on a Karaoke machine.
def light_steps(step, state):
if state:
leds.set_constant_current(step, midled)
else:
leds.set_constant_current(step, offled)
def light_beat(step):
leds.set_constant_current(step, highled)
edit_mode_toggle()
Since we only have one knob, we need to use it for a couple of different functions. Pressing the encoder knob toggles between the two modes so the knob can be used for either changing the tempo or picking among the drum tracks.
The edit_mode_toggle() function is called to toggle the sequencer between editing the tempo (BPM) with the knob and picking the tracks with the knob.
def edit_mode_toggle():
global edit_mode
edit_mode = (edit_mode + 1) % num_modes
display.fill(0)
if edit_mode == 0:
display.print(bpm)
elif edit_mode == 1:
display.print(drum_names[curr_drum])
print_sequence()
This is a convenience function -- it is called when you press the Start/Stop button to stop playback and it prints the current sequence to the REPL/serial monitor. The format is the same as the sequence[] list in the code, so you can copy and paste to change the default sequence.
def print_sequence():
print("sequence = [ ")
for k in range(num_drums):
print(" [" + ",".join('1' if e else '0' for e in sequence[k]) + "], #", drum_names[k])
print("]")
for j in range(sequence_length):
light_steps(j, sequence[curr_drum][j])
Display Setup
The 14-segment LED display is set up on I2C, and then runs a set of marquees to print "Drum Trigger 2040" followed by "BPM" and the initial value of "120".
display = segments.Seg14x4(i2c, address=(0x71))
display.brightness = 0.3
display.fill(0)
display.show()
display.print(bpm)
display.show()
edit_mode = 0 # 0=bpm, 1=voices
num_modes = 2
print("Drum Trigger 2040")
display.fill(0)
display.show()
display.marquee("Drum", 0.05, loop=False)
time.sleep(0.5)
display.marquee("Trigger", 0.075, loop=False)
time.sleep(0.5)
display.marquee("2040", 0.05, loop=False)
time.sleep(1)
display.marquee("BPM", 0.05, loop=False)
time.sleep(0.75)
display.marquee(str(bpm), 0.1, loop=False)
The main loop of the program first checks to see if the start button has been pressed. This toggles the playing state, and if it is being stopped, calls the print_sequence() function. Otherwise, if it is playing, it resets the sequence to the first step and sets the last_step variable based on ticks.
start_button.update()
if start_button.fell: # pushed encoder button plays/stops transport
if playing is True:
print_sequence()
playing = not playing
step_counter = 0
last_step = int(ticks_add(ticks_ms(), -steps_millis))
print("*** Play:", playing)
Playing
When the sequence is playing, the timing is counted accurately based on ticks, the current step LED is flashed, and any drum track steps that are active are played via MIDI.
Between steps the encoder button and knob are checked, and if so, the button will switch edit modes and the knob value is stored as encoder_pos for later action.
if playing:
now = ticks_ms()
diff = ticks_diff(now, last_step)
if diff >= steps_millis:
late_time = ticks_diff(int(diff), int(steps_millis))
last_step = ticks_add(now, - late_time//2)
light_beat(step_counter) # brighten current step
for i in range(num_drums):
if sequence[i][step_counter]: # if there's a 1 at the step for the seq, play it
play_drum(drum_notes[i])
light_steps(step_counter, sequence[curr_drum][step_counter]) # return led to step value
step_counter = (step_counter + 1) % sequence_length
encoder_pos = -encoder.position # only check encoder while playing between steps
knobbutton.update()
if knobbutton.fell:
edit_mode_toggle()
else: # check the encoder all the time when not playing
encoder_pos = -encoder.position
knobbutton.update()
if knobbutton.fell: # change edit mode, refresh display
edit_mode_toggle()
Switch Check
The switches are all checked with switches.events.get() to see if anything is pressed. If a switch is pressed it's light is toggled and its value in the sequence[] list is flipped.
switch = switches.events.get()
if switch:
if switch.pressed:
i = switch.key_number
sequence[curr_drum][i] = not sequence[curr_drum][i] # toggle step
light_steps(i, sequence[curr_drum][i]) # toggle light
Knob Change
If the encoder position was changed (due to knob twiddling) the tempo or track will change, depending on the current edit_mode state.
If in tempo mode the BPM value is increased or decreased (minimum 10, maximum 400!) and the steps_millis value is recalculated. The 14-segment LED display is updated to read out the current tempo.
Otherwise, in track edit mode, the knob will switch among the eleven drum tracks. Each has their own step patterns, which update the LEDs on the fly as you rotate the knob. Also, the 14-segment display shows the current track drum name.
if encoder_pos != last_encoder_pos:
encoder_delta = encoder_pos - last_encoder_pos
if edit_mode == 0:
bpm = bpm + encoder_delta # or (encoder_delta * 5)
bpm = min(max(bpm, 10), 400)
beat_time = 60/bpm # time length of a single beat
beat_millis = beat_time * 1000
steps_millis = beat_millis / steps_per_beat
display.fill(0)
display.print(bpm)
if edit_mode == 1:
curr_drum = (curr_drum + encoder_delta) % num_drums
# quickly set the step leds
for i in range(sequence_length):
light_steps(i, sequence[curr_drum][i])
display.print(drum_names[curr_drum])
last_encoder_pos = encoder_pos
Page last edited January 21, 2025
Text editor powered by tinymce.