docs(product): reshape S16 to Varispeed/Preserve pitch-engine modes (duration-preserving correction)

Per-zone toggle: Varispeed (classic, cheap) vs Preserve (duration-holding,
WDL_SimplePitchShifter candidate — per-voice RT viability assessed: pre-warm
settles onset latency + queue growth; quality basic; hand-rolled pure module
held as upgrade). Pitch envelope rides either engine. Fork S16-F1: default
mode is Daniel's call.
This commit is contained in:
2026-07-26 20:24:44 -04:00
parent c64ab687bf
commit bf1bf5283c
3 changed files with 478 additions and 145 deletions
+203 -75
View File
@@ -690,51 +690,156 @@ orthogonal dimensions (§sequencing note in CONTEXT.md).
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).
> **S15 × S16 pitch-engine interaction (informs the S16 Preserve engine).** S15's amplitude
> semantics are defined over the voice's **source-frame** timeline, which the S16 pitch-engine
> mode (Varispeed vs Preserve) changes underneath them. Contracts to hold:
> - **Trigger %-length** — under **Preserve**, %-length is measured in **source frames**
> (`playEnd = start + round(lengthFraction·(frames start))`, unchanged) but wall-clock is
> now **stable under transpose** (a transposed Trigger keeps its %-length duration). This is
> *cleaner* than Varispeed, where transposing a Trigger also scales its audible length. So
> S15's %-length spec is unchanged; Preserve just makes it pitch-independent.
> - **Gate sustain loop** — under **Preserve**, loop the **source read** (the `[loopStart,
> loopEnd)` source-frame region S15/S2 already defines) and feed the looped source stream
> into the shifter; the shifter transposes the **output**. Contract: *loop the source, shift
> the output* — the loop points stay source-frame facts (S11's markers are unchanged), and
> the Preserve engine sits after the loop read. Under Varispeed the loop read itself carries
> the pitch (today's behavior).
> - **Start point** — unaffected by engine: it is a source-frame offset (initial read
> position) in both, independent of how pitch is applied.
> These are S16-owned build details (the Preserve engine consumes S15's source-frame read);
> S15 lands its amplitude/read machinery source-frame-defined and channel-agnostic (S7), and
> S16's engine wraps it — no change to S15's committed points.
- [ ] 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.
## S16 — pitch engine modes (Varispeed vs Preserve) + pitch envelope (per-voice)
**Goal:** Give the sampler **two pitch behaviors** and a pitch envelope that rides whichever
is chosen. Repitch today is **Varispeed** — resampling that couples pitch and duration (an
octave up halves the note's duration; the classic sampler / RS5K default). Daniel's directive
(2026-07-26, verbatim: *"isn't that ratio stuff going to change the playback rate? I want
duration-preserving repitching"*) adds **Preserve** — duration-preserving repitch, where a
transposed note keeps its original length. Both are musically legitimate: **drums / one-shots
often want varispeed character** (the pitch-down-lengthens-the-hit sound), **tempo-locked
loops and phrases want Preserve** (a repitched loop still lines up to the bar). So the shape
is a **per-zone/per-capture pitch-engine mode** — a **Varispeed** engine (current, cheap,
`readPos_ += ratio_` resampling) vs a **Preserve** engine (duration-preserving pitch shift).
On top of either engine rides a per-voice **AD pitch envelope**, **off by default** — a short
attack-decay pitch modulation (the classic percussive **pitch drop**): under Varispeed it
biases `ratio_`; under Preserve it biases the shifter's shift amount. Per-instance
performance-map state (D-B). CONTEXT.md §Phase S (Pitch engine modes + pitch envelope).
**Feature settled per the directive; the engine default is a flagged Daniel fork (S16-F1),
the Preserve-engine implementation choice is a flagged fork (S16-F2).**
**Verify (in DAW):**
- **Varispeed engine** (per-zone): a note an octave up plays **half as long** as the root
note (pitch and duration coupled) — the current behavior, now explicitly the Varispeed mode.
- **Preserve engine** (per-zone): a note an octave up plays at the **same duration** as the
root note (pitch shifted, length held) — a Gate held note sustains as long as held; a
Trigger one-shot at %-length keeps its %-length wall-clock regardless of transpose.
- **Pitch envelope off** (default) under **either** engine: playback is identical to the
engine's un-modulated output (regression — no pitch modulation applied).
- **Pitch envelope on**: an AD envelope makes a note **start offset in pitch and glide to the
zone's base pitch** over attack+decay (percussive drop when the offset is positive-then-
settle); range settable in **semitones (±)**; per-voice (polyphonic notes each run their own).
- **Preserve CPU / voice-cap:** with a chord of Preserve-mode voices, CPU stays within budget
and no audio dropout at the polyphony cap; if Preserve is materially heavier, a **Preserve-
mode-specific voice cap** kicks in (below the Varispeed cap) rather than glitching.
- **Latency honesty:** a Preserve note has a small onset latency (the shifter's window); the
spec accepts this as a Preserve-mode property, and a **note-onset click/smear is absent**
(the shifter is pre-warmed at voice-allocation, not cold-started in `process`).
- **RT-safety:** no allocation in `process` for **either** engine — the Varispeed path is the
same per-frame tick idiom as the amp envelope; the Preserve shifter's buffers are
pre-sized/pre-warmed at voice allocation and reused (no queue growth in steady state).
- Pure-core assertions: the pitch-envelope curve against known values (offset at t=0, base at
t=attack+decay); the Varispeed ratio math; the Preserve engine's duration invariance (a
transposed render is the same frame-length as the un-transposed render).
**Depends on:** S3 (the voice read-increment + envelope tick idiom; the Varispeed path *is*
the current read loop), S5 (the `PerformanceZone` the mode + envelope attach to), S15 (the
per-zone param plumbing + component-state version bump the mode/envelope hang on; and the
Gate-loop / Trigger-%-length semantics the Preserve engine must honor — see S15 interaction
below). Independent of S7 (both engines operate per-frame, channel-count agnostic).
> **WDL pitch capabilities — verified finding (feeds S15/S16 build, not a committed point).**
- [ ] Core: **pitch-engine mode on the voice/zone**`PitchEngine { Varispeed, Preserve }`.
**Varispeed** = today's path (`readPos_ += ratio_`, `ratio_ = pitchRatio(note,root)`), pitch
and duration coupled. **Preserve** = duration-preserving: the read advances at the **source**
rate (duration held) while a pitch shifter transposes the output by
`2^((noteroot)/12)`. Mode is per-`PerformanceZone` performance state (D-B), additive/
version-bumped; **default is S16-F1 (flagged Daniel fork — lean Preserve, argued below).**
Absent/older blob → the fork's default. Pure where possible: the Varispeed math and the
duration-invariance contract are unit-tested; the Preserve DSP core is unit-tested for
duration invariance and transpose correctness against a known signal.
- [ ] Core: **Preserve engine implementation (fork S16-F2, flagged).** Two viable routes, both
RT-disciplined (pre-allocated, no locks, no `process` allocation):
- **(a) `WDL_SimplePitchShifter`** (`vendor/WDL/WDL/simple_pitchshift.h`) — a per-voice
time-domain OLA shifter. **Now the right category** (duration-preserving is the
requirement, not the wrong tool it was under the varispeed-only framing). Viability from
the header (assessed this pass): push/pull block API (`GetBuffer`/`BufferDone`/
`GetSamples`), `set_shift(2^(semi/12))` for pitch with `set_tempo(1.0)` for held duration
— pitch and duration are **separately controllable**, exactly Preserve. Per-instance memory
is modest (an OLA ring `bsize = window_ms·srate` ≈ a few KB/voice at the ~50 ms quality-0
window, plus a bounded output queue). CPU is cheap (O(length), a few mults + one OLA
crossfade per frame — REAPER's "SimpleWindowed" mode, known-basic but usable), so N
polyphonic voices each running one is **feasible within RT discipline**. **Costs, stated:**
(i) **latency** — inherent ~half-window onset delay (~25 ms at the 50 ms window) plus
fill-up, so Preserve notes have a real onset lag; mitigated by pre-warming the shifter at
voice-allocation, and it lands mostly on sustained/loop material (Varispeed serves the
tight-transient one-shots); (ii) **queue allocation**`BufferDone` grows `m_queue` via
`WDL_Queue::Add`, an RT hazard *only* until steady state; pre-warm with silence at voice-
allocation so the buffers settle and stop growing; (iii) **quality** — basic OLA, audible
warble on large transpositions and no formant preservation (`set_formant_shift` is an
empty stub), acceptable for the loop/phrase use case.
- **(b) hand-rolled OLA/granular pitch core** as a **pure module** (`pitch_shift`, mirroring
the house pattern — CTest-testable, no REAPER/VST3/WDL type at the boundary). More work,
but full control over latency/window/crossfade, RT-shape owned by us, and it sits natively
alongside `peaks`/`wav_trim`. **Lean: start with (a)** to prove the Preserve mode end-to-
end at low cost, and hold (b) as the quality/latency upgrade if the SimpleWindowed warble
or onset lag proves musically unacceptable — the mode's *contract* is identical either way,
so the engine swap is behind the `PitchEngine::Preserve` seam. **`WDL_Resampler` does not
apply here** — it is a *resampler* (couples duration), a Varispeed-quality option, not a
Preserve engine. **elastique is NOT available** (licensed zplane, not in the vendored tree
— restated, not worked around); JUCE / rubberband / signalsmith are **new-dependency forks
carrying full D-A weight** (bare-VST3-no-framework is locked D-A) — not proposed.
- [ ] Core: a per-voice **AD pitch envelope**, engine-aware — `PitchEnvParams { enabled=false,
int64 attackFrames, int64 decayFrames, double peakSemitones }`. **Shape (lean, build-time
residual):** two-segment AD — at note-on the pitch offset rises to `peakSemitones` over
`attackFrames`, then falls to 0 over `decayFrames` (**zero attack** = the pure "start high,
drop to base" percussive drop). Off by default (`enabled=false` → offset always 0). **Applied
per engine:** under **Varispeed** the offset multiplies `ratio_` by
`2^(pitchEnvSemitones(frame)/12)` (the read-rate bias, as before); under **Preserve** the
offset is **added to the shifter's shift amount** — `set_shift(2^((noteroot +
pitchEnvSemitones(frame))/12))` — so the pitch bends without touching duration. Pure, unit-
tested (t=0 offset, t=attack peak, t=attack+decay → 0; and the semitone→shift/ratio mapping
for both engines).
- [ ] Parameter ownership + editor: pitch-engine mode + pitch envelope are per-zone
instrument performance-map state (D-B), additive/version-bumped (absent → engine default per
S16-F1, envelope disabled). Editor exposure: a **per-zone Varispeed/Preserve toggle** in the
S10 guided setup / S10-Z Zones panel (a two-state control next to the S15 mode toggle), plus
a small AD + ±semitone depth control for the envelope (folds into the S12 ADSR-editor tier).
Default-off envelope so the control is discoverable but inert until enabled. The instrument
stays a **read-only bank consumer** (mode/envelope are performance map, never written to the
bank).
> **S16-F1 (FLAGGED — Daniel fork): the default pitch engine.** **Lean: Preserve default.**
> Argued honestly both ways:
> - **For Preserve default** (the lean): Daniel asked for duration-preserving *unprompted*,
> which reads as the behavior he expects; and the capture workflow is **loop/phrase-heavy**
> (banks are captured slices of a project — tempo-locked material that benefits from
> duration preservation when transposed). For that material, Varispeed's tempo-drift on
> transpose is the surprising/wrong-feeling result.
> - **For Varispeed default** (the honest counter): **Varispeed is the classic-sampler
> expectation** (RS5K, hardware samplers, the whole tradition default to it); it is **cheaper
> and zero-latency** (no shifter, no onset lag); and it is bit-identical to the current
> shipped S3/S5 behavior, so a Preserve default is a *behavior change* for any existing feel.
> Percussive one-shot material specifically *wants* the varispeed character.
> - **Recommendation:** default **Preserve** because Daniel asked for it and the material skews
> loops, but make the per-zone toggle **prominent and cheap to flip** so drum/one-shot zones
> trivially opt into Varispeed. **Daniel's call.**
> **S16-F2 (FLAGGED): the Preserve engine implementation.** `WDL_SimplePitchShifter` (route a,
> low-cost proof) vs a hand-rolled pure `pitch_shift` OLA/granular module (route b, more work,
> full control, house-native + CTest-testable). **Lean: (a) first, (b) as the held quality/
> latency upgrade** — same `PitchEngine::Preserve` contract behind the seam either way. Not
> load-bearing for the *feature* decision (S16-F1); a build-time route choice flagged because
> it sets whether a new vendored-WDL usage or a new pure module enters the tree.
> **WDL pitch capabilities — verified finding (feeds 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:
@@ -744,25 +849,36 @@ agnostic).
> 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.
> the **Varispeed** base repitch path* — a per-voice quality toggle (linear = cheap default,
> sinc = quality) — **not** required for S16 and **not** committed here. A resampler couples
> duration, so it is **not** a Preserve engine. Held as a Tier-2/3 Varispeed-quality option.
> - **`WDL_SimplePitchShifter`** (`simple_pitchshift.h`) — a **time-domain overlap-add,
> duration-preserving pitch shifter** (push/pull block API; `set_shift` for pitch and
> `set_tempo` as an independent duration knob; quality parameter selecting window/overlap
> sizes). **Under Daniel's duration-preserving directive this is the right category** — the
> candidate Preserve engine (fork S16-F2, route a). **Viability (from the header):** per-voice
> instantiable at modest memory (an OLA ring ≈ window·srate, a few KB/voice at the ~50 ms
> quality-0 window, plus a bounded output queue); CPU is cheap (O(length), a few mults + one
> OLA crossfade per frame — no FFT); N polyphonic voices each running one is **feasible in
> RT discipline** with **two caveats:** (i) **inherent latency** ~half-window (~25 ms @ 50 ms
> window) + fill-up → a real note-onset lag (pre-warm at voice-allocation; it lands on
> sustained/loop material where it is least harmful); (ii) `BufferDone` grows `m_queue` via
> `WDL_Queue::Add` — an allocation hazard **only until steady state**, pre-warmed away with a
> silence pass at voice-allocation. **Quality is basic** (SimpleWindowed warble on large
> transpositions) and **`set_formant_shift` is an empty stub** (no formant preservation) —
> acceptable for the loop/phrase Preserve use, replaceable by the hand-rolled route (b) if not.
> - **Formant-preserving / high-quality 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. Formant-correct / studio-grade duration-preserving repitch is
> **unavailable to the instrument** without a new third-party dependency (JUCE / rubberband /
> signalsmith would each be a **new-dependency fork carrying D-A weight** — bare-VST3-no-
> framework is the locked D-A choice — and are **not proposed**). **Stated, not worked around.**
> - **Recommendation:** the **Preserve** engine (S16-F2) is either `WDL_SimplePitchShifter`
> (route a, low-cost proof) or a hand-rolled pure `pitch_shift` module (route b, held quality
> upgrade). The **pitch-envelope** modulation stays hand-rolled over whichever engine (a per-
> frame `ratio_` multiply under Varispeed, a per-frame shift-amount add under Preserve).
> `WDL_Resampler` (sinc) remains a held **Varispeed-quality** upgrade only.
> **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
@@ -900,13 +1016,20 @@ 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. **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.
room for, never a commitment. **Note:** S16 lands the *pitch* envelope + the Varispeed/
Preserve pitch-engine mode early (Daniel's directive) — the Tier-3 "filter/pitch envelopes"
line now means the *filter* envelope + LFOs remainder.
- **Sinc Varispeed-quality upgrade (HELD — WDL_Resampler).** `WDL_Resampler`'s sinc mode
beats the core's 2-point linear interp for **Varispeed** 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. (A
resampler couples duration, so it is a Varispeed-quality option only, **not** a Preserve
engine.)
- **Hand-rolled Preserve pitch core (HELD — fork S16-F2 route b).** A pure `pitch_shift`
OLA/granular module (house pattern, CTest-testable) as the quality/latency upgrade for the
S16 Preserve engine if `WDL_SimplePitchShifter`'s SimpleWindowed warble or onset lag proves
musically unacceptable. Same `PitchEngine::Preserve` contract behind the seam. Held — the
build starts with route a (WDL) to prove Preserve at low cost.
- **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.
@@ -942,12 +1065,17 @@ artifact, not extension-only.
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.
(`WDL_Resampler`, sinc/linear, RT-suitable — a *resampler*, couples duration → Varispeed
path) and `simple_pitchshift.h` (`WDL_SimplePitchShifter`, time-domain OLA, **duration-
preserving** — the S16 Preserve-engine candidate, fork S16-F2 route a) are the **whole**
pitch/resample surface; **no** elastique / formant-preserving in the vendored WDL tree.
**S16 Preserve-engine (route a) must-verify at build:** pre-warm `WDL_SimplePitchShifter`
at voice-allocation so `m_queue` reaches steady state (no `process`-thread `WDL_Queue::Add`
growth); measure the per-voice CPU + onset latency (window·srate) against the polyphony cap;
set the **Preserve-mode-specific voice cap** if the per-voice cost demands one. The pitch-
envelope modulation stays hand-rolled over whichever engine. If the held sinc Varispeed-
quality 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)