feat(presets): default pose-instrument mod scheme

This commit is contained in:
L'électron rare
2026-06-29 19:06:55 +02:00
parent e9ca68102a
commit cd77189c12
2 changed files with 101 additions and 29 deletions
@@ -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
@@ -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)