docs(matrix): pitch+res mod targets design
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
# Matrix pitch + res modulation targets (refactor cycle 1)
|
||||
|
||||
**Date**: 2026-06-29
|
||||
**Status**: design approved, pending implementation plan
|
||||
**Scope**: SuperCollider matrix engine (`sound_algo/data_only/matrix.scd`) + web
|
||||
display layer (`web_realart/public/control/js/matrix-state.js`). Cycle 1 of a
|
||||
two-cycle matrix refactor.
|
||||
|
||||
## Context
|
||||
|
||||
The per-voice additive modulation system (shipped 2026-06-29, commits
|
||||
`6964831..8daad40`) lets each matrix voice bind modulation sources (19 pose-
|
||||
derived scalars, 0..1) to targets via `~matVoiceMods[voice]` — a free list of
|
||||
`(source, target, depth)` applied in `~matModPairs`. Current targets:
|
||||
`amp`, `cutoff`, `pan`, `rev`.
|
||||
|
||||
The user wants logical pose→instrument mappings such as "right-hand y → pitch,
|
||||
left-hand x → resonance". Two of those targets don't exist yet. This cycle adds
|
||||
them. The richer default-mapping work + preset variety is **cycle 2** (separate
|
||||
spec); this cycle is the engine prerequisite only.
|
||||
|
||||
### How frequency is computed today (the constraint that shapes pitch)
|
||||
|
||||
Melodic voices (`sub, acid, arp, lead, stab, pad, reese, bells, perc, tom`)
|
||||
emit a custom `\freq` Pfunc (`matrix.scd:~225`):
|
||||
|
||||
```supercollider
|
||||
[\freq, Pfunc { |e| (~lpNote.notNil).if({ ~lpNote.(e[\degree], 0).midicps * freqRatio }, { 440 }) }]
|
||||
```
|
||||
|
||||
`~lpNote(degree, octave)` maps a scale degree to a note (stays in the current
|
||||
scale); `freqRatio = 2.pow(octave)`. There is no `\degree→\note→\freq` standard
|
||||
chain and no `\ctranspose`/`\detune` key, so the SynthDefs receive `freq`
|
||||
directly. Consequence: **pitch modulation cannot be an independent `[\key,
|
||||
Pfunc]` pair** — it must fold into this freq computation. `res` has no such
|
||||
constraint: it is already an independent `\res` Pbind key, gated by
|
||||
`~matResVoices`, and the SynthDefs accept `res` (0..1).
|
||||
|
||||
## Decisions (from brainstorming)
|
||||
|
||||
- **Two cycles.** This is cycle 1 (engine targets only). Cycle 2 = logical
|
||||
default mappings + preset variety.
|
||||
- **Pitch semantics: scale-quantized, bipolar transpose.** A pitch binding
|
||||
shifts the played note by an integer number of **scale degrees** (stays in
|
||||
key), **centered** on the note: source `0.5` = no shift, `<0.5` = down,
|
||||
`>0.5` = up. Per-note (computed at onset) → no glissando.
|
||||
- **Res semantics: like cutoff/rev**, an independent `\res` Pbind key.
|
||||
|
||||
## Architecture
|
||||
|
||||
### 1. `res` target (independent Pbind key, like cutoff)
|
||||
|
||||
- `~matModPairs` gains a `\res` branch: additive `Σ (src · depth)`, clipped
|
||||
`0..1`, emitted as `[\res, Pfunc {...}]`. (Res is naturally 0..1, so a direct
|
||||
scaled sum is the right analogue — simpler than cutoff's log curve; no neutral
|
||||
table needed.)
|
||||
- A `\res` mod binding overrides the colorDef's static `res` for that voice
|
||||
while present (same precedence as cutoff: the mod pair is spliced after the
|
||||
colorDef `res` pair, so it wins).
|
||||
- `~matModTargets[voice]` gains `\res` **only for `~matResVoices`** (`sub,
|
||||
acid, arp, lead, stab, pad, reese, bells, melody, chord`).
|
||||
|
||||
### 2. `pitch` target (scale-degree offset folded into the freq Pfunc)
|
||||
|
||||
- New helper:
|
||||
```supercollider
|
||||
~matPitchOffset = { |name|
|
||||
var off = 0;
|
||||
((~matVoiceMods[name] ? []).select { |b| b[\target] == \pitch }).do { |b|
|
||||
off = off + ((~matModSourceVal.(b[\source]) * 2 - 1) * b[\depth] * ~matPitchRange).round.asInteger
|
||||
};
|
||||
off
|
||||
};
|
||||
~matPitchRange = ~matPitchRange ? 7; // ± scale degrees at full deflection (env-tunable; 7 ≈ 1 octave)
|
||||
```
|
||||
Bipolar (source 0.5 → 0), additive across pitch bindings, rounded to an
|
||||
integer scale-degree offset.
|
||||
- The melodic freq Pfunc (`matrix.scd:~225`) changes to add the offset to the
|
||||
degree **at note onset**:
|
||||
```supercollider
|
||||
[\freq, Pfunc { |e| (~lpNote.notNil).if(
|
||||
{ ~lpNote.(e[\degree] + ~matPitchOffset.(name), 0).midicps * freqRatio }, { 440 }) }]
|
||||
```
|
||||
Because `~lpNote` maps degree→scale note, an integer offset stays in key.
|
||||
- `pitch` is **NOT** produced by `~matModPairs` (freq is already handled);
|
||||
`~matModPairs` simply never emits it (it is filtered as a non-`amp/cutoff/
|
||||
pan/rev/res` target). The freq Pfunc is the sole consumer, via the helper.
|
||||
- **Apply the offset to every freq Pfunc for pitch-capable voices.** A melodic
|
||||
voice may render via the step-pattern path (`~matColorStepPattern`) when it
|
||||
has steps, or via the overlay path (`~matVariationOverlay`) when it does not.
|
||||
The implementation must add `+ ~matPitchOffset.(name)` to the degree in
|
||||
**each** place a melodic `\freq` Pfunc is built (grep `\freq, Pfunc` in
|
||||
matrix.scd), so pitch modulation works regardless of which path a voice
|
||||
takes. If only the step-pattern path computes freq from degree and the
|
||||
overlay path inherits the base pattern's freq, document that and patch the
|
||||
one true site.
|
||||
- `~matModTargets[voice]` gains `\pitch` **only for the freq-Pfunc voices**
|
||||
(`sub, acid, arp, lead, stab, pad, reese, bells, perc, tom`).
|
||||
|
||||
### 3. Target gating (already enforced — no new gate)
|
||||
|
||||
`~matSetVoiceMod` already drops bindings whose target ∉ `~matModTargets[voice]`.
|
||||
Extending `~matModTargets` is therefore sufficient: `pitch` bindings on a
|
||||
non-melodic voice (e.g. `kick`) and `res` bindings on a non-res voice
|
||||
(e.g. `hats`) are rejected at set time, by both the OSC route and the web UI.
|
||||
|
||||
### 4. Web display layer
|
||||
|
||||
- `matrix-state.js`: `MOD_TARGET_LABELS` gains `pitch: "pitch"`, `res:
|
||||
"résonance"`. `MATRIX_MOD_TARGETS` (the per-voice target lists, mirrors SC)
|
||||
gains `pitch` on the 10 melodic voices and `res` on the 10 res-voices.
|
||||
- No other UI change: the target dropdown is already filtered by
|
||||
`MATRIX_MOD_TARGETS[voice]` and labelled via `MOD_TARGET_LABELS`.
|
||||
|
||||
## Testing
|
||||
|
||||
- **SC** (`test/test_matrix.scd`, existing harness — no server, `pass`/`try`,
|
||||
`TEST PASS`):
|
||||
- `res` additive: two res bindings sum and clip 0..1; `~matModPairs` emits a
|
||||
`\res` pair for a res-voice with the summed value.
|
||||
- `~matPitchOffset`: bipolar (source 0.5 → 0), additive, rounded; e.g.
|
||||
source 1.0, depth 1.0, range 7 → +7; source 0.0 → −7; two bindings sum.
|
||||
- freq integration: with a pitch binding active, `~matVariation` of a melodic
|
||||
voice produces a `\freq` that equals `~lpNote(degree + offset, 0).midicps *
|
||||
freqRatio` (assert the transposed freq differs from the un-modulated one in
|
||||
the expected direction).
|
||||
- gating: `~matSetVoiceMod` drops a `pitch` binding on `\kick` and a `\res`
|
||||
binding on `\hats` (target not in `~matModTargets`).
|
||||
- **Web** (`test/matrix-state.test.mjs`, `node --test test/*.test.mjs`):
|
||||
- `MATRIX_MOD_TARGETS` includes `pitch` for melodic voices and `res` for
|
||||
res-voices, and does NOT include them for ineligible voices (e.g. kick has
|
||||
no pitch, hats has no res).
|
||||
- `MOD_TARGET_LABELS` has entries for `pitch` and `res`.
|
||||
|
||||
## Out of scope (→ cycle 2)
|
||||
|
||||
- Default per-instrument pose→target mappings baked into presets.
|
||||
- Preset variety / richer arrangements.
|
||||
- Any new modulation SOURCE (the 19 are unchanged).
|
||||
- New SynthDef args (pitch uses the existing degree→freq path; res uses the
|
||||
existing `res` arg).
|
||||
|
||||
## Decided defaults
|
||||
|
||||
- `~matPitchRange` = 7 (± scale degrees at full deflection), env-tunable.
|
||||
- Pitch is bipolar (neutral at source 0.5); res is direct (source 0 → res 0).
|
||||
- Pitch-capable voices = the 10 freq-Pfunc voices; res-capable = the 10
|
||||
`~matResVoices`.
|
||||
Reference in New Issue
Block a user