# 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.
"""Euclidean rhythm generator for the EuroPi
@author Chris Iverach-Brereton <ve4cib@gmail.com>
from europi_script import EuroPiScript
from experimental.euclid import generate_euclidean_pattern
from experimental.screensaver import Screensaver
from experimental.settings_menu import *
"""Generates the euclidean rhythm for a single output
def __init__(self, cv_out, name, steps=1, pulses=0, rotation=0, skip=0):
"""Create a generator that sends its output to the given CV output
@param cv_out One of the six output jacks (cv1..cv6)
@param steps The initial number of steps (1-32)
@param pulses The initial number of pulses (0-32)
@param rotation The initial rotation (0-32)
@param skip The skip probability (0-1)
setting_prefix = name.lower()
self.steps = SettingMenuItem(
config_point = IntegerConfigPoint(
f"{setting_prefix}_steps",
callback = self.update_steps,
self.rotation = SettingMenuItem(
config_point = IntegerConfigPoint(
f"{setting_prefix}_rotation",
callback = self.update_rotation,
self.pulses = SettingMenuItem(
config_point = IntegerConfigPoint(
f"{setting_prefix}_pulses",
callback = self.update_pulses,
self.skip = SettingMenuItem(
config_point = IntegerConfigPoint(
f"{setting_prefix}_skip_prob",
## The CV output this generator controls
## The name for this channel
## The current position within the pattern
## The on/off pattern we generate
## Cached copy of the string representation
# __str__(self) will do some extra string processing
# if this is None; otherwise its value is simply returned
self.update_steps(self.steps.value, 0, None, None)
def update_steps(self, new_steps, old_steps, config_point, arg=None):
"""Update the max range of pulses & rotation to match the number of steps
self.pulses.modify_choices(choices=list(range(new_steps+1)), new_default=new_steps)
self.rotation.modify_choices(choices=list(range(new_steps+1)), new_default=new_steps)
def update_pulses(self, new_pulses, old_pulses, config_point, arg=None):
def update_rotation(self, new_rot, old_rot, config_point, arg=None):
"""Return a string representation of the pattern
The string consists of 4 characters:
e.g. |.|.^|.|.||. is a 7/12 pattern, where the 5th note
for i in range(len(self.pattern)):
"""Re-calculate the pattern for this generator
Call this after changing any of steps, pulses, or rotation to apply
Changing the pattern will reset the position to zero
self.pattern = generate_euclidean_pattern(self.steps.value, self.pulses.value, self.rotation.value)
# clear the cached string representation
"""Advance to the next step in the pattern and set the CV output
# to ease CPU usage don't do any divisions, just reset to zero
self.position = self.position+1
if self.position >= len(self.pattern):
if self.steps == 0 or self.pattern[self.position] == 0:
if self.skip.value / 100 > random.random():
# clear the cached string representation
class EuclidVisualization(MenuItem):
"""A menu item for displaying a specific Euclidean channel
def __init__(self, generator, children=None, parent=None):
super().__init__(children=children, parent=parent)
self.generator = generator
def draw(self, oled=oled):
pattern_str = str(self.generator)
oled.text(f"-- {self.generator.name} --", 0, 0)
if len(pattern_str) > 16:
pattern_row1 = pattern_str[0:16]
pattern_row2 = pattern_str[16:]
oled.text(f"{pattern_row1}", 0, 10)
oled.text(f"{pattern_row2}", 0, 20)
oled.text(f"{pattern_str}", 0, 10)
class EuclideanRhythms(EuroPiScript):
"""Generates 6 different Euclidean rhythms, one per output
Must be clocked externally into DIN
## The euclidean pattern generators for each CV output
# We pre-load the defaults with some interesting patterns so the script
# does _something_ out of the box
EuclidGenerator(cv1, "CV1", 8, 5),
EuclidGenerator(cv2, "CV2", 16, 7),
EuclidGenerator(cv3, "CV3", 16, 11),
EuclidGenerator(cv4, "CV4", 32, 9),
EuclidGenerator(cv5, "CV5", 32, 15),
EuclidGenerator(cv6, "CV6", 32, 19)
for i in range(len(self.generators)):
self.generators[i].steps,
self.generators[i].pulses,
self.generators[i].rotation,
self.menu = SettingsMenu(
short_press_cb = self.on_menu_short_press,
long_press_cb = self.on_menu_long_press
self.menu.load_defaults(self._state_filename)
# Is the visualization stale (i.e. have we received a pulse and not updated the visualization?)
self.screensaver = Screensaver()
self.last_user_interaction_at = time.ticks_ms()
"""Handler for the rising edge of the input clock
Advance all of the rhythms
for g in self.generators:
"""Handler for the falling edge of the input clock
Turn off all of the CVs so we don't stay on for adjacent pulses
"""Handler for pressing button 1
Advance all of the rhythms
self.last_user_interaction_at = time.ticks_ms()
for g in self.generators:
"""Handler for releasing button 1
Turn off all of the CVs so we don't stay on for adjacent pulses
self.last_user_interaction_at = time.ticks_ms()
def on_menu_long_press(self):
self.last_user_interaction_at = time.ticks_ms()
def on_menu_short_press(self):
self.last_user_interaction_at = time.ticks_ms()
# manually check the state of k1 since it's otherwise not used, but should
# disable the screensaver
prev_k1 = int(k1.percent() * 100)
current_k1 = int(k1.percent() * 100)
if current_k1 != prev_k1:
self.last_user_interaction_at = now
self.last_user_interaction_at = now
if 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)
if self.viz_dirty or self.menu.ui_dirty:
if self.menu.settings_dirty:
self.menu.save(self._state_filename)
EuclideanRhythms().main()