9
9
from europi_script import EuroPiScript
10
10
from experimental .a_to_d import AnalogReaderDigitalWrapper
11
11
from experimental .knobs import KnobBank
12
+ from experimental .screensaver import Screensaver
12
13
from math import floor
13
14
14
15
16
+ # This script operates in microseconds, so for convenience define one second as a constant
17
+ ONE_SECOND = 1000000
18
+
19
+ # Automatically reset if we receive no input clocks after 5 seconds
20
+ # This lets us handle resonably slow input clocks, while also providing some reasonable timing
21
+ CLOCK_IN_TIMEOUT = 5 * ONE_SECOND
22
+
23
+
15
24
def ljust (s , length ):
16
25
"""Re-implements the str.ljust method from standard Python
17
26
@@ -22,53 +31,69 @@ def ljust(s, length):
22
31
n_spaces = max (0 , length - len (s ))
23
32
return s + ' ' * n_spaces
24
33
34
+
25
35
class ClockOutput :
26
36
"""A control class that handles a single output
27
37
"""
38
+ ## The smallest common multiple of the allowed clock divisions (2, 3, 4, 5, 6, 8, 12)
39
+ #
40
+ # Used to reset the input gate counter to avoid integer overflows/performance degredation with large values
41
+ MAX_GATE_COUNT = 120
42
+
28
43
def __init__ (self , output_port , modifier ):
29
44
"""Constructor
30
45
31
46
@param output_port One of the six output CV ports, e.g. cv1, cv2, etc... that this class will control
32
47
@param modifier The initial clock modifier for this output channel
33
48
"""
34
- self .last_external_clock_at = time .ticks_ms ()
35
- self .last_interval_ms = 0
49
+ self .last_external_clock_at = time .ticks_us ()
50
+ self .last_interval_us = 0
36
51
self .modifier = modifier
37
52
self .output_port = output_port
38
53
54
+ # Should the output be high or low?
39
55
self .is_high = False
40
- self .last_state_change_at = time .ticks_ms ()
41
56
42
- def set_external_clock (self , ticks_ms ):
57
+ # Used to implement basic gate-skipping for clock divisions
58
+ self .input_gate_counter = 0
59
+
60
+ def set_external_clock (self , ticks_us ):
43
61
"""Notify this output when the last external clock signal was received.
44
62
45
63
The calculate_state function will use this to calculate the duration of the high/low phases
46
64
of the output gate
47
65
"""
48
- if self .last_external_clock_at != ticks_ms :
49
- self .last_interval_ms = time .ticks_diff (ticks_ms , self .last_external_clock_at )
50
- self .last_external_clock_at = ticks_ms
66
+ if self .last_external_clock_at != ticks_us :
67
+ self .last_interval_us = time .ticks_diff (ticks_us , self .last_external_clock_at )
68
+ self .last_external_clock_at = ticks_us
69
+
70
+ self .input_gate_counter += 1
71
+ if self .input_gate_counter >= self .MAX_GATE_COUNT :
72
+ self .input_gate_counter = 0
51
73
52
- def calculate_state (self , ms ):
74
+ def calculate_state (self , ticks_us ):
53
75
"""Calculate whether this output should be high or low based on the current time
54
76
55
77
Must be called before calling set_output_voltage
56
78
57
- @param ms The current time in ms ; passed as a parameter to synchronize multiple channels
79
+ @param ticks_us The current time in microseconds ; passed as a parameter to synchronize multiple channels
58
80
"""
59
- gate_duration_ms = self .last_interval_ms / self .modifier
60
- hi_lo_duration_ms = gate_duration_ms / 2
81
+ if self .modifier >= 1 :
82
+ # We're in clock multiplication mode; calculate the duration of output gates and set high/low state
83
+ gate_duration_us = self .last_interval_us / self .modifier
84
+ hi_lo_duration_us = gate_duration_us / 2
61
85
62
- elapsed_ms = time .ticks_diff (ms , self .last_state_change_at )
86
+ # The time elapsed since our last external clock
87
+ elapsed_us = time .ticks_diff (ticks_us , self .last_external_clock_at )
63
88
64
- if elapsed_ms > hi_lo_duration_ms :
65
- self .last_state_change_at = ms
66
- if self .is_high :
67
- self .is_high = False
89
+ # The number of phases that have happened since the last incoming clock
90
+ n_phases = elapsed_us // hi_lo_duration_us
68
91
69
- else :
70
- self .is_high = True
71
- self .output_port .on ()
92
+ self .is_high = n_phases % 2 == 0
93
+ else :
94
+ # We're in clock division mode; just do a simple gate-skip to stay in sync with the input
95
+ n_gates = round (1.0 / self .modifier )
96
+ self .is_high = self .input_gate_counter % (n_gates * 2 ) < n_gates
72
97
73
98
def set_output_voltage (self ):
74
99
"""Set the output voltage either high or low.
@@ -85,12 +110,14 @@ def reset(self):
85
110
"""
86
111
self .is_high = False
87
112
self .output_port .off ()
88
- self .last_state_change_at = time .ticks_ms ()
113
+ self .input_gate_counter = 0
114
+
89
115
90
116
class ClockModifier (EuroPiScript ):
91
117
"""The main script class; multiplies and divides incoming clock signals
92
118
"""
93
119
def __init__ (self ):
120
+ self .ui_dirty = False
94
121
state = self .load_state_json ()
95
122
96
123
self .k1_bank = (
@@ -152,6 +179,7 @@ def b1_rising():
152
179
self .channel_markers [0 ] = ' '
153
180
self .channel_markers [1 ] = '>'
154
181
self .channel_markers [2 ] = ' '
182
+ self .ui_dirty = True
155
183
156
184
@b2 .handler
157
185
def b2_rising ():
@@ -162,6 +190,7 @@ def b2_rising():
162
190
self .channel_markers [0 ] = ' '
163
191
self .channel_markers [1 ] = ' '
164
192
self .channel_markers [2 ] = '>'
193
+ self .ui_dirty = True
165
194
166
195
@b1 .handler_falling
167
196
def b1_falling ():
@@ -173,6 +202,7 @@ def b1_falling():
173
202
self .channel_markers [1 ] = ' '
174
203
self .channel_markers [2 ] = ' '
175
204
self .state_dirty = True
205
+ self .ui_dirty = True
176
206
177
207
@b2 .handler_falling
178
208
def b2_falling ():
@@ -184,12 +214,13 @@ def b2_falling():
184
214
self .channel_markers [1 ] = ' '
185
215
self .channel_markers [2 ] = ' '
186
216
self .state_dirty = True
217
+ self .ui_dirty = True
187
218
188
219
@din .handler
189
220
def on_din ():
190
221
"""Record the start time of our rising edge
191
222
"""
192
- self .last_clock_at = time .ticks_ms ()
223
+ self .last_clock_at = time .ticks_us ()
193
224
194
225
def on_ain ():
195
226
"""Reset all channels when AIN goes high
@@ -218,7 +249,20 @@ def save_state(self):
218
249
def main (self ):
219
250
"""The main loop
220
251
"""
252
+ screensaver = Screensaver ()
253
+ last_render_at = time .ticks_us ()
254
+
221
255
knob_choices = list (self .clock_modifiers .keys ())
256
+
257
+ prev_mods = [
258
+ knob_choices [0 ],
259
+ knob_choices [0 ],
260
+ knob_choices [0 ],
261
+ knob_choices [0 ],
262
+ knob_choices [0 ],
263
+ knob_choices [0 ],
264
+ ]
265
+
222
266
while True :
223
267
# update AIN so its rising edge callback can fire
224
268
self .d_ain .update ()
@@ -240,9 +284,9 @@ def main(self):
240
284
for i in range (len (mods )):
241
285
self .outputs [i ].modifier = self .clock_modifiers [mods [i ]]
242
286
243
- # if we don't get an external signal within 1s, stop
244
- now = time .ticks_ms ()
245
- if time .ticks_diff (now , self .last_clock_at ) < 1000 :
287
+ # if we don't get an external signal within the timeout duration, reset the outputs
288
+ now = time .ticks_us ()
289
+ if time .ticks_diff (now , self .last_clock_at ) <= CLOCK_IN_TIMEOUT :
246
290
# separate calculating the high/low state and setting the output voltage into two loops
247
291
# this helps reduce phase-shifting across outputs
248
292
for output in self .outputs :
@@ -256,17 +300,29 @@ def main(self):
256
300
output .reset ()
257
301
258
302
# Update the GUI
259
- # Yes, this is a very long string, but it centers nicely
260
- # It looks something like this:
261
- #
262
- # > 1: x1 4: /2
263
- # 2: x2 5: x3
264
- # 3: /4 6: /3
265
- #
266
- oled .fill (0 )
267
- oled .centre_text (
268
- f"{ self .channel_markers [0 ]} 1:{ ljust (mods [0 ], 3 )} 4:{ ljust (mods [3 ], 3 )} \n { self .channel_markers [1 ]} 2:{ ljust (mods [1 ], 3 )} 5:{ ljust (mods [4 ], 3 )} \n { self .channel_markers [2 ]} 3:{ ljust (mods [2 ], 3 )} 6:{ ljust (mods [5 ], 3 )} "
269
- )
303
+ # This only needs to be done if the modifiers have changed or a button has been pressed/released
304
+ self .ui_dirty = self .ui_dirty or any ([mods [i ] != prev_mods [i ] for i in range (len (mods ))])
305
+ if self .ui_dirty :
306
+ # Yes, this is a very long string, but it centers nicely
307
+ # It looks something like this:
308
+ #
309
+ # > 1: x1 4: /2
310
+ # 2: x2 5: x3
311
+ # 3: /4 6: /3
312
+ #
313
+ oled .fill (0 )
314
+ oled .centre_text (
315
+ f"{ self .channel_markers [0 ]} 1:{ ljust (mods [0 ], 3 )} 4:{ ljust (mods [3 ], 3 )} \n { self .channel_markers [1 ]} 2:{ ljust (mods [1 ], 3 )} 5:{ ljust (mods [4 ], 3 )} \n { self .channel_markers [2 ]} 3:{ ljust (mods [2 ], 3 )} 6:{ ljust (mods [5 ], 3 )} "
316
+ )
317
+ self .ui_dirty = False
318
+ last_render_at = time .ticks_us ()
319
+ elif time .ticks_diff (now , last_render_at ) > screensaver .ACTIVATE_TIMEOUT_US :
320
+ last_render_at = time .ticks_add (now , - screensaver .ACTIVATE_TIMEOUT_US )
321
+ screensaver .draw ()
322
+
323
+ for i in range (len (mods )):
324
+ prev_mods [i ] = mods [i ]
325
+
270
326
271
327
if __name__ == "__main__" :
272
328
ClockModifier ().main ()
0 commit comments