Once you've finished setting up your Feather RP2040 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 as a zipped folder.
# SPDX-FileCopyrightText: 2023 Liz Clark for Adafruit Industries # SPDX-License-Identifier: MIT from random import randint import ulab.numpy as np import board import audiobusio import audiomixer import synthio import simpleio from adafruit_ticks import ticks_ms, ticks_add, ticks_diff from adafruit_ht16k33 import segments from adafruit_ht16k33.matrix import Matrix8x8x2 from adafruit_seesaw import seesaw, rotaryio, digitalio SAMPLE_RATE = 44100 SAMPLE_SIZE = 256 VOLUME = 5000 # waveforms, envelopes and synth setup 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, 4*np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME, dtype=np.int16) saw = np.linspace(VOLUME, -VOLUME, num=SAMPLE_SIZE, dtype=np.int16) noise = np.array([randint(-VOLUME, VOLUME) for i in range(SAMPLE_SIZE)], dtype=np.int16) lfo = synthio.LFO(rate = .5, waveform = sine) amp_env0 = synthio.Envelope(attack_time=0.1, decay_time = 0.1, release_time=0.1, attack_level=1, sustain_level=0.05) amp_env1 = synthio.Envelope(attack_time=0.05, decay_time = 0.1, release_time=0.1, attack_level=1, sustain_level=0.05) # synth plays the notes synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE) # these are the notes synth0 = synthio.Note(frequency = 0.0, envelope=amp_env0, waveform=square, ring_frequency = 0, ring_bend = lfo, ring_waveform = sine) synth1 = synthio.Note(frequency = 0.0, envelope=amp_env1, waveform=sine, ring_frequency = 0, ring_bend = lfo, ring_waveform = sine) synth2 = synthio.Note(frequency = 0.0, envelope=amp_env0, waveform=square, ring_frequency = 0, ring_bend = lfo, ring_waveform = sine) synth3 = synthio.Note(frequency = 0.0, envelope=amp_env1, waveform=sine, ring_frequency = 0, ring_bend = lfo, ring_waveform = sine) synths = [synth0, synth1, synth2, synth3] wave_names = ["SQUR", "SINE", "SAW ", "NOIZ"] waveforms = [square, sine, saw, noise] synth0_wave = 0 synth1_wave = 1 synth2_wave = 0 synth3_wave = 1 # i2s amp setup audio = audiobusio.I2SOut(bit_clock=board.D10, word_select=board.D11, data=board.D9) mixer = audiomixer.Mixer(voice_count=4, sample_rate=SAMPLE_RATE, channel_count=1, bits_per_sample=16, samples_signed=True, buffer_size=2048 ) audio.play(mixer) vol_val = 2 mixer.voice[0].play(synth) mixer.voice[0].level = 0.3 # these are the triads, all major c_tones = [130.81, 164.81, 196.00] g_tones = [196.00, 246.94, 293.66] d_tones = [146.83, 185.00, 220.00] a_tones = [220.00, 277.18, 329.63] e_tones = [164.81, 207.65, 246.94] b_tones = [246.94, 311.13, 369.99] fsharp_tones = [185.00, 233.08, 277.18] csharp_tones = [138.59, 174.61, 207.65] aflat_tones = [207.65, 261.63, 311.13] eflat_tones = [155.56, 196.00, 233.08] bflat_tones = [233.08, 293.66, 349.23] f_tones = [174.61, 220.00, 261.63] # names for the alphanumeric displays chord_names = ["Cmaj", "Gmaj", "Dmaj", "Amaj", "Emaj", "Bmaj", "F#ma", "C#ma", "Abma", "Ebma", "Bbma", "Fmaj"] chords = [c_tones, g_tones, d_tones, a_tones, e_tones, b_tones, fsharp_tones, csharp_tones, aflat_tones, eflat_tones, bflat_tones, f_tones] # i2c setup i2c = board.I2C() # the encoders seesaw0 = seesaw.Seesaw(i2c, addr=0x49) seesaw1 = seesaw.Seesaw(i2c, addr=0x4A) seesaw2 = seesaw.Seesaw(i2c, addr=0x4B) seesaw3 = seesaw.Seesaw(i2c, addr=0x4C) menu_seesaw = seesaw.Seesaw(i2c, addr=0x4D) # the alphanumeric displays display0 = segments.Seg14x4(i2c, address=0x70) display1 = segments.Seg14x4(i2c, address=0x71) display2 = segments.Seg14x4(i2c, address=0x72) display3 = segments.Seg14x4(i2c, address=0x73) menu_display = segments.Seg14x4(i2c, address=0x74) # the matrix matrix0 = Matrix8x8x2(i2c, address=0x75) seesaws = [seesaw0, seesaw1, seesaw2, seesaw3, menu_seesaw] buttons0 = [] buttons1 = [] buttons2 = [] buttons3 = [] menu_buttons = [] button0_states = [] button1_states = [] button2_states = [] button3_states = [] menu_states = [] button0_names = ["Select", "Up", "Left", "Down", "Right"] # setup the buttons on all of the encoders for i in range(1, 6): seesaw0.pin_mode(i, seesaw0.INPUT_PULLUP) seesaw1.pin_mode(i, seesaw1.INPUT_PULLUP) seesaw2.pin_mode(i, seesaw2.INPUT_PULLUP) seesaw3.pin_mode(i, seesaw3.INPUT_PULLUP) menu_seesaw.pin_mode(i, menu_seesaw.INPUT_PULLUP) buttons0.append(digitalio.DigitalIO(seesaw0, i)) buttons1.append(digitalio.DigitalIO(seesaw1, i)) buttons2.append(digitalio.DigitalIO(seesaw2, i)) buttons3.append(digitalio.DigitalIO(seesaw3, i)) menu_buttons.append(digitalio.DigitalIO(menu_seesaw, i)) button0_states.append(False) button1_states.append(False) button2_states.append(False) button3_states.append(False) menu_states.append(False) # make all of the encoders encoder0 = rotaryio.IncrementalEncoder(seesaw0) last_position0 = 0 encoder1 = rotaryio.IncrementalEncoder(seesaw1) last_position1 = 0 encoder2 = rotaryio.IncrementalEncoder(seesaw2) last_position2 = 0 encoder3 = rotaryio.IncrementalEncoder(seesaw3) last_position3 = 0 menu_enc = rotaryio.IncrementalEncoder(menu_seesaw) last_menuPosition = 0 # Python Implementation of Björklund's Algorithm by Brian House # MIT License 2011 # https://github.com/brianhouse/bjorklund def bjorklund(steps, pulses): steps = int(steps) pulses = int(pulses) if pulses > steps: raise ValueError pattern = [] counts = [] remainders = [] divisor = steps - pulses remainders.append(pulses) level = 0 while True: counts.append(divisor // remainders[level]) remainders.append(divisor % remainders[level]) divisor = remainders[level] level = level + 1 if remainders[level] <= 1: break counts.append(divisor) def build(level): if level == -1: pattern.append(0) elif level == -2: pattern.append(1) else: for _ in range(0, counts[level]): build(level - 1) if remainders[level] != 0: build(level - 2) build(level) p = pattern.index(1) pattern = pattern[p:] + pattern[0:p] return pattern # using ticks for time tracking clock = ticks_ms() # default BPM bpm = 120 # beat divison beat_div = [15, 30, 60, 120, 240] beat_index = 2 beat_names = ["1/16", "1/8 ", "1/4 ", "1/2 ", "HOLE"] delay = int((beat_div[beat_index] / bpm) * 1000) # variables for euclidean c0 = 0 c1 = 0 c2 = 0 c3 = 0 r0 = 0 r1 = 0 r2 = 0 r3 = 0 last_r0 = 0 last_r1 = 0 last_r2 = 0 last_r3 = 0 euclid0_steps = 8 euclid0_pulses = 4 euclid1_steps = 8 euclid1_pulses = 4 euclid2_steps = 8 euclid2_pulses = 4 euclid3_steps = 8 euclid3_pulses = 4 rhythm0 = bjorklund(euclid0_steps, euclid0_pulses) rhythm1 = bjorklund(euclid1_steps, euclid1_pulses) rhythm2 = bjorklund(euclid2_steps, euclid2_pulses) rhythm3 = bjorklund(euclid3_steps, euclid3_pulses) # read buttons to update Euclidean rhythms # pylint: disable=too-many-branches def read_buttons(button_array, button_states, euc, e_step, e_pulse, the_step): for b in range(5): if not button_array[b].value and button_states[b] is False: button_states[b] = True if button0_names[b] == "Select": e_step = 8 e_pulse = 4 if the_step >= e_step: the_step = 0 elif button0_names[b] == "Up": if e_step > 16: e_step = 16 else: e_step += 1 elif button0_names[b] == "Down": if e_step < 1: e_step = 1 else: e_step -= 1 if the_step >= e_step: the_step = 0 elif button0_names[b] == "Left": e_pulse -= 1 e_pulse = max(e_pulse, 1) else: e_pulse += 1 e_pulse = min(e_pulse, e_step) euc = bjorklund(e_step, e_pulse) if button_array[b].value and button_states[b] is True: button_states[b] = False if button0_names[b] in ("Select", "Up", "Down"): matrix0.fill(matrix0.LED_OFF) draw_steps(euclid0_steps, 0) draw_steps(euclid1_steps, 2) draw_steps(euclid2_steps, 4) draw_steps(euclid3_steps, 6) return euc, e_step, e_pulse, the_step # play euclidean rhythms and update matrix def play_euclidean(this_synth, n, the_rhythm, rhythm_count, last_count, c, matrix_slot): if last_count <= 7: matrix0[matrix_slot, last_count] = matrix0.LED_GREEN else: c -= 1 matrix0[matrix_slot + 1, (last_count - last_count) + c] = matrix0.LED_GREEN c += 1 if the_rhythm[rhythm_count] == 1: this_synth.frequency = n[randint(0, 2)] synth.press(this_synth) if rhythm_count <= 7: matrix0[matrix_slot, rhythm_count] = matrix0.LED_RED else: matrix0[matrix_slot + 1, (rhythm_count - rhythm_count) + c] = matrix0.LED_RED c += 1 else: synth.release(this_synth) if rhythm_count > 7: c += 1 last_count = rhythm_count rhythm_count += 1 if rhythm_count >= len(the_rhythm): rhythm_count = 0 if rhythm_count == 1: c = 0 return rhythm_count, last_count, c # initial matrix draw def draw_steps(euc_steps, col): dif = 0 for m in range(euc_steps): if m <= 7: matrix0[col, m] = matrix0.LED_GREEN else: matrix0[col + 1, (m - m) + dif] = matrix0.LED_GREEN dif += 1 draw_steps(euclid0_steps, 0) draw_steps(euclid1_steps, 2) draw_steps(euclid2_steps, 4) draw_steps(euclid3_steps, 6) # clocks for playing euclidean and reading menu encoder enc_clock = ticks_ms() menu_clock = ticks_ms() # the modes menu modes = ["PLAY", "EUC ", "BPM ", "BEAT", "ADSR", "WAVE", "RING", "LFO ", "VOL "] mode_index = 0 mode = modes[mode_index] menu_display.print(f" {mode}") # default chords chord0_sel = 0 chord1_sel = 1 chord2_sel = 0 chord3_sel = 1 display0.print(chord_names[chord0_sel]) display1.print(chord_names[chord1_sel]) display2.print(chord_names[chord2_sel]) display3.print(chord_names[chord3_sel]) # arrays of individual buttons select_buttons = [buttons0[0], buttons1[0], buttons2[0], buttons3[0]] left_buttons = [buttons0[2], buttons1[2], buttons2[2], buttons3[2]] right_buttons = [buttons0[4], buttons1[4], buttons2[4], buttons3[4]] select_states = [button0_states[0], button1_states[0], button2_states[0], button3_states[0]] left_states = [button0_states[2], button1_states[2], button2_states[2], button3_states[2]] right_states = [button0_states[4], button1_states[4], button2_states[4], button3_states[4]] select_index = 0 left_index = 0 right_index = 0 # adsr mode adsr_names = ["A", "D", "S", "R"] synth_adsr_indexes = [0, 0, 0, 0] adsr_properties = [0, 1, 4, 2] adsr0_values = [amp_env0.attack_time, amp_env0.decay_time, amp_env0.sustain_level, amp_env0.release_time] adsr1_values = [amp_env1.attack_time, amp_env1.decay_time, amp_env1.sustain_level, amp_env1.release_time] adsr2_values = [amp_env0.attack_time, amp_env0.decay_time, amp_env0.sustain_level, amp_env0.release_time] adsr3_values = [amp_env1.attack_time, amp_env1.decay_time, amp_env1.sustain_level, amp_env1.release_time] all_adsr_values = [adsr0_values, adsr1_values, adsr2_values, adsr3_values] adsr0_val = int(simpleio.map_range(amp_env0.attack_time, 0.0, 1.0, 0, 19)) adsr1_val = int(simpleio.map_range(amp_env0.decay_time, 0.0, 1.0, 0, 19)) adsr2_val = int(simpleio.map_range(amp_env0.sustain_level, 0.0, 1.0, 0, 19)) adsr3_val = int(simpleio.map_range(amp_env0.release_time, 0.0, 1.0, 0, 19)) clock_stretch = False ring0_val = 0 ring1_val = 0 ring2_val = 0 ring3_val = 0 lfo_val = 0 # used to play/pause play_states = [True, True, True, True] while True: # rotary encoder reading if ticks_diff(ticks_ms(), enc_clock) >= 100: position0 = encoder0.position position1 = encoder1.position position2 = encoder2.position position3 = encoder3.position menuPosition = menu_enc.position # menu changes mode if menuPosition != last_menuPosition: if menuPosition > last_menuPosition: mode_index = (mode_index + 1) % len(modes) else: mode_index = (mode_index - 1) % len(modes) if mode in ("EUC ", "ADSR"): clock_stretch = True if mode in ("PLAY", "BPM ", "BEAT", "WAVE") and clock_stretch: clock = ticks_ms() clock_stretch = False mode = modes[mode_index] menu_display.print(f" {mode}") last_menuPosition = menuPosition # encoder functionality depends on mode # encoder 0 has most functionality if position0 != last_position0: if position0 > last_position0: if mode == "PLAY": chord0_sel = (chord0_sel + 1) % len(chords) display0.print(chord_names[chord0_sel]) elif mode == "BEAT": beat_index = (beat_index + 1) % 5 delay = int((beat_div[beat_index] / bpm) * 1000) display0.print(f" {beat_names[beat_index]}") elif mode == "BPM ": bpm += 1 delay = int((beat_div[beat_index] / bpm) * 1000) display0.print(f" {bpm}") elif mode == "ADSR": adsr0_val = (adsr0_val + 1) % 20 mapped_val = simpleio.map_range(adsr0_val, 0, 19, 0.0, 1.0) all_adsr_values[0][synth_adsr_indexes[0]] = mapped_val the_env = synthio.Envelope(attack_time=all_adsr_values[0][0], decay_time = all_adsr_values[0][1], release_time=all_adsr_values[0][3], attack_level=1, sustain_level=all_adsr_values[0][2]) synth0.envelope = the_env elif mode == "WAVE": synth0_wave = (synth0_wave + 1) % len(wave_names) synth0.waveform = waveforms[synth0_wave] elif mode == "RING": ring0_val = (ring0_val + 1) % 25 mapped_val = simpleio.map_range(ring0_val, 0, 24, 0.0, 220.0) synth0.ring_frequency = mapped_val elif mode == "LFO ": lfo_val = (lfo_val + 1) % 10 mapped_val = simpleio.map_range(lfo_val, 0, 9, 0.0, 5.0) lfo.rate = mapped_val elif mode == "VOL ": vol_val = (vol_val + 1) % 10 mapped_val = simpleio.map_range(vol_val, 0, 9, 0.0, 1.0) mixer.voice[0].level = mapped_val else: if mode == "PLAY": chord0_sel = (chord0_sel - 1) % len(chords) display0.print(chord_names[chord0_sel]) elif mode == "BEAT": beat_index = (beat_index - 1) % 5 delay = int((beat_div[beat_index] / bpm) * 1000) display0.print(f" {beat_names[beat_index]}") elif mode == "BPM ": bpm -= 1 display0.print(f" {bpm}") elif mode == "ADSR": adsr0_val = (adsr0_val - 1) % 20 mapped_val = simpleio.map_range(adsr0_val, 0, 19, 0.0, 1.0) all_adsr_values[0][synth_adsr_indexes[0]] = mapped_val the_env = synthio.Envelope(attack_time=all_adsr_values[0][0], decay_time = all_adsr_values[0][1], release_time=all_adsr_values[0][3], attack_level=1, sustain_level=all_adsr_values[0][2]) synth0.envelope = the_env elif mode == "WAVE": synth0_wave = (synth0_wave - 1) % len(wave_names) synth0.waveform = waveforms[synth0_wave] elif mode == "RING": ring0_val = (ring0_val - 1) % 25 mapped_val = simpleio.map_range(ring0_val, 0, 24, 0.0, 220.0) synth0.ring_frequency = mapped_val elif mode == "LFO ": lfo_val = (lfo_val - 1) % 10 mapped_val = simpleio.map_range(lfo_val, 0, 9, 0.0, 5.0) lfo.rate = mapped_val elif mode == "VOL ": vol_val = (vol_val - 1) % 10 mapped_val = simpleio.map_range(vol_val, 0, 9, 0.0, 1.0) mixer.voice[0].level = mapped_val last_position0 = position0 if position1 != last_position1: if position1 > last_position1: if mode == "PLAY": chord1_sel = (chord1_sel + 1) % len(chords) display1.print(chord_names[chord1_sel]) elif mode == "ADSR": adsr1_val = (adsr1_val + 1) % 20 mapped_val = simpleio.map_range(adsr1_val, 0, 19, 0.0, 1.0) all_adsr_values[1][synth_adsr_indexes[1]] = mapped_val the_env = synthio.Envelope(attack_time=all_adsr_values[1][0], decay_time = all_adsr_values[1][1], release_time=all_adsr_values[1][3], attack_level=1, sustain_level=all_adsr_values[1][2]) synth1.envelope = the_env elif mode == "WAVE": synth1_wave = (synth1_wave + 1) % len(wave_names) synth1.waveform = waveforms[synth1_wave] elif mode == "RING": ring1_val = (ring1_val + 1) % 25 mapped_val = simpleio.map_range(ring1_val, 0, 24, 0.0, 220.0) synth1.ring_frequency = mapped_val else: if mode == "PLAY": chord1_sel = (chord1_sel - 1) % len(chords) display1.print(chord_names[chord1_sel]) elif mode == "ADSR": adsr1_val = (adsr1_val - 1) % 20 mapped_val = simpleio.map_range(adsr1_val, 0, 19, 0.0, 1.0) all_adsr_values[1][synth_adsr_indexes[1]] = mapped_val the_env = synthio.Envelope(attack_time=all_adsr_values[1][0], decay_time = all_adsr_values[1][1], release_time=all_adsr_values[1][3], attack_level=1, sustain_level=all_adsr_values[1][2]) synth1.envelope = the_env elif mode == "WAVE": synth1_wave = (synth1_wave - 1) % len(wave_names) synth1.waveform = waveforms[synth1_wave] elif mode == "RING": ring1_val = (ring1_val - 1) % 25 mapped_val = simpleio.map_range(ring1_val, 0, 24, 0.0, 220.0) synth1.ring_frequency = mapped_val last_position1 = position1 if position2 != last_position2: if position2 > last_position2: if mode == "PLAY": chord2_sel = (chord2_sel + 1) % len(chords) elif mode == "ADSR": adsr2_val = (adsr2_val + 1) % 20 mapped_val = simpleio.map_range(adsr2_val, 0, 19, 0.0, 1.0) all_adsr_values[2][synth_adsr_indexes[2]] = mapped_val the_env = synthio.Envelope(attack_time=all_adsr_values[2][0], decay_time = all_adsr_values[2][1], release_time=all_adsr_values[2][3], attack_level=1, sustain_level=all_adsr_values[2][2]) synth2.envelope = the_env elif mode == "WAVE": synth2_wave = (synth2_wave + 1) % len(wave_names) synth2.waveform = waveforms[synth2_wave] elif mode == "RING": ring2_val = (ring2_val + 1) % 25 mapped_val = simpleio.map_range(ring2_val, 0, 24, 0.0, 220.0) synth2.ring_frequency = mapped_val else: if mode == "PLAY": chord2_sel = (chord2_sel - 1) % len(chords) display2.print(chord_names[chord2_sel]) elif mode == "ADSR": adsr2_val = (adsr2_val - 1) % 20 mapped_val = simpleio.map_range(adsr2_val, 0, 19, 0.0, 1.0) all_adsr_values[2][synth_adsr_indexes[2]] = mapped_val the_env = synthio.Envelope(attack_time=all_adsr_values[2][0], decay_time = all_adsr_values[2][1], release_time=all_adsr_values[2][3], attack_level=1, sustain_level=all_adsr_values[2][2]) synth2.envelope = the_env elif mode == "WAVE": synth2_wave = (synth2_wave - 1) % len(wave_names) synth2.waveform = waveforms[synth2_wave] elif mode == "RING": ring2_val = (ring2_val - 1) % 25 mapped_val = simpleio.map_range(ring2_val, 0, 24, 0.0, 220.0) synth2.ring_frequency = mapped_val last_position2 = position2 if position3 != last_position3: if position3 > last_position3: if mode == "PLAY": chord3_sel = (chord3_sel + 1) % len(chords) display3.print(chord_names[chord3_sel]) elif mode == "ADSR": adsr3_val = (adsr3_val + 1) % 20 mapped_val = simpleio.map_range(adsr3_val, 0, 19, 0.0, 1.0) all_adsr_values[3][synth_adsr_indexes[3]] = mapped_val the_env = synthio.Envelope(attack_time=all_adsr_values[3][0], decay_time = all_adsr_values[3][1], release_time=all_adsr_values[3][3], attack_level=1, sustain_level=all_adsr_values[3][2]) synth3.envelope = the_env elif mode == "WAVE": synth3_wave = (synth3_wave + 1) % len(wave_names) synth3.waveform = waveforms[synth3_wave] elif mode == "RING": ring3_val = (ring3_val + 1) % 25 mapped_val = simpleio.map_range(ring3_val, 0, 24, 0.0, 220.0) synth3.ring_frequency = mapped_val else: if mode == "PLAY": chord3_sel = (chord3_sel - 1) % len(chords) display3.print(chord_names[chord3_sel]) elif mode == "ADSR": adsr3_val = (adsr3_val - 1) % 20 mapped_val = simpleio.map_range(adsr3_val, 0, 19, 0.0, 1.0) all_adsr_values[3][synth_adsr_indexes[3]] = mapped_val the_env = synthio.Envelope(attack_time=all_adsr_values[3][0], decay_time = all_adsr_values[3][1], release_time=all_adsr_values[3][3], attack_level=1, sustain_level=all_adsr_values[3][2]) synth3.envelope = the_env elif mode == "WAVE": synth3_wave = (synth3_wave - 1) % len(wave_names) synth3.waveform = waveforms[synth3_wave] elif mode == "RING": ring3_val = (ring3_val - 1) % 25 mapped_val = simpleio.map_range(ring3_val, 0, 24, 0.0, 220.0) synth3.ring_frequency = mapped_val last_position3 = position3 enc_clock = ticks_add(enc_clock, 100) # synth plays based on ticks timing if ticks_diff(ticks_ms(), clock) >= delay: if play_states[0] is True: r0, last_r0, c0 = play_euclidean(synth0, chords[chord0_sel], rhythm0, r0, last_r0, c0, 0) if play_states[1] is True: r1, last_r1, c1 = play_euclidean(synth1, chords[chord1_sel], rhythm1, r1, last_r1, c1, 2) if play_states[2] is True: r2, last_r2, c2 = play_euclidean(synth2, chords[chord2_sel], rhythm2, r2, last_r2, c2, 4) if play_states[3] is True: r3, last_r3, c3 = play_euclidean(synth3, chords[chord3_sel], rhythm3, r3, last_r3, c3, 6) clock = ticks_add(clock, delay) # in PLAY select button controls play/pause if mode == "PLAY": for i in range(4): if not select_buttons[i].value and select_states[i] is False: select_states[i] = True if play_states[i] is True: synth.release(synths[i]) play_states[i] = False else: play_states[i] = True if select_buttons[i].value and select_states[i] is True: select_states[i] = False display0.print(chord_names[chord0_sel]) display1.print(chord_names[chord1_sel]) display2.print(chord_names[chord2_sel]) display3.print(chord_names[chord3_sel]) # EUC menu select resets cycle count elif mode == "EUC ": if not menu_buttons[0].value and menu_states[0] is False: r0 = 0 r1 = 0 r2 = 0 r3 = 0 menu_states[0] = True if menu_buttons[0].value and menu_states[0] is True: menu_states[0] = False rhythm0, euclid0_steps, euclid0_pulses, r0 = read_buttons(buttons0, button0_states, rhythm0, euclid0_steps, euclid0_pulses, r0) rhythm1, euclid1_steps, euclid1_pulses, r1 = read_buttons(buttons1, button1_states, rhythm1, euclid1_steps, euclid1_pulses, r1) rhythm2, euclid2_steps, euclid2_pulses, r2 = read_buttons(buttons2, button2_states, rhythm2, euclid2_steps, euclid2_pulses, r2) rhythm3, euclid3_steps, euclid3_pulses, r3 = read_buttons(buttons3, button3_states, rhythm3, euclid3_steps, euclid3_pulses, r3) display0.print(f" {euclid0_pulses}") display1.print(f" {euclid1_pulses}") display2.print(f" {euclid2_pulses}") display3.print(f" {euclid3_pulses}") # BPM is adjusted elif mode == "BPM ": if not select_buttons[0].value and select_states[0] is False: bpm = 120 select_states[0] = True if select_buttons[0].value and select_states[0] is True: select_states[0] = False display0.print(f" {bpm}") display1.print(" ") display2.print(" ") display3.print(" ") # beat division is changed elif mode == "BEAT": if not select_buttons[0].value and select_states[0] is False: beat_names[beat_index] = 2 select_states[0] = True if select_buttons[0].value and select_states[0] is True: select_states[0] = False display0.print(f" {beat_names[beat_index]}") display1.print(" ") display2.print(" ") display3.print(" ") # adsr for each voice elif mode == "ADSR": for i in range(4): if not left_buttons[i].value and left_states[i] is False: synth_adsr_indexes[i] = (synth_adsr_indexes[i] - 1) % 4 left_states[i] = True the_synth = synths[i] if left_buttons[i].value and left_states[i] is True: left_states[i] = False if not right_buttons[i].value and right_states[i] is False: synth_adsr_indexes[i] = (synth_adsr_indexes[i] + 1) % 4 right_states[i] = True if right_buttons[i].value and right_states[i] is True: right_states[i] = False if not select_buttons[i].value and select_states[i] is False: the_synth = synths[i] all_adsr_values[i][0] = 0.1 all_adsr_values[i][1] = 0.1 all_adsr_values[i][3] = 0.1 all_adsr_values[i][2] = 0.05 the_env = synthio.Envelope(attack_time=all_adsr_values[i][0], decay_time = all_adsr_values[i][1], release_time=all_adsr_values[i][3], attack_level=1, sustain_level=all_adsr_values[i][2]) the_synth.envelope = the_env select_states[i] = True if select_buttons[i].value and select_states[i] is True: select_states[i] = False # pylint: disable=line-too-long display0.print(f"{adsr_names[synth_adsr_indexes[0]]}{synth0.envelope[adsr_properties[synth_adsr_indexes[0]]]:.2f}") display1.print(f"{adsr_names[synth_adsr_indexes[1]]}{synth1.envelope[adsr_properties[synth_adsr_indexes[1]]]:.2f}") display2.print(f"{adsr_names[synth_adsr_indexes[2]]}{synth2.envelope[adsr_properties[synth_adsr_indexes[2]]]:.2f}") display3.print(f"{adsr_names[synth_adsr_indexes[3]]}{synth3.envelope[adsr_properties[synth_adsr_indexes[3]]]:.2f}") # change waveform elif mode == "WAVE": display0.print(f" {wave_names[synth0_wave]}") display1.print(f" {wave_names[synth1_wave]}") display2.print(f" {wave_names[synth2_wave]}") display3.print(f" {wave_names[synth3_wave]}") # adjust ring modulation elif mode == "RING": display0.print(f" {synth0.ring_frequency:.1f}") display1.print(f" {synth1.ring_frequency:.1f}") display2.print(f" {synth2.ring_frequency:.1f}") display3.print(f" {synth3.ring_frequency:.1f}") # adjust lfo rate used for ring modulation elif mode == "LFO ": display0.print("RATE") display1.print(f" {lfo.rate:.1f}") display2.print(" ") display3.print(" ") # overall volume 0.0 - 1.0 elif mode == "VOL ": display0.print(f" {mixer.voice[0].level:.1f}") display1.print(" ") display2.print(" ") display3.print(" ")
Upload the Code and Libraries to the Feather RP2040
After downloading the Project Bundle, plug your Feather RP2040 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 Feather RP2040's CIRCUITPY drive.
- lib folder
- code.py
Your Feather RP2040 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 creating some waveform and ADSR envelope objects. These objects are passed to synthio.Note
objects. There are four Note
objects and they will create four different voices.
The Note
s are played by the Synthesizer
object, which outputs through the Mixer
object.
SAMPLE_RATE = 44100 SAMPLE_SIZE = 256 VOLUME = 5000 # waveforms, envelopes and synth setup 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, 4*np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME, dtype=np.int16) saw = np.linspace(VOLUME, -VOLUME, num=SAMPLE_SIZE, dtype=np.int16) noise = np.array([randint(-VOLUME, VOLUME) for i in range(SAMPLE_SIZE)], dtype=np.int16) lfo = synthio.LFO(rate = .5, waveform = sine) amp_env0 = synthio.Envelope(attack_time=0.1, decay_time = 0.1, release_time=0.1, attack_level=1, sustain_level=0.05) amp_env1 = synthio.Envelope(attack_time=0.05, decay_time = 0.1, release_time=0.1, attack_level=1, sustain_level=0.05) # synth plays the notes synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE) # these are the notes synth0 = synthio.Note(frequency = 0.0, envelope=amp_env0, waveform=square, ring_frequency = 0, ring_bend = lfo, ring_waveform = sine) synth1 = synthio.Note(frequency = 0.0, envelope=amp_env1, waveform=sine, ring_frequency = 0, ring_bend = lfo, ring_waveform = sine) synth2 = synthio.Note(frequency = 0.0, envelope=amp_env0, waveform=square, ring_frequency = 0, ring_bend = lfo, ring_waveform = sine) synth3 = synthio.Note(frequency = 0.0, envelope=amp_env1, waveform=sine, ring_frequency = 0, ring_bend = lfo, ring_waveform = sine) synths = [synth0, synth1, synth2, synth3] wave_names = ["SQUR", "SINE", "SAW ", "NOIZ"] waveforms = [square, sine, saw, noise] synth0_wave = 0 synth1_wave = 1 synth2_wave = 0 synth3_wave = 1 # i2s amp setup audio = audiobusio.I2SOut(bit_clock=board.D10, word_select=board.D11, data=board.D9) mixer = audiomixer.Mixer(voice_count=4, sample_rate=SAMPLE_RATE, channel_count=1, bits_per_sample=16, samples_signed=True, buffer_size=2048 ) audio.play(mixer) vol_val = 2 mixer.voice[0].play(synth) mixer.voice[0].level = 0.3
Tones
Arrays of tones are created for triads. They are all I (tonic) chords in the circle of fifths.
# these are the triads, all major c_tones = [130.81, 164.81, 196.00] g_tones = [196.00, 246.94, 293.66] d_tones = [146.83, 185.00, 220.00] a_tones = [220.00, 277.18, 329.63] e_tones = [164.81, 207.65, 246.94] b_tones = [246.94, 311.13, 369.99] fsharp_tones = [185.00, 233.08, 277.18] csharp_tones = [138.59, 174.61, 207.65] aflat_tones = [207.65, 261.63, 311.13] eflat_tones = [155.56, 196.00, 233.08] bflat_tones = [233.08, 293.66, 349.23] f_tones = [174.61, 220.00, 261.63] # names for the alphanumeric displays chord_names = ["Cmaj", "Gmaj", "Dmaj", "Amaj", "Emaj", "Bmaj", "F#ma", "C#ma", "Abma", "Ebma", "Bbma", "Fmaj"] chords = [c_tones, g_tones, d_tones, a_tones, e_tones, b_tones, fsharp_tones, csharp_tones, aflat_tones, eflat_tones, bflat_tones, f_tones]
I2C
Next are the I2C peripherals. There are five ANO rotary encoders, five alphanumeric displays and one 8x8 matrix.
# i2c setup i2c = board.I2C() # the encoders seesaw0 = seesaw.Seesaw(i2c, addr=0x49) seesaw1 = seesaw.Seesaw(i2c, addr=0x4A) seesaw2 = seesaw.Seesaw(i2c, addr=0x4B) seesaw3 = seesaw.Seesaw(i2c, addr=0x4C) menu_seesaw = seesaw.Seesaw(i2c, addr=0x4D) # the alphanumeric displays display0 = segments.Seg14x4(i2c, address=0x70) display1 = segments.Seg14x4(i2c, address=0x71) display2 = segments.Seg14x4(i2c, address=0x72) display3 = segments.Seg14x4(i2c, address=0x73) menu_display = segments.Seg14x4(i2c, address=0x74) # the matrix matrix0 = Matrix8x8x2(i2c, address=0x75) seesaws = [seesaw0, seesaw1, seesaw2, seesaw3, menu_seesaw] buttons0 = [] buttons1 = [] buttons2 = [] buttons3 = [] menu_buttons = [] button0_states = [] button1_states = [] button2_states = [] button3_states = [] menu_states = [] button0_names = ["Select", "Up", "Left", "Down", "Right"] # setup the buttons on all of the encoders for i in range(1, 6): seesaw0.pin_mode(i, seesaw0.INPUT_PULLUP) seesaw1.pin_mode(i, seesaw1.INPUT_PULLUP) seesaw2.pin_mode(i, seesaw2.INPUT_PULLUP) seesaw3.pin_mode(i, seesaw3.INPUT_PULLUP) menu_seesaw.pin_mode(i, menu_seesaw.INPUT_PULLUP) buttons0.append(digitalio.DigitalIO(seesaw0, i)) buttons1.append(digitalio.DigitalIO(seesaw1, i)) buttons2.append(digitalio.DigitalIO(seesaw2, i)) buttons3.append(digitalio.DigitalIO(seesaw3, i)) menu_buttons.append(digitalio.DigitalIO(menu_seesaw, i)) button0_states.append(False) button1_states.append(False) button2_states.append(False) button3_states.append(False) menu_states.append(False) # make all of the encoders encoder0 = rotaryio.IncrementalEncoder(seesaw0) last_position0 = 0 encoder1 = rotaryio.IncrementalEncoder(seesaw1) last_position1 = 0 encoder2 = rotaryio.IncrementalEncoder(seesaw2) last_position2 = 0 encoder3 = rotaryio.IncrementalEncoder(seesaw3) last_position3 = 0 menu_enc = rotaryio.IncrementalEncoder(menu_seesaw) last_menuPosition = 0
Conjunction Function
There are two functions that are used in the loop. The first reads all of the selector encoder buttons to adjust the Euclidean rhythm parameters.
def read_buttons(button_array, button_states, euc, e_step, e_pulse, the_step): for b in range(5): if not button_array[b].value and button_states[b] is False: button_states[b] = True if button0_names[b] == "Select": e_step = 8 e_pulse = 4 if the_step >= e_step: the_step = 0 elif button0_names[b] == "Up": if e_step > 16: e_step = 16 else: e_step += 1 elif button0_names[b] == "Down": if e_step < 1: e_step = 1 else: e_step -= 1 if the_step >= e_step: the_step = 0 elif button0_names[b] == "Left": e_pulse -= 1 e_pulse = max(e_pulse, 1) else: e_pulse += 1 e_pulse = min(e_pulse, e_step) euc = bjorklund(e_step, e_pulse) if button_array[b].value and button_states[b] is True: button_states[b] = False if button0_names[b] in ("Select", "Up", "Down"): matrix0.fill(matrix0.LED_OFF) draw_steps(euclid0_steps, 0) draw_steps(euclid1_steps, 2) draw_steps(euclid2_steps, 4) draw_steps(euclid3_steps, 6) return euc, e_step, e_pulse, the_step
The second actually plays the Euclidean rhythms with the passed in chord to the designated synth voice.
def play_euclidean(this_synth, n, the_rhythm, rhythm_count, last_count, c, matrix_slot): if last_count <= 7: matrix0[matrix_slot, last_count] = matrix0.LED_GREEN else: c -= 1 matrix0[matrix_slot + 1, (last_count - last_count) + c] = matrix0.LED_GREEN c += 1 if the_rhythm[rhythm_count] == 1: this_synth.frequency = n[randint(0, 2)] synth.press(this_synth) if rhythm_count <= 7: matrix0[matrix_slot, rhythm_count] = matrix0.LED_RED else: matrix0[matrix_slot + 1, (rhythm_count - rhythm_count) + c] = matrix0.LED_RED c += 1 else: synth.release(this_synth) if rhythm_count > 7: c += 1 last_count = rhythm_count rhythm_count += 1 if rhythm_count >= len(the_rhythm): rhythm_count = 0 if rhythm_count == 1: c = 0 return rhythm_count, last_count, c
ADSR Prep
A few arrays and variables are prepared in order to affect the ADSR envelope for each synth voice. When a change is made to an envelope, a new envelope must be instantiated. These arrays allow you to store the previous ADSR values to pass to this new envelope. As a result, if you change the attack value, the previous decay, sustain and release values are retained and passed to the new envelope.
# adsr mode adsr_names = ["A", "D", "S", "R"] synth_adsr_indexes = [0, 0, 0, 0] adsr_properties = [0, 1, 4, 2] adsr0_values = [amp_env0.attack_time, amp_env0.decay_time, amp_env0.sustain_level, amp_env0.release_time] adsr1_values = [amp_env1.attack_time, amp_env1.decay_time, amp_env1.sustain_level, amp_env1.release_time] adsr2_values = [amp_env0.attack_time, amp_env0.decay_time, amp_env0.sustain_level, amp_env0.release_time] adsr3_values = [amp_env1.attack_time, amp_env1.decay_time, amp_env1.sustain_level, amp_env1.release_time] all_adsr_values = [adsr0_values, adsr1_values, adsr2_values, adsr3_values] adsr0_val = int(simpleio.map_range(amp_env0.attack_time, 0.0, 1.0, 0, 19)) adsr1_val = int(simpleio.map_range(amp_env0.decay_time, 0.0, 1.0, 0, 19)) adsr2_val = int(simpleio.map_range(amp_env0.sustain_level, 0.0, 1.0, 0, 19)) adsr3_val = int(simpleio.map_range(amp_env0.release_time, 0.0, 1.0, 0, 19))
The Loop
The loop has four tasks happening: reading the rotary encoders, playing the Euclidean rhythms, updating the alphanumeric displays, and reading the encoder buttons.
The rotary encoders are read with a small delay. The menu encoder controls which mode is active. Each of the four other encoders' functionality changes depending on the mode.
if ticks_diff(ticks_ms(), enc_clock) >= 100: position0 = encoder0.position position1 = encoder1.position position2 = encoder2.position position3 = encoder3.position menuPosition = menu_enc.position # menu changes mode if menuPosition != last_menuPosition: if menuPosition > last_menuPosition: mode_index = (mode_index + 1) % len(modes) else: mode_index = (mode_index - 1) % len(modes) if mode in ("EUC ", "ADSR"): clock_stretch = True if mode in ("PLAY", "BPM ", "BEAT", "WAVE") and clock_stretch: clock = ticks_ms() clock_stretch = False mode = modes[mode_index] menu_display.print(f" {mode}") last_menuPosition = menuPosition ... if position1 != last_position1: if position1 > last_position1: if mode == "PLAY": chord1_sel = (chord1_sel + 1) % len(chords) display1.print(chord_names[chord1_sel]) elif mode == "ADSR": adsr1_val = (adsr1_val + 1) % 20 mapped_val = simpleio.map_range(adsr1_val, 0, 19, 0.0, 1.0) all_adsr_values[1][synth_adsr_indexes[1]] = mapped_val the_env = synthio.Envelope(attack_time=all_adsr_values[1][0], decay_time = all_adsr_values[1][1], release_time=all_adsr_values[1][3], attack_level=1, sustain_level=all_adsr_values[1][2]) synth1.envelope = the_env elif mode == "WAVE": synth1_wave = (synth1_wave + 1) % len(wave_names) synth1.waveform = waveforms[synth1_wave] ...
No matter which mode, the synth plays on; using ticks to keep time. play_states[]
keeps track of whether or not a synth voice is paused.
# synth plays based on ticks timing if ticks_diff(ticks_ms(), clock) >= delay: if play_states[0] is True: r0, last_r0, c0 = play_euclidean(synth0, chords[chord0_sel], rhythm0, r0, last_r0, c0, 0) if play_states[1] is True: r1, last_r1, c1 = play_euclidean(synth1, chords[chord1_sel], rhythm1, r1, last_r1, c1, 2) if play_states[2] is True: r2, last_r2, c2 = play_euclidean(synth2, chords[chord2_sel], rhythm2, r2, last_r2, c2, 4) if play_states[3] is True: r3, last_r3, c3 = play_euclidean(synth3, chords[chord3_sel], rhythm3, r3, last_r3, c3, 6) clock = ticks_add(clock, delay)
Just like the rotary encoders, the buttons and alphanumeric displays have different functionality depending on the mode. The alphanumeric displays will update to show different values. For example, in Wave mode, the waveform for each voice is displayed. As the waveform is changed with the rotary encoder, the display updates.
# in PLAY select button controls play/pause if mode == "PLAY": for i in range(4): if not select_buttons[i].value and select_states[i] is False: select_states[i] = True if play_states[i] is True: synth.release(synths[i]) play_states[i] = False else: play_states[i] = True if select_buttons[i].value and select_states[i] is True: select_states[i] = False display0.print(chord_names[chord0_sel]) display1.print(chord_names[chord1_sel]) display2.print(chord_names[chord2_sel]) display3.print(chord_names[chord3_sel]) ... # change waveform elif mode == "WAVE": display0.print(f" {wave_names[synth0_wave]}") display1.print(f" {wave_names[synth1_wave]}") display2.print(f" {wave_names[synth2_wave]}") display3.print(f" {wave_names[synth3_wave]}")
Text editor powered by tinymce.