Skip to content

Commit 6e0be4d

Browse files
committed
Setup UI
1 parent 4c43ea8 commit 6e0be4d

File tree

2 files changed

+203
-40
lines changed

2 files changed

+203
-40
lines changed

circuitpython/perc/code.py

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
import synthio
1919
import audiofreeverb
2020

21+
from synth_tools.param import ParamRange
22+
from ui import UI, splash_screen
23+
splash_screen(hardware.display)
24+
2125
# Settings
2226

2327
LEVEL = 0.25 # Use `SAMPLES[notenum]["level"]` to override
@@ -104,27 +108,22 @@
104108
voice_count=len(samples),
105109
)
106110

107-
if hardware.is_rp2350:
108-
effect_filter = audiofilters.Filter(
109-
filter=synthio.Biquad(synthio.FilterMode.LOW_PASS, FILTER_MAX, 1.2),
110-
sample_rate=hardware.SAMPLE_RATE,
111-
channel_count=hardware.CHANNEL_COUNT,
112-
buffer_size=hardware.BUFFER_SIZE,
113-
)
114-
effect_reverb = audiofreeverb.Freeverb(
115-
mix=0.0,
116-
sample_rate=hardware.SAMPLE_RATE,
117-
channel_count=hardware.CHANNEL_COUNT,
118-
buffer_size=hardware.BUFFER_SIZE,
119-
)
120-
121-
hardware.audio.play(effect_reverb)
122-
effect_reverb.play(effect_filter)
123-
effect_filter.play(mixer)
124-
125-
else:
126-
hardware.audio.play(mixer)
111+
effect_filter = audiofilters.Filter(
112+
filter=synthio.Biquad(synthio.FilterMode.LOW_PASS, FILTER_MAX, 1.2),
113+
sample_rate=hardware.SAMPLE_RATE,
114+
channel_count=hardware.CHANNEL_COUNT,
115+
buffer_size=hardware.BUFFER_SIZE,
116+
)
117+
effect_reverb = audiofreeverb.Freeverb(
118+
mix=0.0,
119+
sample_rate=hardware.SAMPLE_RATE,
120+
channel_count=hardware.CHANNEL_COUNT,
121+
buffer_size=hardware.BUFFER_SIZE,
122+
)
127123

124+
hardware.audio.play(effect_reverb)
125+
effect_reverb.play(effect_filter)
126+
effect_filter.play(mixer)
128127

129128
for i, wav in enumerate(samples.values()):
130129
mixer.voice[i].play(wav)
@@ -155,23 +154,11 @@ def play_sample(sample:dict) -> None:
155154
mixer.voice[i].loop = sample.get("loop", False)
156155
mixer.voice[i].play(wav)
157156

158-
def stop_sample(sample:dict) -> None:
159-
if sample.get("loop", False):
160-
i = get_sample_index(sample["name"])
157+
def stop_sample(sample:dict|str) -> None:
158+
if type(sample) is str or sample.get("loop", False):
159+
i = get_sample_index(sample if type(sample) is str else sample["name"])
161160
mixer.voice[i].stop()
162161

163-
# Keyboard
164-
165-
async def touch_handler():
166-
while True:
167-
for event in hardware.check_touch():
168-
if (notenum := event.key_number + 36) in SAMPLES and (sample := get_sample(notenum)):
169-
if event.pressed:
170-
play_sample(sample)
171-
elif event.released:
172-
stop_sample(sample)
173-
await asyncio.sleep(0.005)
174-
175162
# MIDI
176163

177164
from adafruit_midi.note_off import NoteOff
@@ -190,15 +177,72 @@ async def midi_handler():
190177

191178
# Controls
192179

