Skip to content

Commit

Permalink
Add DCSN-2 Script (#399)
Browse files Browse the repository at this point in the history
* Add DCSN-2 contrib script

* Add `ellipse` function for drawing circles and ellipses
  • Loading branch information
chrisib authored Jan 9, 2025
1 parent 8fcafa9 commit 78294cc
Show file tree
Hide file tree
Showing 7 changed files with 194 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 @@ -54,6 +54,12 @@ Recording of CV can be primed so that you can record a movement without missing
<i>Author: [anselln](https://github.com/anselln)</i>
<br><i>Labels: sequencer, CV, performance</i>

### DCSN-2 \[ [documentation](/software/contrib/dscn2.md) | [script](/software/contrib/dcsn2.md) \]
A loopable random gate sequencer based on a binary tree. Inspired by the Robaux DCSN3

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

### Egressus Melodium \[ [documentation](/software/contrib/egressus_melodiam.md) | [script](/software/contrib/egressus_melodiam.py) \]
Clockable and free-running LFO and random CV pattern generator

Expand Down
31 changes: 31 additions & 0 deletions software/contrib/dscn2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# DCSN-2

DSCN-2 is a loopable, random gate sequencer based on a binary tree. An incoming clock signal is routed to
one of two child nodes, and from there to one of four grandchild nodes.

Inspired by the [Robaux DCSN3](https://robaux.io/products/dcsn3).

## Ins & Outs

| I/O | Function
|-----|-----------------------------------------------------------------------------------|
| DIN | Incoming clock signal |
| AIN | CV control for randomness |
| K1 | Length of the pattern |
| K2 | Randomness; anticlockwise will lock the loop, clockwise will introduce randomness |
| B1 | Manually advance the pattern |
| B2 | Generates a new random pattern |
| CV1 | Child output 1 |
| CV2 | Grandchild output 1-1 |
| CV3 | Grandchild output 1-2 |
| CV4 | Child output 2 |
| CV5 | Grandchild output 2-1 |
| CV6 | Grandchild output 2-2 |

## Operation

Every time a clock signal is received on `DIN` the one child & one grandchild output is turned on; all other
outputs are turned off. Depending on the randomness the pattern of gates will be looped, fully random, or
somewhere in between

The pulse width of the outputs is determined by the pulse width of the input clock.
149 changes: 149 additions & 0 deletions software/contrib/dscn2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""
Binary tree based looping gate sequencer
"""

from europi import *
from europi_script import EuroPiScript

from framebuf import FrameBuffer, MONO_HLSB
from random import random as rnd


class Dcsn2(EuroPiScript):

randomness_cv = ain
randomness_knob = k2

length_knob = k1

MAX_LENGTH = 16
MIN_LENGTH = 2

children = [cv1, cv4]
grandchildren = [
[cv2, cv3],
[cv5, cv6]
]

loop_image = FrameBuffer(bytearray(b'\x1cZ\x81\x81\x81\x81Z8'), CHAR_WIDTH, CHAR_HEIGHT, MONO_HLSB)

def __init__(self):
super().__init__()

self.unhandled_clock = False

# initialize random pattern
self.pattern = []
for i in range(self.MAX_LENGTH):
self.pattern.append(self.choose_random())

def on_clock_rise():
self.unhandled_clock = True

def on_clock_fall():
turn_off_all_cvs()

def regenerate_pattern():
for i in range(self.MAX_LENGTH):
self.pattern[i] = self.choose_random()

din.handler(on_clock_rise)
din.handler_falling(on_clock_fall)
b1.handler(on_clock_rise)
b1.handler_falling(on_clock_fall)

b2.handler(regenerate_pattern)

self.set_outputs()

def calculate_randomness(self):
"""Combine AIN & K2 to determine the probability that the pattern loops
"""
# this will be in the range [0, 2]
randomness = self.randomness_cv.percent() + self.randomness_knob.percent()

# restrict to [0, 1]
if randomness >= 1:
randomness = 2.0 - randomness

return randomness

def choose_random(self):
"""
Pick a random gate for the output pattern
0: child 1, grandchild 1-1
1: child 1, grandchild 1-2
2: child 2, grandchild 2-1
3: child 2, grandchild 2-2
"""
return int(rnd() * 4)

def set_outputs(self):
turn_off_all_cvs()
g = self.pattern[0]
self.children[g >> 1].on()
self.grandchildren[g >> 1][g & 1].on()

def draw(self, pattern_length, loop_prob):
oled.fill(0)

active_child = self.pattern[0] >> 1
active_grandchild = self.pattern[0] & 1

# draw the tree with lines & circles
oled.ellipse(OLED_WIDTH//2, 5, 4, 4, 1, True) # root, always filled

# children
oled.ellipse(OLED_WIDTH//4, OLED_HEIGHT//2, 4, 4, 1, active_child == 0)
oled.ellipse(3*OLED_WIDTH//4, OLED_HEIGHT//2, 4, 4, 1, active_child != 0)

# grandchildren
oled.ellipse(6, OLED_HEIGHT-5, 4, 4, 1, active_child == 0 and active_grandchild == 0)
oled.ellipse(OLED_WIDTH//2-6, OLED_HEIGHT-5, 4, 4, 1, active_child == 0 and active_grandchild != 0)
oled.ellipse(OLED_WIDTH//2+6, OLED_HEIGHT-5, 4, 4, 1, active_child != 0 and active_grandchild == 0)
oled.ellipse(OLED_WIDTH-6, OLED_HEIGHT-5, 4, 4, 1, active_child != 0 and active_grandchild != 0)

if active_child == 0:
oled.line(OLED_WIDTH//2, 5, OLED_WIDTH//4, OLED_HEIGHT//2, 1)
if active_grandchild == 0:
oled.line(OLED_WIDTH//4, OLED_HEIGHT//2, 6, OLED_HEIGHT-5, 1)
else:
oled.line(OLED_WIDTH//4, OLED_HEIGHT//2, OLED_WIDTH//2-6, OLED_HEIGHT-5, 1)
else:
oled.line(OLED_WIDTH//2, 5, 3*OLED_WIDTH//4, OLED_HEIGHT//2, 1)
if active_grandchild == 0:
oled.line(3*OLED_WIDTH//4, OLED_HEIGHT//2, OLED_WIDTH//2+6, OLED_HEIGHT-5, 1)
else:
oled.line(3*OLED_WIDTH//4, OLED_HEIGHT//2, OLED_WIDTH-6, OLED_HEIGHT-5, 1)

oled.text(f"{pattern_length}", 0, 0, 1)
s = f"{round(loop_prob * 100)}"
oled.blit(self.loop_image, OLED_WIDTH - CHAR_WIDTH * (len(s)+1) - 1, 0)
oled.text(s, OLED_WIDTH - len(s) * CHAR_WIDTH, 0, 1)
oled.text(f"{self.pattern[0]}", OLED_WIDTH//2-CHAR_WIDTH//2, OLED_HEIGHT//2 - CHAR_HEIGHT//2, 1)
oled.show()

def main(self):
while True:
r = rnd()
loop_prob = 1.0 - self.calculate_randomness() # 0 -> random, 1 -> loop
pattern_length = round(self.length_knob.percent() * (self.MAX_LENGTH - self.MIN_LENGTH) + self.MIN_LENGTH)

if self.unhandled_clock:
self.unhandled_clock = False

# shift the pattern over 1 step, introducing randomness as needed
if r <= loop_prob:
tmp = self.pattern[pattern_length - 1]
else:
tmp = self.choose_random()
for i in range(pattern_length - 1):
self.pattern[pattern_length-1-i] = self.pattern[pattern_length-2-i]
self.pattern[0] = tmp
self.set_outputs()

self.draw(pattern_length, loop_prob)

if __name__ == "__main__":
Dcsn2().main()
1 change: 1 addition & 0 deletions software/contrib/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
["Consequencer", "contrib.consequencer.Consequencer"],
["Conway", "contrib.conway.Conway"],
["CVecorder", "contrib.cvecorder.CVecorder"],
["DCSN-2", "contrib.dscn2.Dcsn2"],
["Diagnostic", "contrib.diagnostic.Diagnostic"],
["EgressusMelodiam", "contrib.egressus_melodiam.EgressusMelodiam"],
["EnvelopeGen", "contrib.envelope_generator.EnvelopeGenerator"],
Expand Down
3 changes: 3 additions & 0 deletions software/firmware/experimental/screensaver.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ def rect(self, x, y, width, height, color=1):
def fill_rect(self, x, y, width, height, color=1):
oled.fill_rect(x, y, width, height, color)

def ellipse(self, x, y, xr, yr, colour=1, fill=False):
oled.ellipse(x, y, xr, yr, colour, fill)

def blit(self, buffer, x, y):
oled.blit(buffer, x, y)

Expand Down
1 change: 1 addition & 0 deletions software/oled_tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The pixels are indexed with (0, 0) at the top left, and (127, 31) at the bottom
|vline|x, y, length, colour|Draws a vertical wide line starting at (x, y) with specified length and colour|
|rect|x, y, width, height, colour|Draws a rectangle starting at (x, y) with specified width, height, and outline colour|
|fill_rect|x, y, width, height, colour|Draws a rectangle starting at (x, y) with specified width, height, and fill colour|
|ellipse|x, y, xr, yr, colour[, fill]|Draws an ellipse with horizontal radius xr and vertical radius yr, at postition (x, y) with the speficied colour and optional fill|
|blit|buffer, x, y|Draws a bitmap based on a buffer, starting at (x, y)
|scroll|x, y|Scrolls the contents of the display by (x, y)
|invert|colour|Inverts the display
Expand Down
3 changes: 3 additions & 0 deletions software/tests/mocks/ssd1306.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ def fill_rect(self, *args):
def rect(self, *args):
pass

def ellipse(self, *args):
pass

def text(self, *args):
pass

Expand Down

0 comments on commit 78294cc

Please sign in to comment.