# 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