# 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.
from time import ticks_diff, ticks_ms
#from random import uniform
from europi_script import EuroPiScript
#from europi_config import EuroPiConfig
author: Nik Ansell (github.com/gamecat69)
KNOB_CHANGE_TOLERANCE = 0.001
MIN_MS_BETWEEN_SAVES = 2000
class GatePhaser(EuroPiScript):
# 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.lastSaveState = ticks_ms()
self.pendingSaveState = False
self.screenRefreshNeeded = True
self.calcGateDelays(newList=True)
self.calcGateTimes(newList=True)
# Create intervalStr for the UI
# -----------------------------
# Interupt Handling functions
# -----------------------------
"""Triggered when B1 is pressed and released. Select gate delay multiples"""
self.selectedGateDelayMultiple = (self.selectedGateDelayMultiple + 1) % len(self.gateDelayMultiples)
self.screenRefreshNeeded = True
self.pendingSaveState = True
"""Triggered when B2 is pressed and released. Select gate control multiplier"""
self.selectedGateControlMultiplier = (self.selectedGateControlMultiplier + 1) % len(self.gateDelayControlOptions)
self.screenRefreshNeeded = True
self.pendingSaveState = True
def buildIntervalStr(self):
"""Create a string for the UI showing the gate delay multiples"""
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"""
val = self.gateDelayMultiples[self.selectedGateDelayMultiple][n] * self.slaveGateIntervalMs
self.gateDelays.append(val)
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()
gateOnTime = self.currentTimeStampMs + self.gateDelays[n]
gateOffTime = gateOnTime + GATE_LENGTH_MS
self.gateOnTimes.append(gateOnTime)
self.gateOffTimes.append(gateOffTime)
self.gateStates.append(False)
self.gateOnTimes[n] = gateOnTime
self.gateOffTimes[n] = gateOffTime
self.gateStates[n] = False
"""Get k1 and k2 values and adjust working parameters if knobs have moved"""
# 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)
if abs(self.smoothK2 - self.lastK2Reading) > KNOB_CHANGE_TOLERANCE:
self.slaveGateIntervalMs = max(MIN_PHASE_SHIFT_MS, self.smoothK2 * self.gateDelayControlOptions[self.selectedGateControlMultiplier])
self.screenRefreshNeeded = True
self.pendingSaveState = True
self.lastK1Reading = self.smoothK1
self.lastK2Reading = self.smoothK2
"""Entry point - main loop. See inline comments for more info"""
if self.screenRefreshNeeded:
# 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]:
self.gateStates[n] = False
elif self.currentTimeStampMs >= self.gateOnTimes[n] and not self.gateStates[n]:
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
if self.pendingSaveState and ticks_diff(ticks_ms(), self.lastSaveState) >= MIN_MS_BETWEEN_SAVES:
self.pendingSaveState = False
"""Update the screen only if something has changed. oled.show() hogs the processor and causes latency."""
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)
self.screenRefreshNeeded = False
"""Save working vars to a save state file"""
"masterGateIntervalMs": self.masterGateIntervalMs,
"slaveGateIntervalMs": self.slaveGateIntervalMs,
"selectedGateDelayMultiple": self.selectedGateDelayMultiple,
"selectedGateControlMultiplier": self.selectedGateControlMultiplier,
self.save_state_json(self.state)
self.lastSaveState = ticks_ms()
"""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__":