From 39ac5d0ad17dc29d8ea6fe4cacc2787301e902a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:17:47 +0200 Subject: [PATCH] feat(presets): 6-colour contrast derivation fns --- .../data_only/matrix_presets/patterns.py | 48 +++++++++++++++++++ .../data_only/matrix_presets/test_contrast.py | 44 +++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 sound_algo/data_only/matrix_presets/test_contrast.py diff --git a/sound_algo/data_only/matrix_presets/patterns.py b/sound_algo/data_only/matrix_presets/patterns.py index 52af0dc..8486ae0 100644 --- a/sound_algo/data_only/matrix_presets/patterns.py +++ b/sound_algo/data_only/matrix_presets/patterns.py @@ -39,6 +39,54 @@ def _mel(pairs): return [None if p is None else (int(p[0]), round(float(p[1]), 2)) for p in pairs] +_DRUM_VELS = (0.30, 0.50, 0.80, 1.0) + + +def _dhit(v): + return None if v == 0.0 else (0, round(v, 2)) + + +def derive_drum_colors(base): + """Colours 2-6 from a 16-step drum base (None | (0, vel)).""" + n = len(base) + busier = [base[i] if base[i] is not None else (_dhit(0.30) if i % 4 == 2 else None) + for i in range(n)] + broken = base[-1:] + base[:-1] # rotate right 1: same count, displaced + double = [base[i] if base[i] is not None else (_dhit(0.80) if i % 2 == 0 else None) + for i in range(n)] + sparse = [base[i] if (base[i] is not None and ((base[i][1] >= 1.0) or i % 4 == 0)) else None + for i in range(n)] + fill = list(base) + for k, v in enumerate((0.80, 0.80, 1.0, 1.0)): + fill[n - 4 + k] = _dhit(v) + return [busier, broken, double, sparse, fill] + + +def derive_mel_colors(base): + """Colours 2-6 from a 16-step melodic base (None | (degree, vel)).""" + n = len(base) + actives = [base[i][0] for i in range(n) if base[i] is not None] + last_deg = actives[-1] if actives else 0 + + def prev_deg(i): + for j in range(i, -1, -1): + if base[j] is not None: + return base[j][0] + return last_deg + + busier = [base[i] if base[i] is not None else ((prev_deg(i), 0.5) if i % 4 == 2 else None) + for i in range(n)] + broken = [((base[i][0] + 7, base[i][1]) if (base[i] is not None and i % 4 == 0) else base[i]) + for i in range(n)] + double = [base[i] if base[i] is not None else ((prev_deg(i), 0.6) if i % 2 == 0 else None) + for i in range(n)] + sparse = [base[i] if (base[i] is not None and i % 4 == 0) else None for i in range(n)] + fill = list(base) + for k in range(4): + fill[n - 4 + k] = (last_deg + k, round(0.6 + 0.1 * k, 2)) + return [busier, broken, double, sparse, fill] + + N = None # ============================================================================= diff --git a/sound_algo/data_only/matrix_presets/test_contrast.py b/sound_algo/data_only/matrix_presets/test_contrast.py new file mode 100644 index 0000000..ec89676 --- /dev/null +++ b/sound_algo/data_only/matrix_presets/test_contrast.py @@ -0,0 +1,44 @@ +"""Standalone test for the 6-colour contrast derivation. +Run: cd sound_algo/data_only/matrix_presets && uv run python test_contrast.py +Prints TEST PASS / TEST FAIL; exits non-zero on failure.""" +import sys, os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from patterns import derive_drum_colors, derive_mel_colors, _rhythm, _mel + +ok = True +def check(c, m): + global ok + if not c: ok = False; print("FAIL:", m) + +def dens(p): return sum(1 for s in p if s is not None) + +# --- drums --- +base = _rhythm("X...x...X...x...") # 4 hits +c2, c3, c4, c5, c6 = derive_drum_colors(base) +check(all(len(c) == 16 for c in (c2,c3,c4,c5,c6)), "drum colours 16 steps") +check(dens(c2) >= dens(base), "busier >= base") +check(dens(c4) >= dens(base), "double >= base") +check(dens(c5) <= dens(base), "sparse <= base") +check(dens(c3) == dens(base), "broken keeps hit count") +check([i for i in range(16) if (c3[i] is not None) != (base[i] is not None)] != [], "broken displaces") +check(all(c6[i] is not None for i in range(12, 16)), "fill last 4 active") +# velocities legal +legal = {0.30, 0.50, 0.80, 1.0} +for c in (c2, c3, c4, c5, c6): + check(all(s is None or s[1] in legal for s in c), "drum vel legal") +# determinism +check(derive_drum_colors(base) == derive_drum_colors(base), "drum deterministic") + +# --- mel --- +mbase = _mel([(0,0.8),None,(3,0.6),None,(7,0.7),None,None,None, + (5,0.6),None,(3,0.6),None,(0,0.8),None,None,None]) +m2, m3, m4, m5, m6 = derive_mel_colors(mbase) +check(all(len(c) == 16 for c in (m2,m3,m4,m5,m6)), "mel colours 16 steps") +check(dens(m4) >= dens(mbase), "mel double >= base") +check(dens(m5) <= dens(mbase), "mel sparse <= base") +check(all((s is None) or (isinstance(s[0], int) and isinstance(s[1], float)) for s in m4), "mel step shape") +check(all(m6[i] is not None for i in range(12, 16)), "mel fill last 4 active") +check(derive_mel_colors(mbase) == derive_mel_colors(mbase), "mel deterministic") + +print("TEST PASS" if ok else "TEST FAIL") +sys.exit(0 if ok else 1)