# Copyright 2023 Allen Synthesis
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""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
@author Chris Iverach-Brereton <ve4cib@gmail.com>
from europi_script import EuroPiScript
from experimental.knobs import MedianAnalogInput
from experimental.screensaver import Screensaver
## Trigger outputs are 10ms long (rising/falling edges of gate signals)
# 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):
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.incoming_rise_out = cv2
self.incoming_fall_out = cv3
self.toggle_fall_out = cv6
self.force_toggle = False
self.last_user_interaction_at = 0
self.screensaver = Screensaver()
self.last_rise_at = time.ticks_ms()
self.last_fall_at = time.ticks_ms()
self.last_user_interaction_at = now
self.last_fall_at = time.ticks_ms()
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]
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
# read the knobs with higher samples
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
round(self.quadratic_knob(k1_percent) + cv_percent * k2_percent * 2000),
# 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
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:
# 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()
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()
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:
# CV5: toggle output; flips state every time we get a rising edge on din/b1/b2
self.force_toggle = False
elif not toggle and toggle_on:
# 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()
self.toggle_fall_out.off()
last_k1_percent = k1_percent
last_k2_percent = k2_percent
last_gate_duration = gate_duration
self.last_user_interaction_at = now
oled.centre_text(f"Gate: {gate_duration}ms")
elif time.ticks_diff(now, self.last_user_interaction_at) > self.screensaver.ACTIVATE_TIMEOUT_MS:
self.last_user_interaction_at = time.ticks_add(now, -self.screensaver.ACTIVATE_TIMEOUT_MS)
GatesAndTriggers().main()