Maker.io main logo

Faderwave Synthesizer

78

2024-08-27 | By Adafruit Industries

License: See Original Project Amplifiers Displays LCD / TFT LEDs / Discrete / Modules Sound

Courtesy of Adafruit

Guide by John Park

Overview 

faderwave_1

This project uses 16 analog fader potentiometers to create custom ‎waveforms that are then used as the basis for synthesizer audio. Sine, ‎square, triangle, saw, banana...any shape you can 'draw' will be re-‎created on the fly.‎

The Faderwave is based on the Hardware Reverse Oscilloscope 2 ‎project by Mitxela a synth designed to hand build single-cycle ‎waveforms. In CircuitPython synthio we can generate arbitrary single-‎cycle waveforms, so this seems like a great mashup!‎

The Faderwave was designed to ingest USB MIDI notes and turn them ‎into polyphonic wavetable synth audio, however you could turn this ‎platform into a MIDI fader box, sequencer, sample player, CV box, or ‎who knows what else!‎

 

Parts

or

ADS7830 8-Channel ADC x2‎

Slide Potentiometer x16‎

Faderwave Circuit

circuit_2

The Faderwave circuit has these components and features:

  • ItsyBitsy M4 or RP2040 microcontroller dev board is the brains ‎of the circuit and provides the audio output. Power and USB MIDI ‎come into the system via the ItsyBitsy USB port
  • 3.5mm TRRS breakout (with optional RC filter circuit) allows ‎you to connect the audio output to an external amp/powered ‎speaker
  • Two ADS7830 8-channel 8-bit ADC boards read the sixteen 10k ‎slide potentiometers and send their values over I2C to the ‎ItsyBitsy
  • 1.3" OLED display running over SPI provides a settings menu
  • Rotary encoder with push button is used for menu settings ‎selection and entry
  • Optional AD5693R 16-bit DAC board can be used to send ‎control voltage to vintage/modular (e.g., Eurorack) synthesizers ‎for alternate projects

PCB Design

While it would be possible to wire up the Faderwave using protoboards ‎or breadboards, it would be a pretty wild mess of wires, so it made ‎sense to design a PCB for it instead.‎

I used KiCad to lay out the circuit schematic, using the Adafruit ‎Eaglecad part symbols imported and edited to serve the purpose. I also ‎imported the Adafruit Eaglecad part footprints for the PCB layout.‎

design_3

design_4

design_5

Order PCBs

I ordered my PCBs from JLCPCB, but you can order from a number of ‎different places online including OSHPark, DigiKey, PCBWay and others.‎

The boards I got were $14.84 for five boards, plus shipping. Download ‎the .zip file linked below to get the Gerber and drill files needed to ‎have your own set made.‎

pcbs_6

wavefader_v02.zip

Assemble the Synth

synth_7

Parts Prep

Get all of your parts together, as well as the headers.‎

The DAC is optional, so we'll add that last.‎

parts_8

parts_9

ItsyBitsy Headers

Solder in place the two inner rows of ItsyBitsy short female headers.‎

headers_10

ADC Prep

The ADS7830 analog-to-digital (ADC) boards each have eight channels ‎to read the faders and convert their analog values into 8-bit digital ‎messages sent over the I2C bus.‎

The first ADC board will use the default I2C address and will be ‎soldered closer to the ItsyBitsy. The second one will need to be set to a ‎different I2C address. Heat the AD0 pads and jumper them with solder.‎

prep_11

Be careful to orient the two ADC boards properly, they can accidentally ‎be inserted the wrong way around!‎

Solder the first board into place with header pins as shown, being ‎careful to match the orientation of the silkscreen shown on the ‎Faderwave PCB.‎

Clip the extra pin lengths with diagonal cutters.‎

solder_12

solder_13

Second ADC

Repeat for the second ADC board, being careful to match the ‎silkscreen orientation, as this board is rotated 180º from the first ‎board.‎

second_14

second_15

Reset Button

Insert the tactile button and then solder it in place.‎

button_16

OLED Prep

Solder in short header pins as shown. Then to modify the board to use ‎SPI mode, flip the OLED over and cut the traces for J1 and J2.‎

oled_17

oled_18

OLED Mount

First, solder in place the short header for the OLED display.‎

Then, fasten four M2.5 x 8mm standoffs to the board as shown, ‎screwing four short screws in from under the PCB.‎

Insert the OLED into the headers, and then screw in four screws from ‎the top to secure.‎

mount_19

mount_20

mount_21

mount_22

Audio Out

Use two M2.5 x 6mm screws with nuts to create a stabilizing mount ‎for the TRRS 3.5mm breakout.‎

Then, solder it in place with a header pin row as shown.‎

audio_23

audio_24

audio_25

audio_26

Rotary Encoder

Solder the rotary encoder in place.‎

encoder_27

encoder_28

ItsyBitsy

Solder the short header pins to the Itsy Bitsy as shown, then insert it ‎into the board with the USB jack facing left.‎

solder_29

solder_30

In Go the Faders

Check for any bent pins and straighten them. Then, insert the slide ‎potentiometers into the PCB (they can only go in one way).‎

You can use tape or a rubber band to hold them flush to the board as ‎you flip it over and start soldering!‎

Note, the four hardware tabs can require increased soldering iron heat ‎to get the large copper ground planes hot enough so solder easily.‎

check_31

check_32

check_33

check_34

check_35

Options: you can solder in the resistors and capacitors to use the RC ‎filter circuit to smooth out the audio (particularly PWM audio from an ‎ItsyBitsy RP2040) or use the solder jumper pads to bypass the circuit ‎altogether.‎

Option A: Solder the RC Filter

