Skip to content

Commit a4925f1

Browse files
Add software chromatic quantization (12-TET) for whole-tone layouts
Allows semitones to be hit between whole-tone pads via software quantization. New settings: - chromatic_quantize: Enable 12-TET semitone snapping (default: false) - whole_tone_bias: Adjust zone balance, range -1.0 to 1.0 (default: 0.0) - 0.15 recommended for LinnStrument speed bump surface Requirements: - LinnStrument Quantize=OFF, Quantize Tap=OFF, Quantize Hold=OFF - This setting overrides hardware quantization with proper semitone support
1 parent af1a519 commit a4925f1

4 files changed

Lines changed: 65 additions & 0 deletions

File tree

settings.ini.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
[general]
55
size=128
6+
7+
; Software chromatic quantization (12-TET) - see settings.py for full documentation
8+
; REQUIRED: LinnStrument Quantize=OFF, Quantize Tap=OFF, Quantize Hold=OFF
9+
;chromatic_quantize=true
10+
;whole_tone_bias=0.4375
11+
612
;velocity_curve=1.0
713
;lights=1,9,9,2,2,3,3,5,8,8,11,11
814
;split_lights=4,7,5,7,5,5,7,5,7,5,7,5

src/core.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,37 @@ def cb_midi_in(self, data, timestamp, force_channel=None):
964964
# NOTE: Synth must have MPE enabled for per-note slides
965965
bend_val = decompose_pitch_bend((data[1], data[2]))
966966
bend_val *= self.options.bend_scale
967+
968+
# Software chromatic quantization (12-TET)
969+
# Snaps pitch bends to nearest semitone for whole-tone layouts
970+
# Requires: LinnStrument Quantize=OFF, Quantize Tap=OFF, Quantize Hold=OFF
971+
if self.options.chromatic_quantize:
972+
# Semitone step size in normalized bend units
973+
# column_offset accounts for whole-tone layout (2 semitones per pad)
974+
semitone = 1.0 / (self.options.bend_range * self.options.column_offset)
975+
semitones = bend_val / semitone
976+
977+
bias = self.options.whole_tone_bias
978+
979+
if bias == 0:
980+
nearest = round(semitones)
981+
else:
982+
# Biased rounding to compensate for mechanical/surface differences
983+
# Positive bias = expand whole-tone zones, negative = expand semitone zones
984+
floor_semi = int(semitones) if semitones >= 0 else int(semitones) - 1
985+
frac = semitones - floor_semi
986+
987+
# Even floor = whole tone (pad center), Odd = semitone (between pads)
988+
if floor_semi % 2 == 0:
989+
threshold = 0.5 + bias # Positive bias raises threshold to semitone
990+
else:
991+
threshold = 0.5 - bias # Positive bias lowers threshold to leave semitone
992+
993+
threshold = max(0.05, min(0.95, threshold))
994+
nearest = floor_semi + 1 if frac >= threshold else floor_semi
995+
996+
bend_val = nearest * semitone
997+
967998
data[1], data[2] = compose_pitch_bend(bend_val)
968999

9691000
if self.is_split():
@@ -1401,6 +1432,14 @@ def __init__(self):
14011432
opts, "bend_scale", DEFAULT_OPTIONS.bend_scale
14021433
)
14031434

1435+
# Software chromatic quantization (12-TET)
1436+
self.options.chromatic_quantize = get_option(
1437+
opts, "chromatic_quantize", DEFAULT_OPTIONS.chromatic_quantize
1438+
)
1439+
self.options.whole_tone_bias = get_option(
1440+
opts, "whole_tone_bias", DEFAULT_OPTIONS.whole_tone_bias
1441+
)
1442+
14041443
# self.options.mpe = get_option(
14051444
# opts, "mpe", DEFAULT_OPTIONS.mpe
14061445
# ) or get_option(

src/settings.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,22 @@ class Settings:
109109
# octave splitting the linn and transposing octaves on the right side
110110
octave_split: int = 0
111111

112+
# Software chromatic quantization (12-TET)
113+
# Snaps pitch bends to nearest semitone, allowing semitones between whole-tone pads.
114+
# Without quantization, every note has slight microtonal errors from human imprecision.
115+
# At this pad scale, you can only realistically aim for semitones or whole tones.
116+
# Hardware quantization only snaps to pad centers (whole tones), missing semitones.
117+
# This software quantization snaps to ALL 12 chromatic semitones.
118+
# REQUIRED: Set LinnStrument Quantize=OFF, Quantize Tap=OFF, Quantize Hold=OFF
119+
chromatic_quantize: bool = False
120+
121+
# Whole tone bias: adjusts the rounding threshold between semitones and whole tones.
122+
# Range: -1.0 to 1.0
123+
# 0.0 = equal zones (50/50 split, standard rounding at 0.5 threshold)
124+
# Positive = larger whole-tone zones (need to aim closer to center to hit semitones)
125+
# Negative = larger semitone zones (easier to hit semitones accidentally)
126+
# Recommended: 0.4375 for LinnStrument speed bump surface
127+
whole_tone_bias: float = 0.0
128+
112129
DEFAULT_OPTIONS = Settings()
113130

src/util.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,6 @@ def get_color(col):
129129
if col.startswith("#"):
130130
return webcolors.hex_to_rgb(col)
131131
return webcolors.name_to_rgb(col)
132+
133+
134+

0 commit comments

Comments
 (0)