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:
+133
-16
@@ -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 (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)
|
### 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 0–127)→amp 0–1` + control-point
|
||||||
extended with the **key-track scalar** in the repitch math (unit-tested against known
|
add/move/delete clamped to the 0–127×0–1 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 0–127×0–1 box (one output per velocity) and that control-point edits stay clamped +
|
||||||
|
x-ordered at the boundaries.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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 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)
|
### 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.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
## 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
|
||||||
|
|||||||
Reference in New Issue
Block a user