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