-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathcode.py
258 lines (199 loc) · 8.77 KB
/
code.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# wavesynth_code.py -- wavesynth for qtpy_synth
# 28 Jul 2023 - @todbot / Tod Kurt
# part of https://github.com/todbot/qtpy_synth
#
# UI:
# - Display shows four lines of two parameters each
# - The current editable parameter pair is underlined, adjustable by the knobs
# - The knobs have "catchup" logic (
# (knob must pass through the displayed value before value can be changed)
#
# - Key tap (press & release) == change what editable line (what knobs are editing)
# - Key hold + touch press = load patch 1,2,3,4 (turned off currently)
# - Touch press/release == play note / release note
#
import asyncio
import time
import usb_midi
from qtpy_synth.hardware import Hardware
from qtpy_synth.synthio_instrument import WavePolyTwoOsc, Patch, FiltType
import qtpy_synth.winterbloom_smolmidi as smolmidi
from wavesynth_display import WavesynthDisplay
import microcontroller
microcontroller.cpu.frequency = 250_000_000
time.sleep(2) # let USB settle down
touch_midi_notes = [40, 48, 52, 55] # can be float
patch1 = Patch('oneuno')
patch2 = Patch('twotoo')
patch3 = Patch('three')
patch4 = Patch('fourfor')
patches = (patch1, patch2, patch3, patch4)
patch1.filt_env_params.attack_time = 0.1
patch1.amp_env_params.attack_time = 0.01
patch2.filt_type = FiltType.HP
patch2.wave = 'square'
patch2.detune=1.01
patch2.filt_env_params.attack_time = 0.0 # turn off filter FIXME
patch2.amp_env_params.release_time = 1.0
patch3.waveformB = 'square' # show off wavemixing
patch3.filt_type = FiltType.BP
patch4.wave_type = 'wtb'
patch4.wave = 'PLAITS02' # 'MICROW02' 'BRAIDS04'
patch4.wave_mix_lfo_amount = 0.23
#patch4.detune = 0 # disable 2nd oscillator
patch4.amp_env_params.release_time = 0.5
print("--- qtpy_synth wavesynth starting up ---")
qts = Hardware()
inst = WavePolyTwoOsc(qts.synth, patch4)
wavedisp = WavesynthDisplay(qts.display, inst.patch)
# let's get the midi going
midi_usb_in = smolmidi.MidiIn(usb_midi.ports[0])
midi_uart_in = smolmidi.MidiIn(qts.midi_uart)
def map_range(s, a1, a2, b1, b2): return b1 + ((s - a1) * (b2 - b1) / (a2 - a1))
async def instrument_updater():
while True:
inst.update()
await asyncio.sleep(0.01) # as fast as possible
async def display_updater():
while True:
wavedisp.display_update()
await asyncio.sleep(0.1)
async def midi_handler():
while True:
while msg := midi_usb_in.receive() or midi_uart_in.receive():
if msg.type == smolmidi.NOTE_ON:
inst.note_on(msg.data[0])
qts.led.fill(0xff00ff)
elif msg.type == smolmidi.NOTE_OFF:
inst.note_off(msg.data[0])
qts.led.fill(0x000000)
elif msg.type == smolmidi.CC:
ccnum = msg.data[0]
ccval = msg.data[1]
qts.led.fill(ccval)
if ccnum == 71: # "sound controller 1"
new_wave_mix = ccval/127
print("wave_mix:", new_wave_mix)
inst.patch.wave_mix = new_wave_mix
elif ccnum == 1: # mod wheel
inst.patch.wave_mix_lfo_amount = ccval/127 * 50
#inst.patch.wave_mix_lfo_rate = msg.value/127 * 5
elif ccnum == 74: # filter cutoff
inst.patch.filt_f = ccval/127 * 8000
await asyncio.sleep(0.001)
async def input_handler():
# fixme: put these in qtpy_synth.py? no I think they are part of this "app"
knob_mode = 0 # 0=frequency, 1=wavemix, 2=, 3=
key_held = False
key_with_touch = False
knob_saves = [ (0,0) for _ in range(4) ] # list of knob state pairs
param_saves = [ (0,0) for _ in range(4) ] # list of param state pairs for knobs
knobA_pickup, knobB_pickup = False, False
knobA, knobB = 0,0
def reload_patch(wave_select):
print("reload patch!", wave_select)
# the below seems like the wrong way to do this, needlessly complex
inst.patch.set_by_wave_select( wave_select )
inst.reload_patch()
param_saves[0] = wavedisp.wave_select_pos(), inst.patch.wave_mix
param_saves[1] = inst.patch.detune, inst.patch.wave_mix_lfo_amount
param_saves[2] = inst.patch.filt_type, inst.patch.filt_f
param_saves[3] = inst.patch.filt_q, inst.patch.filt_env_params.attack_time
while True:
# KNOB input
(knobA_new, knobB_new) = qts.read_pots()
# simple knob pickup logic: if the real knob is close enough to
if abs(knobA - knobA_new) <= 1000: # knobs range 0-65535
knobA_pickup = True
if abs(knobB - knobB_new) <= 1000:
knobB_pickup = True
if knobA_pickup:
knobA = knobA_new
if knobB_pickup:
knobB = knobB_new
# TOUCH input
if touches := qts.check_touch():
for touch in touches:
if touch.pressed:
if key_held: # load a patch
print("load patch", touch.key_number)
# disable this for now
#inst.load_patch(patches[i])
#qts.patch = patches[i]
wavedisp.display_update()
key_with_touch = True
else: # trigger a note
qts.led.fill(0xff00ff)
midi_note = touch_midi_notes[touch.key_number]
inst.note_on(midi_note)
if touch.released:
if key_with_touch:
key_with_touch = False
else:
qts.led.fill(0)
midi_note = touch_midi_notes[touch.key_number]
inst.note_off(midi_note)
# KEY input
if key := qts.check_key():
if key.pressed:
key_held = True
if key.released:
key_held = False
if not key_with_touch: # key tap == change what knobs do
# turn off pickup mode since we change what knobs do
knobA_pickup, knobB_pickup = False, False
knob_saves[knob_mode] = knobA, knobB # save knob positions
knob_mode = (knob_mode + 1) % 4 # FIXME: make a max_knob_mode
knobA, knobB = knob_saves[knob_mode] # retrive saved knob positions
print("knob mode:",knob_mode, knobA, knobB)
wavedisp.selected_info = knob_mode # FIXME
# Handle parameter changes depending on knob mode
if knob_mode == 0: # wave selection & wave_mix
wave_select_pos, wave_mix = param_saves[knob_mode]
if knobA_pickup:
wave_select_pos = map_range( knobA, 0,65535, 0, len(wavedisp.wave_selects)-1)
if knobB_pickup:
wave_mix = map_range( knobB, 0,65535, 0, 1)
param_saves[knob_mode] = wave_select_pos, wave_mix
wave_select = wavedisp.wave_selects[ int(wave_select_pos) ]
if inst.patch.wave_select() != wave_select:
reload_patch(wave_select)
inst.patch.wave_mix = wave_mix
elif knob_mode == 1: # osc detune & wave_mix lfo
detune, wave_lfo = param_saves[knob_mode]
if knobA_pickup:
detune = map_range(knobA, 300,65300, 1, 1.1) # RP2040 has bad ADC
if knobB_pickup:
wave_lfo = map_range(knobB, 0,65535, 0, 1)
param_saves[knob_mode] = detune, wave_lfo
inst.patch.wave_mix_lfo_amount = wave_lfo
inst.patch.detune = detune
elif knob_mode == 2: # filter type and filter freq
filt_type, filt_f = param_saves[knob_mode]
if knobA_pickup:
filt_type = int(map_range(knobA, 0,65535, 0,3))
if knobB_pickup:
filt_f = map_range(knobB, 300,65300, 100, 8000)
param_saves[knob_mode] = filt_type, filt_f
inst.patch.filt_type = filt_type
inst.patch.filt_f = filt_f
elif knob_mode == 3:
filt_q, filt_env = param_saves[knob_mode]
if knobA_pickup:
filt_q = map_range(knobA, 0,65535, 0.5,2.5)
if knobB_pickup:
filt_env = map_range(knobB, 300,65300, 1, 0.01)
param_saves[knob_mode] = filt_q, filt_env
inst.patch.filt_q = filt_q
inst.patch.filt_env_params.attack_time = filt_env
else:
pass
await asyncio.sleep(0.02)
print("--- qtpy_synth wavesynth ready ---")
async def main():
task1 = asyncio.create_task(display_updater())
task2 = asyncio.create_task(input_handler())
task3 = asyncio.create_task(midi_handler())
task4 = asyncio.create_task(instrument_updater())
await asyncio.gather(task1, task2, task3, task4)
asyncio.run(main())