docs(presets): default pose mappings plan

This commit is contained in:
L'électron rare
2026-06-29 19:04:25 +02:00
parent ae146ac48e
commit e9ca68102a
@@ -0,0 +1,276 @@
# Default pose→instrument mappings in presets — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Bake a fixed, logical, role-adapted default pose→instrument modulation scheme into all 28 matrix presets (soloists→hands, structure→body), so the body controls instruments expressively out of the box.
**Architecture:** Pure content in the preset generator. Two helpers — `default_mods(voice)` and `default_poses(voice)` — return a voice's fixed bindings (from a per-role table, filtered by a Cycle-1 eligibility map). `emit_voicemods`/`emit_voiceposes` call them for each ACTIVE voice instead of the removed `*_DEMO` literals. No SC or web changes (the engine already supports these targets).
**Tech Stack:** Python 3 (`uv run python`), SuperCollider validation harness (`validate_presets.scd`).
## Global Constraints
- Python: **uv** only.
- No emojis in code/docs/commits.
- Commits: subject ≤ 50 chars, body ≤ 72 chars/line, no AI attribution, no `--no-verify`, no underscore in commit scope.
- Source/target IDs are SC symbols on the wire; emitted as `(source: \\x, target: \\y, depth: 0.NN)` Event literals (existing `emit_sc_mod_binding`).
- Bindings MUST respect Cycle-1 target eligibility — never emit an ineligible binding:
- `cutoff`: perc, acid, arp, lead, stab, pad, reese, melody, chord
- `res`: sub, acid, arp, lead, stab, pad, reese, bells, melody, chord
- `pitch`: sub, acid, arp, lead, stab, pad, reese, bells, perc, tom
- `amp`, `pan`, `rev`: all voices
- SC validation: `/Applications/SuperCollider.app/Contents/MacOS/sclang <harness>` (binary not on PATH; no `timeout` binary). Push only on user request.
## The fixed scheme (verbatim — soloists→hands, structure→body)
| Voices | Bindings (source, target, depth) |
|--------|----------------------------------|
| acid, arp, lead, stab | (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, chord | (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) |
| kick, hats, clap, ride, rim, snare, crash, shaker | (bodyVitesse,rev,0.3) (bodyX,pan,0.3) |
| sweep, fx | (danse,rev,0.5) (bodyVitesse,amp,0.4) |
Poses: `danse→gate` on `fx` and `sweep` only; all other voices none.
## File structure
| File | Change |
|------|--------|
| `sound_algo/data_only/matrix_presets/generate_presets.py` | add eligibility map + `_ROLE_MODS`/`_VOICE_ROLE` + `default_mods`/`default_poses`; rewire `emit_voicemods`/`emit_voiceposes`; remove `VOICE_MODS_DEMO`/`VOICE_POSES_DEMO` |
| `sound_algo/data_only/matrix_presets/test_default_mods.py` | create — standalone assertion test (no pytest dep) |
| `sound_algo/data_only/matrix_presets/*.matrix` | regenerated (28) |
| `sound_algo/data_only/matrix_presets/validate_presets.scd` | extend — assert defaults loaded |
---
### Task 1: generator helpers + wiring + Python test
**Files:**
- Modify: `sound_algo/data_only/matrix_presets/generate_presets.py` (`VOICE_MODS_DEMO`/`VOICE_POSES_DEMO` ~518-529, `emit_voicemods`/`emit_voiceposes` ~542-567)
- Create: `sound_algo/data_only/matrix_presets/test_default_mods.py`
**Interfaces:**
- Produces: `default_mods(voice) -> list[(source,target,depth)]`, `default_poses(voice) -> list[(poseId,action)]`, module-level `_MOD_TARGETS: dict[str,set[str]]`. `emit_voicemods`/`emit_voiceposes` now emit defaults for ACTIVE voices (`patterns_for(voice, preset) is not None`), `[]` otherwise.
- Consumes: existing `VOICES`, `patterns_for`, `emit_sc_mod_binding`, `emit_sc_pose_binding`.
- [ ] **Step 1: Write the failing test** `sound_algo/data_only/matrix_presets/test_default_mods.py`:
```python
"""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)
```
- [ ] **Step 2: Run to verify it FAILS.**
Run: `cd sound_algo/data_only/matrix_presets && uv run python test_default_mods.py`
Expected: ImportError / `TEST FAIL``default_mods`, `default_poses`, `_MOD_TARGETS` don't exist yet.
- [ ] **Step 3: Add the eligibility map + scheme + helpers.** In `generate_presets.py`, REPLACE the `VOICE_MODS_DEMO` and `VOICE_POSES_DEMO` blocks (~518-529) with:
```python
# -- 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")
# 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",
}
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 []
```
- [ ] **Step 4: Rewire `emit_voicemods` and `emit_voiceposes`** to use the defaults for ACTIVE voices. Replace both functions (~542-567):
```python
def emit_voicemods(preset_name):
"""22-element list of SC sub-arrays: default scheme for active voices, else []."""
parts = []
for voice in VOICES:
active = patterns_for(voice, preset_name) is not None
bindings = default_mods(voice) if active else []
if bindings:
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: default poses for active voices, else []."""
parts = []
for voice in VOICES:
active = patterns_for(voice, preset_name) is not None
bindings = default_poses(voice) if active else []
if bindings:
parts.append("[" + ", ".join(emit_sc_pose_binding(*b) for b in bindings) + "]")
else:
parts.append("[]")
return parts
```
(`patterns_for` is the same active-voice signal `emit_colordefs` uses — `None` ⇒ the voice is silent in this preset ⇒ no mappings.)
- [ ] **Step 5: Run to verify it PASSES.**
Run: `cd sound_algo/data_only/matrix_presets && uv run python test_default_mods.py`
Expected: `TEST PASS` (exit 0).
- [ ] **Step 6: Commit.**
```bash
git add sound_algo/data_only/matrix_presets/generate_presets.py sound_algo/data_only/matrix_presets/test_default_mods.py
git commit -m "feat(presets): default pose-instrument mod scheme"
```
---
### Task 2: regenerate presets + SC load validation
**Files:**
- Modify: `sound_algo/data_only/matrix_presets/*.matrix` (regenerated), `sound_algo/data_only/matrix_presets/validate_presets.scd`
**Interfaces:**
- Consumes: Task 1's generator. Produces: 28 regenerated `.matrix` files carrying the default `voiceMods`/`voicePoses`.
- [ ] **Step 1: Regenerate the 28 presets.**
Run: `cd sound_algo/data_only/matrix_presets && uv run python generate_presets.py`
Expected: 28 `.matrix` files written, no errors.
- [ ] **Step 2: Spot-check the emitted defaults.**
Run: `grep -c "voiceMods" sound_algo/data_only/matrix_presets/*.matrix | grep -c ":1"` (28 files each with the key) and `grep -o "(source: \\\\rHandX, target: \\\\amp[^)]*)" sound_algo/data_only/matrix_presets/techno_drive.matrix | head -1`
Expected: the lead hand binding present in a preset that has a lead/acid voice; voiceMods key in all 28.
- [ ] **Step 3: Extend `validate_presets.scd`** to assert the defaults loaded. After the existing 28/28 load loop, add (matching the harness's style — it already stubs `~toscSend`, loads `../../matrix.scd`, loops presets):
```supercollider
// assert default mappings landed: load techno_drive, check a lead voice has its
// 4 hand bindings and a drum voice has its 2 body bindings.
~matLoad.("techno_drive");
~vpass = (~matVoiceMods[\acid].size == 4)
and: { ~matVoiceMods[\acid].any({ |b| b[\target] == \pitch }) }
and: { ~matVoiceMods[\kick].size == 2 }
and: { ~matVoiceMods[\kick][0][\source] == \bodyVitesse };
~vpass.if({ "DEFAULTS PASS".postln }, { "DEFAULTS FAIL".postln });
```
(Use the actual voice that `techno_drive` activates for a lead — if `acid` is inactive there, pick an active lead/melodic voice; confirm via the generated file. If `acid` has no steps in techno_drive, its voiceMods is `[]` by the active-voice filter — choose a voice the preset plays.)
- [ ] **Step 4: Run the validation.**
Run: `/Applications/SuperCollider.app/Contents/MacOS/sclang sound_algo/data_only/matrix_presets/validate_presets.scd`
Expected: `VALIDATE PASS` (28/28 load) AND `DEFAULTS PASS`. If the lead voice chosen is inactive in techno_drive, the harness will reveal a `[]` — switch to an active voice and re-run.
- [ ] **Step 5: Commit.**
```bash
git add sound_algo/data_only/matrix_presets
git commit -m "feat(presets): regenerate with default mappings"
```
---
## Self-review
**Spec coverage:**
- Fixed role-adapted scheme (soloists→hands, structure→body) → Task 1 `_ROLE_MODS`/`_VOICE_ROLE`. ✓
- Eligibility filter (no invalid binding) → Task 1 `_MOD_TARGETS` + `default_mods` filter + the test's eligibility invariant. ✓
- Active-voice-only emission → Task 1 `emit_*` use `patterns_for(...) is not None`. ✓
- DEMO dicts removed → Task 1 Step 3 replaces them. ✓
- Default poses (danse→gate on fx/sweep) → Task 1 `default_poses`. ✓
- Regenerate + SC load validation → Task 2. ✓
- No engine/web change → confirmed (only generator + its test + regenerated data). ✓
**Placeholder scan:** No TBD/TODO. Task 2 Step 3/4 flags the active-voice contingency (pick a lead voice the preset actually plays) with a concrete remedy rather than leaving it vague.
**Type consistency:** `default_mods` returns `(source,target,depth)` tuples consumed by `emit_sc_mod_binding(*b)` (matches its `(source,target,depth)` signature). `default_poses` returns `(poseId,action)` for `emit_sc_pose_binding(*b)`. `_MOD_TARGETS` sets mirror the Global Constraints eligibility lists exactly. The test's expected target-order matches `_ROLE_MODS` insertion order (lead: amp,pitch,cutoff,res; sub: res,pitch,pan; etc.).