Skip to content

Commit 9a1c914

Browse files
gamecat69Nik Ansell
and
Nik Ansell
authored
gate phaser initial commit (#362)
* gate phaser initial commit * minor doc update, updated delay formula * minor doc update --------- Co-authored-by: Nik Ansell <[email protected]>
1 parent d3fc2ee commit 9a1c914

File tree

5 files changed

+282
-0
lines changed

5 files changed

+282
-0
lines changed

software/contrib/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ the duration of the output signals.
6868
<i>Author: [chrisib](https://github.com/chrisib)</i>
6969
<br><i>Labels: gates, triggers</i>
7070

71+
### Gate Phaser \[ [documentation](/software/contrib/gate_phaser.md) | [script](/software/contrib/gate_phaser.py) \]
72+
A script which attempts to answer the question "What would Steve Reich do if he had a EuroPi?"
73+
74+
<i>Author: [gamecat69](https://github.com/gamecat69)</i>
75+
<br><i>Labels: sequencer, gates</i>
76+
7177
### Hamlet \[ [documentation](/software/contrib/hamlet.md) | [script](/software/contrib/hamlet.py) \]
7278
A variation of the Consequencer script specifically geared towards driving voices
7379

Loading

software/contrib/gate_phaser.md

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Gate Phaser
2+
3+
author: Nik Ansell (github.com/gamecat69)
4+
5+
date: May 2024
6+
7+
labels: sequencer, gates
8+
9+
A script which attempts to answer the question "What would Steve Reich do if he had a EuroPi?".
10+
11+
Gates are sent from outputs 1-6 which are offset (or out of phase) with each other. This creates a group of gates that drift in and out of phase with each other over time.
12+
13+
You can use this script to create Piano Phase type patches, dynamic rhythms which evolve over time and eventually resolve back in phase, or ... well lots of other things that would benefit from having gates which are delayed from each other.
14+
15+
# Inputs, Outputs and Controls
16+
17+
![Operating Diagram](./gate_phaser-docs/gate_phaser.png)
18+
19+
# Getting started
20+
21+
1. Patch anything you want to trigger with a gate to any of the outputs, for example
22+
percussions elements, emvelopes, sequencer clocks or samples.
23+
2. Set the Cycle time in milliseconds using knob 1
24+
3. Set delay time in milliseconds using knob 2
25+
4. Set the desired gate delay interval using button 1
26+
5. Use button 2 to change the behaviour of knob 2 to set the desired gate delay time
27+
28+
# So what is this script actually doing?
29+
30+
**Cycle time** is the time in milliseconds between gate outputs at output 1 when the gate delay multiple for output 1 is set to 0.
31+
32+
**Gate delay** is the time in milliseconds that gate outputs are delayed from the master cycle time.
33+
34+
**Gate multiples** are multiples of the gate delay time per output.
35+
36+
## For Example:
37+
38+
- Cycle Time: 1000ms
39+
- Gate Delay Time: 500ms
40+
- Gate Delay Multiples: 0:1:2:3:4:5
41+
42+
The initial output is sent after "Gate Delay Time * Gate Delay Multiple" milliseconds.
43+
Each subsequent output is sent after "Cycle Time + (Delay Time * Gate Delay Multiple)" milliseconds.
44+
45+
Therefore after the initial gate output from each output:
46+
47+
- Output 1 sends a gate every 1000ms
48+
- Output 2 sends a gate every 1500ms
49+
- Output 3 sends a gate every 2000ms
50+
- Output 4 sends a gate every 2500ms
51+
- Output 5 sends a gate every 3000ms
52+
- Output 6 sends a gate every 3500ms
53+
54+
Which results in the following:
55+
56+
| Output | t0 | 500ms | 1000ms | 1500ms | 2000ms | 2500ms | 3000ms | 3500ms | 4000ms |
57+
|--------|---------|---------|---------|---------|---------|---------|---------|---------|---------|
58+
| 1 | x | | x | | x | | x | | x |
59+
| 2 | | x | | | x | | | x | |
60+
| 3 | | | x | | | | x | | |
61+
| 4 | | | | x | | | | | x |
62+
| 5 | | | | | x | | | | |
63+
| 6 | | | | | | x | | | |
64+

software/contrib/gate_phaser.py

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
from europi import *
2+
from time import ticks_diff, ticks_ms
3+
#from random import uniform
4+
from europi_script import EuroPiScript
5+
#from europi_config import EuroPiConfig
6+
7+
"""
8+
Gate Phaser
9+
author: Nik Ansell (github.com/gamecat69)
10+
date: May 2024
11+
labels: sequencer, gates
12+
"""
13+
14+
# Constants
15+
KNOB_CHANGE_TOLERANCE = 0.001
16+
MIN_CYCLE_TIME_MS = 100
17+
MIN_PHASE_SHIFT_MS = 5
18+
MIN_MS_BETWEEN_SAVES = 2000
19+
GATE_LENGTH_MS = 20
20+
21+
class GatePhaser(EuroPiScript):
22+
def __init__(self):
23+
24+
# Initialize variables
25+
26+
# How many multiples of the gate delay time will each gate be delayed?
27+
self.gateDelayMultiples = [ [0,1,2,3,4,5],[2,3,4,5,6,7],[0,1,2,3,6,9],[1,2,3,2,4,6],[5,4,3,2,1,0] ]
28+
# UI only, changes the behaviour of the gate delay control
29+
self.gateDelayControlOptions = [5, 10, 20]
30+
# Lists containing params for each output
31+
self.gateDelays = []
32+
self.gateOnTimes = []
33+
self.gateOffTimes = []
34+
self.gateStates = []
35+
36+
self.lastK1Reading = 0
37+
self.lastK2Reading = 0
38+
self.lastSaveState = ticks_ms()
39+
self.pendingSaveState = False
40+
self.screenRefreshNeeded = True
41+
42+
self.smoothK1 = 0
43+
self.smoothK2 = 0
44+
self.loadState()
45+
46+
# Populate working lists
47+
self.calcGateDelays(newList=True)
48+
self.calcGateTimes(newList=True)
49+
50+
# Create intervalStr for the UI
51+
self.buildIntervalStr()
52+
53+
# -----------------------------
54+
# Interupt Handling functions
55+
# -----------------------------
56+
57+
@din.handler
58+
def resetGates():
59+
"""Resets gate timers"""
60+
self.calcGateDelays()
61+
self.calcGateTimes()
62+
63+
@b1.handler_falling
64+
def b1Pressed():
65+
"""Triggered when B1 is pressed and released. Select gate delay multiples"""
66+
self.selectedGateDelayMultiple = (self.selectedGateDelayMultiple + 1) % len(self.gateDelayMultiples)
67+
self.calcGateDelays()
68+
self.calcGateTimes()
69+
self.buildIntervalStr()
70+
self.screenRefreshNeeded = True
71+
self.pendingSaveState = True
72+
73+
74+
@b2.handler_falling
75+
def b2Pressed():
76+
"""Triggered when B2 is pressed and released. Select gate control multiplier"""
77+
self.selectedGateControlMultiplier = (self.selectedGateControlMultiplier + 1) % len(self.gateDelayControlOptions)
78+
self.calcGateDelays()
79+
self.calcGateTimes()
80+
self.screenRefreshNeeded = True
81+
self.pendingSaveState = True
82+
83+
84+
def buildIntervalStr(self):
85+
"""Create a string for the UI showing the gate delay multiples"""
86+
self.intervalsStr = ''
87+
for i in self.gateDelayMultiples[self.selectedGateDelayMultiple]:
88+
self.intervalsStr = self.intervalsStr + str(i) + ':'
89+
90+
91+
def lowPassFilter(self, alpha, prevVal, newVal):
92+
"""Smooth out some analogue noise. Higher Alpha = more smoothing"""
93+
# Alpha value should be between 0 and 1.0
94+
return alpha * prevVal + (1 - alpha) * newVal
95+
96+
def calcGateDelays(self, newList=False):
97+
"""Populate a list containing the gate delay in ms for each output"""
98+
for n in range(6):
99+
val = self.gateDelayMultiples[self.selectedGateDelayMultiple][n] * self.slaveGateIntervalMs
100+
if newList:
101+
self.gateDelays.append(val)
102+
else:
103+
self.gateDelays[n] = (val)
104+
105+
106+
def calcGateTimes(self, newList=False):
107+
"""Calculate the next gate on and off times based on the current time"""
108+
self.currentTimeStampMs = ticks_ms()
109+
for n in range(6):
110+
gateOnTime = self.currentTimeStampMs + self.gateDelays[n]
111+
gateOffTime = gateOnTime + GATE_LENGTH_MS
112+
if newList:
113+
self.gateOnTimes.append(gateOnTime)
114+
self.gateOffTimes.append(gateOffTime)
115+
self.gateStates.append(False)
116+
else:
117+
self.gateOnTimes[n] = gateOnTime
118+
self.gateOffTimes[n] = gateOffTime
119+
self.gateStates[n] = False
120+
121+
122+
def getKnobValues(self):
123+
"""Get k1 and k2 values and adjust working parameters if knobs have moved"""
124+
changed = False
125+
126+
# Get knob values and smooth using a simple low pass filter
127+
self.smoothK1 = int(self.lowPassFilter(0.15, self.lastK1Reading, k1.read_position(100) + 2))
128+
self.smoothK2 = int(self.lowPassFilter(0.15, self.lastK2Reading, k2.read_position(100) + 2))
129+
130+
if abs(self.smoothK1 - self.lastK1Reading) > KNOB_CHANGE_TOLERANCE:
131+
self.masterGateIntervalMs = max(MIN_CYCLE_TIME_MS, self.smoothK1 * 25)
132+
changed = True
133+
134+
if abs(self.smoothK2 - self.lastK2Reading) > KNOB_CHANGE_TOLERANCE:
135+
self.slaveGateIntervalMs = max(MIN_PHASE_SHIFT_MS, self.smoothK2 * self.gateDelayControlOptions[self.selectedGateControlMultiplier])
136+
changed = True
137+
138+
if changed:
139+
self.calcGateDelays()
140+
self.calcGateTimes()
141+
self.screenRefreshNeeded = True
142+
self.pendingSaveState = True
143+
144+
self.lastK1Reading = self.smoothK1
145+
self.lastK2Reading = self.smoothK2
146+
147+
def main(self):
148+
"""Entry point - main loop. See inline comments for more info"""
149+
while True:
150+
self.getKnobValues()
151+
if self.screenRefreshNeeded:
152+
self.updateScreen()
153+
154+
# Cycle through outputs turning gates on and off as needed
155+
# When a gate is triggered it's next on and off time is calculated
156+
self.currentTimeStampMs = ticks_ms()
157+
for n in range(len(cvs)):
158+
if self.currentTimeStampMs >= self.gateOffTimes[n] and self.gateStates[n]:
159+
cvs[n].off()
160+
self.gateStates[n] = False
161+
elif self.currentTimeStampMs >= self.gateOnTimes[n] and not self.gateStates[n]:
162+
cvs[n].on()
163+
self.gateStates[n] = True
164+
# When will the gate need to turn off?
165+
self.gateOffTimes[n] = self.currentTimeStampMs + GATE_LENGTH_MS
166+
# When will the next gate need to fire?
167+
self.gateOnTimes[n] = self.currentTimeStampMs + self.gateDelays[n] + self.masterGateIntervalMs
168+
169+
# Save state
170+
if self.pendingSaveState and ticks_diff(ticks_ms(), self.lastSaveState) >= MIN_MS_BETWEEN_SAVES:
171+
self.saveState()
172+
self.pendingSaveState = False
173+
174+
def updateScreen(self):
175+
"""Update the screen only if something has changed. oled.show() hogs the processor and causes latency."""
176+
177+
# Clear screen
178+
oled.fill(0)
179+
180+
oled.text("Cycle", 5, 0, 1)
181+
oled.text(str(self.masterGateIntervalMs), 5, 10, 1)
182+
oled.text("Delay", 80, 0, 1)
183+
oled.text(str(self.slaveGateIntervalMs), 80, 10, 1)
184+
oled.text(self.intervalsStr[:-1], 0, 22, 1)
185+
oled.text('x' + str(self.gateDelayControlOptions[self.selectedGateControlMultiplier]), 104, 22, 1)
186+
187+
oled.show()
188+
self.screenRefreshNeeded = False
189+
190+
def saveState(self):
191+
"""Save working vars to a save state file"""
192+
self.state = {
193+
"masterGateIntervalMs": self.masterGateIntervalMs,
194+
"slaveGateIntervalMs": self.slaveGateIntervalMs,
195+
"selectedGateDelayMultiple": self.selectedGateDelayMultiple,
196+
"selectedGateControlMultiplier": self.selectedGateControlMultiplier,
197+
}
198+
self.save_state_json(self.state)
199+
self.lastSaveState = ticks_ms()
200+
201+
def loadState(self):
202+
"""Load a previously saved state, or initialize working vars, then save"""
203+
self.state = self.load_state_json()
204+
self.masterGateIntervalMs = self.state.get("masterGateIntervalMs", 1000)
205+
self.slaveGateIntervalMs = self.state.get("slaveGateIntervalMs", 100)
206+
self.selectedGateDelayMultiple = self.state.get("selectedGateDelayMultiple", 0)
207+
self.selectedGateControlMultiplier = self.state.get("selectedGateControlMultiplier", 0)
208+
209+
if __name__ == "__main__":
210+
dm = GatePhaser()
211+
dm.main()

software/contrib/menu.py

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
["EnvelopeGen", "contrib.envelope_generator.EnvelopeGenerator"],
3333
["Euclid", "contrib.euclid.EuclideanRhythms"],
3434
["Gates & Triggers", "contrib.gates_and_triggers.GatesAndTriggers"],
35+
["Gate Phaser", "contrib.gate_phaser.GatePhaser"],
3536
["Hamlet", "contrib.hamlet.Hamlet"],
3637
["HarmonicLFOs", "contrib.harmonic_lfos.HarmonicLFOs"],
3738
["HelloWorld", "contrib.hello_world.HelloWorld"],

0 commit comments

Comments
 (0)