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
|
||||
> 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user