Maker.io main logo

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 

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

 

制造商零件编号 3800
ITSYBITSY M4EXPRESS ATSAMD51G19A
Adafruit Industries LLC
制造商零件编号 4888
ADAFRUIT ITSYBITSY RP2040
Adafruit Industries LLC
制造商零件编号 5836
ADAFRUIT ADS7830 8-CHANNEL 8-BIT
Adafruit Industries LLC
制造商零件编号 938
GRAPHIC DISPLAY OLED WHITE 1.3"
Adafruit Industries LLC
制造商零件编号 5764
TRRS JACK BREAKOUT BOARD
Adafruit Industries LLC
制造商零件编号 4173
CONN HEADER VERT FOR ITSYBI
Adafruit Industries LLC
制造商零件编号 4174
SHORT FEMALE HEADER KIT FOR ITSY
Adafruit Industries LLC
制造商零件编号 3299
BLACK NYLON SCREW AND STAND-OFF
Adafruit Industries LLC
制造商零件编号 4685
BLACK NYLON SCREW AND STAND-OFF
Adafruit Industries LLC
制造商零件编号 2185
CABLE A PLUG TO MCR B PLUG 6.56'
Adafruit Industries LLC
Add all DigiKey Parts to Cart
TechForum

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.

Visit TechForum