first commit Tyrell desktop synth code

This commit is contained in:
John Park 2023-07-12 13:17:41 -07:00
parent e8b0cd81d7
commit 7c1eba4e64

146
Tyrell_Synth/code.py Normal file
View file

@ -0,0 +1,146 @@
# SPDX-FileCopyrightText: 2023 John Park & Tod Kurt
#
# SPDX-License-Identifier: MIT
# Tyrell Synth Distopia
# based on:
# 19 Jun 2023 - @todbot / Tod Kurt
# - A swirling ominous wub that evolves over time
# - Made for QTPy RP2040 but will work on any synthio-capable board
# - wallow in the sound
#
# Circuit:
# - QT Py RP2040
# - QTPy TX/RX pins for audio out, going through RC filter (1k + 100nF) to TRS jack
# Touch io for eight pins, pairs that -/+ tempo, transpose pitch, filter rate, volume
# use >1MΩ resistors to pull down to ground
#
# Code:
# - Five detuned oscillators are randomly detuned very second or so
# - A low-pass filter is slowly modulated over the filters
# - The filter modulation rate also changes randomly every second (also reflected on neopixel)
# - Every x seconds a new note is randomly chosen from the allowed note list
import time
import random
import board
import audiopwmio
import audiomixer
import synthio
import ulab.numpy as np
import neopixel
import rainbowio
import touchio
from adafruit_debouncer import Debouncer
touch_pins = (board.A0, board.A1, board.A2, board.A3, board.SDA, board.SCL, board.MISO, board.MOSI)
touchpads = []
for pin in touch_pins:
tmp_pin = touchio.TouchIn(pin)
touchpads.append(Debouncer(tmp_pin))
notes = (37, 38, 35, 49) # MIDI C#, D, B
note_duration = 10 # how long each note plays for
num_voices = 6 # how many voices for each note
lpf_basef = 300 # low pass filter lowest frequency
lpf_resonance = 1.7 # filter q
led = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.1)
# PWM pin pair on QTPY RP2040
audio = audiopwmio.PWMAudioOut(left_channel=board.TX, right_channel=board.RX)
mixer = audiomixer.Mixer(channel_count=2, sample_rate=28000, buffer_size=2048)
synth = synthio.Synthesizer(channel_count=2, sample_rate=28000)
audio.play(mixer)
mixer.voice[0].play(synth)
mixer_vol = 0.5
mixer.voice[0].level = mixer_vol
# oscillator waveform, a 512 sample downward saw wave going from +/-30k
wave_saw = np.linspace(30000, -30000, num=512, dtype=np.int16) # max is +/-32k gives us headroom
amp_env = synthio.Envelope(attack_level=1, sustain_level=1)
# set up the voices (aka "Notes" in synthio-speak) w/ initial values
voices = []
for i in range(num_voices):
voices.append(synthio.Note(frequency=0, envelope=amp_env, waveform=wave_saw))
lfo_panning = synthio.LFO(rate=0.1, scale=0.75)
# set all the voices to the "same" frequency (with random detuning)
# zeroth voice is sub-oscillator, one-octave down
def set_notes(n):
for voice in voices:
f = synthio.midi_to_hz(n + random.uniform(0, 0.4))
voice.frequency = f
voice.panning = lfo_panning
voices[0].frequency = voices[0].frequency/2 # bass note one octave down
# the LFO that modulates the filter cutoff
lfo_filtermod = synthio.LFO(rate=0.05, scale=2000, offset=2000)
# we can't attach this directly to a filter input, so stash it in the blocks runner
synth.blocks.append(lfo_filtermod)
note = notes[0]
last_note_time = time.monotonic()
last_filtermod_time = time.monotonic()
# start the voices playing
set_notes(note)
synth.press(voices)
# user input variables
note_offset = (0, 1, 3, 4, 5, 7)
note_offset_index = 0
lfo_subdivision = 8
print("'Prepare to wallow.' \n- Major Jack Dongle")
while True:
for t in range(len(touchpads)):
touchpads[t].update()
if touchpads[t].rose:
if t == 0:
note_offset_index = (note_offset_index + 1) % (len(note_offset))
set_notes(note + note_offset[note_offset_index])
elif t == 1:
note_offset_index = (note_offset_index - 1) % (len(note_offset))
set_notes(note + note_offset[note_offset_index])
elif t == 2:
note_duration = note_duration + 1
elif t == 3:
note_duration = abs(max((note_duration - 1), 1))
elif t == 4:
lfo_subdivision = 20
elif t == 5:
lfo_subdivision = 0.2
elif t == 6: # volume
mixer_vol = max(mixer_vol - 0.05, 0.0)
mixer.voice[0].level = mixer_vol
elif t == 7: # volume
mixer_vol = min(mixer_vol + 0.05, 1.0)
mixer.voice[0].level = mixer_vol
# continuosly update filter, no global filter, so update each voice's filter
for v in voices:
v.filter = synth.low_pass_filter(lpf_basef + lfo_filtermod.value, lpf_resonance)
led.fill(rainbowio.colorwheel(lfo_filtermod.value/20)) # show filtermod moving
if time.monotonic() - last_filtermod_time > 1:
last_filtermod_time = time.monotonic()
# randomly modulate the filter frequency ('rate' in synthio) to make more dynamic
lfo_filtermod.rate = 0.01 + random.random() / lfo_subdivision
if time.monotonic() - last_note_time > note_duration:
last_note_time = time.monotonic()
# pick new note, but not one we're currently playing
note = random.choice([n for n in notes if n != note])
set_notes(note+note_offset[note_offset_index])
print("note", note, ["%3.2f" % v.frequency for v in voices])