Skip to content

Commit

Permalink
gate phaser initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Nik Ansell committed May 5, 2024
1 parent 5c79761 commit f8efc45
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 0 deletions.
6 changes: 6 additions & 0 deletions software/contrib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ the duration of the output signals.
<i>Author: [chrisib](https://github.com/chrisib)</i>
<br><i>Labels: gates, triggers</i>

### Gate Phaser \[ [documentation](/software/contrib/gate_phaser.md) | [script](/software/contrib/gate_phaser.py) \]
A script which attempts to answer the question "What would Steve Reich do if he had a EuroPi?"

<i>Author: [gamecat69](https://github.com/gamecat69)</i>
<br><i>Labels: sequencer, gates</i>

### Hamlet \[ [documentation](/software/contrib/hamlet.md) | [script](/software/contrib/hamlet.py) \]
A variation of the Consequencer script specifically geared towards driving voices

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions software/contrib/gate_phaser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Gate Phaser

author: Nik Ansell (github.com/gamecat69)

date: May 2024

labels: sequencer, gates

A script which attempts to answer the question "What would Steve Reich do if he had a EuroPi?".

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.

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.

# Inputs, Outputs and Controls

![Operating Diagram](./gate_phaser-docs/gate_phaser.png)

# Getting started

1. Patch anything you want to trigger with a gate to any of the outputs, for example
percussions elements, emvelopes, sequencer clocks or samples.
2. Set the Cycle time in milliseconds using knob 1
3. Set delay time in milliseconds using knob 2
4. Set the desired gate delay interval using button 1
5. Use button 2 to change the behaviour of knob 2 to set the desired gate delay time

# So what is this script actually doing?

**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.

**Gate delay** is the time in milliseconds that gate outputs are delayed from the master cycle time.

**Gate multiples** are multiples of the gate delay time per output.

## For Example:

- Cycle Time: 1000ms
- Gate Delay Time: 500ms
- Gate Delay Multiples: 0:1:2:3:4:5

Each output then sends a gate using the following formula:

Delay Time * Gate Delay Multiple

Therefore:

- Output 1 sends a gate every 1000ms
- Output 2 sends a gate every 500ms
- Output 3 sends a gate every 2000ms
- Output 4 sends a gate every 2500ms
- Output 5 sends a gate every 3000ms
- Output 6 sends a gate every 3500ms

Which results in the following:

| Output | t0 | 500ms | 1000ms | 1500ms | 2000ms | 2500ms | 3000ms | 3500ms |
|--------|---------|---------|---------|---------|---------|---------|---------|---------|
| 1 | x | | x | | x | | x | |
| 2 | | x | x | x | x | x | x | x |
| 3 | | | | | x | | | |
| 4 | | | | | | x | | |
| 5 | | | | | | | x | |
| 6 | | | | | | | | x |

211 changes: 211 additions & 0 deletions software/contrib/gate_phaser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,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()
1 change: 1 addition & 0 deletions software/contrib/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
["EnvelopeGen", "contrib.envelope_generator.EnvelopeGenerator"],
["Euclid", "contrib.euclid.EuclideanRhythms"],
["Gates & Triggers", "contrib.gates_and_triggers.GatesAndTriggers"],
["Gate Phaser", "contrib.gate_phaser.GatePhaser"],
["Hamlet", "contrib.hamlet.Hamlet"],
["HarmonicLFOs", "contrib.harmonic_lfos.HarmonicLFOs"],
["HelloWorld", "contrib.hello_world.HelloWorld"],
Expand Down

0 comments on commit f8efc45

Please sign in to comment.