Faderwave Synthesizer
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
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
- 16 x Slide Potentiometer with Knob - 75mm Long
- Monochrome 1.3" 128x64 OLED graphic display - STEMMA QT / Qwiic
- Adafruit AD5693R Breakout Board - 16-Bit DAC with I2C Interface
- Adafruit TRRS Jack Breakout Board
- 1 x Tactile Button switch (6mm)
- 1 x 0.1uF ceramic capacitors
- 1 x Through-Hole Resistors - 1.0K ohm 5% 1/4W
- 2 x Short Male Header Kit
- 2 x Short Female Header Kit
- 1 x Black Nylon Machine Screw and Stand-off Set
- 1 x Black Nylon Machine Screw and Stand-off Set
- 1 x USB A/Micro Cable
Faderwave Circuit
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.
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.
Assemble the Synth
Parts Prep
Get all of your parts together, as well as the headers.
The DAC is optional, so we'll add that last.
ItsyBitsy Headers
Solder in place the two inner rows of ItsyBitsy short female headers.
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.
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.
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.
Reset Button
Insert the tactile button and then solder it in place.
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 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.
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.
Rotary Encoder
Solder the rotary encoder in place.
ItsyBitsy
Solder the short header pins to the Itsy Bitsy as shown, then insert it into the board with the USB jack facing left.
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.
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.
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.
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!
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.
Fasten the panels with four M3 x 12mm standoffs, four M3 hex nuts, and eight short M3 screws.
Add your fader caps and rotary encoder knob and you're ready to play.
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).
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!
You will see a new disk drive appear called ITSYBOOT.
Drag the adafruit_circuitpython_etc.uf2 file to ITSYBOOT.
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! :)
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.
# 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
How it Works
Imports
First the code imports the necessary library modules:
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 to True, 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
Play the Faderwave Synth
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.
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum