docs(plan): tick S9/S15/S16/S18; annotate Preserve as hand-rolled OLA (WDL held as swap)
This commit is contained in:
@@ -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·(frames−start))` 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)
|
||||
|
||||
Reference in New Issue
Block a user