-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
355 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters