first commit Tyrell desktop synth code
This commit is contained in:
parent
e8b0cd81d7
commit
7c1eba4e64
1 changed files with 146 additions and 0 deletions
146
Tyrell_Synth/code.py
Normal file
146
Tyrell_Synth/code.py
Normal 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])
|
||||||
Loading…
Reference in a new issue