Once you've finished setting up your RP2040 Prop-Maker Feather with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2023 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import digitalio
import audiobusio
import board
import neopixel
import adafruit_lis3dh
import synthio
import keypad
from adafruit_ticks import ticks_ms, ticks_add, ticks_diff
import ulab.numpy as np
from adafruit_seesaw.seesaw import Seesaw
from adafruit_seesaw.neopixel import NeoPixel as SS_NeoPixel
from adafruit_seesaw.digitalio import DigitalIO
from adafruit_seesaw.rotaryio import IncrementalEncoder
import audiomixer
import busio
import simpleio
i2c = busio.I2C(board.SCL, board.SDA, frequency=800000)
int1 = digitalio.DigitalInOut(board.ACCELEROMETER_INTERRUPT)
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, int1=int1)
lis3dh.range = adafruit_lis3dh.RANGE_2_G
ss_enc0 = Seesaw(i2c, addr=0x36)
ss_enc0.pin_mode(24, ss_enc0.INPUT_PULLUP)
button0 = DigitalIO(ss_enc0, 24)
button0_state = False
enc0 = IncrementalEncoder(ss_enc0)
ss_enc0.set_GPIO_interrupts(1 << 24, True)
ss_enc0.enable_encoder_interrupt(encoder=0)
ss_enc1 = Seesaw(i2c, addr=0x37)
ss_enc1.pin_mode(24, ss_enc1.INPUT_PULLUP)
button1 = DigitalIO(ss_enc1, 24)
button1_state = False
enc1 = IncrementalEncoder(ss_enc1)
ss_enc1.set_GPIO_interrupts(1 << 24, True)
ss_enc1.enable_encoder_interrupt(encoder=0)
ss_enc2 = Seesaw(i2c, addr=0x38)
ss_enc2.pin_mode(24, ss_enc2.INPUT_PULLUP)
button2 = DigitalIO(ss_enc2, 24)
button2_state = False
enc2 = IncrementalEncoder(ss_enc2)
ss_enc2.set_GPIO_interrupts(1 << 24, True)
ss_enc2.enable_encoder_interrupt(encoder=0)
neokey0 = Seesaw(i2c, addr=0x30)
neokey1 = Seesaw(i2c, addr=0x31)
keys = []
for k in range(4, 8):
key0 = DigitalIO(neokey0, k)
key0.direction = digitalio.Direction.INPUT
key0.pull = digitalio.Pull.UP
keys.append(key0)
for k in range(4, 8):
key1 = DigitalIO(neokey1, k)
key1.direction = digitalio.Direction.INPUT
key1.pull = digitalio.Pull.UP
keys.append(key1)
NUM_PIXELS = 8
NEOPIXEL_PIN = board.EXTERNAL_NEOPIXELS
pixels = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, auto_write=True)
pixels.brightness = 0.1
enable = digitalio.DigitalInOut(board.EXTERNAL_POWER)
enable.direction = digitalio.Direction.OUTPUT
enable.value = True
strum_switch = digitalio.DigitalInOut(board.D12)
strum_switch.direction = digitalio.Direction.INPUT
strum_switch.pull = digitalio.Pull.UP
int_keys = keypad.Keys((board.D5, board.D6, board.D9, board.EXTERNAL_BUTTON),
value_when_pressed=False, pull=True, interval = 0.001)
key_pix0 = SS_NeoPixel(neokey0, 3, 4, auto_write = True)
key_pix0.brightness = 1
key_pix1 = SS_NeoPixel(neokey1, 3, 4, auto_write = True)
key_pix1.brightness = 1
key_pixels = [key_pix0[0], key_pix0[1], key_pix0[2], key_pix0[3],
key_pix1[0], key_pix1[1], key_pix1[2], key_pix1[3]]
# i2s audio
audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
key_states = [False, False, False, False, False, False, False, False]
key_colors = [0xFF0000, 0xFF5500, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF, 0x5500FF, 0xFF00FF]
for c in range(4):
key_pix0[c] = key_colors[c]
key_pix1[c] = key_colors[c + 4]
SAMPLE_RATE = 22050
SAMPLE_SIZE = 512
VOLUME = 32000
square = np.concatenate((np.ones(SAMPLE_SIZE//2, dtype=np.int16)*VOLUME,np.ones(SAMPLE_SIZE//2,
dtype=np.int16)*-VOLUME))
sine = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME,
dtype=np.int16)
amp_env = synthio.Envelope(attack_time=0.01,
sustain_level=0.5,
release_time=0.1)
lfo_tremo = synthio.LFO(waveform=sine, rate=5)
synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE)
synth_notes = []
octave = 12
mult = 2
octave_range = 6
tones = [36, 40, 43, 47, 50, 53, 57, 60]
diatonic = 0
t = [0, 0, 0, 0, 0, 0, 0, 0]
current_freq = []
for s in range(8):
t[s] = tones[s] + (octave * mult)
print(t[s])
note = synthio.Note(frequency=synthio.midi_to_hz(t[s]),
envelope=amp_env, waveform=square,
amplitude = lfo_tremo)
synth_notes.append(note)
current_freq.append(synth_notes[s].frequency)
lfo_frequency = 2000
lfo_resonance = 1.5
lpf = synth.low_pass_filter(lfo_frequency, lfo_resonance)
hpf = synth.high_pass_filter(lfo_frequency, lfo_resonance)
synth_volume = 0.3
last_pos0 = synth_volume
last_pos1 = 0
last_pos2 = 0
mixer = audiomixer.Mixer(voice_count=1, sample_rate=SAMPLE_RATE, channel_count=1,
bits_per_sample=16, samples_signed=True, buffer_size=2048)
audio.play(mixer)
mixer.voice[0].play(synth)
mixer.voice[0].level = synth_volume
int_number = 0
def normalize(val, min_v, max_v):
return max(min(max_v, val), min_v)
key_pressed = 0
strum = False
last_strum = strum
tremolo = True
pressed_notes = []
last_y = 0
accel_time = 0.1
accel_clock = ticks_ms()
accel_time = int(accel_time * 1000)
while True:
interrupt_event = int_keys.events.get()
strum = strum_switch.value
if last_strum != strum:
synth.release_all()
last_strum = strum
if interrupt_event:
int_number = interrupt_event.key_number
if int_number == 0 and interrupt_event.pressed:
pos0 = -enc0.position
if pos0 != last_pos0:
if pos0 > last_pos0:
synth_volume = synth_volume + 0.1
else:
synth_volume = synth_volume - 0.1
synth_volume = normalize(synth_volume, 0.0, 1.0)
print(synth_volume)
mixer.voice[0].level = synth_volume
last_pos0 = pos0
if not button0.value and not button0_state:
button0_state = True
if mixer.voice[0].level > 0:
mixer.voice[0].level = 0
else:
mixer.voice[0].level = synth_volume
if button0.value and button0_state:
button0_state = False
elif int_number == 1 and interrupt_event.pressed:
pos1 = -enc1.position
if pos1 != last_pos1:
if pos1 > last_pos1:
lfo_tremo.rate = lfo_tremo.rate + 0.5
else:
lfo_tremo.rate = lfo_tremo.rate - 0.5
lfo_tremo.rate = normalize(lfo_tremo.rate, 1.0, 20.0)
print(lfo_tremo.rate)
last_pos1 = pos1
if tremolo:
if not button1.value and not button1_state:
button1_state = True
tremolo = False
for i in range(8):
synth_notes[i].amplitude = 1.0
if button1.value and button1_state:
button1_state = False
else:
if not button1.value and not button1_state:
button1_state = True
tremolo = True
for i in range(8):
lfo_tremo.rate = 5.0
synth_notes[i].amplitude = lfo_tremo
if button1.value and button1_state:
button1_state = False
elif int_number == 2 and interrupt_event.pressed:
pos2 = -enc2.position
if pos2 != last_pos2:
if pos2 > last_pos2:
mult = (mult + 1) % octave_range
print(mult)
for o in range(8):
t[o] = tones[o] + (octave * mult)
print(t[o])
synth_notes[o].frequency = synthio.midi_to_hz(t[o])
current_freq[o] = synth_notes[o].frequency
else:
mult = (mult - 1) % octave_range
print(mult)
for o in range(8):
t[o] = tones[o] + (octave * mult)
print(t[o])
synth_notes[o].frequency = synthio.midi_to_hz(t[o])
current_freq[o] = synth_notes[o].frequency
last_pos2 = pos2
if not button2.value and not button2_state:
button2_state = True
diatonic = (diatonic + 1) % 2
print(diatonic)
if diatonic == 0:
new_tones = [36, 40, 43, 47, 50, 53, 57, 60]
for r in range(8):
tones[r] = new_tones[r]
print(tones[r])
else:
new_tones = [36, 38, 40, 41, 43, 45, 47, 48]
for r in range(8):
tones[r] = new_tones[r]
print(tones[r])
for x in range(8):
t[x] = tones[x] + (octave * mult)
print(t[x])
synth_notes[x].frequency = synthio.midi_to_hz(t[x])
current_freq[x] = synth_notes[x].frequency
if button2.value and button2_state:
button2_state = False
elif int_number == 3 and interrupt_event.pressed:
if strum:
for i in range(0, 8):
if not keys[i].value:
pixels.fill(key_colors[i])
pixels.show()
synth.press(synth_notes[i])
elif int_number == 3 and interrupt_event.released:
if strum:
synth.release_all()
ss_enc0.get_GPIO_interrupt_flag()
ss_enc1.get_GPIO_interrupt_flag()
ss_enc2.get_GPIO_interrupt_flag()
if ticks_diff(ticks_ms(), accel_clock) >= accel_time:
x, y, z = [
value / adafruit_lis3dh.STANDARD_GRAVITY for value in lis3dh.acceleration
]
if last_y != y:
if abs(last_y - y) > 0.01:
# print(f"x = {x:.3f} G, y = {y:.3f} G, z = {z:.3f} G")
if y < -0.500:
mapped_freq = simpleio.map_range(y, -0.300, -1, 2000, 10000)
mapped_resonance = simpleio.map_range(y, -0.300, -1, 1.5, 8)
hpf = synth.high_pass_filter(mapped_freq, mapped_resonance)
for i in range(0, 8):
synth_notes[i].filter = hpf
elif y > 0.200:
mapped_freq = simpleio.map_range(y, 0.200, 1, 2000, 100)
mapped_resonance = simpleio.map_range(y, 0.200, 1, 2, 0.5)
lpf = synth.low_pass_filter(mapped_freq, mapped_resonance)
for i in range(0, 8):
synth_notes[i].filter = lpf
else:
for i in range(0, 8):
synth_notes[i].filter = None
last_y = y
accel_clock = ticks_add(accel_clock, accel_time)
if not strum:
for i in range(0, 8):
if not keys[i].value and not key_states[i]:
pixels.fill(key_colors[i])
pixels.show()
synth.press(synth_notes[i])
key_states[i] = True
if keys[i].value and key_states[i]:
key_pixels[i] = key_colors[i]
synth.release(synth_notes[i])
key_states[i] = False
Upload the Code and Libraries to the RP2040 Prop-Maker Feather
After downloading the Project Bundle, plug your RP2040 Prop-Maker Feather into the computer's USB port with a known good USB data+power cable. 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 RP2040 Prop-Maker Feather's CIRCUITPY drive.
- lib folder
- code.py
Your RP2040 Prop-Maker Feather CIRCUITPY drive should look like this after copying the lib folder and the code.py file.
How the CircuitPython Code Works
The code begins by instantiating the onboard accelerometer, three seesaw rotary encoders and both 1x4 NeoKeys over I2C. The rotary encoders are using their interrupt pins.
i2c = busio.I2C(board.SCL, board.SDA, frequency=800000) int1 = digitalio.DigitalInOut(board.ACCELEROMETER_INTERRUPT) lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, int1=int1) lis3dh.range = adafruit_lis3dh.RANGE_2_G ss_enc0 = Seesaw(i2c, addr=0x36) ss_enc0.pin_mode(24, ss_enc0.INPUT_PULLUP) button0 = DigitalIO(ss_enc0, 24) button0_state = False enc0 = IncrementalEncoder(ss_enc0) ss_enc0.set_GPIO_interrupts(1 << 24, True) ss_enc0.enable_encoder_interrupt(encoder=0) ss_enc1 = Seesaw(i2c, addr=0x37) ss_enc1.pin_mode(24, ss_enc1.INPUT_PULLUP) button1 = DigitalIO(ss_enc1, 24) button1_state = False enc1 = IncrementalEncoder(ss_enc1) ss_enc1.set_GPIO_interrupts(1 << 24, True) ss_enc1.enable_encoder_interrupt(encoder=0) ss_enc2 = Seesaw(i2c, addr=0x38) ss_enc2.pin_mode(24, ss_enc2.INPUT_PULLUP) button2 = DigitalIO(ss_enc2, 24) button2_state = False enc2 = IncrementalEncoder(ss_enc2) ss_enc2.set_GPIO_interrupts(1 << 24, True) ss_enc2.enable_encoder_interrupt(encoder=0) neokey0 = Seesaw(i2c, addr=0x30) neokey1 = Seesaw(i2c, addr=0x31)
keys = []
for k in range(4, 8):
key0 = DigitalIO(neokey0, k)
key0.direction = digitalio.Direction.INPUT
key0.pull = digitalio.Pull.UP
keys.append(key0)
for k in range(4, 8):
key1 = DigitalIO(neokey1, k)
key1.direction = digitalio.Direction.INPUT
key1.pull = digitalio.Pull.UP
keys.append(key1)
key_pix0 = SS_NeoPixel(neokey0, 3, 4, auto_write = True)
key_pix0.brightness = 1
key_pix1 = SS_NeoPixel(neokey1, 3, 4, auto_write = True)
key_pix1.brightness = 1
key_pixels = [key_pix0[0], key_pix0[1], key_pix0[2], key_pix0[3],
key_pix1[0], key_pix1[1], key_pix1[2], key_pix1[3]]
key_states = [False, False, False, False, False, False, False, False]
key_colors = [0xFF0000, 0xFF5500, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF, 0x5500FF, 0xFF00FF]
for c in range(4):
key_pix0[c] = key_colors[c]
key_pix1[c] = key_colors[c + 4]
synthio
This guitar uses synthio to make music. Audio is output via the onboard I2S amp on the Feather. The note frequencies are passed as MIDI note numbers so that its mathematically easy to increase or decrease octaves in the loop.
audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
SAMPLE_RATE = 22050
SAMPLE_SIZE = 512
VOLUME = 32000
square = np.concatenate((np.ones(SAMPLE_SIZE//2, dtype=np.int16)*VOLUME,np.ones(SAMPLE_SIZE//2,
dtype=np.int16)*-VOLUME))
sine = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME,
dtype=np.int16)
amp_env = synthio.Envelope(attack_time=0.01,
sustain_level=0.5,
release_time=0.1)
lfo_tremo = synthio.LFO(waveform=sine, rate=5)
synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE)
synth_notes = []
octave = 12
mult = 2
octave_range = 6
tones = [36, 40, 43, 47, 50, 53, 57, 60]
diatonic = 0
t = [0, 0, 0, 0, 0, 0, 0, 0]
current_freq = []
for s in range(8):
t[s] = tones[s] + (octave * mult)
print(t[s])
note = synthio.Note(frequency=synthio.midi_to_hz(t[s]),
envelope=amp_env, waveform=square,
amplitude = lfo_tremo)
synth_notes.append(note)
current_freq.append(synth_notes[s].frequency)
lfo_frequency = 2000
lfo_resonance = 1.5
lpf = synth.low_pass_filter(lfo_frequency, lfo_resonance)
hpf = synth.high_pass_filter(lfo_frequency, lfo_resonance)
The audio output is passed thru a Mixer object, allowing for volume control through software.
synth_volume = 0.3
last_pos0 = synth_volume
last_pos1 = 0
last_pos2 = 0
mixer = audiomixer.Mixer(voice_count=1, sample_rate=SAMPLE_RATE, channel_count=1,
bits_per_sample=16, samples_signed=True, buffer_size=2048)
audio.play(mixer)
mixer.voice[0].play(synth)
mixer.voice[0].level = synth_volume
The Loop
In the loop, the interrupt pins and strummer pin are scanned for any new events. Encoder 0 controls the volume of the synth. If you press the encoder switch it will mute the synth.
if interrupt_event:
int_number = interrupt_event.key_number
if int_number == 0 and interrupt_event.pressed:
pos0 = -enc0.position
if pos0 != last_pos0:
if pos0 > last_pos0:
synth_volume = synth_volume + 0.1
else:
synth_volume = synth_volume - 0.1
synth_volume = normalize(synth_volume, 0.0, 1.0)
print(synth_volume)
mixer.voice[0].level = synth_volume
last_pos0 = pos0
if not button0.value and not button0_state:
button0_state = True
if mixer.voice[0].level > 0:
mixer.voice[0].level = 0
else:
mixer.voice[0].level = synth_volume
if button0.value and button0_state:
button0_state = False
Encoder 1 controls the LFO rate. If you press the encoder button it enables or disables the LFO.
elif int_number == 1 and interrupt_event.pressed:
pos1 = -enc1.position
if pos1 != last_pos1:
if pos1 > last_pos1:
lfo_tremo.rate = lfo_tremo.rate + 0.5
else:
lfo_tremo.rate = lfo_tremo.rate - 0.5
lfo_tremo.rate = normalize(lfo_tremo.rate, 1.0, 20.0)
print(lfo_tremo.rate)
last_pos1 = pos1
if tremolo:
if not button1.value and not button1_state:
button1_state = True
tremolo = False
for i in range(8):
synth_notes[i].amplitude = 1.0
if button1.value and button1_state:
button1_state = False
else:
if not button1.value and not button1_state:
button1_state = True
tremolo = True
for i in range(8):
lfo_tremo.rate = 5.0
synth_notes[i].amplitude = lfo_tremo
if button1.value and button1_state:
button1_state = False
Encoder 2 controls the note frequencies. Turning the encoder increases or decreases the octave range of the guitar. Pressing the encoder button switches between triad or diatonic mode.
elif int_number == 2 and interrupt_event.pressed:
pos2 = -enc2.position
if pos2 != last_pos2:
if pos2 > last_pos2:
mult = (mult + 1) % octave_range
print(mult)
for o in range(8):
t[o] = tones[o] + (octave * mult)
print(t[o])
synth_notes[o].frequency = synthio.midi_to_hz(t[o])
current_freq[o] = synth_notes[o].frequency
else:
mult = (mult - 1) % octave_range
print(mult)
for o in range(8):
t[o] = tones[o] + (octave * mult)
print(t[o])
synth_notes[o].frequency = synthio.midi_to_hz(t[o])
current_freq[o] = synth_notes[o].frequency
last_pos2 = pos2
if not button2.value and not button2_state:
button2_state = True
diatonic = (diatonic + 1) % 2
print(diatonic)
if diatonic == 0:
new_tones = [36, 40, 43, 47, 50, 53, 57, 60]
for r in range(8):
tones[r] = new_tones[r]
print(tones[r])
else:
new_tones = [36, 38, 40, 41, 43, 45, 47, 48]
for r in range(8):
tones[r] = new_tones[r]
print(tones[r])
for x in range(8):
t[x] = tones[x] + (octave * mult)
print(t[x])
synth_notes[x].frequency = synthio.midi_to_hz(t[x])
current_freq[x] = synth_notes[x].frequency
if button2.value and button2_state:
button2_state = False
The final Keypad pin is for the strum bar. If strum mode is enabled, it plays the currently pressed synth note. After scanning, the GPIO interrupt flags are called for the three encoders. This resets the interrupt pins.
elif int_number == 3 and interrupt_event.pressed:
if strum:
for i in range(0, 8):
if not keys[i].value:
pixels.fill(key_colors[i])
pixels.show()
synth.press(synth_notes[i])
elif int_number == 3 and interrupt_event.released:
if strum:
synth.release_all()
ss_enc0.get_GPIO_interrupt_flag()
ss_enc1.get_GPIO_interrupt_flag()
ss_enc2.get_GPIO_interrupt_flag()
Filters
The accelerometer controls whether a high pass or low pass filter is applied to the synth. If the guitar is tilted up, a high pass filter is enabled. If the guitar is tilted down, a low pass filter is enabled. The filter frequency and resonance are affected by the readings from the accelerometer.
if ticks_diff(ticks_ms(), accel_clock) >= accel_time:
x, y, z = [
value / adafruit_lis3dh.STANDARD_GRAVITY for value in lis3dh.acceleration
]
if last_y != y:
if abs(last_y - y) > 0.01:
# print(f"x = {x:.3f} G, y = {y:.3f} G, z = {z:.3f} G")
if y < -0.500:
mapped_freq = simpleio.map_range(y, -0.300, -1, 2000, 10000)
mapped_resonance = simpleio.map_range(y, -0.300, -1, 1.5, 8)
hpf = synth.high_pass_filter(mapped_freq, mapped_resonance)
for i in range(0, 8):
synth_notes[i].filter = hpf
elif y > 0.200:
mapped_freq = simpleio.map_range(y, 0.200, 1, 2000, 100)
mapped_resonance = simpleio.map_range(y, 0.200, 1, 2, 0.5)
lpf = synth.low_pass_filter(mapped_freq, mapped_resonance)
for i in range(0, 8):
synth_notes[i].filter = lpf
else:
for i in range(0, 8):
synth_notes[i].filter = None
last_y = y
accel_clock = ticks_add(accel_clock, accel_time)
if not strum:
for i in range(0, 8):
if not keys[i].value and not key_states[i]:
pixels.fill(key_colors[i])
pixels.show()
synth.press(synth_notes[i])
key_states[i] = True
if keys[i].value and key_states[i]:
key_pixels[i] = key_colors[i]
synth.release(synth_notes[i])
key_states[i] = False
Page last edited January 17, 2025
Text editor powered by tinymce.