docs(product): spec Phase S sampling modes (S15 Trigger/Gate) + pitch envelope (S16)
Gate: AHDSR + loops; Trigger: fade-in / %-length / fade-out, note-off-immune. Start point joins the instrument-side overrides. Pitch env is a per-frame ratio multiply on the existing core — no resampler rewrite. WDL swept: sinc resampler held as optional upgrade; no elastique-class stretch in WDL.
This commit is contained in:
@@ -625,6 +625,145 @@ the relay is proven.**
|
||||
"drop files on the ReaSampler panel to add" affordance rather than silently swallowing the
|
||||
drop — the shipped ingest gesture stays discoverable either way.
|
||||
|
||||
## S15 — sampling modes: Trigger vs Gate (per-sample play-mode; core + editor)
|
||||
**Goal:** Give each played sample a **play mode** — **Gate** (classic held note) or
|
||||
**Trigger** (one-shot) — a per-sample/per-zone performance choice (D-B, instrument-owned).
|
||||
**Gate** is today's behavior grown from ADSR to **AHDSR** (adds a Hold stage): note-on →
|
||||
attack/hold/decay/sustain, note-off → release, sustain **loop points apply** (S11's
|
||||
draggable loop UI is Gate-mode UI). **Trigger** is a one-shot drum-pad: note-on fires
|
||||
playback of a defined **% of sample length** with a **fade-in** and **fade-out** ramp,
|
||||
**ignores note-off**, and uses **no sustain loop**. **Both** modes carry a **modifiable
|
||||
start point** (playback begins at an offset into the sample, not always frame 0). This is
|
||||
an **S3-core extension** (the engine Daniel smoke-tests) plus editor surfacing — the mode +
|
||||
its parameters are instrument performance-map state, never a bank fact. CONTEXT.md §Phase S
|
||||
(Sampling modes — Trigger vs Gate). **Daniel's feature set is settled; the leans below are
|
||||
build-time residuals, not open forks — except the flagged forks S15-F1/F2.**
|
||||
**Verify (in DAW):** a sample in **Gate** mode plays held with the AHDSR envelope (hold
|
||||
stage audible between attack and decay), releases on note-off, and loops its sustain region
|
||||
if loop points are set; a sample in **Trigger** mode fires a fixed % of its length on
|
||||
note-on with audible fade-in/out, **plays through to completion regardless of note-off**,
|
||||
and never sustain-loops; the **start point** offsets playback in both modes (a note starts
|
||||
partway into the sample); the mode + parameters are per-instance component state that
|
||||
survive save/reopen; the pure core's Trigger envelope (fade-in → hold → fade-out over
|
||||
%-length frames) and the AHDSR hold stage are asserted against known signals (mirror of
|
||||
`peaks`); existing Gate/ADSR behavior is unchanged when hold=0 (regression). **Spec
|
||||
channel-count-agnostic** — the mode/envelope logic is per-frame amplitude and read-position,
|
||||
independent of the S7 channel dimension (§sequencing).
|
||||
**Depends on:** S3 (extends the envelope + voice read-position machinery), S5 (the
|
||||
`PerformanceZone` the mode + params attach to), S11 (Gate loop-point UI; Trigger's waveform
|
||||
shows start + %-length + fades on the **same** waveform surface). Independent of S7 —
|
||||
orthogonal dimensions (§sequencing note in CONTEXT.md).
|
||||
|
||||
- [ ] Core: `PlayMode { Gate, Trigger }` on the voice + the envelope split. **Gate** grows
|
||||
`AdsrParams` → `AhdsrParams` (add `holdFrames` between attack and decay; hold=0 is the
|
||||
exact current ADSR — back-compat). **Trigger** is a distinct envelope: play `[start,
|
||||
start + lengthFraction·(frames−start))` with a **fade-in** ramp (0→1 over `fadeInFrames`)
|
||||
and a **fade-out** ramp (1→0 over `fadeOutFrames` ending at the play-length end),
|
||||
**ignoring note-off** (release is a no-op in Trigger). Fade curve default **equal-power**
|
||||
(constant-power `sin`/`cos`, click-free on one-shots) with the shape noted; linear is a
|
||||
build-time residual. Pure, unit-tested against a known signal.
|
||||
- [ ] Core: **modifiable start point** — the voice's initial `readPos_` is `startFrame`
|
||||
(frame offset), applied in both modes; the existing per-frame `readPos_ += ratio_` read
|
||||
and loop/interp machinery is otherwise unchanged. Clamp `0 ≤ startFrame < frames`.
|
||||
- [ ] Core: **% length → frames + fade mapping** for Trigger. `lengthFraction ∈ (0,1]`
|
||||
resolves to `playEnd = start + round(lengthFraction·(frames − start))`; `fadeInFrames` /
|
||||
`fadeOutFrames` clamp so their sum ≤ play length (fade-out anchored to `playEnd`). Note-off
|
||||
in Trigger does nothing; the voice frees when `readPos_ ≥ playEnd` (mirror of the current
|
||||
run-off-end idle). **Choke on note-off is NOT in scope** (fork S15-F1, held below).
|
||||
- [ ] Parameter ownership (per-sample/per-zone, instrument-owned): the play mode + its
|
||||
params (Gate: AHDSR; Trigger: %-length, fade-in, fade-out; both: start point) attach to
|
||||
the **capture selection / zone**, stored in the **performance map** (D-B). **Lean
|
||||
(build-time residual):** start point joins `rootOverride`/loop-override as another
|
||||
per-`PerformanceZone` optional override, and a per-zone `PlayMode` + its param struct is
|
||||
added additively (version-bumped component state, back-compat — a truncated/older blob
|
||||
defaults to **Gate**, hold=0, start=0, no fades = exactly today's behavior). **Fork
|
||||
S15-F2 (flagged):** whether these live per-capture-selection (S10's single-capture flow)
|
||||
**and** per-zone, or per-zone only with the single-capture case as a one-zone map. Lean:
|
||||
per-zone only — the single capture is already a one-zone map (S10-Z back-compat lift),
|
||||
so one storage site serves both. Flagged because it touches S10's single-capture setup
|
||||
surface shape.
|
||||
- [ ] Editor (S11 waveform surface, mode-aware): **Gate** shows draggable **start + loop
|
||||
markers** (S11's loop UI); **Trigger** shows **start + %-length end + fade-in/out**
|
||||
handles on the same waveform. A **mode toggle** per capture/zone in the guided setup
|
||||
(S10) / Zones panel (S10-Z). Marker/handle geometry is pure (extends the S11
|
||||
`frame↔pixel` + marker-grab module); commits off-thread via `commitMapAndReload`. The
|
||||
instrument stays a **read-only bank consumer** (mode/params are performance map, never
|
||||
written to the bank).
|
||||
|
||||
## S16 — pitch envelope (AD, off by default; per-voice pitch modulation)
|
||||
**Goal:** Add a per-voice **pitch envelope** on top of a zone's base repitch — a short
|
||||
**AD** (attack-decay) modulation of playback pitch, **off by default**, that biases the
|
||||
voice's read-increment over time. The classic use is the percussive **pitch drop** (start
|
||||
offset above the target, glide down to the base pitch) and its inverse (pitch-up sweep).
|
||||
Because the core's resampler is already an arbitrary per-frame `readPos_ += ratio_` linear
|
||||
interpolation (**confirmed from `sampler_core.cpp`** — the read increment is recomputed-able
|
||||
per frame at no structural cost), the pitch envelope is a **per-frame multiply of `ratio_`**
|
||||
by `2^(envSemitones(frame)/12)`; no new resampler is needed. Per-instance performance-map
|
||||
state (D-B), off by default so existing playback is bit-unchanged. CONTEXT.md §Phase S
|
||||
(Pitch envelope). **Settled feature; the shape lean below is a build-time residual.**
|
||||
**Verify (in DAW):** with the pitch envelope **off** (default), playback is identical to
|
||||
S15/S5 (regression — no pitch modulation applied); enabling an AD pitch envelope on a zone
|
||||
makes a held/triggered note **start offset in pitch and glide to the zone's base pitch** over
|
||||
the attack+decay time (a percussive pitch drop when the offset is positive-then-settle);
|
||||
range is settable in **semitones (±)**; the modulation is per-voice (polyphonic notes each
|
||||
run their own envelope); the pure pitch-envelope curve is asserted against known values
|
||||
(offset at t=0, base at t=attack+decay); RT-safe (no allocation in `process` — the envelope
|
||||
is the same per-frame tick idiom as the amp envelope).
|
||||
**Depends on:** S3 (the voice read-increment + envelope tick idiom this reuses), S5 (the
|
||||
`PerformanceZone` it attaches to). Composes with S15 (Gate or Trigger) — the pitch envelope
|
||||
is orthogonal to the amp mode. Independent of S7 (pitch is a read-rate scalar, channel-count
|
||||
agnostic).
|
||||
|
||||
- [ ] Core: a per-voice **AD pitch envelope** — `PitchEnvParams { enabled=false, int64
|
||||
attackFrames, int64 decayFrames, double peakSemitones }`. **Shape (lean, build-time
|
||||
residual):** *start-offset → glide to base* (the classic pitch drop) — at note-on the
|
||||
pitch offset is `peakSemitones`, ramps to 0 over `attackFrames` (or holds at peak for
|
||||
attack then decays to 0 over `decayFrames` — pick the two-segment AD: **rise to peak over
|
||||
attack, fall to 0 over decay**, so a zero attack gives the pure "start high, drop to base"
|
||||
drop; documented). Off by default (`enabled=false` → offset always 0 → `ratio_` unchanged,
|
||||
bit-identical to today). Pure, unit-tested (t=0 offset, t=attack peak, t=attack+decay → 0).
|
||||
- [ ] Core: apply the envelope as a **per-frame `ratio_` multiply** — effective read
|
||||
increment = `pitchRatio(note,root) · 2^(pitchEnvSemitones(frame)/12)`. Reuses the existing
|
||||
`renderFrame` read loop (the one confirmed to advance `readPos_` by an arbitrary ratio each
|
||||
frame); no new resampler, no WDL dependency for the modulation path (see the WDL finding —
|
||||
WDL_Resampler is a *quality* option for the base repitch, **not** needed for envelope
|
||||
modulation). Semitone range ± (settable; default range noted at build).
|
||||
- [ ] Parameter ownership + editor: the pitch envelope is per-zone instrument performance-map
|
||||
state (D-B), additive/version-bumped (back-compat: absent → disabled). Editor exposure is a
|
||||
small AD + depth control (folds into the S12 ADSR-editor ergonomics tier — attack/decay
|
||||
sliders + a ±semitone depth); default-off so the control is discoverable but inert until
|
||||
turned on.
|
||||
|
||||
> **WDL pitch capabilities — verified finding (feeds S15/S16 build, not a committed point).**
|
||||
> The full WDL pitch/resample surface was swept (`vendor/WDL/WDL/resample.h`,
|
||||
> `simple_pitchshift.h` — the only two pitch/resample headers; no elastique, no
|
||||
> formant-preserving/time-stretch anywhere in the vendored tree). Findings, honest:
|
||||
> - **`WDL_Resampler`** (`resample.h`) — a real **sinc/linear resampler** (`SetMode(interp,
|
||||
> filtercnt, sinc, sinc_size, sinc_interpsize)`; sinc up to 64-tap). It is **RT-suitable**
|
||||
> (streaming `ResamplePrepare`/`ResampleOut`, prealloc-able, no per-block alloc if
|
||||
> pre-sized) and its **sinc mode beats the core's current 2-point linear interp** for
|
||||
> repitch quality (fewer aliasing artifacts on large transpositions) at a real CPU cost
|
||||
> (64-tap conv per output sample vs. one lerp). **Fit:** an *optional quality upgrade for
|
||||
> the base repitch path* — a per-voice quality toggle (linear = cheap default, sinc =
|
||||
> quality) — **not** required for S15/S16 and **not** committed here. Held as a Tier-2/3
|
||||
> quality option (see "held" below), because per-voice `WDL_Resampler` instances are
|
||||
> heavier and the pitch-envelope modulation (S16) is cleaner hand-rolled anyway.
|
||||
> - **`WDL_SimplePitchShifter`** (`simple_pitchshift.h`) — a **time-domain overlap-add
|
||||
> pitch shifter** (window/overlap, quality param). It is *pitch-shift-preserving-duration*,
|
||||
> the opposite of what a sampler wants (a sampler repitches by resampling, changing both
|
||||
> pitch and duration together). Its `set_formant_shift` is an **explicit empty stub** — so
|
||||
> **no formant preservation**. Not a fit for the sampler repitch/envelope path; noted for
|
||||
> completeness.
|
||||
> - **Formant-preserving / time-stretch (elastique-class): NOT in WDL, confirmed.** REAPER's
|
||||
> elastique is **licensed (zplane), not part of the open WDL/reaper-sdk vendored tree** —
|
||||
> grep of `vendor/WDL` for elastique/formant/time-stretch found only unrelated libpng/giflib
|
||||
> string matches. So formant-correct repitch is **unavailable to the instrument** without a
|
||||
> new third-party dependency (out of scope, D5-adjacent). **Stated, not worked around.**
|
||||
> - **Recommendation:** the S16 pitch-envelope ratio-modulation stays **hand-rolled** (a
|
||||
> per-frame `ratio_` multiply over the existing linear-interp read — simplest, RT-safe,
|
||||
> already supported). `WDL_Resampler` (sinc) is the *only* WDL piece worth adopting, and
|
||||
> only as an **optional base-repitch quality upgrade** — held, not scheduled.
|
||||
|
||||
> **S14 moved to Phase L (2026-07-26).** The dock-panel refresh that stood here is now
|
||||
> **Phase L point L2** on `dev` — and, per Daniel's DS-3 call, expanded from a light re-skin
|
||||
> into a **thorough dock-panel layout redesign** that lays out the full M11-aware button
|
||||
@@ -761,7 +900,16 @@ artifact, not extension-only.
|
||||
Tier 0–1 proves the instrument belongs — **its points are not drawn up here.**
|
||||
- **Tier 3 — "instrument polish" (optional-forever).** Filters, filter/pitch
|
||||
envelopes, LFOs, per-voice pan, choke groups, a modest FX slot. A direction to leave
|
||||
room for, never a commitment.
|
||||
room for, never a commitment. **Note:** S16 lands the *pitch* envelope early (Daniel's
|
||||
directive) — the Tier-3 "filter/pitch envelopes" line now means the *filter* envelope +
|
||||
LFOs remainder.
|
||||
- **Sinc repitch quality upgrade (HELD — WDL_Resampler).** `WDL_Resampler`'s sinc mode
|
||||
beats the core's 2-point linear interp for base-repitch quality (see the S16 WDL
|
||||
finding). An optional per-voice quality toggle (linear default / sinc), RT-suitable but
|
||||
heavier. Held as a Tier-2/3 quality option — not needed for S15/S16, not scheduled.
|
||||
- **Trigger choke-on-note-off (HELD — fork S15-F1).** A future option for Trigger mode to
|
||||
*cut* (choke) on note-off or on a same-group re-trigger (hi-hat open/closed). Deliberately
|
||||
out of S15 scope (Trigger ignores note-off entirely there); a Tier-3 choke-group direction.
|
||||
|
||||
## Phase S — must-verify-before-build (carried from CONTEXT.md §Phase S)
|
||||
- **Steinberg VST3 SDK surface** — interface members, base-class overrides,
|
||||
@@ -793,6 +941,13 @@ artifact, not extension-only.
|
||||
- **Bank-generation ext-state read (S9)** — confirm no torn-read hazard on the single
|
||||
integer generation key for a bridge read on the instrument's UI/timer thread concurrent
|
||||
with an extension write.
|
||||
- **WDL pitch/resample surface (S15/S16)** — **verified this pass:** `resample.h`
|
||||
(`WDL_Resampler`, sinc/linear, RT-suitable) and `simple_pitchshift.h`
|
||||
(`WDL_SimplePitchShifter`, time-domain OLA, `set_formant_shift` is an empty stub) are the
|
||||
**whole** pitch/resample surface; **no** elastique / formant-preserving / time-stretch in
|
||||
the vendored WDL tree. S16's pitch-envelope modulation stays hand-rolled over the existing
|
||||
linear-interp read (no WDL needed). If the held sinc-repitch upgrade is ever taken, verify
|
||||
`WDL_Resampler` streaming/prealloc against per-voice RT budget before use.
|
||||
- **Drop-and-load (S17) — three surfaces.** (1) `TrackFX_AddByName` — **verified present**
|
||||
in `reaper_plugin_functions.h` (signature confirmed; the `"VST3:"` name prefix and the
|
||||
negative-`instantiate`-always-adds semantics are documented in the header comment). (2)
|
||||
|
||||
Reference in New Issue
Block a user