# Copyright 2025 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.
from europi_script import EuroPiScript
from experimental.a_to_d import AnalogReaderDigitalWrapper
from experimental.math_extras import rescale
reset_input = AnalogReaderDigitalWrapper(ain)
end_of_sequence_output = cv2
sequence_length_knob = k1
# Duration of triggers sent to DFAM to advance
# based on experimentation, 1ms is sufficient and doesn't produce
# audio artefacts when burst-advancing
# What length of sequences can we output?
# We allow up to DFAM's max length of 8
# How many steps does DFAM advance with each trigger
# This is all modular, so setting to 7 will advance 1 space backwards
# DFAM has an 8-step sequencer
DFAM_SEQUENCER_LENGTH = 8
# The size of the circles we draw on the UI
class DfamController(EuroPiScript):
self.reset_request = False
self.advance_request = False
# the current step within our internal sequencer
# the maximum number of steps in the sequence
self.max_steps = MAX_SEQUENCE_LEN
# the number of advances we output per input step
self.step_size = MIN_STEP
# the current step on DFAM's sequencer
self.dfam_sync_counter = 0
clock_input.handler(self.request_advance)
advance_button.handler(self.request_advance)
reset_input.handler(self.request_reset)
reset_button.handler(self.request_reset)
self.reset_request = True
def request_advance(self):
self.advance_request = True
def reset(self, force_trigger=False):
pulses = (DFAM_SEQUENCER_LENGTH - self.dfam_sync_counter) % DFAM_SEQUENCER_LENGTH
if force_trigger and pulses == 0:
pulses = DFAM_SEQUENCER_LENGTH
self.dfam_sync_counter = 0
time.sleep(TRIGGER_DURATION)
time.sleep(TRIGGER_DURATION)
for _ in range(self.step_size):
time.sleep(TRIGGER_DURATION)
time.sleep(TRIGGER_DURATION)
self.dfam_sync_counter = (self.dfam_sync_counter + self.step_size) % DFAM_SEQUENCER_LENGTH
reset_input.update() # a-to-d wrapper, so we need to poll it!
new_steps = int(rescale(sequence_length_knob.percent(), 0, 1, MIN_SEQUENCE_LEN, MAX_SEQUENCE_LEN))
if new_steps != self.max_steps:
self.max_steps = new_steps
new_size = int(rescale(step_size_knob.percent(), 0, 1, MIN_STEP, MAX_STEP))
if new_size != self.step_size:
self.step_size = new_size
self.reset_request = False
self.advance_request = False
if self.current_step >= self.max_steps:
end_of_sequence_output.on()
self.reset(force_trigger=True)
end_of_sequence_output.off()
oled.centre_text(f"{self.current_step + 1}/{self.max_steps}\nx{self.step_size}\n", auto_show=False, clear_first=False)
for i in range(DFAM_SEQUENCER_LENGTH):
# the 0th LED is actually the last one, so the pattern should be shifted 1 to the left
if i == (self.dfam_sync_counter - 1) % DFAM_SEQUENCER_LENGTH:
i * OLED_WIDTH // DFAM_SEQUENCER_LENGTH + CIRCLE_SIZE,
OLED_HEIGHT - CIRCLE_SIZE - 1,
if __name__ == "__main__":