Skip to content

Commit 7c1eba4

Browse files
committed
first commit Tyrell desktop synth code
1 parent e8b0cd8 commit 7c1eba4

File tree

1 file changed

+146
-0
lines changed

1 file changed

+146
-0
lines changed

Tyrell_Synth/code.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# SPDX-FileCopyrightText: 2023 John Park & Tod Kurt
2+
#
3+
# SPDX-License-Identifier: MIT
4+
# Tyrell Synth Distopia
5+
# based on:
6+
# 19 Jun 2023 - @todbot / Tod Kurt
7+
# - A swirling ominous wub that evolves over time
8+
# - Made for QTPy RP2040 but will work on any synthio-capable board
9+
# - wallow in the sound
10+
#
11+
# Circuit:
12+
# - QT Py RP2040
13+
# - QTPy TX/RX pins for audio out, going through RC filter (1k + 100nF) to TRS jack
14+
# Touch io for eight pins, pairs that -/+ tempo, transpose pitch, filter rate, volume
15+
# use >1MΩ resistors to pull down to ground
16+
#
17+
# Code:
18+
# - Five detuned oscillators are randomly detuned very second or so
19+
# - A low-pass filter is slowly modulated over the filters
20+
# - The filter modulation rate also changes randomly every second (also reflected on neopixel)
21+
# - Every x seconds a new note is randomly chosen from the allowed note list
22+
23+
import time
24+
import random
25+
import board
26+
import audiopwmio
27+
import audiomixer
28+
import synthio
29+
import ulab.numpy as np
30+
import neopixel
31+
import rainbowio
32+
import touchio
33+
from adafruit_debouncer import Debouncer
34+
35+
touch_pins = (board.A0, board.A1, board.A2, board.A3, board.SDA, board.SCL, board.MISO, board.MOSI)
36+
touchpads = []
37+
for pin in touch_pins:
38+
tmp_pin = touchio.TouchIn(pin)
39+
touchpads.append(Debouncer(tmp_pin))
40+
41+
notes = (37, 38, 35, 49) # MIDI C#, D, B
42+
note_duration = 10 # how long each note plays for
43+
num_voices = 6 # how many voices for each note
44+
lpf_basef = 300 # low pass filter lowest frequency
45+
lpf_resonance = 1.7 # filter q
46+
47+
led = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.1)
48+
49+
# PWM pin pair on QTPY RP2040
50+
audio = audiopwmio.PWMAudioOut(left_channel=board.TX, right_channel=board.RX)
51+
52+
mixer = audiomixer.Mixer(channel_count=2, sample_rate=28000, buffer_size=2048)
53+
synth = synthio.Synthesizer(channel_count=2, sample_rate=28000)
54+
audio.play(mixer)
55+
mixer.voice[0].play(synth)
56+
mixer_vol = 0.5
57+
mixer.voice[0].level = mixer_vol
58+
59+
# oscillator waveform, a 512 sample downward saw wave going from +/-30k
60+
wave_saw = np.linspace(30000, -30000, num=512, dtype=np.int16) # max is +/-32k gives us headroom
61+
amp_env = synthio.Envelope(attack_level=1, sustain_level=1)
62+
63+
# set up the voices (aka "Notes" in synthio-speak) w/ initial values
64+
voices = []
65+
for i in range(num_voices):
66+
voices.append(synthio.Note(frequency=0, envelope=amp_env, waveform=wave_saw))
67+
68+
lfo_panning = synthio.LFO(rate=0.1, scale=0.75)
69+
70+
# set all the voices to the "same" frequency (with random detuning)
71+
# zeroth voice is sub-oscillator, one-octave down
72+
def set_notes(n):
73+
for voice in voices:
74+
f = synthio.midi_to_hz(n + random.uniform(0, 0.4))
75+
voice.frequency = f
76+
voice.panning = lfo_panning
77+
voices[0].frequency = voices[0].frequency/2 # bass note one octave down
78+
79+
# the LFO that modulates the filter cutoff
80+
lfo_filtermod = synthio.LFO(rate=0.05, scale=2000, offset=2000)
81+
# we can't attach this directly to a filter input, so stash it in the blocks runner
82+
synth.blocks.append(lfo_filtermod)
83+
84+
note = notes[0]
85+
last_note_time = time.monotonic()
86+
last_filtermod_time = time.monotonic()
87+
88+
# start the voices playing
89+
set_notes(note)
90+
synth.press(voices)
91+
92+
# user input variables
93+
note_offset = (0, 1, 3, 4, 5, 7)
94+
note_offset_index = 0
95+
96+
lfo_subdivision = 8
97+
98+
print("'Prepare to wallow.' \n- Major Jack Dongle")
99+
100+
101+
while True:
102+
for t in range(len(touchpads)):
103+
touchpads[t].update()
104+
if touchpads[t].rose:
105+
if t == 0:
106+
note_offset_index = (note_offset_index + 1) % (len(note_offset))
107+
set_notes(note + note_offset[note_offset_index])
108+
elif t == 1:
109+
note_offset_index = (note_offset_index - 1) % (len(note_offset))
110+
set_notes(note + note_offset[note_offset_index])
111+
112+
elif t == 2:
113+
note_duration = note_duration + 1
114+
elif t == 3:
115+
note_duration = abs(max((note_duration - 1), 1))
116+
117+
elif t == 4:
118+
lfo_subdivision = 20
119+
elif t == 5:
120+
lfo_subdivision = 0.2
121+
122+
elif t == 6: # volume
123+
mixer_vol = max(mixer_vol - 0.05, 0.0)
124+
mixer.voice[0].level = mixer_vol
125+
126+
elif t == 7: # volume
127+
mixer_vol = min(mixer_vol + 0.05, 1.0)
128+
mixer.voice[0].level = mixer_vol
129+
130+
# continuosly update filter, no global filter, so update each voice's filter
131+
for v in voices:
132+
v.filter = synth.low_pass_filter(lpf_basef + lfo_filtermod.value, lpf_resonance)
133+
134+
led.fill(rainbowio.colorwheel(lfo_filtermod.value/20)) # show filtermod moving
135+
136+
if time.monotonic() - last_filtermod_time > 1:
137+
last_filtermod_time = time.monotonic()
138+
# randomly modulate the filter frequency ('rate' in synthio) to make more dynamic
139+
lfo_filtermod.rate = 0.01 + random.random() / lfo_subdivision
140+
141+
if time.monotonic() - last_note_time > note_duration:
142+
last_note_time = time.monotonic()
143+
# pick new note, but not one we're currently playing
144+
note = random.choice([n for n in notes if n != note])
145+
set_notes(note+note_offset[note_offset_index])
146+
print("note", note, ["%3.2f" % v.frequency for v in voices])

0 commit comments

Comments
 (0)