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:
2026-07-26 20:09:15 -04:00
parent 991c190bb8
commit c64ab687bf
3 changed files with 396 additions and 8 deletions
+166 -1
View File
@@ -1320,6 +1320,148 @@ stereo; the current mono downmix is a Tier-0 simplification, not a permanent sha
REAPER's mono/stereo instrument-bus expectations against the vendored Steinberg SDK +
`reaper_vst3_interfaces.h`.
## Sampling modes — Trigger vs Gate + pitch envelope (S15/S16; core + editor)
**Daniel's directive (2026-07-26, verbatim):** *"Sampling mode: Trigger vs Gate. Gate has
an AHDSR envelope. Trigger has fade in, % length, and fade out. Both modes have modifiable
start point, Gate has modifiable loop points too. In addition to amp env, there will be a
pitch envelope/curve (AD?) which is off by default."* The feature set is **settled**; two
forks (S15-F1 choke, S15-F2 param granularity) are flagged with leans below.
### Play mode — Gate vs Trigger (S15)
Each played sample carries a **play mode** — a per-sample/per-zone **performance choice**
(D-B, instrument-owned, never a bank fact). Two modes, precisely:
- **Gate — classic held note (grows the current path).** Note-on enters the amp envelope;
note-off enters release; a **sustain loop** applies for held notes (S11's draggable loop
markers are Gate-mode UI). The current core envelope is **ADSR**; Gate adds a **Hold**
stage → **AHDSR**: `0→1` over attack, **hold at 1** over `holdFrames`, `1→sustain` over
decay, hold sustain until note-off, `level→0` over release. **`holdFrames == 0` is
exactly today's ADSR** — a back-compat degenerate, no behavior change for existing Gate
play. Segment math is the existing linear-ramp idiom (`AdsrEnvelope::tick`) with one new
stage inserted between Attack and Decay.
- **Trigger — one-shot drum-pad.** Note-on fires playback of a defined **% of sample
length** with a **fade-in** and **fade-out** ramp; **note-off is ignored** (the voice
plays through); **no sustain loop**. Envelope math (distinct from AHDSR): play the frame
span `[startFrame, playEnd)` where `playEnd = startFrame + round(lengthFraction·(frames
startFrame))`, `lengthFraction ∈ (0,1]`; amplitude ramps `0→1` over `fadeInFrames`
(fade-in) at the head and `1→0` over `fadeOutFrames` anchored to `playEnd` (fade-out),
unity between; fades clamp so `fadeInFrames + fadeOutFrames ≤ play length`. The voice
frees when `readPos_ ≥ playEnd` (mirror of the current run-off-end idle). **Fade curve
default: equal-power** (constant-power `sin`/`cos` — click-free on one-shots); linear is a
build-time residual. **Note-off in Trigger is a no-op** (choke is held — fork S15-F1).
**Both modes: modifiable start point.** Playback begins at `startFrame` (a frame offset into
the sample, clamped `0 ≤ startFrame < frames`), not always frame 0. This is the voice's
initial `readPos_`; the existing per-frame `readPos_ += ratio_` read and linear-interp/loop
machinery are otherwise unchanged. Gate additionally has **modifiable loop points** (already
the S2 loop intrinsic + S11 override); Trigger has none (it is a one-shot).
**Voice-stealing interaction (unchanged).** The S3 stealing policy (oldest-in-release, else
oldest-overall) is mode-agnostic — a Trigger one-shot is a normal active voice until it runs
off `playEnd`; it can be stolen like any voice. No new stealing rule.
**Confirmed from the core (`sampler_core.cpp`):** the read loop advances `readPos_` by an
arbitrary `ratio_` per frame with 2-point linear interpolation, and the amp is a per-frame
`env_.tick()` multiply — so both the AHDSR hold stage and the Trigger fade/%-length envelope
are **per-frame amplitude functions** over the existing read machinery, and the start point
is just a non-zero initial `readPos_`. No resampler or voice-lifecycle rewrite is needed.
**Parameter ownership (D-B).** The play mode + its params (Gate: AHDSR; Trigger: %-length +
fade-in + fade-out; both: start point) attach to the **capture selection / zone** and live
in the instrument's **performance map** (component state, version-bumped, back-compat: a
truncated/older blob defaults to **Gate, hold=0, start=0, no fades = exactly today**). Start
point joins `rootOverride` / loop-override as another per-`PerformanceZone` optional
override; a per-zone `PlayMode` + param struct is added additively. **Fork S15-F2 (flagged):**
per-capture-selection *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's back-compat lift), so one storage site serves both; flagged because it touches
S10's single-capture setup surface shape.
**Editor (mode-aware, on the S11 waveform surface).** Gate shows draggable **start + loop
markers**; Trigger shows **start + %-length end + fade-in/out** handles — same waveform, same
pure `frame↔pixel` + marker-grab geometry module (S11), mode switches which markers draw. A
**mode toggle** per capture/zone sits in the S10 guided setup / S10-Z Zones panel. Every edit
commits **off-thread** via `commitMapAndReload`; the instrument stays a **read-only bank
consumer** (mode/params are performance map, never written to `Sample` or the bank).
### Pitch envelope — AD, off by default (S16)
A per-voice **pitch modulation curve** on top of a zone's base repitch — a short **AD**
(attack-decay) envelope that biases the voice's read-increment over time. **Off by default**
(so existing playback is bit-identical). The classic use is a percussive **pitch drop**.
- **Shape (lean, build-time residual): two-segment AD** — at note-on the pitch offset rises
to `peakSemitones` over `attackFrames`, then falls to 0 (base pitch) over `decayFrames`.
A **zero attack** gives the pure "start high, drop to base" percussive drop; a positive
peak that settles to 0 is the classic sampler pitch envelope. Documented; the alternative
(start-offset → monotone glide to base) is the degenerate `attack=0` case of this.
- **Range: semitones (±).** `peakSemitones` is signed (positive = start/peak above base,
negative = below); default depth range noted at build.
- **RT implication — confirmed clean.** The core's resampler is already an **arbitrary
per-frame ratio** linear interpolation (`readPos_ += ratio_`, verified in
`sampler_core.cpp`). The pitch envelope is therefore a **per-frame multiply of `ratio_`**
by `2^(pitchEnvSemitones(frame)/12)` — the effective read increment varies frame-by-frame
at no structural cost. **No new resampler, no WDL dependency** for the modulation path. The
envelope is the same per-frame `tick()` idiom as the amp envelope — RT-safe, no allocation
in `process`, per-voice (polyphonic notes each run their own).
- **Ownership + editor.** Per-zone instrument performance-map state (D-B), additive/version-
bumped (absent → disabled). Editor exposure folds into the S12 ADSR-editor tier: attack +
decay + a ±semitone depth control, default-off (discoverable but inert until enabled).
### WDL pitch/resample surface — verified finding (feeds S15/S16, not a committed point)
The **full** vendored WDL pitch/resample surface was swept — `vendor/WDL/WDL/resample.h`
and `vendor/WDL/WDL/simple_pitchshift.h` are the **only** two pitch/resample headers; there
is **no** elastique / formant-preserving / time-stretch anywhere in the tree. Honest
findings:
- **`WDL_Resampler` (`resample.h`) — sinc/linear resampler, RT-suitable.**
`SetMode(interp, filtercnt, sinc, sinc_size≤64, sinc_interpsize)`; streaming
`ResamplePrepare`/`ResampleOut` with `Prealloc`. Its **sinc mode beats the core's 2-point
linear interp** for base-repitch quality (less aliasing on large transpositions) at a real
CPU cost (up-to-64-tap convolution per output sample vs. one lerp). **Fit:** an *optional
quality upgrade for the base repitch path* (a per-voice linear/sinc toggle) — **not
required** for S15/S16 and **not committed**; held as a Tier-2/3 quality option (per-voice
`WDL_Resampler` instances are heavier, and S16's modulation is cleaner hand-rolled).
- **`WDL_SimplePitchShifter` (`simple_pitchshift.h`) — time-domain OLA pitch shifter, wrong
tool.** It shifts pitch *preserving duration* — the opposite of a sampler's
repitch-by-resampling (which changes pitch and duration together). Its `set_formant_shift`
is an **explicit empty stub** (no formant preservation). Not a fit for the sampler
repitch/envelope path; noted for completeness.
- **Formant-preserving / time-stretch (elastique-class): NOT available.** REAPER's elastique
is **licensed (zplane)**, not in the open WDL/reaper-sdk vendored tree (grep found only
unrelated libpng/giflib string matches). Formant-correct repitch is **unavailable to the
instrument without a new third-party dependency** — out of scope (D5-adjacent). Stated, not
worked around.
- **Recommendation:** S16's pitch-envelope ratio-modulation stays **hand-rolled** (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.
### Sequencing (S15/S16 against S7 stereo, S10 editor)
S15 and S16 are **S3-core extensions** — they touch the engine Daniel smoke-tests, like S7.
They are **channel-count-agnostic by construction**: the play-mode envelope is a per-frame
**amplitude** function and the pitch envelope is a per-frame **read-rate scalar** — both
independent of how many channels `SampleData` carries. So they **compose cleanly with S7's
channel dimension** rather than conflicting: S7 adds a channel axis to the read/mix; S15/S16
add an amplitude-shape axis and a read-rate axis; the three are orthogonal. **Recommended
order:** **S15 before S16** (S16's pitch envelope reuses S15's per-voice param-plumbing +
component-state version bumps; landing S15's `PlayMode`/param struct first gives S16 a home
to hang the pitch-env params on). **S15/S16 relative to S7:** no hard dependency — spec them
so the envelope/mode code never assumes a channel count (it operates per-frame, pre-mix), and
S7 can land before, after, or interleaved. **Relative to S10 (editor):** S15's mode toggle +
Trigger handles and S16's AD control **surface through** the S10/S11 waveform + guided-setup
work, so the *core* halves of S15/S16 can land independently of the editor, with the editor
surfacing following S10/S11 (the same way S12's ADSR editor follows the S3 ADSR math). Land
the **core** engine work (mode split, start point, %-length/fades, pitch-env modulation) as
soon as it is ready — it is testable in CTest without the editor — and wire the UI as the
S10/S11 surfaces mature. **Land S15/S16 core after S10's policy-reversal is settled** only if
sharing the same component-state blob would otherwise churn the version tag twice; otherwise
they are independent.
## Ingest through the bank — the extension owns ingest (decided "option 1", 2026-07-26; PLAN.md S8)
**Decided: loading a sample into the sampler is ONE gesture — capture/import-into-bank AND
@@ -1605,6 +1747,12 @@ Set by Daniel on DAW-testing the S1S6 instrument, alongside the UX-overhaul d
- **Bank-generation seam (S9).** New forever-stable `ext_keys.h` key for the generation
counter; read over the same bridge `GetProjExtState` path S4 already uses. No new API —
confirm no torn-read hazard on the integer key.
- **WDL pitch/resample (S15/S16).** **Verified this pass:** `vendor/WDL/WDL/resample.h`
(`WDL_Resampler` — sinc/linear, RT-suitable) and `vendor/WDL/WDL/simple_pitchshift.h`
(`WDL_SimplePitchShifter` — time-domain OLA, `set_formant_shift` empty stub) are the whole
pitch/resample surface; **no** elastique / formant-preserving / time-stretch in the tree.
S16 modulation is hand-rolled (no WDL). If the held sinc-repitch quality upgrade is taken,
verify `WDL_Resampler` streaming/prealloc against the per-voice RT budget before use.
- **LICE/SWELL editor.** Reuses the `bank_panel` LICE/SWELL drawing surface; verify the
`IPlugView`↔LICE window/bitmap bridge at the spike (window creation, sizing, event
routing) — the least-trodden edge of the phase.
@@ -1774,7 +1922,24 @@ a multi-capture drag over the FX button is either rejected or loads the first).
S7 stereo is *not* a Tier-2 feature — it is a channel-count dimension on the existing
Tier 01 engine, orthogonal to Tier 2's velocity-layers / round-robin / per-sample trim.
(S7's stereo loop read is the same loop the core already has, extended per-channel — not
the Tier-2 "sustain loops" feature.)
the Tier-2 "sustain loops" feature.) **Likewise S15/S16** (Trigger/Gate modes + pitch
envelope) are Daniel-directed engine features on the Tier 01 core, *not* Tier 2/3 — the
AHDSR hold, Trigger one-shot, start point, and AD pitch envelope are orthogonal amplitude-
shape / read-rate dimensions, not the held velocity-layers / round-robin / filter work.
- **S15/S16 params are performance choices, not bank facts.** Play mode, start point,
%-length, fades, AHDSR, and the pitch envelope are per-instance performance-map state
(component state), never written to `Sample` or the bank (D-B). The bank carries file
facts (root note, loop intrinsic, channel count); the instrument owns how they are played.
- **S15/S16 stay channel-count-agnostic (S7 interplay).** The mode/envelope logic is
per-frame amplitude and read-rate, independent of the S7 channel dimension. Any S15/S16
code that assumes a fixed channel count (mono) — rather than operating per-frame pre-mix —
is a bug that would collide with S7. Spec and build them channel-agnostic.
- **Trigger ignores note-off; choke is out of scope (S15).** In Trigger mode note-off is a
no-op and the one-shot plays to `playEnd`. Choke-on-note-off / choke-groups are held
(fork S15-F1, Tier-3-adjacent) — do not add a choke path in S15.
- **Pitch envelope is off by default (S16).** Default-disabled → offset always 0 → `ratio_`
unchanged → playback bit-identical to pre-S16. A regression that applies pitch modulation
when the envelope is off is a bug.
- **Drop-and-load must not regress the two existing drags.** S17 inserts a new middle case
(`InstrumentDrop`) between the M11 OS drag-out and the internal bank-to-bank drag; both
existing gestures stay byte-for-byte unchanged in their own regions. Drop-and-load never
+156 -1
View File
@@ -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·(framesstart))` 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 01 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)
+74 -6
View File
@@ -7,7 +7,12 @@ the **product framing behind a scoped phase**. Its build roadmap lives in **PLAN
*why* (the plugin-format reasoning, the bare-VST3-vs-JUCE assessment, the settled
decision record).
Status: framed by product-designer (2026-07-26), **revised 2026-07-26 (r6)**. r6 records the
Status: framed by product-designer (2026-07-26), **revised 2026-07-26 (r7)**. r7 records the
**sampling-modes engine directive** (Daniel, 2026-07-26): Trigger vs Gate play modes (Gate =
AHDSR, Trigger = one-shot with %-length + fades), a modifiable start point in both, and an
off-by-default AD pitch envelope — specced as **new Phase S points S15/S16**, with the WDL
pitch surface swept and reported (sinc `WDL_Resampler` held as an optional quality upgrade;
no formant-preserving/elastique in WDL). See the "sampling modes" Addendum in §4. r6 records the
**workflow-first reframe of S10** (Daniel, 2026-07-26): the editor's default face becomes a
**capture browser + guided single-capture setup**, a fresh instance is **silent with a "pick
a capture" empty state** (reversing the S4 first-sample auto-play), and multi-zone editing is
@@ -770,6 +775,60 @@ binding can be more nuanced than "UID only" (an FXID match is involved), so UID-
is **not** safe to assert from source. Verify by save-rename-reopen in the DAW; if REAPER keys
partly on filename, fall back to keeping the filename and record that as shipped.
### Addendum — sampling modes (Trigger/Gate) + pitch envelope (Daniel, 2026-07-26)
Daniel directed a set of engine features for the sampler, specced as **new Phase S points
S15 (Trigger vs Gate) and S16 (pitch envelope)**. **The feature set is settled** — recorded
here per the doc's settled-decisions convention; PLAN.md §S15/S16 and CONTEXT.md §Sampling
modes carry the spec. Two forks are flagged with leans (S15-F1 choke, S15-F2 param
granularity); the WDL question was resolved by inspection.
**Directive, verbatim (Daniel, 2026-07-26):** *"let's have product spec out some features
for the sampler: Sampling mode: Trigger vs Gate. Gate has an AHDSR envelope. Trigger has
fade in, % length, and fade out. Both modes have modifiable start point, Gate has modifiable
loop points too. In addition to amp env, there will be a pitch envelope/curve (AD?) which is
off by default. Explore using WDL pitch capabilities."*
**What was specced (the shape is product judgment; the feature set is Daniel's):**
1. **Play mode — Gate vs Trigger (S15), per-sample/per-zone, instrument-owned (D-B).**
- **Gate** = classic held note: the current ADSR grows a **Hold** stage → **AHDSR**
(hold=0 is exactly today's ADSR, back-compat); note-off → release; **sustain loop
applies** (S11's loop markers become Gate-mode UI).
- **Trigger** = one-shot drum-pad: note-on fires a **% of sample length** with a
**fade-in** and **fade-out**, **ignores note-off**, **no loop**. Fade default
**equal-power** (click-free); note-off is a no-op (choke held, fork S15-F1).
- **Both:** a **modifiable start point** (non-zero initial read position).
- **Confirmed from `sampler_core.cpp`:** the read loop already advances by an arbitrary
per-frame ratio with linear interp and applies a per-frame amp tick, so both envelopes
are per-frame amplitude functions and the start point is a non-zero initial `readPos_`
— no resampler or voice-lifecycle rewrite.
2. **Pitch envelope — AD, off by default (S16).** A per-voice AD curve biasing the read
increment (the classic pitch drop). **RT clean, confirmed:** the resampler is already an
arbitrary per-frame `readPos_ += ratio_`, so the envelope is a per-frame multiply of
`ratio_` by `2^(semitones/12)`**hand-rolled, no new resampler, no WDL dependency**.
Off by default → bit-identical to pre-S16.
3. **WDL pitch capabilities — verified, not lore (full surface swept).** The whole vendored
WDL pitch/resample surface is two headers: **`resample.h`** (`WDL_Resampler`, a real
sinc/linear RT-suitable resampler — its sinc mode *beats* the core's 2-point linear interp
for base-repitch quality at a CPU cost; **held as an optional quality upgrade**, not
needed for S15/S16) and **`simple_pitchshift.h`** (`WDL_SimplePitchShifter`, a time-domain
OLA *duration-preserving* pitch shifter — wrong tool for a sampler; `set_formant_shift` is
an **empty stub**). **No elastique / formant-preserving / time-stretch exists in WDL**
REAPER's elastique is licensed (zplane), not in the vendored tree. **Recommendation:** S16
modulation stays hand-rolled; `WDL_Resampler` (sinc) is the only WDL piece worth adopting
and only as a held base-repitch quality upgrade.
4. **Sequencing.** S15 before S16 (S16 reuses S15's param plumbing). Both are S3-core
extensions but **channel-count-agnostic by construction** (per-frame amplitude + read-rate,
pre-mix), so they **compose with S7 stereo** rather than conflicting. Core halves land in
CTest independently of the editor; the mode toggle / Trigger handles / AD control surface
through the S10/S11 waveform + setup work.
**Forks flagged (leans given):** *S15-F1 (choke on note-off)***held**, out of S15 scope
(Trigger ignores note-off; choke-groups are Tier-3-adjacent). *S15-F2 (param granularity)*
**lean per-zone only** (the single capture is already a one-zone map), flagged because it
touches S10's single-capture setup surface.
---
## Where this landed
@@ -819,15 +878,24 @@ Post-DAW-test directives (2026-07-26; see the "product name + UX overhaul" Adden
current drawing and adopt the L1 kit when it lands — not gated on Phase L.** Answers
Daniel's "the VST is dogshit / temple os / does Cockos have a toolkit" (2026-07-26,
post-S1S6 DAW test).
10. **Sampling modes + pitch envelope → engine features** (**S15** Trigger vs Gate,
**S16** pitch envelope; see the "sampling modes" Addendum in §4). Gate = AHDSR held note
(hold added to today's ADSR); Trigger = one-shot with %-length + fade-in/out, ignores
note-off; both carry a modifiable start point; Gate keeps loop points. Pitch envelope =
per-voice AD, off by default, hand-rolled over the existing per-frame ratio read (no WDL
needed). WDL swept: `WDL_Resampler` (sinc) is a held optional repitch-quality upgrade;
no formant-preserving/elastique in WDL. Forks S15-F1 (choke, held) / S15-F2 (param
granularity, lean per-zone) flagged. Feature set settled.
**Authoritative from here:** **PLAN.md §Phase S** is the roadmap (S1S6 the original
dependency chain: spike → `Sample` fields → pure sampler core → Tier 0 → Tier 1 → embedded
UI; then **S7** stereo, **S8** ingest, **S9** change-detection, **S10S13** the ReaSampler
9000 UX overhaul); **CONTEXT.md §Phase S** is the spec (seam-field semantics, scope
contracts, the channel-mode / ingest / bank-generation contracts, the UX-overhaul spec, the
product-name convention, the pure/shell split, the must-verify SDK/bridge surfaces). This
doc is the framing/decision record they point back to. The "no PLAN.md footprint" era is
over.
9000 UX overhaul, **S15/S16** the Trigger-vs-Gate + pitch-envelope engine features);
**CONTEXT.md §Phase S** is the spec (seam-field semantics, scope contracts, the channel-mode
/ ingest / bank-generation / sampling-mode / pitch-envelope contracts, the UX-overhaul spec,
the product-name convention, the pure/shell split, the WDL finding, the must-verify
SDK/bridge surfaces). This doc is the framing/decision record they point back to. The "no
PLAN.md footprint" era is over.
---