Skip to content

Commit 1b49628

Browse files
committed
WIP: Add script to generate quantizer tables, currently 12EDO only
1 parent 4b8a1a6 commit 1b49628

File tree

3 files changed

+141
-1
lines changed

3 files changed

+141
-1
lines changed

tools/generate_scales.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright (c) 2021 Alethea Katherine Flowers.
4+
# Published under the standard MIT License.
5+
# Full text available at: https://opensource.org/licenses/MIT
6+
7+
import math
8+
9+
from libgemini.gemini import Gemini
10+
from libgemini.gem_quantizer import GemQuantizerConfig
11+
12+
### Example scales
13+
14+
# Build a scale based on equally divided octaves
15+
# Special case: 12 notes per octave gives the standard Western 12-tone equal tempered scale
16+
def build_edo_scale(notes_per_octave):
17+
# Gemini supports 7 octaves, including the "C" at both ends
18+
num_notes = 7 * notes_per_octave + 1
19+
volts_per_note = 1. / notes_per_octave
20+
21+
scale = []
22+
for note in range(num_notes):
23+
note_voltage = note * volts_per_note
24+
threshold = note_voltage - (volts_per_note / 2.)
25+
scale.append((threshold, note_voltage))
26+
27+
return GemQuantizerConfig(scale=scale)
28+
29+
def read_scale(gemini):
30+
print("Reading scale")
31+
quantizer = gemini.read_quantizer()
32+
print("Hysteresis:", quantizer.hysteresis)
33+
print("Num notes:", len(quantizer.scale))
34+
print("First note:", repr(quantizer.scale[0]))
35+
print("Last note:", repr(quantizer.scale[-1]))
36+
37+
def write_scale(gemini, scale):
38+
print("Writing scale")
39+
gemini.save_quantizer(scale)
40+
41+
def main():
42+
gemini = Gemini()
43+
if 0:
44+
scale = build_edo_scale(12)
45+
write_scale(gemini, scale)
46+
read_scale(gemini)
47+
else:
48+
read_scale(gemini)
49+
50+
if __name__ == "__main__":
51+
main()

tools/libgemini/gem_quantizer.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# TODO: Auto-generate this with `structy`
2+
# For now it is hand-written
3+
4+
from dataclasses import dataclass
5+
from typing import ClassVar, List, Tuple
6+
7+
import structy
8+
import struct
9+
10+
MAX_NOTES = 255 # Needs to match MAX_QUANTIZER_TABLE_SIZE in firmware/src/gem_quantizer.h
11+
12+
def unpack_one(fmt, value):
13+
return struct.unpack(fmt, value)[0]
14+
15+
FIX16_MIN = -(1 << 31)
16+
FIX16_MAX = (1 << 31) - 1
17+
18+
def clamp(value, lo, hi):
19+
if value < lo:
20+
return lo
21+
elif value > hi:
22+
return hi
23+
else:
24+
return value
25+
26+
def clamp_fix16(value):
27+
return clamp(value, FIX16_MIN, FIX16_MAX)
28+
29+
# Convert from a Python float to a Gemini-compatible `fix16` integer
30+
# This is a 32-bit format with 16 fractional bits
31+
def float_to_fix16(value):
32+
value = value * 0x00010000
33+
value += 0.5 if value >= 0 else -0.5
34+
return clamp_fix16(int(value))
35+
36+
def fix16_to_float(value):
37+
return float(value) / float(0x00010000)
38+
39+
@dataclass
40+
class GemQuantizerConfig:
41+
scale: List[Tuple[float, float]]
42+
hysteresis: float = 0.005
43+
44+
def pack(self):
45+
num_notes = len(self.scale)
46+
assert num_notes <= MAX_NOTES
47+
buffer = bytearray()
48+
buffer.extend(struct.pack(">i", float_to_fix16(self.hysteresis)))
49+
buffer.extend(struct.pack(">B", num_notes))
50+
51+
for (threshold, output) in self.scale:
52+
buffer.extend(struct.pack(">i", float_to_fix16(threshold)))
53+
buffer.extend(struct.pack(">i", float_to_fix16(output)))
54+
55+
return buffer
56+
57+
@classmethod
58+
def unpack(buffer):
59+
hysteresis = fix16_to_float(unpack_one(">i", buffer[0:4]))
60+
num_notes = unpack_one(">B", buffer[4])
61+
62+
buffer = buffer[5:]
63+
scale = []
64+
for i in range(num_notes):
65+
threshold = fix16_to_float(unpack_one(">i", buffer[0:4]))
66+
output = fix16_to_float(unpack_one(">i", buffer[4:8]))
67+
scale.append((threshold, output))
68+
buffer = buffer[8:]
69+
70+
return scale

tools/libgemini/gemini.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from wintertools import midi, teeth
1212

13-
from libgemini import gem_settings
13+
from libgemini import gem_settings, gem_quantizer
1414

1515

1616
class SysExCommands(enum.IntEnum):
@@ -25,6 +25,9 @@ class SysExCommands(enum.IntEnum):
2525
RESET_SETTINGS = 0x07
2626
READ_SETTINGS = 0x18
2727
WRITE_SETTINGS = 0x19
28+
RESET_QUANTIZER = 0x08
29+
READ_QUANTIZER = 0x1A
30+
WRITE_QUANTIZER = 0x1B
2831
#WRITE_LUT_ENTRY = 0x0A
2932
#WRITE_LUT = 0x0B
3033
#ERASE_LUT = 0x0C
@@ -72,6 +75,22 @@ def save_settings(self, settings):
7275
SysExCommands.WRITE_SETTINGS, settings_buf, encode=True, response=True
7376
)
7477

78+
def reset_quantizer(self):
79+
self.sysex(SysExCommands.RESET_QUANTIZER)
80+
81+
def read_quantizer(self):
82+
quantizer_buf = self.sysex(
83+
SysExCommands.READ_QUANTIZER, response=True, decode=True
84+
)
85+
quantizer = gem_quantizer.GemQuantizerConfig.unpack(quantizer_buf)
86+
return quantizer
87+
88+
def save_quantizer(self, quantizer):
89+
quantizer_buf = quantizer.pack()
90+
self.sysex(
91+
SysExCommands.WRITE_QUANTIZER, quantizer_buf, encode=True, response=True
92+
)
93+
7594
def soft_reset(self):
7695
self.sysex(SysExCommands.SOFT_RESET)
7796

0 commit comments

Comments
 (0)