docs(plan): tick S9/S15/S16/S18; annotate Preserve as hand-rolled OLA (WDL held as swap)

This commit is contained in:
2026-07-27 00:30:30 -04:00
parent f79b9cafa3
commit ddc2ec1e75
+41 -35
View File
@@ -381,16 +381,16 @@ cleanly (treated as generation 0; first bump refreshes).
side is extension-only and independent of S8; consumed by S8 and M10 recapture. Best landed
alongside S8.
- [ ] Writer (extension): a monotonic **bank-generation counter** stamped into
- [x] Writer (extension): a monotonic **bank-generation counter** stamped into
`"reasampler"` ext-state (new `ext_keys.h` constant — forever-stable spelling), bumped
on every bank-content mutation that changes what an instance would play (capture add,
recapture-in-place, sample-remove, move/copy affecting the active bank). Additive to the
persist blob; defaults to 0 for projects saved before the stamp exists.
- [ ] Reader (instrument): poll the generation over the bridge on a safe **off-audio-thread
- [x] Reader (instrument): poll the generation over the bridge on a safe **off-audio-thread
cadence** (a UI/timer tick, not `process`), compare to the last-seen value, and call the
existing `reloadFromBank()` on change — reusing S4's atomic pointer-swap handoff so a
refresh mid-play does not glitch. No new audio-thread work; no allocation in `process`.
- [ ] Cadence + coalescing: pick a poll interval that is responsive but cheap (build-time
- [x] Cadence + coalescing: pick a poll interval that is responsive but cheap (build-time
residual — a low-frequency UI timer, coalescing multiple bumps between polls into one
reload). **Must-verify before build:** that a bridge ext-state read on the instrument's
UI/timer thread is safe against a concurrent extension write (the read already tolerates a
@@ -576,7 +576,10 @@ into the voice engine; today they are fixed defaults), S5 (the map the numeric f
> search** (a name-substring narrow over the same cards, composing with S10's bank filter —
> bank filter picks the bank, search narrows within it). The scroll/search geometry is pure,
> layered over the `capture_browser` module S10 builds. Net: S12 = scroll + search over the
> S10 browser + numeric entry + ADSR; the browser *card* work is S10's.
> S10 browser + numeric entry + ADSR; the browser *card* work is S10's. **S12 now also
> carries the S15/S16 control surfaces** (per-zone Gate/Trigger mode toggle, AHDSR hold
> control, Trigger %-length/fade controls, Varispeed/Preserve engine toggle, and the AD pitch
> envelope depth/shape controls) — deferred here from S15 and S16 per spec.
- [ ] Scrollable, searchable capture browser: a scroll offset (wheel + scrollbar drag) so a
bank longer than the panel is fully reachable; a **type-to-filter search** that narrows
@@ -657,7 +660,7 @@ independent of the S7 channel dimension (§sequencing).
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
- [x] 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`)
@@ -665,15 +668,15 @@ orthogonal dimensions (§sequencing note in CONTEXT.md).
**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`
- [x] 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]`
- [x] 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
- [x] 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
@@ -685,13 +688,13 @@ orthogonal dimensions (§sequencing note in CONTEXT.md).
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
- [x] 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).
written to the bank). **Editor control surface deferred to S12 tier (spec-sanctioned).**
> **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
@@ -759,7 +762,7 @@ per-zone param plumbing + component-state version bump the mode/envelope hang on
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).
- [ ] Core: **pitch-engine mode on the voice/zone**`PitchEngine { Varispeed, Preserve }`.
- [x] 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
@@ -768,7 +771,7 @@ below). Independent of S7 (both engines operate per-frame, channel-count agnosti
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
- [x] 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
@@ -787,7 +790,9 @@ below). Independent of S7 (both engines operate per-frame, channel-count agnosti
`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.
empty stub), acceptable for the loop/phrase use case. **Implemented as hand-rolled pure
OLA (`pitch_shift` module, house pattern — CTest-testable, no WDL/REAPER/VST3 type at the
boundary); WDL_SimplePitchShifter excluded by include-chain (windows.h), held as swap.**
- **(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
@@ -799,7 +804,7 @@ below). Independent of S7 (both engines operate per-frame, channel-count agnosti
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,
- [x] 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,
@@ -810,14 +815,14 @@ below). Independent of S7 (both engines operate per-frame, channel-count agnosti
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
- [x] 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).
bank). **Editor control surface deferred to S12 tier (spec-sanctioned).**
> **S16-F1 (FLAGGED — Daniel fork): the default pitch engine.** **Lean: Preserve default.**
> Argued honestly both ways:
@@ -1073,7 +1078,7 @@ namespace, so it inherits this isolation with no S18 change; no coordination edi
only the shared awareness that all wire keys must live under `kProjExtNamespace()` (already
the rule). Dispatchable in parallel with, or immediately after, the in-flight waves.
- [ ] **Beta VST3 class UID (the permanent commitment).** Mint a second FOREVER-STABLE
- [x] **Beta VST3 class UID (the permanent commitment).** Mint a second FOREVER-STABLE
class UID in `reasampler_vst.h` alongside the existing `kReaSamplerProcessorUID`
(`REASAMPLER_PROC_UID_1..4`) — a distinct forever-frozen constant (e.g.
`REASAMPLER_PROC_UID_BETA_1..4` + `kReaSamplerProcessorUIDBeta`). The channel bit selects
@@ -1087,7 +1092,7 @@ the rule). Dispatchable in parallel with, or immediately after, the in-flight wa
VST ships). Lean: **mint now** — mirrors how the stable UID was minted at the S1 spike
(locked long before ship), removes a "remember to mint before shipping" landmine, and the
cost of an unused-until-beta constant is zero. Flagged because it is a forever commitment.
- [ ] **Channel-derived binary + display identity (no scattered `#ifdef`s — the V4
- [x] **Channel-derived binary + display identity (no scattered `#ifdef`s — the V4
invariant).** Route all VST identity through `app_version` accessors, mirroring how the
extension's `OUTPUT_NAME` already derives from `REASAMPLER_OUTPUT_NAME`. (a) **Binary
name:** the CMake VST3 target's `OUTPUT_NAME` forks by channel — `reasampler_9000`
@@ -1100,7 +1105,7 @@ the rule). Dispatchable in parallel with, or immediately after, the in-flight wa
`vstPluginName()`), not a literal in `reasampler_vst.h`/`vst_entry.cpp`. (c) **Editor title
band + S6 embed-strip label** are channel-aware from the same accessor. All fan out from the
ONE channel bit — no per-file `#ifdef`.
- [ ] **Factory vendor/version strings channel-aware where V4 does the equivalent.** The
- [x] **Factory vendor/version strings channel-aware where V4 does the equivalent.** The
`BEGIN_FACTORY` vendor/url/email and the `PClassInfo2` version string
(`REASAMPLER_VST_VERSION`, currently a fixed `"0.1.0.0"`) align with the channel where the
extension's V4 equivalents do — the version display carries the `-beta` render
@@ -1108,7 +1113,7 @@ the rule). Dispatchable in parallel with, or immediately after, the in-flight wa
unless V4 qualified them (**check V4's treatment and match it** — V4 kept the lane-name
prefix shared, so shared-where-V4-shares is the default). No new forever-stable string
beyond the UID and the id-family V4 already owns.
- [ ] **Pairing-surface invariant recorded (no new code — a documented guarantee).** Write
- [x] **Pairing-surface invariant recorded (no new code — a documented guarantee).** Write
the complete pairing surface as an invariant in CONTEXT.md: a channel's VST talks to that
channel's extension **only**, because (1) plugin identity (UID + filename + display) is
channel-forked here, and (2) **all** wire keys — `banks`, `assign_request`, S9's
@@ -1117,7 +1122,7 @@ the rule). Dispatchable in parallel with, or immediately after, the in-flight wa
isolation is structural — no per-key opt-in — so a future wire key that forgets to isolate
is impossible by construction** (it keys under the namespace accessor, not a raw literal).
This is the guard that S9/S8/S17's cross-artifact keys inherit isolation for free.
- [ ] **DAW-verify contract (the acceptance gate, no unit test — identity is a shell fact).**
- [x] **DAW-verify contract (the acceptance gate, no unit test — identity is a shell fact).**
Both channels installed side-by-side: each browser sees only its channel's banks; a
stable-project + beta-VST opens clean-empty (not error); the S-NAME-1 rename/rebind compat
test extended to the beta UID (save a beta instance, confirm it rebinds by the beta UID on
@@ -1140,11 +1145,11 @@ the rule). Dispatchable in parallel with, or immediately after, the in-flight wa
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.
- **WDL_SimplePitchShifter swap (HELD — fork S16-F2 route a).** `WDL_SimplePitchShifter`
as a drop-in swap for the `pitch_shift` pure module if the hand-rolled OLA onset latency
or warble proves musically unacceptable. Same `PitchEngine::Preserve` contract behind the
seam. WDL excluded from the shipped build by include-chain (windows.h); held as the
quality/latency alternative.
- **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.
@@ -1163,10 +1168,14 @@ the rule). Dispatchable in parallel with, or immediately after, the in-flight wa
exposed via `queryInterface` on the controller; inline strip drawn into REAPER's
embed bitmap (LICE idiom, no HWND); `WM_GETMINMAXINFO` size hints confirmed;
embed open/close/resize lifecycle clean.
- **VST3 bus arrangement (S7)** — `setBusArrangements` / `getBusArrangement` and REAPER's
- **VST3 bus arrangement (S7)** — ~~`setBusArrangements` / `getBusArrangement` and REAPER's
mono/stereo instrument-bus expectations, against the vendored Steinberg SDK +
`reaper_vst3_interfaces.h`. The channel-mode toggle depends on the output bus
re-negotiating cleanly.
re-negotiating cleanly.~~ **verified (S7):** `setBusArrangements` accepts only the mode's
arrangement (kMono/kStereo); `getBusArrangement` (base default) reports the valid mode;
a runtime mode change calls `restartComponent(kIoChanged)` so REAPER re-negotiates;
verified against the vendored Steinberg SDK (`ivstaudioprocessor.h`,
`vstsinglecomponenteffect.cpp` base impl, `ivsteditcontroller.h` kIoChanged).
- **Media Explorer surface (S8)** — confirmed thin against the vendored headers:
`OpenMediaExplorer` (open/select) + `MediaExplorerGetLastPlayedFileInfo` (read the one
last-played/selected file + range) are the whole contract; **no** enumerate-selected and
@@ -1184,13 +1193,10 @@ the rule). Dispatchable in parallel with, or immediately after, the in-flight wa
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.
**S16 Preserve engine shipped as hand-rolled pure OLA (`pitch_shift` module); WDL excluded
by include-chain (windows.h), held as swap.** The pitch-envelope modulation is hand-rolled
over the 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)