Adafruit_Learning_System_Gu.../Faderwave_Synth/code.py
2023-12-15 18:31:37 -08:00

315 lines
9.9 KiB
Python

# 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