Skip to content

Commit 803b765

Browse files
authored
Add Turing Machine mode to Pam's as another wave shape (#401)
* Add Turing Machine mode to Pam's as another wave shape * Fixes a bug where a SettingMenuItem could have multiple copies of the autoselect items appended to it if its choice array is shared across objects.
1 parent d827717 commit 803b765

File tree

4 files changed

+175
-20
lines changed

4 files changed

+175
-20
lines changed
6.6 KB
Loading

software/contrib/pams.md

+37-7
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,16 @@ The submenu for each CV output has the following options:
132132
triangle to ramp)
133133
- ![Sine Wave](./pams-docs/wave_sine.png) Sine: bog-standard sine wave
134134
- ![ADSR Envelope](./pams-docs/wave_adsr.png) ADSR: an Attack/Decay/Sustain/Release envelope
135+
- ![Turing](./pams-docs/wave_turing.png) Turing: a shift-register like the Music Thing Modular Turing Machine. Can either
136+
output pulses or stepped CV.
135137
- ![Random Wave](./pams-docs/wave_random.png) Random: outputs a random voltage at the start of every euclidean pulse,
136138
holding that voltage until the next pulse (if `EStep` is zero then every clock tick is assumed to be a euclidean
137139
pulse)
138-
- ![AIN](./pams-docs/wave_ain.png) AIN: acts as a sample & hold of `ain`, with a sample taken at the start of every
140+
- ![AIN](./pams-docs/wave_ain.png) AIN (S&H): acts as a sample & hold of `ain`, with a sample taken at the start of every
139141
euclidean pulse (if `EStep` is zero then every clock tick is assumed to be a euclidean pulse)
140-
- ![KNOB](./pams-docs/wave_knob.png) KNOB: acts as a sample & hold of `k1`, with a sample taken at the start of every
142+
- ![KNOB](./pams-docs/wave_knob.png) KNOB (S&H): acts as a sample & hold of `k1`, with a sample taken at the start of every
141143
euclidean pulse (if `EStep` is zero then every clock tick is assumed to be a euclidean pulse)
144+
142145
- `Width` -- width of the resulting wave. See below for the effects of width adjustment on different wave shapes
143146
- `Phase` -- the phase offset of the wave. Starting a triangle at 50% would start it midway through
144147
- `Ampl.` -- the maximum amplitude of the output as a percentage of the 10V hardware maximum
@@ -147,6 +150,9 @@ The submenu for each CV output has the following options:
147150
- `Sustain` -- the percentage level of the sustain phase of an ADSR envelope
148151
- `Release` -- the percentage of of the cyle time minus the attack & decay phases dedicated to the release phase of
149152
an ADSR envelope
153+
- `TLen` -- The length of the Turning machine shift register
154+
- `TLock` -- The lock value of the Turing machine shift register
155+
- `TMode` -- The mode of the Turing machine: either `Gate` or `CV`
150156
- `Skip%` -- the probability that a square pulse or euclidean trigger will be skipped
151157
- `EStep` -- the number of steps in the euclidean rhythm. If zero, the euclidean generator is disabled
152158
- `ETrig` -- the number of pulses in the euclidean rhythm
@@ -299,15 +305,38 @@ CV1 Output
299305
_____| |____| |__________
300306
```
301307

308+
### Turing Waves
309+
310+
`Turing` wave mode can operate in one of two sub-modes: `Gate` (default) or `CV`.
311+
312+
In `Gate` mode, it outputs simple of/off square waves.
313+
314+
Like the [original Turing Machine](https://github.com/TomWhitwell/TuringMachine) module, the `TLock` parameter
315+
controls the likelihood that bits are randomly flipped when the register shifts. When `TLock` is _positive_, the
316+
output pattern is equal to the `TLength` parameter. When `TLock` is _negative_ the output pattern is the initial
317+
`TLength` bits, followed by their compliment. The further from zero in either direction, the less likely it is that
318+
bits will be flipped during the shift.
319+
320+
For example if `TLength` is 8 and the register contains `10101100`, and `TLock` is `+100` the gate output will be
321+
`[1, 0, 1, 0, 1, 1, 0, 0]`. If `TLock` is `-100` instead the gate output will be
322+
`[1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1]`.
323+
324+
In `CV` mode it outputs a pseudo-random control voltage,similar to the `Random` wave. The value of the wave is
325+
determined by looking at the lowest 8 bits in the register (the register always contains 16 bits, regardless of
326+
`TLength`), treating them as a binary integer, and dividing that value by 256. The resulting value in the range
327+
`[0, 1)` is multiplied by the output amplitude to generate the output voltage.
328+
302329
### Effects of Width on Different Wave Shapes
303330

304331
- Square: Duty cycle control. 0% is always off, 100% is always on
305332
- Triangle: Symmetry control. 50% results in a symmetrical wave, 0% results in a saw wave,
306333
100% results in a ramp
307334
- Sine: ignored
308335
- Random: offset voltage as a percentage of the maximum output
309-
- AIN: ignored
310-
- KNOB: ignored
336+
- AIN (S&H): ignored
337+
- KNOB (S&H): ignored
338+
- Turing (Gate): Duty cycle control. 0% is always off, 100% will smear adjacent on-gates together
339+
- Turing (CV): ignored.
311340

312341
### Reset and Start Triggers
313342

@@ -382,9 +411,10 @@ as an offset voltage.
382411
There is digital attenuation/gain via the `AIN > Gain` or `KNOB > Gain` settings. This sets the
383412
percentage of the input signal that is passed to settings listenting to these inputs.
384413

385-
For example, if your modulation source can only output up to 5V you should set the gain to
386-
`12.0 / 5.0 * 100.0 = 240%`. This will allow the modulation source to fully sweep the
387-
range of options available.
414+
For example, if EuroPi is configured to use 10V as its maximum input voltage, but you patch
415+
an LFO that can only generate 0-5V, choosing `AIN` will only allow the LFO to sweep through the
416+
first half of the available options. To sweep the whole range, set `AIN > Gain` to
417+
`MAX_INPUT_VOLTAGE / (voltage source's max output) * 100 = 10.0 / 5.0 * 100 = 200`.
388418

389419
The `AIN > Precision` and `KNOB > Precision` settings allow control over the number of samples
390420
taken when reading the input. Higher precision can result in slower processing, which may introduce

software/contrib/pams.py

+137-12
Original file line numberDiff line numberDiff line change
@@ -205,29 +205,53 @@
205205
# etc...
206206
WAVE_KNOB = 6
207207

208+
## Turing machine shift register
209+
#
210+
# Requires a sub-setting for either gate or CV mode
211+
WAVE_TURING = 7
212+
208213
## Available wave shapes
214+
#
215+
# These must be placed in the desired order
209216
WAVE_SHAPES = [
210217
WAVE_SQUARE,
211218
WAVE_TRIANGLE,
212219
WAVE_SIN,
213220
WAVE_ADSR,
221+
WAVE_TURING,
214222
WAVE_RANDOM,
215223
WAVE_AIN,
216-
WAVE_KNOB
224+
WAVE_KNOB,
217225
]
218226

219-
## Ordered list of labels for the wave shape chooser menu
227+
## Labels for the wave shape chooser menu
220228
WAVE_SHAPE_LABELS = {
221229
WAVE_SQUARE: "Square",
222230
WAVE_TRIANGLE: "Triangle",
223231
WAVE_SIN: "Sine",
224232
WAVE_ADSR: "ADSR",
233+
WAVE_TURING: "Turing",
225234
WAVE_RANDOM: "Random",
226235
WAVE_AIN: "AIN (S&H)",
227236
WAVE_KNOB: "KNOB (S&H)",
228237
}
229238

230-
## Images of teh wave shapes
239+
# Turing machine modes of operation
240+
#
241+
# We can either output the gate pulses OR we can
242+
# output the semi-random CV
243+
MODE_TURING_GATE = 0
244+
MODE_TURING_CV = 1
245+
TURING_MODES = [
246+
MODE_TURING_GATE,
247+
MODE_TURING_CV,
248+
]
249+
TURING_MODE_LABELS = {
250+
MODE_TURING_GATE: "Gate",
251+
MODE_TURING_CV: "CV",
252+
}
253+
254+
## Images of the wave shapes
231255
#
232256
# These are 12x12 bitmaps. See:
233257
# - https://github.com/Allen-Synthesis/EuroPi/blob/main/software/oled_tips.md
@@ -237,6 +261,7 @@
237261
WAVE_TRIANGLE: bytearray(b'\x06\x00\x06\x00\t\x00\t\x00\x10\x80\x10\x80 @ @@ @ \x80\x10\x80\x10'),
238262
WAVE_SIN: bytearray(b'\x10\x00(\x00D\x00D\x00\x82\x00\x82\x00\x82\x10\x82\x10\x01\x10\x01\x10\x00\xa0\x00@'),
239263
WAVE_ADSR: bytearray(b' \x00 \x000\x000\x00H\x00H\x00G\xc0@@\x80 \x80 \x80\x10\x80\x10'),
264+
WAVE_TURING: bytearray(b'\xff\xf0\x04\x00\xf8\x00\x00\x00\xff\xf0\x04\x00\xf8\x00\x00\x00\xff\xf0\x04\x00\xf8\x00\x00\x00'),
240265
WAVE_RANDOM: bytearray(b'\x00\x00\x08\x00\x08\x00\x14\x00\x16\x80\x16\xa0\x11\xa0Q\xf0Pp`P@\x10\x80\x00'),
241266
WAVE_AIN: bytearray(b'\x00\x00|\x00|\x00d\x00d\x00g\x80a\x80\xe1\xb0\xe1\xb0\x01\xf0\x00\x00\x00\x00'),
242267
WAVE_KNOB: bytearray(b'\x06\x00\x19\x80 @@ @ \x80\x10\x82\x10A @\xa0 @\x19\x80\x06\x00'),
@@ -540,6 +565,9 @@ def __init__(self, cv_out, clock, n):
540565
self.cv_out = cv_out
541566
self.clock = clock
542567

568+
# 16-bit integer, initially random
569+
self.turing_register = random.randint(0, 65535)
570+
543571
## What quantization are we using?
544572
#
545573
# See contrib.pams.QUANTIZERS
@@ -799,6 +827,42 @@ def __init__(self, cv_out, clock, n):
799827
autoselect_cv = True,
800828
)
801829

