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:
2026-07-27 13:35:01 -04:00
parent 3b9b78b82c
commit 5a859cc9bf
3 changed files with 279 additions and 18 deletions
+133 -16
View File
@@ -2143,12 +2143,14 @@ not a raw literal). What S18 adds is only the missing *plugin identity* layer.
> **Additive sub-phase of Phase S — an *editor* redesign, not an engine change.** Re-partitions > **Additive sub-phase of Phase S — an *editor* redesign, not an engine change.** Re-partitions
> the ReaSampler 9000 editor from today's two-view toggle (Browser + Zones) into a **three-view > the ReaSampler 9000 editor from today's two-view toggle (Browser + Zones) into a **three-view
> model where the loaded sample is the home**, adds two new performance parameters (key-tracking, > model where the loaded sample is the home**, adds three new performance parameters (key-tracking,
> preview velocity) and two visual components (envelope overlay, real piano-key pattern), and > preview velocity, and the r10 velocity→amp transfer curve) and three visual components (envelope
> overlay, real piano-key pattern, and the r10 velocity-curve editor), and
> frames two engineering prerequisites (drop-to-FX bug, default window size). The S3 voice > frames two engineering prerequisites (drop-to-FX bug, default window size). The S3 voice
> engine, keymap resolution, and read-only-over-bank contract are **unchanged**; the > engine, keymap resolution, and read-only-over-bank contract are **unchanged**; the
> component-state format extends additively for key-tracking. Product framing: `docs/product/ > component-state format extends additively (key-tracking + the r10 velocity curve on the
> midi-playback.md` §Addendum r9. Same standing discipline: **LICE/SWELL drawing only, > zones-payload axis, preview velocity on the envelope axis). Product framing: `docs/product/
> midi-playback.md` §Addendum r9 + r10. Same standing discipline: **LICE/SWELL drawing only,
> all layout/hit-test in pure geometry modules, RT-safe, VST3 class UID unchanged, verify every > all layout/hit-test in pure geometry modules, RT-safe, VST3 class UID unchanged, verify every
> API name/signature against the vendored headers before use.** > API name/signature against the vendored headers before use.**
@@ -2366,6 +2368,93 @@ Covered under View 3 above. Pure: `keyboard_strip` gains the natural/accidental
draws the bright/dark overlay over the existing pastel spectral fill. Shared by the Zone strip and draws the bright/dark overlay over the existing pastel spectral fill. Shared by the Zone strip and
the Sample root affordance. the Sample root affordance.
### The velocity → amp transfer-curve editor — visual component (new; Daniel, 2026-07-27, r10)
A **visual transfer-curve editor** mapping MIDI velocity to an amp scalar: **X = velocity (0127),
Y = amp scalar (01)**, an editable bezier from a flat default to an arbitrary multi-point curve.
It gives fully shapeable velocity dynamics per sound. Today the engine maps velocity to gain
*linearly*`velocityGain_ = velocity / 127.0`, computed once at note-on in `Voice::start()`
(`src/vst/sampler_core.cpp:261`); this replaces that fixed line with an editable curve evaluated at
the same point.
**State home — per-`PerformanceZone` (instrument-owned, D-B).** Velocity response is a per-sound
performance characteristic — a sibling of the AHDSR amp envelope, the pitch engine, and the r9
`keyTrack` scalar, all of which already live on `PerformanceZone`. A drum and a pad want different
velocity curves, so the curve varies **per zone**, not per instance. This is deliberately **not**
`ComponentState` (per-instance): that is where **preview velocity** correctly lives, because
preview velocity is a *utility* setting (one per instrument, like a metronome level), whereas the
transfer curve is a *musical* setting (one per sound). The curve is an **additive field on
`PerformanceZone`** riding the **zones-payload version axis** (contrast preview velocity's
envelope-v6 bump — a top-level per-instance field on the *envelope* axis; the two version axes are
independent, as the header documents). The single-capture Sample face reads/writes the same
one-zone storage site (S15-F2), so Sample and Zone views share one curve store per zone.
**Default — flat y=1, a deliberate behavior change (fork R10-F1, Daniel's call).** Daniel's
verbatim default is *"any velocity plays at full level"* — a flat curve at y=1. This is **not**
bit-identical to today's shipped linear `velocity/127` map: today soft hits are quieter; under a
flat-y=1 default every hit plays at unity, so every existing zone's felt dynamics change. This is
the one genuine fork the feature carries:
- **Option A (Daniel's stated default): flat y=1.** Honors the directive; velocity is inert until a
curve is drawn. *Con:* NOT back-compat — already-saved instances and new captures get flatter
dynamics than today until a curve is drawn.
- **Option B: default = today's linear ramp (y = x/127).** Bit-identical to the shipped engine; the
editor's flat y=1 is one drawn state, not the default. *Con:* contradicts the verbatim *"by
default … full level"* (the default line is a diagonal, not flat).
- **Lean: Option A (flat y=1)** — it is what Daniel asked for and the feature's point is opt-in
velocity dynamics — but flagged loudly as a **shipped-behavior change**, not a silent regression.
Option B is the safe fallback and costs only the default curve's seeded control points. Whichever
wins, the *stored* default is a curve the editor draws and the core evaluates; the options differ
only in which curve is seeded.
**Pure module — `velocity_curve` (REAPER/LICE-free, unit-tested).** Mirror of `envelope_edit` /
`card_drag`. Two responsibilities:
- **Evaluation:** `eval(velocity 0127) → amp scalar 01` — a bezier through the control points,
clamped to the 0127 × 01 box, **monotonic in x** by construction (each velocity has exactly one
output). Called at note-on, never per frame.
- **Editing:** add / move / delete control points, each clamped into the box and **x-ordered** (a
point cannot cross its neighbours in x — the same monotonic grammar as the envelope nodes); a
point hit-test (point → which control point, pick radius) and a pixel-delta → clamped-point
inverse map (mirror of `envelope_edit`'s inverse map). The flat identity curve (per R10-F1) is a
named constructor.
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 + the box/order constraints; add/delete assert
the point count and ordering invariants.
**Voice-engine application point — `Voice::start()`, off the per-frame path.** Confirmed from
source: `Voice::start(int note, int velocity, …)` (`sampler_core.cpp:252`) computes
`velocityGain_ = velocity / 127.0` **once at note-on** (line 261); the per-frame render then just
multiplies the cached scalar (`advanceFrame`, line 408: `gain = amp * velocityGain_`). The transfer
curve slots in at exactly that line — `velocityGain_ = curve.eval(velocity)` at note-on — **off the
audio-thread-hostile per-frame path** (evaluated once per voice, no new `process`-thread work, no
allocation). The curve reaches 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 `velocity_curve` core owns the eval; the voice reads it.
**The curve-editor UI — Sample view, adjacent to the envelope overlay.** Lives on the **Sample
view** (the r9 home face), a compact band next to the hero-waveform envelope overlay — the two are
the same grammar (a drawn 2-D curve with draggable handles), and amp-over-time beside
amp-over-velocity reads naturally. Draws through the **L1 kit** like every S-VIEW surface: a
bordered box (X = velocity 0127, Y = amp 01), the bezier traced in an accent hue, small draggable
node markers per control point (hover/drag-lit via kit states), add-point on empty-space click,
delete on modifier-click / drag-off. The shell (`reasampler_editor.cpp`) does the LICE draw + mouse
routing; **all geometry/hit-test/clamp math lives 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.
**Wave-plan slot (concurrency-aware).** The curve lands on `PerformanceZone`, so it is **blocked by
Wave 1 track T-KEYTRK** (which owns the `PerformanceZone` schema + zones-payload version bump). The
`velocityCurve` field must sequence as a **LATER additive payload bump AFTER T-KEYTRK merges** — the
two are sequential additive extensions of the same zones-payload record, not simultaneous ones, so
they never collide on one payload version. It is **not** blocked by T-STATE (that owns the
per-instance `ComponentState` v5→v6 envelope bump for preview velocity — a different struct on the
independent envelope version axis). Concretely: a **follow-on foundation track** (pure
`velocity_curve` + the `Voice::start` application point + 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 through the L1 kit), gated on the foundation track and composing with the
S-VIEW-2 Sample face + S-VIEW-3 envelope overlay.
### Engineering prerequisite 1 — drop-to-FX bug (routed to implementation, NOT a design call) ### Engineering prerequisite 1 — drop-to-FX bug (routed to implementation, NOT a design call)
**Symptom (Daniel):** dropping a capture onto a track's FX chain does not instantiate + init **Symptom (Daniel):** dropping a capture onto a track's FX chain does not instantiate + init
@@ -2422,11 +2511,17 @@ face without scrolling, and cannot be resized below the constraint floor.
- **Pure (new/extended):** `envelope_overlay` (AHDSR/Trigger params + frame-length → polyline in a - **Pure (new/extended):** `envelope_overlay` (AHDSR/Trigger params + frame-length → polyline in a
rect; unit-tested); `envelope_edit` (NEW — node hit-test + pixel-delta→clamped-param inverse map rect; unit-tested); `envelope_edit` (NEW — node hit-test + pixel-delta→clamped-param inverse map
for the draggable envelope handles; mirror of `card_drag`; unit-tested at the monotonic/clamp for the draggable envelope handles; mirror of `card_drag`; unit-tested at the monotonic/clamp
boundaries); `keyboard_strip` extended with the natural/accidental predicate; the sampler core boundaries); `velocity_curve` (NEW, r10 — bezier `eval(velocity 0127)→amp 01` + control-point
extended with the **key-track scalar** in the repitch math (unit-tested against known add/move/delete clamped to the 0127×01 box, x-ordered, hit-test + pixel-delta inverse map;
note/root/keyTrack → ratio); `sample_map` (`PerformanceZone`) extended with the additive mirror of `envelope_edit`; unit-tested at eval + clamp/order boundaries); `keyboard_strip`
`keyTrack` field, plus the **envelope-v6 `previewVelocity` field on `ComponentState`** (S-VIEW-F1) extended with the natural/accidental predicate; the sampler core extended with the **key-track
— both additive, component-state version bumped, back-compat defaults on read. scalar** in the repitch math (unit-tested against known note/root/keyTrack → ratio) **and the
velocity-curve eval at `Voice::start()`** replacing the linear `velocity/127` (r10; off the
per-frame path); `sample_map` (`PerformanceZone`) extended with the additive `keyTrack` field
**and the additive `velocityCurve` field** (r10 — both on the zones-payload version axis; the
velocity-curve field sequences AFTER T-KEYTRK's `keyTrack` bump), plus the **envelope-v6
`previewVelocity` field on `ComponentState`** (S-VIEW-F1) — all additive, version bumped,
back-compat defaults on read.
- **Shell (`reasampler_editor.cpp`):** re-partition the paint/hit-test into the three views - **Shell (`reasampler_editor.cpp`):** re-partition the paint/hit-test into the three views
(Sample face, Browse modal overlay, Zone surface) replacing the two-view toggle; add the (Sample face, Browse modal overlay, Zone surface) replacing the two-view toggle; add the
preview-trigger button + velocity knob wired to an off-audio-thread preview note through the preview-trigger button + velocity knob wired to an off-audio-thread preview note through the
@@ -2441,12 +2536,18 @@ face without scrolling, and cannot be resized below the constraint floor.
- **Read-only bank consumer (unchanged).** Key-tracking, preview velocity, envelope-node edits, and - **Read-only bank consumer (unchanged).** Key-tracking, preview velocity, envelope-node edits, and
every marker/mode control are the instrument's **performance map / editor state** (D-B) — held in every marker/mode control are the instrument's **performance map / editor state** (D-B) — held in
the instrument's own VST3 component state, **never written to `Sample` or the bank.** the instrument's own VST3 component state, **never written to `Sample` or the bank.**
- **Additive, back-compat component state.** Two additive fields land: `keyTrack` (per- - **Additive, back-compat component state.** Three additive fields land: `keyTrack` (per-
`PerformanceZone`, default 100%) inside the zones payload, and `previewVelocity` (per-instance) `PerformanceZone`, default 100%) and — sequenced after it on the same zones-payload axis —
as a new top-level `ComponentState` field via an **envelope bump to v6** — the zones-payload `velocityCurve` (per-`PerformanceZone`, r10, default the R10-F1 curve) inside the zones payload,
version axis is untouched (independent version axes). Every older blob lifts on read (absent and `previewVelocity` (per-instance) as a new top-level `ComponentState` field via an **envelope
`keyTrack` → 100%, absent `previewVelocity` → the chosen mid default), so already-saved instances bump to v6** on the independent envelope axis. Every older blob lifts on read (absent `keyTrack` →
restore **playback bit-identical** under the same engine. No existing field changes. 100%, absent `velocityCurve` → the R10-F1 default curve, absent `previewVelocity` → the chosen mid
default), so already-saved instances restore cleanly. **Playback back-compat carries a caveat for
the velocity curve:** under R10-F1 Option A (flat y=1 default) an already-saved zone with no
stored curve now plays every velocity at unity — **not** bit-identical to the linear `velocity/127`
it played before; under Option B (linear default) it is bit-identical. This is the one non-back-
compat surface in S-VIEW and is the substance of fork R10-F1. `keyTrack` and `previewVelocity`
remain fully bit-identical on lift. No existing field changes.
- **RT discipline (unchanged).** The preview-trigger fires a note through the existing voice engine - **RT discipline (unchanged).** The preview-trigger fires a note through the existing voice engine
via the off-audio-thread commit path (`commitMapAndReload` idiom); no new `process`-thread work, via the off-audio-thread commit path (`commitMapAndReload` idiom); no new `process`-thread work,
no allocation on the audio thread. no allocation on the audio thread.
@@ -2463,7 +2564,13 @@ preview velocity **persists** via envelope-v6 `ComponentState`; the envelope ove
- **S-VIEW-F3 — Browse modal presentation.** Full-window overlay (lean — the modal-picker feel) vs. - **S-VIEW-F3 — Browse modal presentation.** Full-window overlay (lean — the modal-picker feel) vs.
a large centered sheet with a dimmed Sample behind. Presentation detail; either satisfies "easy a large centered sheet with a dimmed Sample behind. Presentation detail; either satisfies "easy
to summon and dismiss." **Still Daniel's call** — the only S-VIEW fork left open. to summon and dismiss." **Still Daniel's call.**
- **R10-F1 — velocity-curve default (Daniel, 2026-07-27).** **Option A: flat y=1** (Daniel's
verbatim default — every velocity plays at full level; NOT back-compat with today's linear
`velocity/127`, so existing zones' dynamics change) vs. **Option B: linear y = x/127** (bit-
identical to the shipped engine; contradicts the verbatim "full level" default). **Lean A**,
flagged as a deliberate shipped-behavior change, not a silent regression. This is the only
non-back-compat surface the velocity-curve feature introduces. **Daniel's call.**
### Must-verify before build (S-VIEW) ### Must-verify before build (S-VIEW)
@@ -2485,6 +2592,16 @@ preview velocity **persists** via envelope-v6 `ComponentState`; the envelope ove
never diverge), and that the monotonic time-node constraint holds at the segment boundaries. never diverge), and that the monotonic time-node constraint holds at the segment boundaries.
- **Envelope-overlay time base** — confirm the seconds→frames resolution the overlay draws against - **Envelope-overlay time base** — confirm the seconds→frames resolution the overlay draws against
matches the voice engine's live-rate resolution so the drawn shape lines up with the waveform. matches the voice engine's live-rate resolution so the drawn shape lines up with the waveform.
- **Velocity-curve application point (r10)** — confirm `Voice::start()` (`sampler_core.cpp:252`) is
the sole velocity→gain site and that replacing `velocityGain_ = velocity / 127.0` (line 261) with
`velocityGain_ = curve.eval(velocity)` keeps the eval at note-on only, off the per-frame render
path (line 408 `gain = amp * velocityGain_` unchanged). No new `process`-thread work or allocation.
- **Velocity-curve payload sequencing (r10)** — confirm the additive `velocityCurve` field on
`PerformanceZone` lands as a zones-payload bump AFTER T-KEYTRK's `keyTrack` bump (not simultaneous),
so the two sequential extensions of the same record never collide on one payload version number.
- **Velocity-curve eval monotonicity (r10)** — confirm the pure `velocity_curve` bezier is monotonic
in x over the 0127×01 box (one output per velocity) and that control-point edits stay clamped +
x-ordered at the boundaries.
--- ---
+29 -2
View File
@@ -181,8 +181,9 @@ is **ReaSampler 9000**. Framing + propagation surfaces:
> **Additive Phase S sub-phase (S-VIEW; Daniel, 2026-07-27, r9).** Re-partitions the ReaSampler > **Additive Phase S sub-phase (S-VIEW; Daniel, 2026-07-27, r9).** Re-partitions the ReaSampler
> 9000 editor from a two-view toggle into a **three-view model where the loaded sample is the > 9000 editor from a two-view toggle into a **three-view model where the loaded sample is the
> home** — Sample (default face), Browse (modal picker over Sample), Zone (dedicated keymap > home** — Sample (default face), Browse (modal picker over Sample), Zone (dedicated keymap
> surface). Adds two performance parameters (key-tracking, preview velocity) and two visual > surface). Adds three performance parameters (key-tracking, preview velocity, and the r10
> components (envelope overlay, real piano-key pattern), and frames two engineering prerequisites > velocity→amp transfer curve) and three visual components (envelope overlay, real piano-key
> pattern, and the r10 velocity-curve editor), and frames two engineering prerequisites
> (drop-to-FX bug, default window size). An **editor** redesign — the S3 voice engine, keymap > (drop-to-FX bug, default window size). An **editor** redesign — the S3 voice engine, keymap
> resolution, and read-only-over-bank contract are **unchanged**; component state extends > resolution, and read-only-over-bank contract are **unchanged**; component state extends
> additively for key-tracking; VST3 class UID unchanged. Authoritative spec: **CONTEXT.md §Phase > additively for key-tracking; VST3 class UID unchanged. Authoritative spec: **CONTEXT.md §Phase
@@ -255,6 +256,26 @@ piano-key pattern work; drop-a-capture-onto-FX instantiates a playing instance.
- [ ] **S-VIEW-8 — Zone view retained + wired.** Keep +Add Zone / Delete, the per-zone keyboard - [ ] **S-VIEW-8 — Zone view retained + wired.** Keep +Add Zone / Delete, the per-zone keyboard
strip (now with the piano pattern), the Low/High/Root numeric-entry legend, and the per-zone strip (now with the piano pattern), the Low/High/Root numeric-entry legend, and the per-zone
param panel; add the key-tracking control. Nothing from today's Zones view dropped. param panel; add the key-tracking control. Nothing from today's Zones view dropped.
- [ ] **S-VIEW-9 — velocity → amp transfer-curve: pure core + engine application (foundation, r10).**
New pure `velocity_curve` module (bezier `eval(velocity 0127)→amp 01`, monotonic-in-x, clamped
to the 0127×01 box; control-point add/move/delete x-ordered + box-clamped; hit-test + pixel-
delta→clamped-point inverse map; mirror of `envelope_edit`; unit-tested at eval + clamp/order
boundaries). Additive `velocityCurve` field on `PerformanceZone` (instrument-owned, D-B), on the
**zones-payload version axis**, default = the R10-F1 curve; older blobs lift to that default.
Apply at **`Voice::start()`** — replace `velocityGain_ = velocity / 127.0` (`sampler_core.cpp:261`)
with `velocityGain_ = curve.eval(velocity)`, off the per-frame path (no new RT work). **BLOCKED BY
Wave 1 T-KEYTRK** (`PerformanceZone` schema + payload bump) — this is a LATER additive payload bump
sequencing AFTER T-KEYTRK merges (the two never collide on one payload version). NOT blocked by
T-STATE (that owns the per-instance `ComponentState` v6 axis for preview velocity). See CONTEXT.md
§S-VIEW velocity-curve sub-section.
- [ ] **S-VIEW-10 — velocity-curve editor UI (shell, Sample + Zone views, r10).** A compact transfer-
curve editor band on the **Sample view** adjacent to the envelope overlay (X = velocity 0127, Y =
amp 01; bezier traced in an accent hue; draggable node markers per control point; add on empty-
space click, delete on modifier-click/drag-off), drawn through the **L1 kit**; the same editor in
the Zone per-zone param panel (one curve per zone). Shell (`reasampler_editor.cpp`) does the LICE
draw + mouse routing; **all geometry/hit-test/clamp math in the pure `velocity_curve` module**.
**Wave 2 shell-integration item** — gated on S-VIEW-9 (foundation) + composes with S-VIEW-2 (Sample
face) / S-VIEW-3 (envelope overlay).
### Phase S editor prerequisites — routed to staff-engineer (bug + default; NOT design forks) ### Phase S editor prerequisites — routed to staff-engineer (bug + default; NOT design forks)
@@ -282,6 +303,12 @@ velocity persists via envelope-v6 `ComponentState`. F2: envelope nodes are dragg
`envelope_edit` module. Only F3 remains open.)* `envelope_edit` module. Only F3 remains open.)*
- **S-VIEW-F3 — Browse modal presentation:** full-window overlay (**lean**) vs. a centered sheet - **S-VIEW-F3 — Browse modal presentation:** full-window overlay (**lean**) vs. a centered sheet
over a dimmed Sample. Presentation detail. over a dimmed Sample. Presentation detail.
- **R10-F1 — velocity-curve default (r10):** **Option A flat y=1** (**lean** — Daniel's verbatim
"any velocity plays at full level"; NOT back-compat with today's linear `velocity/127`, so
existing zones' dynamics change — flagged as a deliberate shipped-behavior change) vs. **Option B
linear y = x/127** (bit-identical to the shipped engine; contradicts the verbatim "full level"
default). The only non-back-compat surface the velocity-curve feature introduces. **Daniel's
call.**
--- ---
+117
View File
@@ -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 (0127) and y = amp scalar (01). 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, 0127), Y is the
output multiplier (amp scalar, 01), 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 0127) → amp scalar 01`
for the voice engine (a bezier through the control points, clamped to the 0127 × 01 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 0127, Y = amp 01), 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 ## Where this landed
With D1/D5/D6 locked and **D-A..D-D all settled (2026-07-26)**, the instrument is scoped With D1/D5/D6 locked and **D-A..D-D all settled (2026-07-26)**, the instrument is scoped