# Copyright 2024 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.
A script meant to recreate the Music Thing Modular Turning Machine Random Sequencer as faithfully
as possible on the EuroPi hardware using bit shift operations to mimic the analog shift register.
ain - cv control over the big knob, added to the knobs value
k1 - the big knob (probability that the sequence changes)
k2 - output scale (0-10v) or sequence length (2-16 steps)
b1 - write (clear bits) (or config value write_value)
cv1 - pulse 1 (or config value cv1_pulse_bit)
cv2 - pulse 2 (or config value cv2_pulse_bit)
cv3 - pulse 4 (or config value cv3_pulse_bit)
If you'd like to use different bits for the pulse outputs you can update the `CVX_PULSE_BIT`
The length, scale, and bit pattern are saved whenever the knob 2 state is changed, or when the user
from random import getrandbits, randint
from firmware import europi
from firmware.europi import clamp, MAX_UINT16
from firmware.europi import din, ain, k1, k2, b1, b2, cv1, cv2, cv3, cv4, cv5, cv6, oled
from firmware.experimental.knobs import KnobBank
from europi import clamp, MAX_UINT16
from europi import din, ain, k1, k2, b1, b2, cv1, cv2, cv3, cv4, cv5, cv6, oled
from experimental.knobs import KnobBank
from europi_script import EuroPiScript
MAX_OUTPUT_VOLTAGE = europi.MAX_OUTPUT_VOLTAGE
"""A class meant to recreate the Music Thing Modular Turning Machine Random Sequencer as faithfully as possible in
micropython using bit shift operations to mimic the TM's analog shift register.
The `TuringMachine` class keeps its state in several internal member variables which are accessed via property
variables. See `flip_probability`, `scale`, `length` and `write`. users of the class can modify these
variables in order to configure the TM's behavior. In many cases script authors will want to tie one of these
variables to a hardware UI element, for example, assigning the `flip_probability` to a knob. In this case clients
can override the corresponding `*_getter` function. For example, to tie the write switch to button 1::
This form allows clients to override where the `TuringMachine` obtains some of its state variables without
explicitly sub-classing the `TuringMachine` class. See `EuroPiTuringMachine` for a more detailed example.
bit_count=DEFAULT_BIT_COUNT,
max_output_voltage=MAX_OUTPUT_VOLTAGE,
scale=MAX_OUTPUT_VOLTAGE,
length=DEFAULT_BIT_COUNT,
"""Create a new TuringMachine with a shift register of the specified bit count. Default is 16, minimum is 8.
The maximum output voltage is also configurable and defaults to `europi.MAX_OUTPUT_VOLTAGE`
raise ValueError(f"Specified bit_count ({bit_count}) is less than the minimum (8).")
self.bit_count = bit_count
self.bits = getrandbits(self.bit_count)
self._flip_probability = flip_probability
self.max_output_voltage = max_output_voltage
self.clear_on_write = clear_on_write
self.flip_probability_getter = lambda: self._flip_probability
self.scale_getter = lambda: self._scale
self.length_getter = lambda: self._length
self.write_getter = lambda: self._write
self.step_handler = lambda: None
self.bits = ((self.bits << 1) % (1 << self.bit_count)) | (
(self.bits >> (self.length - 1)) & 1
"""Move the turing machine to its next state based on its current state. Parameters that affect the next state
include: `flip_probability`, `length`, `write`, and, the internal bit register.
Typically this method would be called in response to a clock tick.
self.bits = self.bits & ~1
self.bits = self.bits | (1 << 0)
if randint(0, 99) < self.flip_probability:
self.bits = self.bits ^ 0b1
"""Returns the least significant eight bits from the internal bit register, which are the same bits used to
determine the voltage returned by `get_voltage()`. This method is useful when displaying the current state in a
"""Returns the bit at the specified index. This method exists to support the Pulses Expander functionality."""
return self.bits >> i & 1
def get_bit_and(self, *args):
"""Returns the result of a bitwise and of the bits at the specified indexes. This method exists to support the
Pulses Expander functionality."""
result = result & self.get_bit(i)
"""Returns the voltage described by the eight least significant bits of the internal bit register, scaled by the
current `scale` factor."""
return self.get_8_bits() / INT_MAX_8 * self.scale
def flip_probability(self):
"""Returns the probability that the most significant bit will be flipped when it is rotated into the least
significant bit's position. This translates to the probability that the sequence will change. The
flip_probability is represented as an integer in the range [0, 100].
return self.flip_probability_getter()
def flip_probability(self, probability: int):
"""Set the flip probability as an integer in the range [0, 100]."""
if probability < 0 or probability > 100:
f"Probability of {probability} is outside the expected range of [0,100]"
self._flip_probability = probability
"""Returns the current scaling factor, used to reduce the range of the output voltage to something lower than
the `max_output_voltage`. Represented by a float in the range [0, `max_output_voltage`]"""
return self.scale_getter()
"""Set the scale factor as a float in the range [0, `max_output_voltage`]"""
if scale < 0 or scale > self.max_output_voltage:
f"Scale of {scale} is outside the expected range of [0,{self.max_output_voltage}]"
"""Returns the length of the current sequence as an integer in the range of [2, `bit_count`]"""
return self.length_getter()
def length(self, length):
"""Sets the length of the current sequence as an integer in the range of [2, `bit_count`]"""
if length < 2 or length > self.bit_count:
f"Length of {length} is outside the expected range of [2,{self.bit_count}]"
"""Returns the current value of the 'write switch'. When true the least significant bit will be cleared during
rotation, regardless of the `flip_probability`. This allows for real-time user manipulation of the sequence.
return self.write_getter()
def write(self, value: bool):
"""Set the state of the 'write switch'. `True` means that the least significant bit will be cleared."""
class EuroPiTuringMachine(EuroPiScript):
def __init__(self, bit_count=DEFAULT_BIT_COUNT, max_output_voltage=MAX_OUTPUT_VOLTAGE):
self.LENGTH_CHOICES = [2, 3, 4, 5, 6, 8, 12, 16] # TODO: vary based on bit_count?
initial_scale_percent = 0.5 # TODO: load from saved state
initial_length = 8 # TODO: load from saved state
max_output_voltage=max_output_voltage,
clear_on_write=self.config.WRITE_VALUE == 0,
scale=MAX_OUTPUT_VOLTAGE * initial_scale_percent,
self.tm.flip_probability_getter = self.flip_probability
self.tm.scale_getter = self.scale
self.tm.length_getter = self.length
self.tm.write_getter = self.write
self.tm.step_handler = self.step_handler
self.request_next_k2 = False
.with_locked_knob("scale", initial_percentage_value=initial_scale_percent)
initial_percentage_value=(self.LENGTH_CHOICES.index(initial_length) * 2 + 1)
/ (len(self.LENGTH_CHOICES) * 2),
self.cv1_pulse_bit = self.config.CV1_PULSE_BIT
self.cv2_pulse_bit = self.config.CV2_PULSE_BIT
self.cv3_pulse_bit = self.config.CV3_PULSE_BIT
def request_next_k2_mode():
self.request_next_k2 = True
if self.kb2.current_name == "scale":
self.tm.scale = self.scale()
elif self.kb2.current_name == "length":
self.tm.length = self.length()
self.request_next_k2 = False
cv1.value(self.tm.get_bit(self.cv1_pulse_bit))
cv2.value(self.tm.get_bit(self.cv2_pulse_bit))
cv3.value(self.tm.get_bit(self.cv3_pulse_bit))
cv4.value(self.tm.get_bit_and(self.cv1_pulse_bit, self.cv2_pulse_bit))
cv5.value(self.tm.get_bit_and(self.cv2_pulse_bit, self.cv3_pulse_bit))
cv6.voltage(self.tm.get_voltage())
def flip_probability(self):
return clamp(int(round(1 - k1.percent() - ain.percent(), 2) * 100), 0, 100)
if self.kb2.current_name == "scale":
return self.kb2.scale.percent() * self.tm.max_output_voltage
if self.kb2.current_name == "length":
return self.kb2.length.choice(self.LENGTH_CHOICES)
def bits_as_led_line(oled, bits):
width = int(europi.OLED_WIDTH / 8)
oled.hline(x_pos, 0, width - 1, 1)
range_max = min(DEFAULT_BIT_COUNT, 8) - 1
configuration.choice(name="WRITE_VALUE", choices=[0, 1], default=0),
# simulate the actual bits available in the pulses expander (1-7)
configuration.integer(name="CV1_PULSE_BIT", minimum=range_min, maximum=range_max, default=1),
configuration.integer(name="CV2_PULSE_BIT", minimum=range_min, maximum=range_max, default=2),
configuration.integer(name="CV3_PULSE_BIT", minimum=range_min, maximum=range_max, default=4),
prob = self.tm.flip_probability
if self.tm.flip_probability == 0
if self.tm.flip_probability == 100
f"{'*' if self.kb2.current_name == 'scale' else ' '}scale:{self.tm.scale:3.1f}"
len_str = f"{'*' if self.kb2.current_name == 'length' else ' '}{self.tm.length:2} steps"
self.bits_as_led_line(oled, self.tm.get_8_bits())
oled.text(f" {prob}%", 0, line1_y, 1)
oled.text(f"{scale_str}", 40, line1_y, 1)
oled.text(f"{prob_2}", 0, line2_y, 1)
oled.text(f"{len_str}", 63, line2_y, 1)
if __name__ == "__main__":
EuroPiTuringMachine(DEFAULT_BIT_COUNT, europi.MAX_OUTPUT_VOLTAGE).main()