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