Skip to content

Commit 0139a3d

Browse files
authored
Optimize Euclidean generator UI rendering (#380)
* Refactor euclidean rhythms script to minimize UI renders. This should make it more responsive to shorter triggers * Use the new class variable when applying setting changes * Exit the screensaver with a knob movement OR with a button press * Re-render when k2 is moved on the channel menu; this isn't optimal, but makes exiting the screensaver more reliable * Update software/contrib/euclid.py
1 parent 7044d38 commit 0139a3d

File tree

1 file changed

+108
-39
lines changed

1 file changed

+108
-39
lines changed

software/contrib/euclid.py

+108-39
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
"""
1111

1212
import random
13+
import time
1314

1415
from europi import *
1516
from europi_script import EuroPiScript
1617

1718
from experimental.euclid import generate_euclidean_pattern
18-
from experimental.screensaver import OledWithScreensaver
19+
from experimental.screensaver import Screensaver
1920

20-
ssoled = OledWithScreensaver()
2121

2222
class EuclidGenerator:
2323
"""Generates the euclidean rhythm for a single output
@@ -141,23 +141,45 @@ def __init__(self, script):
141141
@param script The EuclideanRhythms script that owns this menu
142142
"""
143143
self.script = script
144+
self.first_render = True
145+
146+
self.generator_index = None
147+
self.k2_percent = None
148+
self.read_knobs()
149+
150+
def read_knobs(self):
151+
"""Read the current state of the knobs and return whether or not the state has changed, requiring a re-render
152+
153+
@return True if we should re-render the GUI, False if we can keep the previous render
154+
"""
155+
index = k1.range(len(self.script.generators))
156+
percent = round(k2.percent() * 100)
157+
dirty = (
158+
self.generator_index != index or
159+
self.k2_percent != percent
160+
)
161+
if dirty:
162+
self.generator_index = index
163+
self.k2_percent = percent
164+
165+
return dirty or self.first_render
144166

145167
def draw(self):
146-
generator_index = k1.range(len(self.script.generators))
147-
g = self.script.generators[generator_index]
168+
self.first_render = False
169+
g = self.script.generators[self.generator_index]
148170
pattern_str = str(g)
149171

150-
ssoled.fill(0)
151-
ssoled.text(f"-- CV {generator_index+1} --", 0, 0)
172+
oled.fill(0)
173+
oled.text(f"-- CV {self.generator_index+1} --", 0, 0)
152174
if len(pattern_str) > 16:
153175
pattern_row1 = pattern_str[0:16]
154176
pattern_row2 = pattern_str[16:]
155-
ssoled.text(f"{pattern_row1}", 0, 10)
156-
ssoled.text(f"{pattern_row2}", 0, 20)
177+
oled.text(f"{pattern_row1}", 0, 10)
178+
oled.text(f"{pattern_row2}", 0, 20)
157179
else:
158-
ssoled.text(f"{pattern_str}", 0, 10)
180+
oled.text(f"{pattern_str}", 0, 10)
159181

160-
ssoled.show()
182+
oled.show()
161183

162184
class SettingsMenu:
163185
"""A menu screen for controlling a single setting of the generator
@@ -181,6 +203,15 @@ def __init__(self, script):
181203
"Skip %"
182204
]
183205