180+
button_held = False
181+
button_with_touch = False
182+
183+
params = (
184+
185+
ParamRange("FiltFreq", "filter frequency", FILTER_MAX, "%4d", FILTER_MIN, FILTER_MAX,
186+
setter=lambda x: setattr(effect_filter.filter, "frequency", x),
187+
getter=lambda: getattr(effect_filter.filter, "frequency")
188+
),
189+
ParamRange("FilterRes", "filter resonance", 0.7, "%1.2f", 0.1, 2.5,
190+
setter=lambda x: setattr(effect_filter.filter, "Q", x),
191+
getter=lambda: getattr(effect_filter.filter, "Q")
192+
),
193+
194+
ParamRange("ReverbMix", "reverb mix", 0.0, "%1.2f", 0.0, 1.0,
195+
setter=lambda x: setattr(effect_reverb, "mix", x),
196+
getter=lambda: getattr(effect_reverb, "mix")
197+
),
198+
ParamRange("RevrbSize", "reverb roomsize", 0.5, "%1.2f", 0.0, 1.0,
199+
setter=lambda x: setattr(effect_reverb, "roomsize", x),
200+
getter=lambda: getattr(effect_reverb, "roomsize")
201+
),
202+
203+
)
204+
205+
ui = UI(hardware.display, params, hardware.knobA.value, hardware.knobB.value)
206+
ui.set_patch_name("drums")
207+
208+
async def touch_handler():
209+
global button_held, button_with_touch, ui
210+
while True:
211+
for event in hardware.check_touch():
212+
if not button_held:
213+
if (notenum := event.key_number + 36) in SAMPLES and (sample := get_sample(notenum)):
214+
if event.pressed:
215+
play_sample(sample)
216+
# Special case for hihat
217+
if notenum in (42, 44):
218+
stop_sample("hihat_open")
219+
elif event.released:
220+
stop_sample(sample)
221+
elif event.pressed and not button_with_touch:
222+
button_with_touch = True
223+
if event.key_number < (ui.num_params//2):
224+
ui.select_pair(event.key_number)
225+
elif event.released:
226+
button_with_touch = False
227+
await asyncio.sleep(0.005)
228+
193229
async def controls_handler():
230+
global button_held, button_with_touch, ui
194231
while True:
232+
hardware.display.refresh()
233+
195234
for event in hardware.check_buttons():
196-
pass
235+
if event.key_number == 0:
236+
pass
237+
elif event.key_number == 1:
238+
if event.pressed:
239+
button_held = True
240+
if event.released:
241+
button_held = False
242+
button_with_touch = False
197243

198-
knobA, knobB = hardware.knobA.value, hardware.knobB.value
199-
if hardware.is_rp2350:
200-
effect_filter.filter.frequency = ((knobA / 65535.0) ** 2) * (FILTER_MAX - FILTER_MIN) + FILTER_MIN
201-
effect_reverb.mix = knobB / 65535.0
244+
ui.setA(hardware.knobA.value >> 8)
245+
ui.setB(hardware.knobB.value >> 8)
202246

203247
await asyncio.sleep(0.005)
204248

circuitpython/perc/ui.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 Cooper Dalrymple
2+
# SPDX-License-Identifier: MIT
3+
import displayio
4+
import terminalio
5+
from adafruit_display_text import bitmap_label as label
6+
7+
from synth_tools.gauge_cluster import GaugeCluster
8+
from synth_tools.param_scaler import ParamScaler
9+
10+
def splash_screen(display):
11+
splash_group = displayio.Group()
12+
splash_group.append(label.Label(terminalio.FONT, text="pico_test_synth",
13+
color=0xFFFFFF, x=1, y=10, scale=1))
14+
splash_group.append(label.Label(terminalio.FONT, text="sampler",
15+
color=0xFFFFFF, x=1, y=30, scale=2))
16+
splash_group.append(label.Label(terminalio.FONT, text="@relic_se",
17+
color=0xFFFFFF, x=1, y=50, scale=1))
18+
display.root_group = splash_group
19+
display.refresh()
20+
21+
22+
class UI(displayio.Group):
23+
def __init__(self, display, params, knobAval, knobBval):
24+
super().__init__()
25+
display.root_group = self
26+
self.params = params
27+
self.num_params = len(params)
28+
29+
self.cluster = GaugeCluster(self.num_params, x=1, y=13, width=6, height=20, xstride=2.3)
30+
self.append(self.cluster.gauges)
31+
self.append(self.cluster.select_lines) # indicates which param set is editable
32+
33+
# text of the currently editable parameters
34+
self.textA = label.Label(terminalio.FONT, text="tA", color=0xFFFFFF, x=1, y=44, scale=2)
35+
self.textB = label.Label(terminalio.FONT, text="tB", color=0xFFFFFF, x=64, y=44, scale=2)
36+
37+
# labels for the currently editable parameters
38+
self.labelA = label.Label(terminalio.FONT, text="labA", color=0xFFFFFF, x=1, y=58, scale=1)
39+
self.labelB = label.Label(terminalio.FONT, text="labB", color=0xFFFFFF, x=104, y=58, scale=1)
40+
41+
for l in (self.textA, self.textB, self.labelA, self.labelB):
42+
self.append(l)
43+
44+
# text for patch info
45+
self.labelP = label.Label(terminalio.FONT, text="patch:patchname", color=0xFFFFFF, x=20, y=4, scale=1)
46+
self.append(self.labelP)
47+
48+
self.scalerA = ParamScaler(self.params[0].get_by_gauge_val(), knobAval)
49+
self.scalerB = ParamScaler(self.params[1].get_by_gauge_val(), knobBval)
50+
self.pairnum = 0
51+
self.select_pair(0)
52+
53+
self.lastA = knobAval
54+
self.lastB = knobBval
55+
self.knobMin = 1
56+
self.refresh_gauge_cluster()
57+
58+
def set_patch_name(self,pname):
59+
self.labelP.text="patch:"+pname
60+
61+
def _fix_textB_right_justified(self):
62+
self.textB.scale=2
63+
w = self.textB.width * 2 # scale=2 above
64+
if w > 80:
65+
self.textB.scale=1
66+
self.textB.x = 128 - w//2
67+
else:
68+
self.textB.x = 128 - w
69+
70+
def refresh_gauge_cluster(self):
71+
"""Set the gauge values to what the params are"""
72+
for i in range(self.num_params):
73+
self.cluster.set_gauge_val(i, int(self.params[i].get_by_gauge_val()))
74+
self.select_pair(self.pairnum) # causes redraw of param text
75+
76+
def select_pair(self,p):
77+
"""Select a given pair of params to edit"""
78+
self.cluster.select_line(self.pairnum, False) # deselect old
79+
self.pairnum = p
80+
self.cluster.select_line(self.pairnum) # select new
81+
82+
i = self.pairnum * 2
83+
self.labelA.text = self.params[i+0].name
84+
self.labelB.text = self.params[i+1].name
85+
self.labelB.x = 128 - self.labelB.width
86+
self.textA.text = self.params[i+0].get_text()
87+
self.textB.text = self.params[i+1].get_text()
88+
# self.textB.x = 128 - (self.textB.width*2) # sigh
89+
self._fix_textB_right_justified()
90+
self.scalerA.reset(self.params[i+0].get_by_gauge_val())
91+
self.scalerB.reset(self.params[i+1].get_by_gauge_val())
92+
93+
def setA(self,v): # v = 0-255
94+
if abs(v-self.lastA) < self.knobMin:
95+
return
96+
self.lastA = v
97+
i = self.pairnum * 2
98+
v = int(self.scalerA.update(v))
99+
self.cluster.set_gauge_val(i,v)
100+
self.params[i].set_by_gauge_val(v)
101+
t = self.params[i].get_text()
102+
if t != self.textA.text:
103+
self.textA.text = t
104+
# fixme: need formatting info
105+
106+
def setB(self, v): # v = 0-255
107+
if abs(v-self.lastB) < self.knobMin:
108+
return
109+
self.lastB = v
110+
i = self.pairnum * 2 + 1
111+
v = self.scalerB.update(v)
112+
self.params[i].set_by_gauge_val(v)
113+
v = self.params[i].get_by_gauge_val()
114+
self.cluster.set_gauge_val(i,int(v))
115+
t = self.params[i].get_text()
116+
if t != self.textB.text:
117+
self.textB.text = t
118+
self._fix_textB_right_justified()
119+
#self.textB.x = 128 - (self.textB.width*2)

0 commit comments

Comments
 (0)