docs(phase-s): fold velocity->amp transfer-curve editor into S-VIEW (r10)
Per-PerformanceZone state home, new pure velocity_curve module, application at Voice::start(), Sample-view editor UI. New PLAN points S-VIEW-9 (foundation, blocked by Wave 1 T-KEYTRK) + S-VIEW-10 (Wave 2 shell). Fork R10-F1 (flat-y=1 default is a non-back-compat behavior change) left open for Daniel.
This commit is contained in:
@@ -1100,6 +1100,123 @@ v6, both with back-compat defaults on read) are the same load-bearing core.
|
||||
|
||||
---
|
||||
|
||||
### Addendum — velocity → amp transfer-curve editor (Daniel, 2026-07-27, r10)
|
||||
|
||||
**Framing.** Folds one more control into the S-VIEW redesign: a **visual velocity → amp
|
||||
transfer-curve editor**. Today the engine maps velocity to gain *linearly* (`velocityGain_ =
|
||||
velocity / 127.0`, `sampler_core.cpp:261`), applied once at note-on in `Voice::start()`. Daniel
|
||||
wants that mapping to become an **editable transfer curve** — a bezier from a default flat line to
|
||||
an arbitrary multi-point curve — so velocity dynamics are fully shapeable per sound.
|
||||
|
||||
**Directive, verbatim (Daniel, 2026-07-27):** *"By default any velocity plays at full level. Add
|
||||
a visual transfer-curve editor: an editable curve from the default flat y=1 to any bezier curve
|
||||
with arbitrary points, where x = velocity (0–127) and y = amp scalar (0–1). Enables very dynamic
|
||||
control over velocity → level."*
|
||||
|
||||
**The reference grammar.** A 2-D transfer-curve editor is the standard sampler/synth velocity-
|
||||
response surface — Kontakt's velocity/mod curve editor, Serum's env/curve nodes, Ableton's
|
||||
velocity device, any DAW's automation-curve editor. X is the input (velocity, 0–127), Y is the
|
||||
output multiplier (amp scalar, 0–1), and draggable control points bend the mapping. It is the
|
||||
same direct-manipulation, one-source-multiple-views instinct as the r9 envelope overlay — and it
|
||||
**reuses the r9 `envelope_edit` house pattern almost verbatim** (a pure geometry/clamp module +
|
||||
a LICE shell that draws handles and routes the mouse).
|
||||
|
||||
**The four calls (r10).**
|
||||
|
||||
1. **State home → per-zone (`PerformanceZone`), sibling of the amp envelope + keyTrack.** Velocity
|
||||
response is a *per-sound performance characteristic*, exactly like the AHDSR envelope, the pitch
|
||||
engine, and the r9 key-tracking scalar — all of which already live on `PerformanceZone` (D-B,
|
||||
instrument-owned, never a bank fact). A punchy drum wants a different velocity curve than a
|
||||
sustained pad, so the curve must vary per zone, not per instance. This rules out `ComponentState`
|
||||
(per-instance, where preview velocity correctly lives — that is a *utility* setting, one per
|
||||
instrument; the transfer curve is a *musical* setting, one per sound). The curve is an additive
|
||||
field on `PerformanceZone`, defaulting to the flat identity, and rides the **zones-payload
|
||||
version axis** — NOT the envelope version axis (contrast preview velocity's envelope-v6 bump,
|
||||
which is a per-instance top-level field). The single-capture Sample face reads/writes the same
|
||||
one-zone storage site (S15-F2), so the Sample view and the Zone view share one curve store.
|
||||
|
||||
2. **Default → flat y=1 (all velocities full level), which is a DELIBERATE behavior change — fork
|
||||
R10-F1.** Daniel's verbatim default is *"any velocity plays at full level"* — a flat curve at
|
||||
y=1. That is **not** bit-identical to today's shipped linear `velocity/127` map: today a soft hit
|
||||
is quieter; under a flat-y=1 default every hit plays at unity. This is the one genuine fork the
|
||||
feature carries, and it is Daniel's call:
|
||||
- *Option A (Daniel's stated default): flat y=1.* Honors the verbatim directive; velocity is
|
||||
inert until the user draws a curve. Con: **not** back-compat — already-saved instances (and new
|
||||
captures) get louder/flatter dynamics than today's build until a curve is drawn. Every existing
|
||||
zone's felt response changes.
|
||||
- *Option B: default = the current linear ramp (y = x/127).* Bit-identical to today's engine; the
|
||||
editor's "flat y=1" is then one drawn state among many, not the default. Con: contradicts the
|
||||
verbatim *"by default … full level"* — the default line would be a diagonal, not flat.
|
||||
- *Recommendation: Option A (flat y=1) as the stored default, because it is what Daniel asked for
|
||||
and the feature's whole point is that velocity is opt-in dynamics — but flag it loudly as a
|
||||
shipped-behavior change so it is a decision, not a silent regression.* If the changed feel on
|
||||
existing projects is unwanted, Option B is the safe fallback and costs only the default curve's
|
||||
control-point values. **Daniel's call.** (Whichever wins, the *stored* default is a curve the
|
||||
editor can draw and the core can evaluate — the two options differ only in which curve is
|
||||
seeded.)
|
||||
|
||||
3. **Pure module → `velocity_curve` (REAPER/LICE-free, unit-tested).** Mirror of `envelope_edit` /
|
||||
`card_drag`. Two responsibilities: **(a) evaluation** — `eval(velocity 0–127) → amp scalar 0–1`
|
||||
for the voice engine (a bezier through the control points, clamped to the 0–127 × 0–1 box,
|
||||
monotonic-in-x by construction so a given velocity has exactly one output); and **(b) editing** —
|
||||
add / move / delete control points, each clamped into the box, x-ordered (a point cannot cross
|
||||
its neighbours in x, same monotonic grammar as the envelope nodes), with a point hit-test
|
||||
(point → which control point, pick radius) and a pixel-delta → clamped-point inverse map. Both
|
||||
halves are unit-tested at the boundaries: a known curve + known velocity asserts the eval output;
|
||||
a known drag asserts the clamped point set and the box/order constraints. The flat identity curve
|
||||
(whichever default R10-F1 picks) is a named constructor. **No RT concern in the module** —
|
||||
evaluation is called at note-on, not per frame (see call 4).
|
||||
|
||||
4. **Voice-engine application point → `Voice::start()`, replacing the linear `velocity/127`.**
|
||||
Confirmed from source: `sampler_core.cpp:261` computes `velocityGain_ = velocity / 127.0` **once
|
||||
at note-on** inside `Voice::start()`; the per-frame render path (`advanceFrame`, line 408:
|
||||
`gain = amp * velocityGain_`) then just multiplies the cached scalar. So the transfer curve
|
||||
slots in at exactly one line: `velocityGain_ = curve.eval(velocity)` at note-on — **off the
|
||||
audio-thread-hostile per-frame path**, evaluated once per voice, honoring the house RT
|
||||
discipline (no new per-frame work, no allocation in `process`). The curve travels to the voice
|
||||
the same way the AHDSR/keyTrack params do: on the zone's `SampleData::play` bundle (resolved from
|
||||
the stored `PerformanceZone` at keymap build), read by the voice at `start()`. The pure core owns
|
||||
the eval; the voice reads it.
|
||||
|
||||
**The curve-editor UI — Sample view, near the envelope overlay.** It lives on the **Sample view**
|
||||
(the r9 home face), as a compact band adjacent to the hero-waveform envelope overlay — the two are
|
||||
the same grammar (a drawn 2-D curve with draggable handles) and reading them side by side is
|
||||
natural (amp-over-time next to amp-over-velocity). Draws through the **L1 kit** like every S-VIEW
|
||||
surface: a bordered box (X = velocity 0–127, Y = amp 0–1), the bezier traced in an accent hue,
|
||||
small draggable node markers at each control point (hover/drag-lit via kit states), add-point on
|
||||
click in empty space, delete on a modifier-click or drag-off. The shell (`reasampler_editor.cpp`)
|
||||
does the LICE draw + mouse routing; **all geometry/hit-test/clamp math is in the pure
|
||||
`velocity_curve` module**. On the Zone view the same editor appears in the per-zone param panel
|
||||
(one curve per zone). Additive and bit-identical-for-existing-projects only under R10-F1 Option B;
|
||||
under Option A (the lean) it is additive-but-behavior-changing, per call 2.
|
||||
|
||||
**What does NOT change (guardrails).** Instrument stays a **read-only bank consumer** — the curve
|
||||
is *performance map* on `PerformanceZone`, never written to `Sample` or the bank (D-B). LICE/SWELL
|
||||
drawing only; all math in the pure `velocity_curve` module (D-A). RT discipline untouched — eval
|
||||
is at note-on, not per frame. VST3 class UID unchanged — an additive per-zone field is not a compat
|
||||
event. Zones-payload version bump only; the envelope version axis (preview velocity's v6) is
|
||||
independent and untouched.
|
||||
|
||||
**Wave-plan slot (concurrency-aware — six engineers are in worktrees now).** Because the curve
|
||||
lands on `PerformanceZone`, it is **blocked by Wave 1 track T-KEYTRK** (which owns the
|
||||
`PerformanceZone` schema + the zones-payload version bump right now). The velocity-curve field must
|
||||
sequence as a **LATER additive payload bump AFTER T-KEYTRK merges**, so the two do not collide on a
|
||||
single payload version number — T-KEYTRK's `keyTrack` field and this `velocityCurve` field are
|
||||
sequential additive extensions of the same zones-payload record, not simultaneous ones. It is
|
||||
**not** blocked by T-STATE (that track owns the per-instance `ComponentState` v5→v6 envelope bump
|
||||
for preview velocity — a different struct on a different version axis). Concretely this is:
|
||||
- **a follow-on foundation track** (pure `velocity_curve` module + the core application point at
|
||||
`Voice::start` + the additive `PerformanceZone` field + payload bump), gated on T-KEYTRK; plus
|
||||
- **a Wave 2 shell-integration item** (the Sample-view + Zone-panel curve-editor UI drawn through
|
||||
the L1 kit, routing mouse through `velocity_curve`), gated on the foundation track and composing
|
||||
with the S-VIEW-2 Sample face + S-VIEW-3 envelope-overlay work.
|
||||
|
||||
**Where the spec lives:** CONTEXT.md §Phase S — editor view-model redesign (S-VIEW), velocity-curve
|
||||
sub-section; PLAN.md §Phase S — editor view-model redesign (S-VIEW-9/S-VIEW-10 + fork R10-F1). This
|
||||
Addendum is the *why*; those are the *what/how*.
|
||||
|
||||
---
|
||||
|
||||
## Where this landed
|
||||
|
||||
With D1/D5/D6 locked and **D-A..D-D all settled (2026-07-26)**, the instrument is scoped
|
||||
|
||||
Reference in New Issue
Block a user