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 button below, and uncompress the .zip file.
Connect your computer to the board via a known good USB power+data cable. A new flash drive should show up as CIRCUITPY.
Drag the contents of the uncompressed bundle directory onto your board CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.
# SPDX-FileCopyrightText: Copyright (c) 2023 john park for Adafruit Industries # # SPDX-License-Identifier: MIT ''' Faderwave Synthesizer use 16 faders to create the single cycle waveform rotary encoder adjusts other synth parameters audio output: line level over 3.5mm TRS optional CV output via DAC ''' import board import busio import ulab.numpy as np import rotaryio from digitalio import DigitalInOut, Pull import displayio from adafruit_display_text import label from adafruit_display_shapes.rect import Rect import terminalio import synthio import audiomixer from adafruit_debouncer import Debouncer import adafruit_ads7830.ads7830 as ADC from adafruit_ads7830.analog_in import AnalogIn import adafruit_displayio_ssd1306 import adafruit_ad569x import usb_midi import adafruit_midi from adafruit_midi.note_on import NoteOn from adafruit_midi.note_off import NoteOff displayio.release_displays() DEBUG = False # turn on print debugging messages ITSY_TYPE = 0 # Pick your ItsyBitsy: 0=M4, 1=RP2040 # neopixel setup for RP2040 only if ITSY_TYPE == 1: import neopixel pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.3) pixel.fill(0x004444) i2c = busio.I2C(board.SCL, board.SDA, frequency=1_000_000) midi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], in_channel=0) NUM_FADERS = 16 num_oscs = 1 # how many oscillators for each note to start detune = 0.000 # how much to detune the oscillators volume = 0.6 # mixer volume lpf_freq = 12000 # user Low Pass Filter frequency setting lpf_basef = 500 # filter lowest frequency lpf_resonance = 0.1 # filter q faders_pos = [0] * NUM_FADERS last_faders_pos = [0] * NUM_FADERS # Initialize ADS7830 adc_a = ADC.ADS7830(i2c, address=0x48) # default address 0x48 adc_b = ADC.ADS7830(i2c, address=0x49) # A0 jumper 0x49, A1 0x4A faders = [] # list for fader objects on first ADC for fdr in range(8): # add first group to list faders.append(AnalogIn(adc_a, fdr)) for fdr in range(8): # add second group faders.append(AnalogIn(adc_b, fdr)) # Initialize AD5693R for CV out dac = adafruit_ad569x.Adafruit_AD569x(i2c) dac.gain = True dac.value = faders[0].value # set dac out to the slider level # Rotary encoder setup ENC_A = board.D9 ENC_B = board.D10 ENC_SW = board.D7 button_in = DigitalInOut(ENC_SW) # defaults to input button_in.pull = Pull.UP # turn on internal pull-up resistor button = Debouncer(button_in) encoder = rotaryio.IncrementalEncoder(ENC_A, ENC_B) encoder_pos = encoder.position last_encoder_pos = encoder.position # display setup OLED_RST = board.D13 OLED_DC = board.D12 OLED_CS = board.D11 spi = board.SPI() display_bus = displayio.FourWire(spi, command=OLED_DC, chip_select=OLED_CS, reset=OLED_RST, baudrate=30_000_000) display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64) # Create display group group = displayio.Group() # Set the font for the text label font = terminalio.FONT # Create text label title = label.Label(font, x=2, y=4, text=("FADERWAVE SYNTHESIZER"), color=0xffffff) group.append(title) column_x = (8, 60, 100) row_y = (22, 34, 46, 58) midi_lbl_rect = Rect(column_x[2]-3, row_y[0]-5, 28, 10, fill=0xffffff) group.append(midi_lbl_rect) midi_lbl = label.Label(font, x=column_x[2], y=row_y[0], text="MIDI", color=0x000000) group.append(midi_lbl) midi_rect = Rect(column_x[2]-3, row_y[1]-5, 28, 10, fill=0xffffff) group.append(midi_rect) midi_counter_lbl = label.Label(font, x=column_x[2]+8, y=row_y[1], text='-', color=0x000000) group.append(midi_counter_lbl) # Create menu selector menu_sel = 0 menu_sel_txt = label.Label(font, text=(">"), color=0xffffff) menu_sel_txt.x = column_x[0]-10 menu_sel_txt.y = row_y[menu_sel] group.append(menu_sel_txt) # Create detune text det_txt_a = label.Label(font, text=("Detune "), color=0xffffff) det_txt_a.x = column_x[0] det_txt_a.y = row_y[0] group.append(det_txt_a) det_txt_b = label.Label(font, text=(str(detune)), color=0xffffff) det_txt_b.x = column_x[1] det_txt_b.y = row_y[0] group.append(det_txt_b) # Create number of oscs text num_oscs_txt_a = label.Label(font, text=("Num Oscs "), color=0xffffff) num_oscs_txt_a.x = column_x[0] num_oscs_txt_a.y = row_y[1] group.append(num_oscs_txt_a) num_oscs_txt_b = label.Label(font, text=(str(num_oscs)), color=0xffffff) num_oscs_txt_b.x = column_x[1] num_oscs_txt_b.y = row_y[1] group.append(num_oscs_txt_b) # Create volume text vol_txt_a = label.Label(font, text=("Volume "), color=0xffffff) vol_txt_a.x = column_x[0] vol_txt_a.y = row_y[2] group.append(vol_txt_a) vol_txt_b = label.Label(font, text=(str(volume)), color=0xffffff) vol_txt_b.x = column_x[1] vol_txt_b.y = row_y[2] group.append(vol_txt_b) # Create lpf frequency text lpf_txt_a = label.Label(font, text=("LPF "), color=0xffffff) lpf_txt_a.x = column_x[0] lpf_txt_a.y = row_y[3] group.append(lpf_txt_a) lpf_txt_b = label.Label(font, text=(str(lpf_freq)), color=0xffffff) lpf_txt_b.x = column_x[1] lpf_txt_b.y = row_y[3] group.append(lpf_txt_b) # Show the display group display.root_group = group # Synthio setup if ITSY_TYPE == 0: import audioio audio = audioio.AudioOut(left_channel=board.A0, right_channel=board.A1) # M4 built-in DAC if ITSY_TYPE == 1: import audiopwmio audio = audiopwmio.PWMAudioOut(board.A1) # if using I2S amp: # audio = audiobusio.I2SOut(bit_clock=board.MOSI, word_select=board.MISO, data=board.SCK) mixer = audiomixer.Mixer(channel_count=2, sample_rate=44100, buffer_size=4096) synth = synthio.Synthesizer(channel_count=2, sample_rate=44100) audio.play(mixer) mixer.voice[0].play(synth) mixer.voice[0].level = 0.75 wave_user = np.array([0]*NUM_FADERS, dtype=np.int16) amp_env = synthio.Envelope(attack_time=0.3, attack_level=1, sustain_level=0.65, release_time=0.3) def faders_to_wave(): for j in range(NUM_FADERS): wave_user[j] = int(map_range(faders_pos[j], 0, 127, -32768, 32767)) notes_pressed = {} # which notes being pressed. key=midi note, val=note object def note_on(n): voices = [] # holds our currently sounding voices ('Notes' in synthio speak) fo = synthio.midi_to_hz(n) lpf = synth.low_pass_filter(lpf_freq, lpf_resonance) for k in range(num_oscs): f = fo * (1 + k*detune) voices.append(synthio.Note(frequency=f, filter=lpf, envelope=amp_env, waveform=wave_user)) synth.press(voices) note_off(n) # help to prevent double note_on for same note which can get stuck notes_pressed[n] = voices def note_off(n): note = notes_pressed.get(n, None) if note: synth.release(note) # simple range mapper, like Arduino map() def map_range(s, a1, a2, b1, b2): return b1 + ((s - a1) * (b2 - b1) / (a2 - a1)) notes_on = 0 print("Welcome to Faderwave") while True: # get midi messages msg = midi.receive() if isinstance(msg, NoteOn) and msg.velocity != 0: note_on(msg.note) notes_on = notes_on + 1 if DEBUG: print("MIDI notes on: ", msg.note, " Polyphony:", " "*notes_on, notes_on) midi_counter_lbl.text = str(msg.note) elif isinstance(msg, NoteOff) or (isinstance(msg, NoteOn) and msg.velocity == 0): note_off(msg.note) notes_on = notes_on - 1 if DEBUG: print("MIDI notes off:", msg.note, " Polyphony:", " "*notes_on, notes_on) midi_counter_lbl.text = "-" # check faders for i in range(len(faders)): faders_pos[i] = faders[i].value//512 if faders_pos[i] is not last_faders_pos[i]: faders_to_wave() last_faders_pos[i] = faders_pos[i] if DEBUG: print("fader", [i], faders_pos[i]) # send out a DAC value based on fader 0 # if i == 1: # dac.value = faders[1].value # check encoder button button.update() if button.fell: menu_sel = (menu_sel+1) % 4 menu_sel_txt.y = row_y[menu_sel] # check encoder encoder_pos = encoder.position if encoder_pos > last_encoder_pos: delta = encoder_pos - last_encoder_pos if menu_sel == 0: detune = detune + (delta * 0.001) detune = min(max(detune, -0.030), 0.030) formatted_detune = str("{:.3f}".format(detune)) det_txt_b.text = formatted_detune elif menu_sel == 1: num_oscs = num_oscs + delta num_oscs = min(max(num_oscs, 1), 5) formatted_num_oscs = str(num_oscs) num_oscs_txt_b.text = formatted_num_oscs elif menu_sel == 2: volume = volume + (delta * 0.01) volume = min(max(volume, 0.00), 1.00) mixer.voice[0].level = volume formatted_volume = str("{:.2f}".format(volume)) vol_txt_b.text = formatted_volume elif menu_sel == 3: lpf_freq = lpf_freq + (delta * 1000) lpf_freq = min(max(lpf_freq, 1000), 20_000) formatted_lpf = str(lpf_freq) lpf_txt_b.text = formatted_lpf last_encoder_pos = encoder.position if encoder_pos < last_encoder_pos: delta = last_encoder_pos - encoder_pos if menu_sel == 0: detune = detune - (delta * 0.001) detune = min(max(detune, -0.030), 0.030) formatted_detune = str("{:.3f}".format(detune)) det_txt_b.text = formatted_detune elif menu_sel == 1: num_oscs = num_oscs - delta num_oscs = min(max(num_oscs, 1), 8) formatted_num_oscs = str(num_oscs) num_oscs_txt_b.text = formatted_num_oscs elif menu_sel == 2: volume = volume - (delta * 0.01) volume = min(max(volume, 0.00), 1.00) mixer.voice[0].level = volume formatted_volume = str("{:.2f}".format(volume)) vol_txt_b.text = formatted_volume elif menu_sel == 3: lpf_freq = lpf_freq - (delta * 1000) lpf_freq = min(max(lpf_freq, 1000), 20_000) formatted_lpf = str(lpf_freq) lpf_txt_b.text = formatted_lpf last_encoder_pos = encoder.position
import board import busio import ulab.numpy as np import rotaryio from digitalio import DigitalInOut, Pull import displayio from adafruit_display_text import label from adafruit_display_shapes.rect import Rect import terminalio import synthio import audiomixer from adafruit_debouncer import Debouncer import adafruit_ads7830.ads7830 as ADC from adafruit_ads7830.analog_in import AnalogIn import adafruit_displayio_ssd1306 import adafruit_ad569x import usb_midi import adafruit_midi from adafruit_midi.note_on import NoteOn from adafruit_midi.note_off import NoteOff
Setup and Constants
First some setup, including user constants for DEBUG
and ITSY_TYPE
. Also to release the display before setting it up for use.
displayio.release_displays() DEBUG = False # turn on print debugging messages ITSY_TYPE = 0 # Pick your ItsyBitsy: 0=M4, 1=RP2040 # neopixel setup for RP2040 only if ITSY_TYPE == 1: import neopixel pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.3) pixel.fill(0x004444)
I2C, MIDI, Synthio, and Driver Setup
-
busio.I2C
initializes one I2C bus on the STEMMA QT port pins -
adafruit_midi
is set up for USB MIDI input - Initial
synthio
settings are established to define parameters for the synthesizer, such as the number of faders, initial number of oscillators, detune amount, volume, and low-pass filter settings.
i2c = busio.I2C(board.SCL, board.SDA, frequency=1_000_000) midi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], in_channel=0) NUM_FADERS = 16 num_oscs = 1 # how many oscillators for each note to start detune = 0.000 # how much to detune the oscillators volume = 0.6 # mixer volume lpf_freq = 12000 # user Low Pass Filter frequency setting lpf_basef = 500 # filter lowest frequency lpf_resonance = 0.1 # filter q faders_pos = [0] * NUM_FADERS last_faders_pos = [0] * NUM_FADERS
Fader Initialization
Initializes the ADC for reading values from faders. Creates a list of AnalogIn
objects representing the faders.
adc_a = ADC.ADS7830(i2c, address=0x48) # default address 0x48 adc_b = ADC.ADS7830(i2c, address=0x49) # A0 jumper 0x49, A1 0x4A faders = [] # list for fader objects on first ADC for fdr in range(8): # add first group to list faders.append(AnalogIn(adc_a, fdr)) for fdr in range(8): # add second group faders.append(AnalogIn(adc_b, fdr))
DAC Setup
The DAC is initialized with its value being tied to the first fader's value.
# Initialize AD5693R for CV out dac = adafruit_ad569x.Adafruit_AD569x(i2c) dac.gain = True dac.value = faders[0].value # set dac out to the slider level
Rotary Encoder
Next, to initialize the rotary encoder.
ENC_A = board.D9 ENC_B = board.D10 ENC_SW = board.D7 button_in = DigitalInOut(ENC_SW) # defaults to input button_in.pull = Pull.UP # turn on internal pull-up resistor button = Debouncer(button_in) encoder = rotaryio.IncrementalEncoder(ENC_A, ENC_B) encoder_pos = encoder.position last_encoder_pos = encoder.position
OLED Display Setup
Now to set up the OLED display using the SSD1306 driver and initialize the displayio
group.
OLED_RST = board.D13 OLED_DC = board.D12 OLED_CS = board.D11 spi = board.SPI() display_bus = displayio.FourWire(spi, command=OLED_DC, chip_select=OLED_CS, reset=OLED_RST, baudrate=30_000_000) display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64)
Screen Elements
Here all of the various screen elements are created, including text labels, selector cursor, and value fields.
group = displayio.Group() # Set the font for the text label font = terminalio.FONT # Create text label title = label.Label(font, x=2, y=4, text=("FADERWAVE SYNTHESIZER"), color=0xffffff) group.append(title) column_x = (8, 60, 100) row_y = (22, 34, 46, 58) midi_lbl_rect = Rect(column_x[2]-3, row_y[0]-5, 28, 10, fill=0xffffff) group.append(midi_lbl_rect) midi_lbl = label.Label(font, x=column_x[2], y=row_y[0], text="MIDI", color=0x000000) group.append(midi_lbl) midi_rect = Rect(column_x[2]-3, row_y[1]-5, 28, 10, fill=0xffffff) group.append(midi_rect) midi_counter_lbl = label.Label(font, x=column_x[2]+8, y=row_y[1], text='-', color=0x000000) group.append(midi_counter_lbl) # Create menu selector menu_sel = 0 menu_sel_txt = label.Label(font, text=(">"), color=0xffffff) menu_sel_txt.x = column_x[0]-10 menu_sel_txt.y = row_y[menu_sel] group.append(menu_sel_txt) # Create detune text det_txt_a = label.Label(font, text=("Detune "), color=0xffffff) det_txt_a.x = column_x[0] det_txt_a.y = row_y[0] group.append(det_txt_a) det_txt_b = label.Label(font, text=(str(detune)), color=0xffffff) det_txt_b.x = column_x[1] det_txt_b.y = row_y[0] group.append(det_txt_b) # Create number of oscs text num_oscs_txt_a = label.Label(font, text=("Num Oscs "), color=0xffffff) num_oscs_txt_a.x = column_x[0] num_oscs_txt_a.y = row_y[1] group.append(num_oscs_txt_a) num_oscs_txt_b = label.Label(font, text=(str(num_oscs)), color=0xffffff) num_oscs_txt_b.x = column_x[1] num_oscs_txt_b.y = row_y[1] group.append(num_oscs_txt_b) # Create volume text vol_txt_a = label.Label(font, text=("Volume "), color=0xffffff) vol_txt_a.x = column_x[0] vol_txt_a.y = row_y[2] group.append(vol_txt_a) vol_txt_b = label.Label(font, text=(str(volume)), color=0xffffff) vol_txt_b.x = column_x[1] vol_txt_b.y = row_y[2] group.append(vol_txt_b) # Create lpf frequency text lpf_txt_a = label.Label(font, text=("LPF "), color=0xffffff) lpf_txt_a.x = column_x[0] lpf_txt_a.y = row_y[3] group.append(lpf_txt_a) lpf_txt_b = label.Label(font, text=(str(lpf_freq)), color=0xffffff) lpf_txt_b.x = column_x[1] lpf_txt_b.y = row_y[3] group.append(lpf_txt_b) # Show the display group display.root_group = group
synthio Setup
Next: set up synthio
. This can work with different audio output types depending on the ItsyBitsy board you use. I also set up a mixer object and the wave_user
object that is the single-cycle waveform you'll be editing on the fly with the faders.
# Synthio setup if ITSY_TYPE == 0: import audioio audio = audioio.AudioOut(left_channel=board.A0, right_channel=board.A1) # M4 built-in DAC if ITSY_TYPE == 1: import audiopwmio audio = audiopwmio.PWMAudioOut(board.A1) # if using I2S amp: # audio = audiobusio.I2SOut(bit_clock=board.MOSI, word_select=board.MISO, data=board.SCK) mixer = audiomixer.Mixer(channel_count=2, sample_rate=44100, buffer_size=4096) synth = synthio.Synthesizer(channel_count=2, sample_rate=44100) audio.play(mixer) mixer.voice[0].play(synth) mixer.voice[0].level = 0.75 wave_user = np.array([0]*NUM_FADERS, dtype=np.int16) amp_env = synthio.Envelope(attack_time=0.3, attack_level=1, sustain_level=0.65, release_time=0.3)
Functions
We'll create a number of functions to call during play.
faders_to_wave()
remaps the fader positions to the wavetable array points. This is the key to the whole thing!
note_on()
and note_off()
are called when MIDI messages for note on/off are received.
def faders_to_wave(): for j in range(NUM_FADERS): wave_user[j] = int(map_range(faders_pos[j], 0, 127, -32768, 32767)) notes_pressed = {} # which notes being pressed. key=midi note, val=note object def note_on(n): voices = [] # holds our currently sounding voices ('Notes' in synthio speak) fo = synthio.midi_to_hz(n) lpf = synth.low_pass_filter(lpf_freq, lpf_resonance) for k in range(num_oscs): f = fo * (1 + k*detune) voices.append(synthio.Note(frequency=f, filter=lpf, envelope=amp_env, waveform=wave_user)) synth.press(voices) note_off(n) # help to prevent double note_on for same note which can get stuck notes_pressed[n] = voices def note_off(n): note = notes_pressed.get(n, None) if note: synth.release(note) # simple range mapper, like Arduino map() def map_range(s, a1, a2, b1, b2): return b1 + ((s - a1) * (b2 - b1) / (a2 - a1))
Main Loop
The main loop continuously checks for MIDI messages, updates the synthesizer state based on input, and adjusts parameters using faders and the rotary encoder.
MIDI Input Handling (note_on
and note_off
):
- Processes MIDI messages, triggers note on/off events, and updates a counter label on the display.
Fader Handling (faders_to_wave
):
- Reads values from faders and updates a waveform array. Also, sends out a DAC value based on the first fader.
Encoder Handling:
- Monitors the rotary encoder and adjusts parameters based on the selected menu.
Display Updates:
- Updates the OLED display with the current values of parameters like detune, number of oscillators, volume, and LPF frequency.
Synthio Operation:
- Creates a waveform based on the fader values and triggers note events based on MIDI input.
Hardware Interaction:
- Manages hardware components such as DAC, NeoPixel, and the display.
Debugging Output:
- If
DEBUG
is set toTrue
, it prints debugging messages, including MIDI notes on/off and polyphony information.
msg = midi.receive() if isinstance(msg, NoteOn) and msg.velocity != 0: note_on(msg.note) notes_on = notes_on + 1 if DEBUG: print("MIDI notes on: ", msg.note, " Polyphony:", " "*notes_on, notes_on) midi_counter_lbl.text=str(msg.note) elif isinstance(msg, NoteOff) or (isinstance(msg, NoteOn) and msg.velocity == 0): note_off(msg.note) notes_on = notes_on - 1 if DEBUG: print("MIDI notes off:", msg.note, " Polyphony:", " "*notes_on, notes_on) midi_counter_lbl.text="-" # check faders for i in range(len(faders)): faders_pos[i] = faders[i].value//512 if faders_pos[i] is not last_faders_pos[i]: faders_to_wave() last_faders_pos[i] = faders_pos[i] if DEBUG: print("fader", [i], faders_pos[i]) # send out a DAC value based on fader 0 # if i == 1: # dac.value = faders[1].value # check encoder button button.update() if button.fell: menu_sel = (menu_sel+1) % 4 menu_sel_txt.y = row_y[menu_sel] # check encoder encoder_pos = encoder.position if encoder_pos > last_encoder_pos: delta = encoder_pos - last_encoder_pos if menu_sel == 0: detune = detune + (delta * 0.001) detune = min(max(detune, -0.030), 0.030) formatted_detune = str("{:.3f}".format(detune)) det_txt_b.text = formatted_detune elif menu_sel == 1: num_oscs = num_oscs + delta num_oscs = min(max(num_oscs, 1), 5) formatted_num_oscs = str(num_oscs) num_oscs_txt_b.text = formatted_num_oscs elif menu_sel == 2: volume = volume + (delta * 0.01) volume = min(max(volume, 0.00), 1.00) mixer.voice[0].level = volume formatted_volume = str("{:.2f}".format(volume)) vol_txt_b.text = formatted_volume elif menu_sel == 3: lpf_freq = lpf_freq + (delta * 1000) lpf_freq = min(max(lpf_freq, 1000), 20_000) formatted_lpf = str(lpf_freq) lpf_txt_b.text = formatted_lpf last_encoder_pos = encoder.position if encoder_pos < last_encoder_pos: delta = last_encoder_pos - encoder_pos if menu_sel == 0: detune = detune - (delta * 0.001) detune = min(max(detune, -0.030), 0.030) formatted_detune = str("{:.3f}".format(detune)) det_txt_b.text = formatted_detune elif menu_sel == 1: num_oscs = num_oscs - delta num_oscs = min(max(num_oscs, 1), 8) formatted_num_oscs = str(num_oscs) num_oscs_txt_b.text = formatted_num_oscs elif menu_sel == 2: volume = volume - (delta * 0.01) volume = min(max(volume, 0.00), 1.00) mixer.voice[0].level = volume formatted_volume = str("{:.2f}".format(volume)) vol_txt_b.text = formatted_volume elif menu_sel == 3: lpf_freq = lpf_freq - (delta * 1000) lpf_freq = min(max(lpf_freq, 1000), 20_000) formatted_lpf = str(lpf_freq) lpf_txt_b.text = formatted_lpf last_encoder_pos = encoder.position
Text editor powered by tinymce.