-
Notifications
You must be signed in to change notification settings - Fork 86
/
gates_and_triggers.py
194 lines (153 loc) · 6.43 KB
/
gates_and_triggers.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
#!/usr/bin/env python3
"""Convert incoming triggers to gates or gates to triggers
Also outputs a toggle input which changes state every time an incoming gate is received, or when either button is
pressed.
@author Chris Iverach-Brereton <[email protected]>
@year 2023-4
"""
import time
from europi import *
from europi_script import EuroPiScript
from math import sqrt
from experimental.knobs import MedianAnalogInput
from experimental.screensaver import Screensaver
## Trigger outputs are 10ms long (rising/falling edges of gate signals)
TRIGGER_DURATION_MS = 10
# Gate outputs have a range of 50ms to 1s normally
# Maximum actual value can be increased via CV
MIN_GATE_DURATION_MS = 50
MAX_GATE_DURATION_MS = 1000
class GatesAndTriggers(EuroPiScript):
def __init__(self):
super().__init__()
self.on_incoming_rise_start_time = 0
self.on_incoming_fall_start_time = 0
self.ain = MedianAnalogInput(ain)
self.k1 = MedianAnalogInput(k1)
self.k2 = MedianAnalogInput(k2)
# assign each of the CV outputs to specific duties
self.gate_out = cv1
self.incoming_rise_out = cv2
self.incoming_fall_out = cv3
self.gate_fall_out = cv4
self.toggle_out = cv5
self.toggle_fall_out = cv6
turn_off_all_cvs()
self.last_rise_at = 0
self.last_fall_at = 0
self.force_toggle = False
self.last_user_interaction_at = 0
self.screensaver = Screensaver()
@din.handler
def on_din_rising():
self.last_rise_at = time.ticks_ms()
@din.handler_falling
def on_din_falling():
self.last_fall_at = time.ticks_ms()
@b1.handler
def on_b1_press():
now = time.ticks_ms()
self.last_rise_at = now
self.last_user_interaction_at = now
@b1.handler_falling
def on_b1_release():
self.last_fall_at = time.ticks_ms()
@b2.handler
def on_b2_press():
self.force_toggle = True
self.last_user_interaction_at = time.ticks_ms()
def quadratic_knob(self, x):
"""Some magic math to give us a quadratic response on the knob percentage
This gives us 50ms @ 0% and 1000ms @ 100% with greater precision at the higher end
where the differences will be more noticeable
@param x The value of the knob in the range [0, 100]
"""
if x <= 0:
return MIN_GATE_DURATION_MS
# /10 == sqrt(x) at maximum value
return (MAX_GATE_DURATION_MS - MIN_GATE_DURATION_MS)/10 * sqrt(x) + MIN_GATE_DURATION_MS
def main(self):
ui_dirty = True
gate_duration = 0
gate_on = False
gate_fall_at = 0
toggle = False
toggle_on = False
toggle_fall_at = 0
last_k1_percent = 0
last_k2_percent = 0
last_gate_duration = 0
while(True):
# read the knobs with higher samples
# keep 1 decimal place
k1_percent = round(self.k1.percent() * 100) # 0-100
k2_percent = round(self.k2.percent() * 100) / 100 # 0-1
cv_percent = round(self.ain.percent() * 100) / 100 # 0-1
gate_duration = max(
round(self.quadratic_knob(k1_percent) + cv_percent * k2_percent * 2000),
MIN_GATE_DURATION_MS
)
# Refresh the GUI if the knobs have moved or the gate duration has changed
ui_dirty = last_k1_percent != k1_percent or last_k2_percent != k2_percent or last_gate_duration != gate_duration
now = time.ticks_ms()
time_since_din_rise = time.ticks_diff(now, self.last_rise_at)
time_since_din_fall = time.ticks_diff(now, self.last_fall_at)
# CV1: gate output based on rising edge of din/b1
if time_since_din_rise >= 0 and time_since_din_rise <= gate_duration:
self.gate_out.on()
if not gate_on:
gate_on = True
toggle = not toggle
elif gate_on:
gate_on = False
gate_fall_at = now
self.gate_out.off()
else:
self.gate_out.off()
# CV2: trigger output for the rising edge of din/b1
if time_since_din_rise >= 0 and time_since_din_rise <= TRIGGER_DURATION_MS:
self.incoming_rise_out.on()
else:
self.incoming_rise_out.off()
# CV3: trigger output for falling edge if din/b1
if time_since_din_fall >= 0 and time_since_din_fall <= TRIGGER_DURATION_MS:
self.incoming_fall_out.on()
else:
self.incoming_fall_out.off()
# CV4: trigger output for falling edge of cv1
time_since_gate_fall = time.ticks_diff(now, gate_fall_at)
if time_since_gate_fall >= 0 and time_since_gate_fall <= TRIGGER_DURATION_MS:
self.gate_fall_out.on()
else:
self.gate_fall_out.off()
# CV5: toggle output; flips state every time we get a rising edge on din/b1/b2
if self.force_toggle:
toggle = not toggle
self.force_toggle = False
if toggle:
self.toggle_out.on()
toggle_on = True
elif not toggle and toggle_on:
self.toggle_out.off()
toggle_on = False
toggle_fall_at = now
else:
self.toggle_out.off()
# CV6: trigger output for falling edge of cv5
time_since_toggle_fall = time.ticks_diff(now, toggle_fall_at)
if time_since_toggle_fall >= 0 and time_since_toggle_fall <= TRIGGER_DURATION_MS:
self.toggle_fall_out.on()
else:
self.toggle_fall_out.off()
last_k1_percent = k1_percent
last_k2_percent = k2_percent
last_gate_duration = gate_duration
if ui_dirty:
self.last_user_interaction_at = now
oled.centre_text(f"Gate: {gate_duration}ms")
ui_dirty = False
elif time.ticks_diff(now, self.last_user_interaction_at) > self.screensaver.ACTIVATE_TIMEOUT_MS:
self.screensaver.draw()
self.last_user_interaction_at = time.ticks_add(now, -self.screensaver.ACTIVATE_TIMEOUT_MS)
if __name__=="__main__":
GatesAndTriggers().main()