diff --git a/CONTEXT.md b/CONTEXT.md index 20ecc00..bbb716b 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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 > 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, -> preview velocity) and two visual components (envelope overlay, real piano-key pattern), and +> model where the loaded sample is the home**, adds three new performance parameters (key-tracking, +> 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 > engine, keymap resolution, and read-only-over-bank contract are **unchanged**; the -> component-state format extends additively for key-tracking. Product framing: `docs/product/ -> midi-playback.md` §Addendum r9. Same standing discipline: **LICE/SWELL drawing only, +> component-state format extends additively (key-tracking + the r10 velocity curve on the +> 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 > 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 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 (0–127), +Y = amp scalar (0–1)**, 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 0–127) → amp scalar 0–1` — a bezier through the control points, + clamped to the 0–127 × 0–1 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 0–127, Y = amp 0–1), 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) **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 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 - boundaries); `keyboard_strip` extended with the natural/accidental predicate; the sampler core - extended with the **key-track scalar** in the repitch math (unit-tested against known - note/root/keyTrack → ratio); `sample_map` (`PerformanceZone`) extended with the additive - `keyTrack` field, plus the **envelope-v6 `previewVelocity` field on `ComponentState`** (S-VIEW-F1) - — both additive, component-state version bumped, back-compat defaults on read. + boundaries); `velocity_curve` (NEW, r10 — bezier `eval(velocity 0–127)→amp 0–1` + control-point + add/move/delete clamped to the 0–127×0–1 box, x-ordered, hit-test + pixel-delta inverse map; + mirror of `envelope_edit`; unit-tested at eval + clamp/order boundaries); `keyboard_strip` + extended with the natural/accidental predicate; the sampler core extended with the **key-track + 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 (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 @@ -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 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.** -- **Additive, back-compat component state.** Two additive fields land: `keyTrack` (per- - `PerformanceZone`, default 100%) inside the zones payload, and `previewVelocity` (per-instance) - as a new top-level `ComponentState` field via an **envelope bump to v6** — the zones-payload - version axis is untouched (independent version axes). Every older blob lifts on read (absent - `keyTrack` → 100%, absent `previewVelocity` → the chosen mid default), so already-saved instances - restore **playback bit-identical** under the same engine. No existing field changes. +- **Additive, back-compat component state.** Three additive fields land: `keyTrack` (per- + `PerformanceZone`, default 100%) and — sequenced after it on the same zones-payload axis — + `velocityCurve` (per-`PerformanceZone`, r10, default the R10-F1 curve) inside the zones payload, + and `previewVelocity` (per-instance) as a new top-level `ComponentState` field via an **envelope + bump to v6** on the independent envelope axis. Every older blob lifts on read (absent `keyTrack` → + 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 via the off-audio-thread commit path (`commitMapAndReload` idiom); no new `process`-thread work, 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. 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) @@ -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. - **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. +- **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 0–127×0–1 box (one output per velocity) and that control-point edits stay clamped + + x-ordered at the boundaries. --- diff --git a/PLAN.md b/PLAN.md index 10fcc28..91f6156 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 > 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 -> surface). Adds two performance parameters (key-tracking, preview velocity) and two visual -> components (envelope overlay, real piano-key pattern), and frames two engineering prerequisites +> surface). Adds three performance parameters (key-tracking, 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). An **editor** redesign — the S3 voice engine, keymap > 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 @@ -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 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. +- [ ] **S-VIEW-9 — velocity → amp transfer-curve: pure core + engine application (foundation, r10).** + New pure `velocity_curve` module (bezier `eval(velocity 0–127)→amp 0–1`, monotonic-in-x, clamped + to the 0–127×0–1 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 0–127, Y = + amp 0–1; 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) @@ -282,6 +303,12 @@ velocity persists via envelope-v6 `ComponentState`. F2: envelope nodes are dragg `envelope_edit` module. Only F3 remains open.)* - **S-VIEW-F3 — Browse modal presentation:** full-window overlay (**lean**) vs. a centered sheet 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.** --- diff --git a/docs/product/midi-playback.md b/docs/product/midi-playback.md index 0814fc7..4f3c7b5 100644 --- a/docs/product/midi-playback.md +++ b/docs/product/midi-playback.md @@ -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