206+
self.first_render = True
207+
208+
self.menu_item = None
209+
self.lower_bound = None
210+
self.upper_bound = None
211+
self.current_setting = None
212+
self.new_setting = None
213+
self.read_knobs()
214+
184215
def set_generator(self, g):
185216
"""Configure this menu to control a given EuclideanGenerator
186217
@@ -191,9 +222,8 @@ def set_generator(self, g):
191222
def read_knobs(self):
192223
"""Returns a tuple with the current options
193224
194-
@return a tuple of the form (menu_item, lower_bound, upper_bound, current_setting, new_setting)
225+
@return True if the user has moved any inputs, requiring a re-render. Otherwise False
195226
"""
196-
197227
menu_item = k1.range(len(self.menu_items))
198228
lower_bound = 0
199229
upper_bound = 0
@@ -218,36 +248,49 @@ def read_knobs(self):
218248

219249
new_setting = k2.range(upper_bound-lower_bound+1) + lower_bound
220250

221-
return (menu_item, lower_bound, upper_bound, current_setting, new_setting)
251+
dirty = (
252+
self.menu_item != menu_item or
253+
self.lower_bound != lower_bound or
254+
self.upper_bound != upper_bound or
255+
self.current_setting != current_setting or
256+
self.new_setting != new_setting
257+
)
222258

223-
def draw(self):
224-
(menu_item, lower_bound, upper_bound, current_setting, new_setting) = self.read_knobs()
259+
if dirty:
260+
self.menu_item = menu_item
261+
self.lower_bound = lower_bound
262+
self.upper_bound = upper_bound
263+
self.current_setting = current_setting
264+
self.new_setting = new_setting
265+
266+
return dirty or self.first_render
225267

226-
ssoled.fill(0)
227-
ssoled.text(f"-- {self.menu_items[menu_item]} --", 0, 0)
228-
ssoled.text(f"{current_setting} <- {new_setting}", 0, 10)
229-
ssoled.show()
268+
def draw(self):
269+
self.first_render = False
270+
oled.fill(0)
271+
oled.text(f"-- {self.menu_items[self.menu_item]} --", 0, 0)
272+
oled.text(f"{self.current_setting} <- {self.new_setting}", 0, 10)
273+
oled.show()
230274

231275
def apply_setting(self):
232276
"""Apply the current setting
233277
"""
234-
(menu_item, lower_bound, upper_bound, current_setting, new_setting) = self.read_knobs()
235-
236-
if menu_item == self.MENU_ITEMS_STEPS:
237-
self.generator.steps = new_setting
238-
if self.generator.pulses > new_setting:
239-
self.generator.pulses = new_setting
240-
if self.generator.rotation > new_setting:
241-
self.generator.rotation = new_setting
242-
elif menu_item == self.MENU_ITEMS_PULSES:
243-
self.generator.pulses = new_setting
244-
elif menu_item == self.MENU_ITEMS_ROTATION:
245-
self.generator.rotation = new_setting
246-
elif menu_item == self.MENU_ITEMS_SKIP:
247-
self.generator.skip = new_setting / 100.0
278+
if self.menu_item == self.MENU_ITEMS_STEPS:
279+
self.generator.steps = self.new_setting
280+
if self.generator.pulses > self.new_setting:
281+
self.generator.pulses = self.new_setting
282+
if self.generator.rotation > self.new_setting:
283+
self.generator.rotation = self.new_setting
284+
elif self.menu_item == self.MENU_ITEMS_PULSES:
285+
self.generator.pulses = self.new_setting
286+
elif self.menu_item == self.MENU_ITEMS_ROTATION:
287+
self.generator.rotation = self.new_setting
288+
elif self.menu_item == self.MENU_ITEMS_SKIP:
289+
self.generator.skip = self.new_setting / 100.0
248290

249291
self.generator.regenerate()
250292

293+
251294
class EuclideanRhythms(EuroPiScript):
252295
"""Generates 6 different Euclidean rhythms, one per output
253296
@@ -272,8 +315,17 @@ def __init__(self):
272315

273316
self.load()
274317

318+
# Do we need to save the current settings?
319+
self.settings_dirty = False
320+
321+
# Do we need to re-draw the GUI?
322+
self.ui_dirty = True
323+
324+
self.last_user_interaction_at = time.ticks_ms()
325+
275326
self.channel_menu = ChannelMenu(self)
276327
self.settings_menu = SettingsMenu(self)
328+
self.screensaver = Screensaver()
277329

278330
self.active_screen = self.channel_menu
279331

@@ -285,33 +337,35 @@ def on_rising_clock():
285337
"""
286338
for g in self.generators:
287339
g.advance()
340+
self.ui_dirty = True
288341

289342
@din.handler_falling
290343
def on_falling_clock():
291344
"""Handler for the falling edge of the input clock
292345
293346
Turn off all of the CVs so we don't stay on for adjacent pulses
294347
"""
295-
for cv in cvs:
296-
cv.off()
348+
turn_off_all_cvs()
297349

298350
@b1.handler
299351
def on_b1_press():
300352
"""Handler for pressing button 1
301353
"""
302-
ssoled.notify_user_interaction()
354+
self.ui_dirty = True
355+
self.last_user_interaction_at = time.ticks_ms()
303356

304357
if self.active_screen == self.channel_menu:
305358
self.activate_settings_menu()
306359
else:
307360
self.settings_menu.apply_setting()
308-
self.save()
361+
self.settings_dirty = True
309362

310363
@b2.handler
311364
def on_b2_press():
312365
"""Handler for pressing button 2
313366
"""
314-
ssoled.notify_user_interaction()
367+
self.ui_dirty = True
368+
self.last_user_interaction_at = time.ticks_ms()
315369

316370
if self.active_screen == self.channel_menu:
317371
self.activate_settings_menu()
@@ -371,7 +425,22 @@ def save(self):
371425

372426
def main(self):
373427
while True:
374-
self.active_screen.draw()
428+
now = time.ticks_ms()
429+
430+
if self.settings_dirty:
431+
self.settings_dirty = False
432+
self.save()
433+
434+
if self.active_screen.read_knobs():
435+
self.last_user_interaction_at = now
436+
self.ui_dirty = True
437+
438+
if time.ticks_diff(now, self.last_user_interaction_at) >= self.screensaver.ACTIVATE_TIMEOUT_MS:
439+
self.last_user_interaction_at = time.ticks_add(now, -self.screensaver.ACTIVATE_TIMEOUT_MS)
440+
self.screensaver.draw()
441+
elif self.ui_dirty:
442+
self.ui_dirty = False
443+
self.active_screen.draw()
375444

376445
if __name__=="__main__":
377446
EuclideanRhythms().main()

0 commit comments

Comments
 (0)