If you want to filter the audio a bit from the high end, solder in the 1k ‎resistors and 0.1uF capacitors as shown. There is one RC pair per ‎channel of the stereo output that will massage the audio on its way to ‎the left and right channels of the 3.5mm TRS output.‎

filter_36

Option B: RC Circuit Bypass

To bypass the resistor/capacitor filter on the audio outputs (they can ‎be used with either ItsyBitsy RP2040 or M4 but are more helpful with ‎the PWM out of the RP2040), cut the trace between the left and middle ‎pad of JP1 and JP2. Then, solder a jumper blob between the middle and ‎right pad of both.‎

bypass_37

bypass_38

bypass_39

Optional DAC Output

The Faderwave is a platform for audio experimentation, so I decided to ‎include a 16-bit DAC output for possible control voltage (CV) use on ‎vintage and modular synthesizers, such as Eurorack modules. This is ‎optional, but if you think you may want to try it out at some point, go ‎ahead and solder it in place now!‎

output_40

output_41

output_42

output_43

Panels

You can optionally have a front and back panel laser cut for the ‎Faderwave. Use the SVG file linked below. The vector curves are meant ‎to be cut from 1/8" thick acrylic, with the hatch pattern for the type, ‎level indicators, and waveform graphic being raster etched at 400dpi.‎

laser_44

etched_47

faderwave_case_05.svg

case_48

Fasten the panels with four M3 x 12mm standoffs, four M3 hex nuts, ‎and eight short M3 screws.‎

fasten_49

fasten_50

fasten_51

fasten_53

Add your fader caps and rotary encoder knob and you're ready to play.

caps_54

CircuitPython on ItsyBitsy

CircuitPython is a derivative of MicroPython designed to simplify ‎experimentation and education on low-cost microcontrollers. It makes ‎it easier than ever to get prototyping by requiring no upfront desktop ‎software downloads. Simply copy and edit files on the CIRCUITPY drive ‎to iterate.‎

Set up CircuitPython Quick Start!‎

Follow this quick step-by-step for super-fast Python power :)‎

Download the latest version of CircuitPython for this board via ‎CircuitPython.org

Further Information

For more detailed info on installing CircuitPython, check out Installing ‎CircuitPython.‎

Click the link above and download the latest UF2 file.‎

Download and save it to your desktop (or wherever is handy).‎

click_55

Plug your Itsy M4 into your computer using a known-good USB cable.‎

A lot of people end up using charge-only USB cables and it is ‎very frustrating! So, make sure you have a USB cable you know ‎is good for data sync.‎

Double-click the Reset button on your board, and you will see the ‎DotStar RGB LED turn green. If it turns red, check the USB cable, try ‎another USB port, etc.‎

If double-clicking doesn't work the first time, try again. Sometimes it ‎can take a few tries to get the rhythm right!‎

adafruit_products_3800_56

You will see a new disk drive appear called ITSYBOOT.‎

Drag the adafruit_circuitpython_etc.uf2 file to ITSYBOOT.‎

drive_57

drive_58

The LED will flash. Then, the ITSYBOOT drive will disappear, and a new ‎disk drive called CIRCUITPY will appear.‎

That's it, you're done! :)‎

led_59

Code the Faderwave Synth

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.‎

Download Project Bundle

Copy Code
# 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

View on GitHub

How it Works

Imports

First the code imports the necessary library modules:‎

Download File

Copy Code
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.‎

Download File

Copy Code
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

Download File

Copy Code
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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
# 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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
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.‎

Download File

Copy Code
# 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.‎

Download File

Copy Code
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 to True, it prints debugging messages, including ‎MIDI notes on/off and polyphony information

Download File

Copy Code
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

Play the Faderwave Synth

play_60

play_61

 

To play the Faderwave, plug it into a powered speaker or amp with a ‎stereo TRS 3.5mm audio cable. Then, plug it into your computer or ‎other USB MIDI Host controller and send MIDI note on/note off ‎messages. You'll hear the synth play!‎

Adjust the faders to make different waveforms, which will change the ‎timbre of the sound by emphasizing or deemphasizing different ‎harmonic content of the audio waveform.‎

Click the encoder to move between menu items on the OLED. You can ‎turn the rotary encoder to:‎

  • increase or decrease the detune amount between multiple voices
  • adjust the number of voices for a thicker sound
  • adjust the volume
  • change the low pass filter cutoff frequency

You can also write new menu items into the code if you like to further ‎customize your Faderwave synth.‎

write_62

 

Mfr Part # 3800
ITSYBITSY M4EXPRESS ATSAMD51G19A
Adafruit Industries LLC
Mfr Part # 4888
ADAFRUIT ITSYBITSY RP2040
Adafruit Industries LLC
Mfr Part # 5836
ADAFRUIT ADS7830 8-CHANNEL 8-BIT
Adafruit Industries LLC
Mfr Part # 938
GRAPHIC DISPLAY OLED WHITE 1.3"
Adafruit Industries LLC
Mfr Part # 5764
TRRS JACK BREAKOUT BOARD
Adafruit Industries LLC
Mfr Part # 4173
CONN HEADER VERT FOR ITSYBI
Adafruit Industries LLC
Mfr Part # 4174
SHORT FEMALE HEADER KIT FOR ITSY
Adafruit Industries LLC
Mfr Part # 3299
BLACK NYLON SCREW AND STAND-OFF
Adafruit Industries LLC
Mfr Part # 4685
BLACK NYLON SCREW AND STAND-OFF
Adafruit Industries LLC
Mfr Part # 2185
CABLE A PLUG TO MCR B PLUG 6.56'
Adafruit Industries LLC
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.