docs(presets): default pose mappings design

This commit is contained in:
L'électron rare
2026-06-29 18:54:37 +02:00
parent 0c3d410e98
commit ae146ac48e
@@ -0,0 +1,118 @@
# Default pose→instrument mappings in presets (refactor SP-A)
**Date**: 2026-06-29
**Status**: design approved, pending implementation plan
**Scope**: preset generator only (`sound_algo/data_only/matrix_presets/generate_presets.py`) + its validation. NO engine changes (the Cycle-1 `pitch`/`res` targets + the per-voice mod system are already shipped). Sub-project A of the matrix refactor (cycle 2).
## Context
The matrix engine supports per-voice modulation bindings `~matVoiceMods[voice]` =
list of `(source, target, depth)`, targets `amp/cutoff/pan/rev/res/pitch`, 19
pose sources, applied live. Presets carry these via `voiceMods`/`voicePoses`
keys, but only `techno_drive` has demo bindings (`VOICE_MODS_DEMO`/
`VOICE_POSES_DEMO`, generate_presets.py:518-529); the other 27 presets ship
empty. This sub-project bakes a **fixed, logical, role-adapted default mapping
scheme** into every preset so the body/hands control instruments expressively
out of the box.
### Constraints carried in
- **No runtime switching.** Mappings are FIXED per instrument, identical across
all presets (one global consistent scheme). No close/far mode, no distance
watcher — explicitly out of scope (the engine stays untouched).
- **Mixed sources by role** (`project_concert_mode`: hand tracking dies at
full-body distance). Soloist voices use HAND sources (expressive, close-up);
energy/structure voices use BODY sources (robust at any distance).
- **Cycle-1 target eligibility** (which voices accept which target) must be
respected — emit only valid bindings:
- `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
- **Cycle-1 minors honored**: pitch is bipolar (neutral at source 0.5 → the
resting `rHandY`/`bodyY` give 0 transpose) and applies only on the step path
(active melodic voices have steps → fine); `res` is additive-from-0 (a source
resting low gives low resonance) so res bindings use modest depth and sources
whose rest reads musically (limbSpan low when arms down opens up as they
spread; lHandX similar).
## The fixed mapping scheme
Applied to each voice ACTIVE in a preset (skip muted voices), filtered by
eligibility:
| Role | Voices | Bindings (source → target, depth) |
|------|--------|-----------------------------------|
| Lead (hands) | acid, arp, lead, stab | rHandX→amp 0.4 · rHandY→pitch 0.4 · lHandY→cutoff 0.5 · lHandX→res 0.4 |
| High-mel (hands) | bells | rHandX→amp 0.4 · rHandY→pitch 0.4 · lHandX→res 0.4 |
| High-mel (hands) | melody | rHandX→amp 0.4 · lHandY→cutoff 0.5 · lHandX→res 0.4 |
| Bass (body) | reese | bodyVitesse→cutoff 0.5 · bodyY→pitch 0.3 · limbSpan→res 0.4 · bodyX→pan 0.3 |
| Bass (body) | sub | bodyVitesse→res 0.4 · bodyY→pitch 0.3 · bodyX→pan 0.3 |
| Pad/harm (body) | pad, chord | bodyVitesse→cutoff 0.4 · limbSpan→rev 0.4 · bodyX→pan 0.3 |
| Pitched perc (body) | perc | bodyVitesse→cutoff 0.4 · bodyY→pitch 0.3 · bodyX→pan 0.3 |
| Pitched perc (body) | tom | bodyY→pitch 0.3 · bodyVitesse→amp 0.3 · bodyX→pan 0.3 |
| Drums (body) | kick, hats, clap, ride, rim, snare, crash, shaker | bodyVitesse→rev 0.3 · bodyX→pan 0.3 |
| FX (body) | sweep, fx | danse→rev 0.5 · bodyVitesse→amp 0.4 |
Every binding above is eligibility-valid for its voice (verified against the
Cycle-1 table: e.g. sub uses `res` not `cutoff`; bells omits `cutoff`; melody
omits `pitch`; perc/tom omit `res`).
**Default poses** (`voicePoses`): minimal — `danse → gate` on `fx` and `sweep`
(the dance gesture swells the effects). All other voices: empty. Fine-grained
pose triggers stay a per-performance live choice.
## Architecture (pure content)
- New helper `default_mods(voice)` in `generate_presets.py`: returns the role's
binding list for the voice, each binding a `(source, target, depth)` tuple,
**filtered through an eligibility map** (a Python mirror of the Cycle-1
per-voice target sets) so an invalid binding can never be emitted. A voice
with no role match returns `[]`.
- New helper `default_poses(voice)`: returns `[("danse","gate")]` for `fx`/
`sweep`, else `[]`.
- The existing `emit_voicemods(name)` / `emit_voiceposes(name)` (generate_
presets.py:542-567) source their per-voice lists from these helpers. The
`VOICE_MODS_DEMO`/`VOICE_POSES_DEMO` literals (incl. the techno_drive demo)
are REMOVED — the helpers are the single source of default bindings (YAGNI:
no per-preset override layer for now; the scheme is uniform across presets).
- **Active-voice filter**: emit default mappings only for voices the preset
actually plays (has a non-empty step pattern / is in the kit). If determining
"active" per voice is impractical in `to_sc`, emitting for all 22 is an
acceptable fallback (mappings on silent voices are inert — no notes to
modulate); prefer active-only for tidiness, document whichever is chosen.
- No SC, no web changes. The web target dropdown + SC gating already handle
these bindings (Cycle 1).
## Testing
- **Python** (the generator's own test surface; use `uv`):
- `default_mods("lead")` returns the 4 lead-hand bindings; `default_mods("sub")`
returns res (not cutoff) + pitch + pan; `default_mods("bells")` omits cutoff;
`default_mods("melody")` omits pitch; `default_mods("perc")`/`("tom")` omit
res; `default_mods("kick")` returns only body amp/pan/rev-eligible bindings
(bodyVitesse→rev, bodyX→pan).
- Eligibility filter: every binding `default_mods(v)` returns has a target in
that voice's eligibility set (assert across all 22 voices — no invalid
emission).
- `default_poses("fx")` == `[("danse","gate")]`; `default_poses("kick")` == `[]`.
- **SC validation** (extend the existing `matrix_presets/validate_presets.scd`):
regenerate the 28 presets, load all 28 via `~matLoadFile` (28/28 PASS), and
assert a representative preset has a lead voice with its 4 hand bindings in
`~matVoiceMods` and a drum voice with its body bindings — proving the emitted
defaults parse and survive load.
## Out of scope
- Any engine change (no mode switch, no new sources/targets — Cycle 1 done).
- Preset variety / auto-color arrangement / new presets (sub-projects B/C/D).
- Per-preset bespoke mappings (the scheme is uniform across presets; per-preset
tuning / an override layer can come later if needed).
## Decided defaults
- One uniform fixed scheme across all presets; soloists→hands, structure→body.
- Depths: 0.30.5 (modest, musical). Pitch depth 0.30.4 (≈ ±2-3 scale degrees
at `~matPitchRange` 7).
- `danse→gate` on fx/sweep is the only default pose.
- Emit for active voices (all-22 fallback acceptable, documented).