-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Initial commit of a sequential switch implementation * Put the screensaver timeout back in; it got accidentally deleted * Add the logic script * Copypasta * Remove the logic script * Revise the readme to use proper knob/button names, add full UI list at the top * Replace comment blocks with docstrings, streamline some of the code * Implement reverse & ping-pong modes for advancing the active gate. Add the patch idea section to the readme * Add a section on the limitations of the script * Clean up some comments, use europi.cvs instead of hand-bombing our own array * Missed one * One last bit of copypasta from the quantizer * Use knob.range instead of manually rescaling the knob values --------- Co-authored-by: Chris I-B <c.iverachbrereton@gmail.com>
- Loading branch information
Showing
4 changed files
with
361 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# Sequential Switch | ||
|
||
This script acts as a sequential switch, routing a copy of the analogue input | ||
to one of the outputs. When a gate is received the output is changed. | ||
|
||
Note that the output is a digital copy of the input, and not a direct circuit | ||
path. The output may undergo some transformation in the process. See the | ||
section on limitations, below. | ||
|
||
- `ain`: the input signal that is copied to one of the 6 output channels | ||
- `din`: when a rising edge is detected, the active output changes | ||
- `cv1-6`: one of these will have a copy of `ain`, the others will be zero | ||
- `button 1`: manually advance the output (on default view), or apply the current | ||
option (in menu view) | ||
- `button 2`: cycle between default & menu views | ||
- `knob 1`: cycle through the menu items (in menu view) | ||
- `knob 2`: cycle through the options for the current menu item (in menu view) | ||
|
||
Pressing button 1 will manually advance the output, just like a trigger | ||
on the digital input. | ||
|
||
Pressing button 2 will enter the options menu. Use knob 1 to | ||
advance through the available options. Knob 2 will cycle through the | ||
values for the selected option. Pressing button 1 will apply the | ||
current selection. Pressing button 2 will return to the default view. | ||
|
||
Options: | ||
- number of outputs: 2-6, determines the number of output ports used | ||
- mode: one of: | ||
- sequential: port changes in order 1->2->3->4->5->6->1->.... | ||
- reverse: port changes in order 1->6->5->4->3->2->1->6->.... | ||
- ping-pong: port changes in order 1->2->3->4->5->6->5->4->... | ||
- random: port changes randomly, with a 1/n chance of repeating | ||
the current port | ||
|
||
After 20 minutes of idle time the screen will go blank. While blank the module | ||
will continue to operate normally. | ||
|
||
Pressing button 1 while the screen is blank will wake the module up | ||
_and_ advance the output. Pressing button 2 will only wake up the screen. | ||
|
||
|
||
## Limitations | ||
|
||
Because the Sequential Switch script reads the input voltage, processes it | ||
through an A-to-D converter and uses that to determine the output voltage | ||
the output signal will never be a perfect 1:1 copy of the input. | ||
|
||
Audio signals will be completely destroyed. You should only use this program | ||
for routing control voltages or gates/triggers. | ||
|
||
Square LFOs, gate, and trigger signals may be slightly noisy, but should still | ||
be within Eurorack tolerance for triggering external modules. | ||
|
||
Constant input voltages will also be noisy. Be careful if you're sending a | ||
quantized input into the module, as the quantization may be ruined in the | ||
A-to-D and D-to-A conversions. | ||
|
||
Smoothly-changing voltages, like triangle or sine LFOs may undergo some | ||
bit-crushing effects, which depending on your use may be desirable. | ||
|
||
|
||
## Patch Idea 1 | ||
|
||
Patch a constant voltage of 1V into `ain`. Every time the output changes it will | ||
effectively trigger a gate that will last until the next cycle. This will let | ||
you trigger effects, envelopes, etc... in sequence. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,286 @@ | ||
"""Sequential switch for the EuroPi | ||
@author Chris Iverach-Brereton <ve4cib@gmail.com> | ||
@date 2023-02-13 | ||
""" | ||
|
||
from europi import * | ||
from europi_script import EuroPiScript | ||
import time | ||
import random | ||
|
||
## Move in order 1>2>3>4>5>6>1>2>... | ||
MODE_SEQUENTIAL=0 | ||
|
||
## Move in order 1>6>5>4>3>2>1>6>... | ||
MODE_REVERSE=1 | ||
|
||
## Move in order 1>2>3>4>5>6>5>4>... | ||
MODE_PINGPONG=2 | ||
|
||
## Pick a random output, which can be the same as the one we're currently using | ||
MODE_RANDOM=3 | ||
|
||
## How many milliseconds of idleness do we need before we trigger the screensaver? | ||
# | ||
# =20 minutes | ||
SCREENSAVER_TIMEOUT_MS = 1000 * 60 * 20 | ||
|
||
class ScreensaverScreen: | ||
"""Blank the screen when idle | ||
Eventually it might be neat to have an animation, but that's | ||
not necessary for now | ||
""" | ||
|
||
def __init__(self, parent): | ||
self.parent = parent | ||
|
||
def on_button1(self): | ||
self.parent.active_screen = self.parent.switch_screen | ||
self.parent.on_trigger() | ||
|
||
def draw(self): | ||
oled.fill(0) | ||
oled.show() | ||
|
||
class SwitchScreen: | ||
"""Default display: shows what output is currently active | ||
""" | ||
|
||
def __init__(self, parent): | ||
self.parent = parent | ||
|
||
def on_button1(self): | ||
# the button can be used to advance the output | ||
self.parent.on_trigger() | ||
|
||
def draw(self): | ||
oled.fill(0) | ||
|
||
# Show all 6 outputs as a string on 2 lines to mirror the panel | ||
switches = "" | ||
for i in range(2): | ||
for j in range(3): | ||
out_no = i*3 + j | ||
if out_no < self.parent.num_outputs: | ||
# output is enabled; is it hot? | ||
if self.parent.current_output == out_no: | ||
switches = switches + " [*] " | ||
else: | ||
switches = switches + " [ ] " | ||
else: | ||
# output is disabled; mark it with . | ||
switches = switches + " . " | ||
|
||
switches = switches + "\n" | ||
|
||
oled.centre_text(switches) | ||
|
||
oled.show() | ||
|
||
class MenuScreen: | ||
"""Advanced menu options screen | ||
""" | ||
def __init__(self, parent): | ||
self.parent = parent | ||
|
||
self.menu_items = [ | ||
NumOutsChooser(parent), | ||
ModeChooser(parent) | ||
] | ||
|
||
def draw(self): | ||
self.menu_items[self.parent.menu_item].draw() | ||
|
||
def on_button1(self): | ||
self.menu_items[self.parent.menu_item].on_button1() | ||
|
||
class NumOutsChooser: | ||
"""Used by MenuScreen to choose the number of outputs | ||
""" | ||
|
||
def __init__(self, parent): | ||
self.parent = parent | ||
|
||
def read_num_outs(self): | ||
return k2.range(5) + 2 # result should be 2-6 | ||
|
||
def on_button1(self): | ||
num_outs = self.read_num_outs() | ||
self.parent.num_outputs = num_outs | ||
self.parent.current_output = self.parent.current_output % num_outs | ||
self.parent.save() | ||
|
||
def draw(self): | ||
num_outs = self.read_num_outs() | ||
oled.fill(0) | ||
oled.text(f"-- # Outputs --", 0, 0) | ||
oled.text(f"{self.parent.num_outputs} <- {num_outs}", 0, 10) | ||
oled.show() | ||
|
||
class ModeChooser: | ||
"""Used by MenuScreen to choose the operating mode | ||
""" | ||
def __init__(self, parent): | ||
self.parent = parent | ||
|
||
self.mode_names = [ | ||
"Seq.", | ||
"Rev.", | ||
"P-P", | ||
"Rand." | ||
] | ||
|
||
def read_mode(self): | ||
return k2.range(len(self.mode_names)) | ||
|
||
def on_button1(self): | ||
new_mode = self.read_mode() | ||
self.parent.mode = new_mode | ||
self.parent.save() | ||
|
||
def draw(self): | ||
new_mode = self.read_mode() | ||
oled.fill(0) | ||
oled.text(f"-- Mode --", 0, 0) | ||
oled.text(f"{self.mode_names[self.parent.mode]} <- {self.mode_names[new_mode]}", 0, 10) | ||
oled.show() | ||
|
||
class SequentialSwitch(EuroPiScript): | ||
"""The main workhorse of the whole module | ||
Copies the analog input to one of the 6 outputs, cycling which output | ||
whenever a trigger is received | ||
""" | ||
|
||
def __init__(self): | ||
super().__init__() | ||
|
||
# keep track of the last time the user interacted with the module | ||
# if we're idle for too long, start the screensaver | ||
self.last_interaction_time = time.ticks_ms() | ||
|
||
# How do we advance the output? | ||
self.mode = MODE_SEQUENTIAL | ||
|
||
# Use all 6 outputs by default | ||
self.num_outputs = 6 | ||
|
||
# The index of the current outputs | ||
self.current_output = 0 | ||
|
||
# For MODE_PINGPONG, this indicates the direction of travel | ||
# it will always be +1 or -1 | ||
self.direction = 1 | ||
|
||
# GUI/user interaction | ||
self.switch_screen = SwitchScreen(self) | ||
self.menu_screen = MenuScreen(self) | ||
self.screensaver = ScreensaverScreen(self) | ||
self.active_screen = self.switch_screen | ||
|
||
self.menu_item = 0 # the active item from the advanced menu | ||
|
||
self.load() | ||
|
||
def load(self): | ||
"""Load the persistent settings from storage and apply them | ||
""" | ||
state = self.load_state_json() | ||
|
||
self.mode = state.get("mode", self.mode) | ||
self.num_outputs = state.get("num_outputs", self.num_outputs) | ||
|
||
def save(self): | ||
"""Save the current settings to persistent storage | ||
""" | ||
state = { | ||
"mode": self.mode, | ||
"num_outputs": self.num_outputs | ||
} | ||
self.save_state_json(state) | ||
|
||
@classmethod | ||
def display_name(cls): | ||
return "Seq. Switch" | ||
|
||
def on_trigger(self): | ||
"""Handler for the rising edge of the input clock | ||
Also used for manually advancing the output on a button press | ||
""" | ||
# to save on clock cycles, don't use modular arithmetic | ||
# instead just to integer math and handle roll-over manually | ||
next_out = self.current_output | ||
if self.mode == MODE_SEQUENTIAL: | ||
next_out = next_out + 1 | ||
elif self.mode == MODE_REVERSE: | ||
next_out = next_out - 1 | ||
elif self.mode == MODE_PINGPONG: | ||
next_out = next_out + self.direction | ||
else: | ||
next_out = random.randint(0, self.num_outputs-1) | ||
|
||
if next_out < 0: | ||
if self.mode == MODE_REVERSE: | ||
next_out = self.num_outputs-1 | ||
else: | ||
next_out = -next_out | ||
self.direction = -self.direction | ||
elif next_out >= self.num_outputs: | ||
if self.mode == MODE_SEQUENTIAL: | ||
next_out = 0 | ||
else: | ||
next_out = self.num_outputs-2 | ||
self.direction = -self.direction | ||
|
||
self.current_output = next_out | ||
|
||
def main(self): | ||
"""The main loop | ||
Connects event handlers for clock-in and button presses | ||
and runs the main loop | ||
""" | ||
|
||
@din.handler | ||
def on_rising_clock(): | ||
self.on_trigger() | ||
|
||
@b1.handler | ||
def on_b1_press(): | ||
self.last_interaction_time = time.ticks_ms() | ||
self.active_screen.on_button1() | ||
|
||
@b2.handler | ||
def on_b2_press(): | ||
self.last_interaction_time = time.ticks_ms() | ||
|
||
if self.active_screen == self.switch_screen: | ||
self.active_screen = self.menu_screen | ||
else: | ||
self.active_screen = self.switch_screen | ||
|
||
while True: | ||
# keep the menu items sync'd with the left knob | ||
self.menu_item = k1.range(len(self.menu_screen.menu_items)) | ||
|
||
# check if we've been idle for too long; if so, blank the screen | ||
# to prevent burn-in | ||
now = time.ticks_ms() | ||
if time.ticks_diff(now, self.last_interaction_time) > SCREENSAVER_TIMEOUT_MS: | ||
self.active_screen = self.screensaver | ||
|
||
# read the input and send it to the current output | ||
# all other outputs should be zero | ||
input_volts = ain.read_voltage() | ||
for i in range(len(cvs)): | ||
if i == self.current_output: | ||
cvs[i].voltage(input_volts) | ||
else: | ||
cvs[i].voltage(0) | ||
|
||
self.active_screen.draw() | ||
|
||
if __name__ == "__main__": | ||
SequentialSwitch().main() |