|
| 1 | +# SPDX-FileCopyrightText: Copyright (c) 2023 john park for Adafruit Industries |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | +''' Faderwave Synthesizer |
| 5 | + use 16 faders to create the single cycle waveform |
| 6 | + rotary encoder adjusts other synth parameters |
| 7 | + audio output: line level over 3.5mm TRS |
| 8 | + CV output via DAC ''' |
| 9 | + |
| 10 | +import board |
| 11 | +import busio |
| 12 | +import ulab.numpy as np |
| 13 | +import rotaryio |
| 14 | +import neopixel |
| 15 | +from digitalio import DigitalInOut, Pull |
| 16 | +import displayio |
| 17 | +from adafruit_display_text import label |
| 18 | +import terminalio |
| 19 | +import synthio |
| 20 | +import audiomixer |
| 21 | +from adafruit_debouncer import Debouncer |
| 22 | +import adafruit_ads7830.ads7830 as ADC |
| 23 | +from adafruit_ads7830.analog_in import AnalogIn |
| 24 | +import adafruit_displayio_ssd1306 |
| 25 | +import adafruit_ad569x |
| 26 | +import usb_midi |
| 27 | +import adafruit_midi |
| 28 | +from adafruit_midi.note_on import NoteOn |
| 29 | +from adafruit_midi.note_off import NoteOff |
| 30 | + |
| 31 | +displayio.release_displays() |
| 32 | + |
| 33 | +ITSY_TYPE = 0 # Pick your ItsyBitsy: 0=M4, 1=RP2040 |
| 34 | + |
| 35 | +# neopixel setup for RP2040 only |
| 36 | +if ITSY_TYPE == 1: |
| 37 | + pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.3) |
| 38 | + pixel.fill(0x004444) |
| 39 | + |
| 40 | +i2c = busio.I2C(board.SCL, board.SDA, frequency=1_000_000) |
| 41 | + |
| 42 | +midi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], in_channel=0) |
| 43 | + |
| 44 | +NUM_FADERS = 16 |
| 45 | +num_oscs = 2 # how many oscillators for each note |
| 46 | +detune = 0.003 # how much to detune the oscillators |
| 47 | +volume = 0.6 # mixer volume |
| 48 | +lpf_freq = 12000 # user Low Pass Filter frequency setting |
| 49 | +lpf_basef = 500 # filter lowest frequency |
| 50 | +lpf_resonance = 0.1 # filter q |
| 51 | + |
| 52 | +faders_pos = [0] * NUM_FADERS |
| 53 | +last_faders_pos = [0] * NUM_FADERS |
| 54 | + |
| 55 | +# Initialize ADS7830 |
| 56 | +adc_a = ADC.ADS7830(i2c, address=0x48) # default address 0x48 |
| 57 | +adc_b = ADC.ADS7830(i2c, address=0x4A) # A0 jumper 0x49, A1 0x4A |
| 58 | + |
| 59 | +faders = [] # list for fader objects on first ADC |
| 60 | +for fdr in range(8): # add first group to list |
| 61 | + faders.append(AnalogIn(adc_a, fdr)) |
| 62 | +for fdr in range(8): # add second group |
| 63 | + faders.append(AnalogIn(adc_b, fdr)) |
| 64 | + |
| 65 | +# Initialize AD5693R for CV out |
| 66 | +dac = adafruit_ad569x.Adafruit_AD569x(i2c) |
| 67 | +dac.gain = True |
| 68 | +dac.value = faders[0].value # set dac out to the slider level |
| 69 | + |
| 70 | +# Rotary encoder setup |
| 71 | +ENC_A = board.D9 |
| 72 | +ENC_B = board.D10 |
| 73 | +ENC_SW = board.D7 |
| 74 | + |
| 75 | +button_in = DigitalInOut(ENC_SW) # defaults to input |
| 76 | +button_in.pull = Pull.UP # turn on internal pull-up resistor |
| 77 | +button = Debouncer(button_in) |
| 78 | + |
| 79 | +encoder = rotaryio.IncrementalEncoder(ENC_A, ENC_B) |
| 80 | +encoder_pos = encoder.position |
| 81 | +last_encoder_pos = encoder.position |
| 82 | + |
| 83 | +# display setup |
| 84 | +OLED_RST = board.D13 |
| 85 | +OLED_DC = board.D12 |
| 86 | +OLED_CS = board.D11 |
| 87 | + |
| 88 | +spi = board.SPI() |
| 89 | +display_bus = displayio.FourWire(spi, command=OLED_DC, chip_select=OLED_CS, |
| 90 | + reset=OLED_RST, baudrate=30_000_000) |
| 91 | +display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64) |
| 92 | + |
| 93 | +# Create display group |
| 94 | +group = displayio.Group() |
| 95 | +# Create background rectangle |
| 96 | +# bg_rect = Rect(0, 0, display.width, display.height, fill=0x0) |
| 97 | +# group.append(bg_rect) |
| 98 | +# Set the font for the text label |
| 99 | +font = terminalio.FONT |
| 100 | + |
| 101 | +# Create text label |
| 102 | +title = label.Label(font, x=2, y=4, text=("Faderwave Synthesizer"), color=0xffffff) |
| 103 | +group.append(title) |
| 104 | + |
| 105 | +title2 = label.Label(font, x=2, y=10, text=("---------------------"), color=0xffffff) |
| 106 | +group.append(title2) |
| 107 | + |
| 108 | +column_x = (20, 90) |
| 109 | +row_y = (22, 34, 48, 60) |
| 110 | + |
| 111 | +# Create menu selector |
| 112 | +menu_sel = 0 |
| 113 | +menu_sel_txt = label.Label(font, text=("->"), color=0xffffff) |
| 114 | +menu_sel_txt.x = column_x[0]-16 |
| 115 | +menu_sel_txt.y = row_y[menu_sel] |
| 116 | +group.append(menu_sel_txt) |
| 117 | + |
| 118 | +# Create detune text |
| 119 | +det_txt_a = label.Label(font, text=("Detune....."), color=0xffffff) |
| 120 | +det_txt_a.x = column_x[0] |
| 121 | +det_txt_a.y = row_y[0] |
| 122 | +group.append(det_txt_a) |
| 123 | + |
| 124 | +det_txt_b = label.Label(font, text=(str(0.003)), color=0xffffff) |
| 125 | +det_txt_b.x = column_x[1] |
| 126 | +det_txt_b.y = row_y[0] |
| 127 | +group.append(det_txt_b) |
| 128 | + |
| 129 | +# Create number of oscs text |
| 130 | +num_oscs_txt_a = label.Label(font, text=("Num Oscs..."), color=0xffffff) |
| 131 | +num_oscs_txt_a.x = column_x[0] |
| 132 | +num_oscs_txt_a.y = row_y[1] |
| 133 | +group.append(num_oscs_txt_a) |
| 134 | + |
| 135 | +num_oscs_txt_b = label.Label(font, text=(str(num_oscs)), color=0xffffff) |
| 136 | +num_oscs_txt_b.x = column_x[1] |
| 137 | +num_oscs_txt_b.y = row_y[1] |
| 138 | +group.append(num_oscs_txt_b) |
| 139 | + |
| 140 | +# Create volume text |
| 141 | +vol_txt_a = label.Label(font, text=("Volume....."), color=0xffffff) |
| 142 | +vol_txt_a.x = column_x[0] |
| 143 | +vol_txt_a.y = row_y[2] |
| 144 | +group.append(vol_txt_a) |
| 145 | + |
| 146 | +vol_txt_b = label.Label(font, text=(str(volume)), color=0xffffff) |
| 147 | +vol_txt_b.x = column_x[1] |
| 148 | +vol_txt_b.y = row_y[2] |
| 149 | +group.append(vol_txt_b) |
| 150 | + |
| 151 | + |
| 152 | +# Create lpf frequency text |
| 153 | +lpf_txt_a = label.Label(font, text=("LPF........"), color=0xffffff) |
| 154 | +lpf_txt_a.x = column_x[0] |
| 155 | +lpf_txt_a.y = row_y[3] |
| 156 | +group.append(lpf_txt_a) |
| 157 | + |
| 158 | +lpf_txt_b = label.Label(font, text=(str(lpf_freq)), color=0xffffff) |
| 159 | +lpf_txt_b.x = column_x[1] |
| 160 | +lpf_txt_b.y = row_y[3] |
| 161 | +group.append(lpf_txt_b) |
| 162 | + |
| 163 | + |
| 164 | +# Show the display group |
| 165 | +display.root_group = group |
| 166 | + |
| 167 | +# Synthio setup |
| 168 | +if ITSY_TYPE == 0: |
| 169 | + import audioio |
| 170 | + audio = audioio.AudioOut(left_channel=board.A0, right_channel=board.A1) # M4 built-in DAC |
| 171 | +if ITSY_TYPE == 1: |
| 172 | + import audiopwmio |
| 173 | + audio = audiopwmio.PWMAudioOut(board.A1) |
| 174 | +# if using I2S amp: |
| 175 | +# audio = audiobusio.I2SOut(bit_clock=board.MOSI, word_select=board.MISO, data=board.SCK) |
| 176 | + |
| 177 | +mixer = audiomixer.Mixer(channel_count=2, sample_rate=44100, buffer_size=4096) |
| 178 | +synth = synthio.Synthesizer(channel_count=2, sample_rate=44100) |
| 179 | +audio.play(mixer) |
| 180 | +mixer.voice[0].play(synth) |
| 181 | +mixer.voice[0].level = 0.75 |
| 182 | + |
| 183 | +wave_user = np.array([0]*NUM_FADERS, dtype=np.int16) |
| 184 | +amp_env = synthio.Envelope(attack_time=0.3, attack_level=1, sustain_level=0.65, release_time=0.3) |
| 185 | + |
| 186 | +def faders_to_wave(): |
| 187 | + for j in range(NUM_FADERS): |
| 188 | + wave_user[j] = int(map_range(faders_pos[j], 0, 255, -32768, 32767)) |
| 189 | + |
| 190 | +notes_pressed = {} # which notes being pressed. key=midi note, val=note object |
| 191 | + |
| 192 | +def note_on(n): |
| 193 | + voices = [] # holds our currently sounding voices ('Notes' in synthio speak) |
| 194 | + fo = synthio.midi_to_hz(n) |
| 195 | + lpf = synth.low_pass_filter(lpf_freq, lpf_resonance) |
| 196 | + |
| 197 | + for k in range(num_oscs): |
| 198 | + f = fo * (1 + k*detune) |
| 199 | + voices.append(synthio.Note(frequency=f, filter=lpf, envelope=amp_env, waveform=wave_user)) |
| 200 | + synth.press(voices) |
| 201 | + notes_pressed[n] = voices |
| 202 | + |
| 203 | +def note_off(n): |
| 204 | + note = notes_pressed.get(n, None) |
| 205 | + if note: |
| 206 | + synth.release(note) |
| 207 | + |
| 208 | +# simple range mapper, like Arduino map() |
| 209 | +def map_range(s, a1, a2, b1, b2): |
| 210 | + return b1 + ((s - a1) * (b2 - b1) / (a2 - a1)) |
| 211 | + |
| 212 | + |
| 213 | +while True: |
| 214 | + # get midi messages |
| 215 | + msg = midi.receive() |
| 216 | + if isinstance(msg, NoteOn) and msg.velocity != 0: |
| 217 | + note_on(msg.note) |
| 218 | + elif isinstance(msg, NoteOff) or isinstance(msg, NoteOn) and msg.velocity == 0: |
| 219 | + note_off(msg.note) |
| 220 | + |
| 221 | + # check faders |
| 222 | + for i in range(len(faders)): |
| 223 | + faders_pos[i] = faders[i].value//256 |
| 224 | + if faders_pos[i] is not last_faders_pos[i]: |
| 225 | + faders_to_wave() |
| 226 | + last_faders_pos[i] = faders_pos[i] |
| 227 | + |
| 228 | + # send out a DAC value based on fader 0 |
| 229 | + if i == 0: |
| 230 | + dac.value = faders[0].value |
| 231 | + |
| 232 | + # check encoder button |
| 233 | + button.update() |
| 234 | + if button.fell: |
| 235 | + menu_sel = (menu_sel+1) % 4 |
| 236 | + menu_sel_txt.y = row_y[menu_sel] |
| 237 | + |
| 238 | + # check encoder |
| 239 | + encoder_pos = encoder.position |
| 240 | + if encoder_pos > last_encoder_pos: |
| 241 | + delta = encoder_pos - last_encoder_pos |
| 242 | + if menu_sel == 0: |
| 243 | + detune = detune + (delta * 0.001) |
| 244 | + detune = min(max(detune, -0.030), 0.030) |
| 245 | + formatted_detune = str("{:.3f}".format(detune)) |
| 246 | + det_txt_b.text = formatted_detune |
| 247 | + |
| 248 | + elif menu_sel == 1: |
| 249 | + num_oscs = num_oscs + delta |
| 250 | + num_oscs = min(max(num_oscs, 1), 8) |
| 251 | + formatted_num_oscs = str(num_oscs) |
| 252 | + num_oscs_txt_b.text = formatted_num_oscs |
| 253 | + |
| 254 | + elif menu_sel == 2: |
| 255 | + volume = volume + (delta * 0.01) |
| 256 | + volume = min(max(volume, 0.00), 1.00) |
| 257 | + mixer.voice[0].level = volume |
| 258 | + formatted_volume = str("{:.2f}".format(volume)) |
| 259 | + vol_txt_b.text = formatted_volume |
| 260 | + |
| 261 | + elif menu_sel == 3: |
| 262 | + lpf_freq = lpf_freq + (delta * 1000) |
| 263 | + lpf_freq = min(max(lpf_freq, 1000), 20_000) |
| 264 | + formatted_lpf = str(lpf_freq) |
| 265 | + lpf_txt_b.text = formatted_lpf |
| 266 | + |
| 267 | + last_encoder_pos = encoder.position |
| 268 | + |
| 269 | + if encoder_pos < last_encoder_pos: |
| 270 | + delta = last_encoder_pos - encoder_pos |
| 271 | + if menu_sel == 0: |
| 272 | + detune = detune - (delta * 0.001) |
| 273 | + detune = min(max(detune, -0.030), 0.030) |
| 274 | + formatted_detune = str("{:.3f}".format(detune)) |
| 275 | + det_txt_b.text = formatted_detune |
| 276 | + |
| 277 | + elif menu_sel == 1: |
| 278 | + num_oscs = num_oscs - delta |
| 279 | + num_oscs = min(max(num_oscs, 1), 8) |
| 280 | + formatted_num_oscs = str(num_oscs) |
| 281 | + num_oscs_txt_b.text = formatted_num_oscs |
| 282 | + |
| 283 | + elif menu_sel == 2: |
| 284 | + volume = volume - (delta * 0.01) |
| 285 | + volume = min(max(volume, 0.00), 1.00) |
| 286 | + mixer.voice[0].level = volume |
| 287 | + formatted_volume = str("{:.2f}".format(volume)) |
| 288 | + vol_txt_b.text = formatted_volume |
| 289 | + |
| 290 | + elif menu_sel == 3: |
| 291 | + lpf_freq = lpf_freq - (delta * 1000) |
| 292 | + lpf_freq = min(max(lpf_freq, 1000), 20_000) |
| 293 | + formatted_lpf = str(lpf_freq) |
| 294 | + lpf_txt_b.text = formatted_lpf |
| 295 | + |
| 296 | + last_encoder_pos = encoder.position |
0 commit comments