From cd77189c12fca6b7c1dcd5883e57e148211d1b52 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 19:06:55 +0200 Subject: [PATCH] feat(presets): default pose-instrument mod scheme --- .../matrix_presets/generate_presets.py | 83 ++++++++++++------- .../matrix_presets/test_default_mods.py | 47 +++++++++++ 2 files changed, 101 insertions(+), 29 deletions(-) create mode 100644 sound_algo/data_only/matrix_presets/test_default_mods.py diff --git a/sound_algo/data_only/matrix_presets/generate_presets.py b/sound_algo/data_only/matrix_presets/generate_presets.py index d130f17..6af8f8e 100644 --- a/sound_algo/data_only/matrix_presets/generate_presets.py +++ b/sound_algo/data_only/matrix_presets/generate_presets.py @@ -505,28 +505,55 @@ def emit_step(s): from patterns import patterns_for -# === per-voice mods/poses: demo bindings for a few presets =================== -# Default is [] for every voice. Only a handful of presets carry demo bindings -# so generated presets exercise the feature without over-designing it. -# -# voiceMods: voice -> [(source_id, target, depth)] -# allowed targets: amp/pan/rev for all voices; -# cutoff only for perc/acid/arp/lead/stab/pad/reese/melody/chord -# voicePoses: voice -> [(poseId, action)] -# action is "trigger" or "gate" +# -- Default pose->instrument mapping scheme (fixed, role-adapted) -- +# Eligibility mirrors the SC engine ~matModTargets (cycle 1): a binding is only +# emitted if its target is valid for that voice. +_MOD_TARGETS = {v: {"amp", "pan", "rev"} for v in VOICES} +for v in ("perc", "acid", "arp", "lead", "stab", "pad", "reese", "melody", "chord"): + _MOD_TARGETS[v].add("cutoff") +for v in ("sub", "acid", "arp", "lead", "stab", "pad", "reese", "bells", "melody", "chord"): + _MOD_TARGETS[v].add("res") +for v in ("sub", "acid", "arp", "lead", "stab", "pad", "reese", "bells", "perc", "tom"): + _MOD_TARGETS[v].add("pitch") -VOICE_MODS_DEMO = { - "techno_drive": { - "acid": [("bodyVitesse", "cutoff", 0.6)], # body energy drives acid cutoff - "lead": [("rHandY", "rev", 0.4)], # right hand height -> lead reverb - }, +# Per-role default bindings (source, target, depth). Soloists -> hand sources +# (expressive, close-up); energy/structure -> body sources (robust at distance). +_ROLE_MODS = { + "lead": [("rHandX", "amp", 0.4), ("rHandY", "pitch", 0.4), ("lHandY", "cutoff", 0.5), ("lHandX", "res", 0.4)], + "bells": [("rHandX", "amp", 0.4), ("rHandY", "pitch", 0.4), ("lHandX", "res", 0.4)], + "melody": [("rHandX", "amp", 0.4), ("lHandY", "cutoff", 0.5), ("lHandX", "res", 0.4)], + "reese": [("bodyVitesse", "cutoff", 0.5), ("bodyY", "pitch", 0.3), ("limbSpan", "res", 0.4), ("bodyX", "pan", 0.3)], + "sub": [("bodyVitesse", "res", 0.4), ("bodyY", "pitch", 0.3), ("bodyX", "pan", 0.3)], + "pad": [("bodyVitesse", "cutoff", 0.4), ("limbSpan", "rev", 0.4), ("bodyX", "pan", 0.3)], + "perc": [("bodyVitesse", "cutoff", 0.4), ("bodyY", "pitch", 0.3), ("bodyX", "pan", 0.3)], + "tom": [("bodyY", "pitch", 0.3), ("bodyVitesse", "amp", 0.3), ("bodyX", "pan", 0.3)], + "drums": [("bodyVitesse", "rev", 0.3), ("bodyX", "pan", 0.3)], + "fx": [("danse", "rev", 0.5), ("bodyVitesse", "amp", 0.4)], +} +_VOICE_ROLE = { + "acid": "lead", "arp": "lead", "lead": "lead", "stab": "lead", + "bells": "bells", "melody": "melody", + "reese": "reese", "sub": "sub", + "pad": "pad", "chord": "pad", + "perc": "perc", "tom": "tom", + "kick": "drums", "hats": "drums", "clap": "drums", "ride": "drums", + "rim": "drums", "snare": "drums", "crash": "drums", "shaker": "drums", + "sweep": "fx", "fx": "fx", } -VOICE_POSES_DEMO = { - "techno_drive": { - "kick": [("danse", "trigger")], # danse action retriggers kick - }, -} + +def default_mods(voice): + """Fixed default mod bindings for a voice, filtered by target eligibility.""" + role = _VOICE_ROLE.get(voice) + if role is None: + return [] + elig = _MOD_TARGETS.get(voice, {"amp", "pan", "rev"}) + return [(s, t, d) for (s, t, d) in _ROLE_MODS[role] if t in elig] + + +def default_poses(voice): + """Fixed default pose bindings: danse->gate swells the FX voices.""" + return [("danse", "gate")] if voice in ("fx", "sweep") else [] def emit_sc_mod_binding(source, target, depth): @@ -540,28 +567,26 @@ def emit_sc_pose_binding(pose_id, action): def emit_voicemods(preset_name): - """22-element list of SC sub-arrays, one per voice (default []).""" - demos = VOICE_MODS_DEMO.get(preset_name, {}) + """22-element list of SC sub-arrays: default scheme for active voices, else [].""" parts = [] for voice in VOICES: - bindings = demos.get(voice, []) + active = patterns_for(voice, preset_name) is not None + bindings = default_mods(voice) if active else [] if bindings: - bstr = ", ".join(emit_sc_mod_binding(*b) for b in bindings) - parts.append("[" + bstr + "]") + parts.append("[" + ", ".join(emit_sc_mod_binding(*b) for b in bindings) + "]") else: parts.append("[]") return parts def emit_voiceposes(preset_name): - """22-element list of SC sub-arrays, one per voice (default []).""" - demos = VOICE_POSES_DEMO.get(preset_name, {}) + """22-element list of SC sub-arrays: default poses for active voices, else [].""" parts = [] for voice in VOICES: - bindings = demos.get(voice, []) + active = patterns_for(voice, preset_name) is not None + bindings = default_poses(voice) if active else [] if bindings: - bstr = ", ".join(emit_sc_pose_binding(*b) for b in bindings) - parts.append("[" + bstr + "]") + parts.append("[" + ", ".join(emit_sc_pose_binding(*b) for b in bindings) + "]") else: parts.append("[]") return parts diff --git a/sound_algo/data_only/matrix_presets/test_default_mods.py b/sound_algo/data_only/matrix_presets/test_default_mods.py new file mode 100644 index 0000000..2ebfef8 --- /dev/null +++ b/sound_algo/data_only/matrix_presets/test_default_mods.py @@ -0,0 +1,47 @@ +"""Standalone test for the default pose->instrument mapping scheme. +Run: cd sound_algo/data_only/matrix_presets && uv run python test_default_mods.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 generate_presets import default_mods, default_poses, _MOD_TARGETS, VOICES + +ok = True +def check(cond, msg): + global ok + if not cond: + ok = False + print("FAIL:", msg) + +# lead voices: the 4 hand bindings +for v in ("acid", "arp", "lead", "stab"): + tgts = [t for (_s, t, _d) in default_mods(v)] + check(tgts == ["amp", "pitch", "cutoff", "res"], "%s lead bindings, got %s" % (v, tgts)) + +# bells: no cutoff; melody: no pitch +check([t for (_s, t, _d) in default_mods("bells")] == ["amp", "pitch", "res"], "bells") +check([t for (_s, t, _d) in default_mods("melody")] == ["amp", "cutoff", "res"], "melody") + +# sub: res (not cutoff) + pitch + pan ; reese: cutoff+pitch+res+pan +check([t for (_s, t, _d) in default_mods("sub")] == ["res", "pitch", "pan"], "sub") +check([t for (_s, t, _d) in default_mods("reese")] == ["cutoff", "pitch", "res", "pan"], "reese") + +# perc: cutoff+pitch+pan (no res) ; tom: pitch+amp+pan (no res/cutoff) +check([t for (_s, t, _d) in default_mods("perc")] == ["cutoff", "pitch", "pan"], "perc") +check([t for (_s, t, _d) in default_mods("tom")] == ["pitch", "amp", "pan"], "tom") + +# drums: body rev+pan ; fx: danse rev + body amp +check(default_mods("kick") == [("bodyVitesse", "rev", 0.3), ("bodyX", "pan", 0.3)], "kick") +check([s for (s, _t, _d) in default_mods("fx")] == ["danse", "bodyVitesse"], "fx sources") + +# ELIGIBILITY INVARIANT: every emitted binding's target is eligible for its voice +for v in VOICES: + for (_s, t, _d) in default_mods(v): + check(t in _MOD_TARGETS[v], "ineligible %s->%s on %s" % (_s, t, v)) + +# poses: danse->gate on fx/sweep only +check(default_poses("fx") == [("danse", "gate")], "fx pose") +check(default_poses("sweep") == [("danse", "gate")], "sweep pose") +check(default_poses("kick") == [], "kick no pose") + +print("TEST PASS" if ok else "TEST FAIL") +sys.exit(0 if ok else 1)