830+
# Turing machine settings
831+
self.t_length = SettingMenuItem(
832+
config_point = IntegerConfigPoint(
833+
f"cv{n}_t_len",
834+
2,
835+
16,
836+
8
837+
),
838+
prefix = f"CV{n}",
839+
title = "TLen",
840+
autoselect_knob = True,
841+
autoselect_cv = True,
842+
)
843+
self.t_lock = SettingMenuItem(
844+
config_point = IntegerConfigPoint(
845+
f"cv{n}_t_lock",
846+
-100,
847+
100,
848+
0
849+
),
850+
prefix = f"CV{n}",
851+
title = "TLock",
852+
autoselect_knob = True,
853+
autoselect_cv = True,
854+
)
855+
self.t_mode = SettingMenuItem(
856+
config_point = ChoiceConfigPoint(
857+
f"cv{n}_t_mode",
858+
TURING_MODES,
859+
MODE_TURING_GATE,
860+
),
861+
prefix = f"CV{n}",
862+
title = "TMode",
863+
labels = TURING_MODE_LABELS,
864+
)
865+
802866
## All settings in an array so we can iterate through them in reset_settings(self)
803867
self.all_settings = [
804868
self.quantizer,
@@ -811,6 +875,9 @@ def __init__(self, cv_out, clock, n):
811875
self.e_step,
812876
self.e_trig,
813877
self.e_rot,
878+
self.t_length,
879+
self.t_lock,
880+
self.t_mode,
814881
self.skip,
815882
self.swing,
816883
self.mute,
@@ -873,18 +940,17 @@ def update_menu_visibility(self, new_value=None, old_value=None, config_point=No
873940
show_width = wave_shape != WAVE_AIN and wave_shape != WAVE_KNOB and wave_shape != WAVE_SIN
874941
self.width.is_visible = show_width
875942

943+
# hide the turing machine settings if we're not in Turing mode
944+
show_turing = wave_shape == WAVE_TURING
945+
self.t_length.is_visible = show_turing
946+
self.t_lock.is_visible = show_turing
947+
self.t_mode.is_visible = show_turing
948+
876949
def change_e_length(self, new_value=None, old_value=None, config_point=None, arg=None):
877950
self.e_trig.modify_choices(list(range(self.e_step.value+1)), self.e_step.value)
878951
self.e_rot.modify_choices(list(range(self.e_step.value+1)), self.e_step.value)
879952
self.recalculate_e_pattern()
880953

881-
def request_clock_mod(self, new_value=None, old_value=None, config_point=None, arg=None):
882-
self.clock_mod_dirty = True
883-
884-
def change_clock_mod(self):
885-
self.real_clock_mod = self.clock_mod.mapped_value
886-
self.clock_mod_dirty = False
887-
888954
def recalculate_e_pattern(self, new_value=None, old_value=None, config_point=None, arg=None):
889955
"""Recalulate the euclidean pattern this channel outputs
890956
"""
@@ -895,6 +961,13 @@ def recalculate_e_pattern(self, new_value=None, old_value=None, config_point=Non
895961

896962
self.next_e_pattern = e_pattern
897963

964+
def request_clock_mod(self, new_value=None, old_value=None, config_point=None, arg=None):
965+
self.clock_mod_dirty = True
966+
967+
def change_clock_mod(self):
968+
self.real_clock_mod = self.clock_mod.mapped_value
969+
self.clock_mod_dirty = False
970+
898971
def square_wave(self, tick, n_ticks):
899972
"""Calculate the [0, 1] value of a square wave with PWM
900973
@@ -911,8 +984,10 @@ def square_wave(self, tick, n_ticks):
911984
start_tick = self.phase.value * n_ticks / 100.0
912985
end_tick = (start_tick + duty_cycle) % n_ticks
913986

914-
if (start_tick < end_tick and tick >= start_tick and tick < end_tick) or \
915-
(start_tick > end_tick and (tick < end_tick or tick >= start_tick)):
987+
if (
988+
(start_tick < end_tick and tick >= start_tick and tick < end_tick) or
989+
(start_tick > end_tick and (tick < end_tick or tick >= start_tick))
990+
):
916991
return 1.0
917992
else:
918993
return 0.0
@@ -978,6 +1053,9 @@ def adsr_wave(self, tick, n_ticks):
9781053
/ \
9791054
-A--D---S---R-
9801055
---n_ticks----
1056+
1057+
@param tick The current tick, in the range [0, n_ticks)
1058+
@param n_ticks The number of ticks in which the wave must complete
9811059
"""
9821060

9831061
# apply the phase offset
@@ -1011,6 +1089,48 @@ def adsr_wave(self, tick, n_ticks):
10111089
# outside of the ADSR
10121090
return 0.0
10131091

1092+
def turing_shift(self):
1093+
"""Shift the turing machine register by 1 bit
1094+
"""
1095+
r = random.randint(0, 99)
1096+
if r >= abs(self.t_lock.value):
1097+
incoming_bit = random.randint(0, 1)
1098+
else:
1099+
incoming_bit = (self.turing_register >> (self.t_length.value - 1)) & 0x01
1100+
self.turing_register = ((self.turing_register << 1) & 0xffff) | incoming_bit
1101+
1102+
def turing_wave(self, tick, n_ticks):
1103+
"""Calculate the [0, 1] output of a Turing Machine wave
1104+
1105+
@param tick The current tick, in the range [0, n_ticks)
1106+
@param n_ticks The number of ticks in which the wave must complete
1107+
"""
1108+
# respect phase shifting when updating the shift register
1109+
start_tick = int(self.phase.value * n_ticks / 100.0)
1110+
if tick == start_tick:
1111+
self.turing_shift()
1112+
1113+
active_bit = self.turing_register & 0x0001
1114+
if self.t_lock.value < 0 and self.wave_counter % (2 * self.t_length.value) >= self.t_length.value:
1115+
# turing machine outputs the [register, ~register] when "locked-left",
1116+
# effectively doubling the length of the pattern
1117+
active_bit = active_bit ^ 0x01
1118+
1119+
if self.t_mode.value == MODE_TURING_GATE:
1120+
if active_bit:
1121+
return self.square_wave(tick, n_ticks)
1122+
else:
1123+
return 0
1124+
else:
1125+
value = self.turing_register & 0xff # consider only the lowest 8 bits
1126+
1127+
if self.t_lock.value < 0 and self.wave_counter % (2 * self.t_length.value) >= self.t_length.value:
1128+
# if we're in the second half of a doubled pattern, invert the value
1129+
value = (~value) & 0xff
1130+
1131+
return value / 256
1132+
return 0
1133+
10141134
def reset(self):
10151135
"""Reset the current output to the beginning
10161136
"""
@@ -1097,6 +1217,8 @@ def tick(self):
10971217
wave_sample = wave_sample * self.sine_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0)
10981218
elif self.wave_shape.value == WAVE_ADSR:
10991219
wave_sample = wave_sample * self.adsr_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0)
1220+
elif self.wave_shape.value == WAVE_TURING:
1221+
wave_sample = self.turing_wave(wave_position, ticks_per_note) * (self.amplitude.value / 100.0)
11001222
else:
11011223
wave_sample = 0.0
11021224

@@ -1211,6 +1333,9 @@ def __init__(self):
12111333
ch.clock_mod.add_child(ch.e_step)
12121334
ch.clock_mod.add_child(ch.e_trig)
12131335
ch.clock_mod.add_child(ch.e_rot)
1336+
ch.clock_mod.add_child(ch.t_length)
1337+
ch.clock_mod.add_child(ch.t_lock)
1338+
ch.clock_mod.add_child(ch.t_mode)
12141339
ch.clock_mod.add_child(ch.swing)
12151340
ch.clock_mod.add_child(ch.quantizer)
12161341
ch.clock_mod.add_child(ch.root)

software/firmware/experimental/settings_menu.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ def get_option_list(self):
421421
elif t is BooleanConfigPoint:
422422
items = [False, True]
423423
elif t is ChoiceConfigPoint:
424-
items = self.src_config.choices
424+
items = list(self.src_config.choices) # make a copy of the items so we can append the autoselect items!
425425
else:
426426
raise Exception(f"Unsupported ConfigPoint type: {type(self.src_config)}")
427427

0 commit comments

Comments
 (0)