|
| 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() |
0 commit comments