316 lines
9.9 KiB
Python
316 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
|
|
import fourwire
|
|
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 = fourwire.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
|