Skip to content

Commit 45846c6

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

File tree

3 files changed

+125
-1
lines changed

3 files changed

+125
-1
lines changed

tools/generate_scales.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.gem_quantizer import GemQuantizerConfig
10+
11+
### Example scales
12+
13+
# Build a scale based on equally divided octaves
14+
# Special case: 12 notes per octave gives the standard Western 12-tone equal tempered scale
15+
def build_edo_scale(notes_per_octave):
16+
# Gemini supports 7 octaves, including the "C" at both ends
17+
num_notes = 7 * notes_per_octave + 1
18+
volts_per_note = 1. / notes_per_octave
19+
20+
scale = []
21+
for note in range(num_notes):
22+
note_voltage = note * volts_per_note
23+
threshold = note_voltage - (volts_per_note / 2.)
24+
scale.append((threshold, note_voltage))
25+
26+
return GemQuantizerConfig(scale=scale)
27+
28+
def main():
29+
scale = build_edo_scale(12)
30+
packed = scale.pack()
31+
print(packed[:19])
32+
33+
if __name__ == "__main__":
34+
main()

tools/libgemini/gem_quantizer.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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
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+
hysteresis: float = 0.005
42+
43+
scale: List[(float, float)] = []
44+
45+
def pack(self):
46+
num_notes = len(self.scale)
47+
assert num_notes <= MAX_NOTES
48+
buffer = bytearray()
49+
buffer.extend(struct.pack(">i", float_to_fix16(self.hysteresis)))
50+
buffer.extend(struct.pack(">B", num_notes))
51+
52+
for (threshold, output) in self.scale:
53+
buffer.extend(struct.pack(">i", float_to_fix16(threshold)))
54+
buffer.extend(struct.pack(">i", float_to_fix16(output)))
55+
56+
return buffer
57+
58+
@classmethod
59+
def unpack(buffer):
60+
hysteresis = fix16_to_float(unpack_one(">i", buffer[0:4]))
61+
num_notes = unpack_one(">B", buffer[4])
62+
63+
buffer = buffer[5:]
64+
scale = []
65+
for i in range(num_notes):
66+
threshold = fix16_to_float(unpack_one(">i", buffer[0:4]))
67+
output = fix16_to_float(unpack_one(">i", buffer[4:8]))
68+
scale.append((threshold, output))
69+
buffer = buffer[8:]
70+
71+
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)