# Copyright 2022 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 randint, uniform, choice
from europi_script import EuroPiScript
author: Sean Bechhofer (github.com/seanbechhofer)
labels: sequencer, triggers, drums, randomness
A gate and CV sequencer that builds on Nik Ansell's Consequencer. Changes are:
* Slimmed down drum patterns to two instruments, BD/HH
* Two channels of gate and random stepped CV with sparsity control. As
the sparsity is turned up, notes will drop out of the pattern.
knob_2: select pre-loaded drum pattern
button_1: Short Press: Play previous CV Pattern. Long Press: Change CV track length multiplier
button_2: Short Press: Generate a new random cv pattern for Tracks 1 and 2. Long Press: Cycle through analogue input modes
output_1: trigger 1 / Bass Drum
output_2: trigger 2 / Hi-Hat
output_3: gates for track 1
output_4: gates for track 2
output_5: randomly generated track 2 CV (cycled by pushing button 2)
output_6: randomly generated track 1 CV (cycled by pushing button 2)
class Hamlet(EuroPiScript):
# Initialize sequencer pattern arrays
self.trigger_duration_ms = 50
self.minAnalogInputVoltage = 0.9
self.analogInputMode = 1 # 1: Randomness, 2: Pattern, 3: CV Pattern
# Generate random CV for cv4-6
self.track_multiplier = 1 # multiplies CV pattern length so we can have 16, 32, 64
self.generateNewRandomCVPattern()
# Triggered when button 2 is released.
# Short press: Generate random CV for Tracks 1 and 2
# Long press: Change operating mode
if ticks_diff(ticks_ms(), b2.last_pressed()) > 300:
if self.analogInputMode < 3:
self.analogInputMode += 1
# Move to next cv pattern if one already exists, otherwise create a new one
if self.CvPattern == len(self.track_1):
self.generateNewRandomCVPattern()
# Triggered when button 1 is released
# Short press: Play previous CV pattern for cv4-6
if ticks_diff(ticks_ms(), b1.last_pressed()) > 300:
if self.track_multiplier >=4:
self.track_multiplier = 1
self.track_multiplier *= 2
# Play previous CV Pattern, unless we are at the first pattern
# Triggered on each clock into digital input. Output triggers.
self.step_length = len(self.BD[self.pattern])
# As the randomness value gets higher, the chance of a randomly selected int being lower gets higher
if randint(0,99) < self.randomness:
self.bd.value(randint(0, 1))
self.bd.value(randint(0, 1))
self.bd.value(int(self.BD[self.pattern][self.drum_step]))
self.hh.value(int(self.HH[self.pattern][self.drum_step]))
# A pattern was selected which is shorter than the current step. Set to zero to avoid an error
if self.drum_step >= self.step_length:
track_1_cv, track_1_sparsity = self.track_1[self.CvPattern][self.track_step]
track_2_cv, track_2_sparsity = self.track_2[self.CvPattern][self.track_step]
# Set track 1 and 2 voltage outputs based on previously
# generated random pattern and sparsity. CV only changed
# if sparsity value is above the current control level. So
# as sparsity increases, notes decrease.
if track_1_sparsity > self.sparsity:
self.cv_1.voltage(track_1_cv)
if track_2_sparsity > self.sparsity:
self.cv_2.voltage(track_2_cv)
# Reset clock step at 128 to avoid a HUGE integer if running for a long time
# over a really long period of time this would look like a memory leak
if self.clock_step < 128:
# Reset step number at step_length -1 as pattern arrays are zero-based
if self.drum_step < self.step_length - 1:
if self.track_step < (self.step_length * self.track_multiplier) - 1:
def generateNewRandomCVPattern(self):
"""Generate new random CV patterns for the voice tracks"""
self.step_length = len(self.BD[self.pattern])
# CV patterns are up to 4 times the length of the drum pattern
patt = (self.generateRandomPattern(self.step_length, 0, 9) +
self.generateRandomPattern(self.step_length, 0, 9) +
self.generateRandomPattern(self.step_length, 0, 9) +
self.generateRandomPattern(self.step_length, 0, 9))
self.track_1.append(patt)
patt = (self.generateRandomPattern(self.step_length, 0, 9) +
self.generateRandomPattern(self.step_length, 0, 9) +
self.generateRandomPattern(self.step_length, 0, 9) +
self.generateRandomPattern(self.step_length, 0, 9))
self.track_2.append(patt)
"""Read from knob 2 or CV (if in appropriate mode) and change the drum pattern accordingly"""
# If mode 2 and there is CV on the analogue input use it, if
# not use the knob position
val = 100 * ain.percent()
if self.analogInputMode == 2 and val > self.minAnalogInputVoltage:
self.pattern = int((len(self.BD) / 100) * val)
self.pattern = k2.read_position(len(self.BD))
self.step_length = len(self.BD[self.pattern])
def updateCvPattern(self):
"""Read from CV (if in appropriate mode) and change the CV pattern accordingly"""
# If analogue input mode 3, get the CV pattern from CV input
if self.analogInputMode != 3:
# Get the analogue input voltage as a percentage
CvpVal = 100 * ain.percent()
# Is there a voltage on the analogue input and are we configured to use it?
# Convert percentage value to a representative index of the pattern array
self.CvPattern = int((len(self.track_1) / 100) * CvpVal)
def generateRandomPattern(self, length, min, max):
"""Generate a new random pattern"""
# Returns a list of pairs of value and a sparsity
# Sparsity values are from 1 to length. This means that notes
# will progressively drop out as sparsity increases. Using
# random values would give different behaviour.
# No shuffle in micropython random :-(
# sparsities = random.shuffle(range(1,length+1))
numbers = list(range(1,length+1))
sparsities.append(chosen)
for i in range(0, length):
self.t.append((uniform(0,9),sparsities[i]))
def updateSparsity(self):
"""Update sparsity value from knob 1"""
# Don't use Analog input for now
self.sparsity = k1.read_position(steps=len(self.BD[self.pattern])+1)
def updateRandomness(self):
"""Read randomness from CV (if in appropriate mode)"""
# If mode 1 and there is CV on the analogue input use it
val = 100 * ain.percent()
if self.analogInputMode == 1 and val > self.minAnalogInputVoltage:
# If I have been running, then stopped for longer than
# reset_timeout, reset the steps and clock_step to 0
if self.clock_step != 0 and ticks_diff(ticks_ms(), din.last_triggered()) > self.reset_timeout:
def visualizePattern(self, pattern):
self.t = self.t.replace('1','o')
self.t = self.t.replace('0',' ')
def visualizeTrack(self, track):
for i in range(0, len(track)):
track_cv, track_sparsity = track[i]
if track_sparsity > self.sparsity:
# Show selected pattern visually
oled.text(self.visualizePattern(self.BD[self.pattern]),0,0,1)
oled.text(self.visualizePattern(self.HH[self.pattern]),0,8,1)
oled.text(self.visualizeTrack(self.track_1[self.CvPattern]),0,16,1)
# Show the analogInputMode
oled.text('M' + str(self.analogInputMode), 112, 25, 1)
oled.text('S' + str(int(self.sparsity)), 32, 25, 1)
oled.text('C' + str(self.CvPattern), 60, 25, 1)
oled.text('x' + str(self.track_multiplier), 80, 25, 1)
oled.text('R' + str(int(self.randomness)), 4, 25, 1)
# Initialize pattern lists
# Add patterns. This is a smaller number of patterns than in the
BD.append("0000000000000000")
HH.append("0000000000000000")
BD.append("1000100010001000")
HH.append("0000000000000000")
BD.append("1000100010001000")
HH.append("0010001000100010")
BD.append("1000100010001000")
HH.append("0010001000100011")
BD.append("1000100010001000")
HH.append("0111011101110111")
BD.append("1000100010001000")
HH.append("1111111111111111")
BD.append("0000000000000000")
HH.append("0010001000100010")
BD.append("1000100010001000")
HH.append("0000000001111111")
BD.append("0000000000000000")
HH.append("1111111011111110")
BD.append("1000100010010100")
HH.append("1111111011101110")
BD.append("1000001100100000")
HH.append("1111111111111111")
BD.append("1000100010001000")
HH.append("1111101111111101")
BD.append("1000100010010100")
HH.append("0010001000100011")
BD.append("1000100010010010")
HH.append("0000000000000000")
BD.append("1000100010010010")
HH.append("0010001000100010")
if __name__ == '__main__':
# Reset module display state.