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.