Skip to content

Commit

Permalink
Add Itty Bitty script (#398)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisib authored Jan 9, 2025
1 parent 8924ba2 commit 8fcafa9
Show file tree
Hide file tree
Showing 4 changed files with 355 additions and 0 deletions.
7 changes: 7 additions & 0 deletions software/contrib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ The division of the master clock that each LFO runs at, as well as each of their
<i>Author: [roryjamesallen](https://github.com/roryjamesallen)</i>
<br><i>Labels: LFO</i>

### Itty Bitty \[ [documentation](/software/contrib/itty_bitty.md) | [script](/software/contrib/itty_bitty.py) \]

Dual-channel 8-bit trigger+gate+cv sequencer based on the binary representation of an 8-bit number.

<i>Author: [chrisib](https://github.com/chrisib)</i>
<br><i>Labels: sequencer, gate, trigger, cv</i>

### Kompari \[ [documentation](/software/contrib/kompari.md) | [script](/software/contrib/kompari.py) \]

Compares `AIN` to `K1` and `K2`, outputting 5V digital signals on `CV1`-`CV5`, and an analogue output signal based on
Expand Down
142 changes: 142 additions & 0 deletions software/contrib/itty_bitty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Itty Bitty

A gate & CV sequencer that uses the binary representation of an 8-bit integer to determine the of/off pattern.

The module has 2 channels, A and B. Channel A is controlled by `K1` and outputs on `CV1-3`. Channel B is controlled
by `K2` and outputs on `CV4-6`.

Inspired by an [Instagram post by Schreibmaschine](https://www.instagram.com/p/DDaZklkgzbr) describing an idea for a
new module.

## Ins & Outs

- `DIN`: an input clock/gate/trigger signal used to advance the sequences
- `AIN`: optional CV control for sequence A and/or B (see configuration, below)
- `K1`: determines the value of sequence A: 0-255
- `K2`: determines the value of sequence B: 0-255
- `B1`: manually advance sequence A
- `B2`: manually advance sequence B
- `CV1`: trigger output of sequence A
- `CV2`: gate output of sequence A
- `CV3`: CV output of sequence A
- `CV4`: trigger output of sequence B
- `CV5`: gate output of sequence B
- `CV6`: CV output of sequence B

## How the sequence works

The numbers 0-255 can be represented in binary in 8 bits:
- `0`: `00000000`
- `1`: `00000001`
- `2`: `00000010`
- `3`: `00000011`
- ...
- `253`: `11111101`
- `254`: `11111110`
- `255`: `11111111`

Let `0 <= n <= 255` be the value the user selects with the knob. Every time we receive a clock signal we rotate
the bits 1 place to the left, giving us `n'`:
```
...
00000001
00000010
00000100
00001000
00010000
00100000
01000000
10000000
00000001
...
```

The "current bit` is the most-significant bit.

The trigger output will emit a trigger signal if the current 1s bit is 1, and no trigger if the current bit is 0. The
duration of the trigger is the same as the incoming clock signal (or the duration of the button press).

The gate output will go high if the current bit is 1, and will go low if the current bit is 0.

The CV output set to `MAX_OUTPUT_VOLTAGE * reverse(n') / 255`. The bits are reversed so as to prevent a situation
where the CV is always high when the active bit is also high; this forcibly de-couples the gate & CV outputs,
which can lead to more interesting interactions between them.

### Example sequence

Let's assume sequence 83 is selected. `83 = 01010011`

| Step | Gate (High/Low) | Trigger (Y/N) | CV Out (10V max) |
|------|-----------------|---------------|------------------|
| 1 | Low | N | 7.921V |
| 2 | High | Y | 3.961V |
| 3 | Low | N | 6.980V |
| 4 | High | Y | 3.490V |
| 5 | Low | N | 6.745V |
| 6 | Low | N | 3.723V |
| 7 | High | Y | 1.686V |
| 8 | High | Y | 5.843V |

Time graph
```
Clock In
____ ____ ____ ____ ____ ____ ____ ____
| | | | | | | | | | | | | | |
|____| |____| |____| |____| |____| |____| |____| |____
. . . . . . .
Gate Out . . . . . . .
._________. ._________. . .____________________
| | | | . | .
_________| |_________| |___________________| .
. . . . . . .
Trigger Out . . . . . .
._ . ._ . . ._ ._
| | . | | . . | | | |
_________| |_________________| |___________________________| |_______| |________
. . . . . . .
CV Out (approx) . . . . . .
10V--- . . . . . . .
. . . . . . .
_________. . . . . . .
| ._________. ._________. . .
| | | | | . .__________
5V---- | | | | | . |
|_________| |_________| |_________. |
. . , . . | |
. . . . . |_________|
. . . . . . .
0V---- . . . . . . .
```

## Configuration

This program has the following configuration options:

- `USE_AIN_A`: if `true`, channel A's value is determined by `AIN` and `k1` will act as an attenuator for the
CV signal connected to `AIN`
- `USE_AIN_B`: if `true`, channel B's value is determined by `AIN` and `k2` will act as an attenuator for the
CV signal connected to `AIN`
- `USE_GRAY_ENCODING`: if `true`, instead of traditional binary encoding, the pattern is encoded using
[gray encoding](https://en.wikipedia.org/wiki/Gray_encoding). This means that consecutive sequences will
always differ by exactly 1 bit.

| Decimal value | Traditional binary | Gray encoding |
|---------------|--------------------|---------------|
| 0 | `00000000` | `000000000` |
| 1 | `00000001` | `000000001` |
| 2 | `00000010` | `000000011` |
| 3 | `00000011` | `000000010` |
| 4 | `00000100` | `000000110` |
| 5 | `00000101` | `000000111` |
| 6 | `00000110` | `000000101` |
| 7 | `00000111` | `000000100` |
| ... | ... | ... |

To enable Gray encoding, create/edit `/config/IttyBitty.json` to contain the following:
```json
{
"USE_GRAY_ENCODING": true
}

```
205 changes: 205 additions & 0 deletions software/contrib/itty_bitty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
"""
Two 8-step trigger, gate & CV sequencers based on the binary representation of an 8-bit number
"""

from europi import *
from europi_script import EuroPiScript

import configuration
import time


class BittySequence:
"""
A container class for one sequencer
"""
def __init__(self, button_in, trigger_out, gate_out, cv_out, use_gray_encoding=False):
"""
Create a new sequencer
@param button_in The button the user can press to manually advance the sequence
@param trigger_out The CV output for the trigger signal
@param gate_out The CV output for the gate signal
@param cv_out The CV output for the CV signal
@param use_gray_encoding If true, we use gray encoding instead of traditional binary
"""
button_in.handler(self.advance)
button_in.handler_falling(self.trigger_off)

self.trigger_out = trigger_out
self.gate_out = gate_out
self.cv_out = cv_out

self.use_gray_encoding = use_gray_encoding

# the integer representation of our sequence
# if we're not using gray encoding this should be the same as our binary sequence
self.sequence_n = 0

# the raw binary pattern that represents our sequence
self.binary_sequence = 0x00

# the current step
self.step = 0

# is the current output state in need of refreshing?
self.output_dirty = False

# turn everything off initially
self.trigger_out.off()
self.gate_out.off()
self.cv_out.off()

def advance(self):
self.step = (self.step + 1) & 0x07 # restrict this to 0-7
self.output_dirty = True

def trigger_off(self):
self.trigger_out.off()

def apply_output(self):
now = time.ticks_ms()

if self.current_bit:
self.trigger_out.on()
self.gate_out.on()
else:
self.trigger_out.off()
self.gate_out.off()

self.cv_out.voltage(europi_config.MAX_OUTPUT_VOLTAGE * self.cv_sequence / 255)

self.output_dirty = False

def change_sequence(self, n):
self.sequence_n = n

if self.use_gray_encoding:
# convert the number from traditional binary to its gray encoding equivalent
n = (n & 0xff) ^ ((n & 0xff) >> 1)
else:
n = n & 0xff

self.binary_sequence = n

@property
def shifted_sequence(self):
return ((self.binary_sequence << self.step) & 0xff) | ((self.binary_sequence & 0xff) >> (8 - self.step))

@property
def cv_sequence(self):
# reverse the bits of the shifted sequence
s = self.shifted_sequence
cv = 0x00
while s:
cv = cv << 1
cv = cv | (s & 0x01)
s = s >> 1
return cv & 0xff

@property
def current_bit(self):
return (self.shifted_sequence >> 7) & 0x01


class IttyBitty(EuroPiScript):
def __init__(self):
super().__init__()

self.sequencers = [
BittySequence(b1, cv1, cv2, cv3, use_gray_encoding=self.config.USE_GRAY_ENCODING),
BittySequence(b2, cv4, cv5, cv6, use_gray_encoding=self.config.USE_GRAY_ENCODING),
]

@din.handler
def on_clock_rise():
for s in self.sequencers:
s.advance()

@din.handler_falling
def on_clock_fall():
for s in self.sequencers:
s.trigger_off()

@classmethod
def config_points(cls):
return [
# If true, use gray encoding instead of standard binary
# Gray encding flips a single bit at each step, meaning any two adjacent
# sequences differ by only 1 bit
configuration.boolean(
"USE_GRAY_ENCODING",
False
),

# Flags to enable AIN to control channel A and/or channel B
# when enabled, the knob acts as an attenuator instead of a selector
configuration.boolean(
"USE_AIN_A",
False
),
configuration.boolean(
"USE_AIN_B",
False
),
]

def main(self):
TEXT_TOP = CHAR_HEIGHT
BITS_LEFT = CHAR_WIDTH * 6

N_STEPS = 256
N_SAMPLES = 200

while True:
cv = ain.percent(samples=N_SAMPLES)

if self.config.USE_AIN_A:
atten = k1.percent(samples=N_SAMPLES)
n1 = round(cv * atten * N_STEPS)
if n1 == N_STEPS:
# prevent bounds problems since percent() returns [0, 1], not [0, 1)
n1 = N_STEPS - 1
else:
n1 = k1.read_position(steps=N_STEPS, samples=N_SAMPLES)

if self.config.USE_AIN_B:
atten = k2.percent(samples=N_SAMPLES)
n2 = round(cv * atten * N_STEPS)
if n2 == N_STEPS:
n2 = N_STEPS - 1
else:
n2 = k2.read_position(steps=N_STEPS, samples=N_SAMPLES)

self.sequencers[0].change_sequence(n1)
self.sequencers[1].change_sequence(n2)

oled.fill(0)
for i in range(len(self.sequencers)):
s = self.sequencers[i]

# Set the output voltages if needed
if s.output_dirty:
s.apply_output()

# Show the sequence number, sequence, and draw a box around the active bit
oled.text(f"{s.sequence_n:5} {s.binary_sequence:08b}", 0, CHAR_HEIGHT*i + TEXT_TOP, 1)
oled.fill_rect(
BITS_LEFT + s.step * CHAR_WIDTH,
CHAR_HEIGHT*i + TEXT_TOP,
CHAR_WIDTH,
CHAR_HEIGHT,
1
)
oled.text(
f"{s.current_bit}",
BITS_LEFT + s.step * CHAR_WIDTH,
CHAR_HEIGHT*i + TEXT_TOP,
0
)

oled.show()


if __name__ == "__main__":
IttyBitty().main()
1 change: 1 addition & 0 deletions software/contrib/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
["Hamlet", "contrib.hamlet.Hamlet"],
["HarmonicLFOs", "contrib.harmonic_lfos.HarmonicLFOs"],
["HelloWorld", "contrib.hello_world.HelloWorld"],
["Itty Bitty", "contrib.itty_bitty.IttyBitty"],
["KnobPlayground", "contrib.knob_playground.KnobPlayground"],
["Kompari", "contrib.kompari.Kompari"],
["Logic", "contrib.logic.Logic"],
Expand Down

0 comments on commit 8fcafa9

Please sign in to comment.