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:
+165
-57
@@ -1328,6 +1328,15 @@ start point, Gate has modifiable loop points too. In addition to amp env, there
|
||||
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.
|
||||
|
||||
**Daniel's S16 correction (2026-07-26, verbatim):** *"isn't that ratio stuff going to change
|
||||
the playback rate? I want duration-preserving repitching."* Correct — the `readPos_ += ratio_`
|
||||
path is **varispeed** (pitch and duration coupled). S16 is revised from "pitch envelope only"
|
||||
into a **pitch-engine mode (Varispeed vs Preserve) + pitch envelope** (see §Pitch engine
|
||||
modes below). Two new S16 forks are flagged: **S16-F1** (the engine default — lean Preserve)
|
||||
and **S16-F2** (the Preserve implementation — lean `WDL_SimplePitchShifter` first, hand-rolled
|
||||
held). The prior WDL finding that dismissed `WDL_SimplePitchShifter` is **corrected in place**
|
||||
below (duration-preserving is now the requirement, so that shifter is the Preserve candidate).
|
||||
|
||||
### Play mode — Gate vs Trigger (S15)
|
||||
|
||||
Each played sample carries a **play mode** — a per-sample/per-zone **performance choice**
|
||||
@@ -1386,72 +1395,154 @@ pure `frame↔pixel` + marker-grab geometry module (S11), mode switches which ma
|
||||
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)
|
||||
### Pitch engine modes — Varispeed vs Preserve (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**.
|
||||
**Daniel's correction (2026-07-26, verbatim):** *"isn't that ratio stuff going to change the
|
||||
playback rate? I want duration-preserving repitching."* Correct: the `readPos_ += ratio_`
|
||||
resampling path is **Varispeed** — pitch and duration are coupled (an octave up halves the
|
||||
note's duration). Daniel wants **duration-preserving** repitch. So S16 grows a per-voice/
|
||||
per-zone **pitch-engine mode**, not just a pitch envelope:
|
||||
|
||||
- **Varispeed engine (current path).** `ratio_ = pitchRatio(note,root)`, `readPos_ += ratio_`
|
||||
with 2-point linear interp — resampling that couples pitch and duration. This is the
|
||||
**classic sampler / RS5K** behavior and today's shipped S3/S5 output. Cheap, zero-latency.
|
||||
Musically right for **drums / one-shots** (pitch-down-lengthens-the-hit is a feature there).
|
||||
- **Preserve engine (duration-preserving).** The read advances at the **source** rate
|
||||
(duration held) while a **pitch shifter** transposes the output by `2^((note−root)/12)`.
|
||||
Musically right for **tempo-locked loops and phrases** — a transposed loop still lines up to
|
||||
the bar. Since captured banks are project slices (loop/phrase-heavy), this is the default
|
||||
lean (fork S16-F1).
|
||||
|
||||
Mode is **per-`PerformanceZone` performance state (D-B)** — instrument-owned, never a bank
|
||||
fact — additive/version-bumped (absent/older blob → the S16-F1 default). A per-zone
|
||||
**Varispeed/Preserve toggle** surfaces in the S10 guided setup / S10-Z Zones panel.
|
||||
|
||||
**Preserve engine implementation (fork S16-F2).** Two RT-disciplined routes behind the
|
||||
`PitchEngine::Preserve` seam (identical contract either way):
|
||||
- **(a) `WDL_SimplePitchShifter`** (`vendor/WDL/WDL/simple_pitchshift.h`) — a per-voice
|
||||
time-domain OLA shifter. Under the duration-preserving directive this is **the right
|
||||
category** (see the corrected WDL finding below). `set_shift(2^(semi/12))` for pitch,
|
||||
`set_tempo(1.0)` to hold duration — pitch and duration are separately controllable. **Lean:
|
||||
route (a) first** (low-cost proof), with two costs owned in the build: an inherent
|
||||
**onset latency** (~half-window, ~25 ms @ the 50 ms quality-0 window; pre-warm at voice-
|
||||
allocation, and it lands on sustained/loop material where least harmful) and a **queue-growth
|
||||
allocation** hazard in `BufferDone` (`WDL_Queue::Add`) that is settled by a silence pre-warm
|
||||
at voice-allocation so no `process`-thread allocation occurs in steady state.
|
||||
- **(b) hand-rolled pure `pitch_shift` OLA/granular module** (house pattern — CTest-testable,
|
||||
no REAPER/VST3/WDL type at the boundary) — **held** as the quality/latency upgrade if the
|
||||
SimpleWindowed warble or onset lag proves musically unacceptable.
|
||||
|
||||
**`WDL_Resampler` is not a Preserve engine** — it is a *resampler* (couples duration); it
|
||||
remains a held **Varispeed-quality** upgrade only. **elastique is NOT available** (licensed
|
||||
zplane, not vendored — restated). JUCE / rubberband / signalsmith are **new-dependency forks
|
||||
carrying D-A weight** (bare-VST3-no-framework is the locked D-A) — **not proposed**.
|
||||
|
||||
**S15 × S16 interaction (Preserve consumes S15's source-frame read).** S15's amplitude
|
||||
semantics are defined over the voice's **source-frame** timeline; the Preserve engine wraps
|
||||
that read and transposes the output, so:
|
||||
- **Trigger %-length** stays a source-frame fact (`playEnd = start + round(lengthFraction·
|
||||
(frames − start))`); under Preserve its **wall-clock is stable under transpose** — *cleaner*
|
||||
than Varispeed, where transposing a Trigger also scales its audible length.
|
||||
- **Gate sustain loop** — under Preserve, **loop the source read** (the `[loopStart, loopEnd)`
|
||||
source-frame region) and feed the looped stream into the shifter, which transposes the
|
||||
**output**. Contract: *loop the source, shift the output*; loop points stay source-frame
|
||||
facts (S11 markers unchanged). Under Varispeed the loop read itself carries the pitch.
|
||||
- **Start point** is a source-frame offset in both engines (engine-independent).
|
||||
|
||||
### Pitch envelope — AD, off by default, engine-aware (S16)
|
||||
|
||||
A per-voice **pitch modulation curve** riding on top of whichever engine — a short **AD**
|
||||
(attack-decay) envelope that biases pitch over time. **Off by default** (so existing playback
|
||||
is bit-identical under the same engine). 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).
|
||||
A **zero attack** gives the pure "start high, drop to base" percussive drop.
|
||||
- **Range: semitones (±).** `peakSemitones` is signed; default depth range noted at build.
|
||||
- **Applied per engine.** Under **Varispeed** the offset is a **per-frame multiply of
|
||||
`ratio_`** by `2^(pitchEnvSemitones(frame)/12)` (the effective read increment varies frame-
|
||||
by-frame at no structural cost — the same per-frame `tick()` idiom as the amp envelope,
|
||||
RT-safe, no `process` allocation). Under **Preserve** the offset is **added to the shifter's
|
||||
shift amount** — `set_shift(2^((note−root + pitchEnvSemitones(frame))/12))` — bending pitch
|
||||
without touching duration. 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)
|
||||
### WDL pitch/resample surface — corrected finding (feeds 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:
|
||||
**Corrected 2026-07-26 (Daniel's duration-preserving directive).** The prior sweep dismissed
|
||||
`WDL_SimplePitchShifter` as "wrong tool (duration-preserving)". Under the directive,
|
||||
**duration-preserving is the requirement**, so that header is the Preserve-engine candidate,
|
||||
not a mismatch — a real viability assessment replaces the dismissal.
|
||||
|
||||
The **full** vendored WDL pitch/resample surface is `vendor/WDL/WDL/resample.h` and
|
||||
`vendor/WDL/WDL/simple_pitchshift.h` — the **only** two pitch/resample headers; there is
|
||||
**no** elastique / formant-preserving 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.
|
||||
`ResamplePrepare`/`ResampleOut` with `Prealloc`. Its sinc mode beats the core's 2-point
|
||||
linear interp for **Varispeed** base-repitch quality (less aliasing on large transpositions)
|
||||
at a real CPU cost. **A resampler couples duration** → a Varispeed-quality option, **not a
|
||||
Preserve engine.** Held as a Tier-2/3 Varispeed-quality toggle; not committed.
|
||||
- **`WDL_SimplePitchShifter` (`simple_pitchshift.h`) — time-domain OLA, duration-preserving —
|
||||
the S16 Preserve-engine candidate (fork S16-F2 route a).** Viability from the header:
|
||||
- **API shape:** push/pull, block-based. `GetBuffer(size)` returns an input buffer to fill;
|
||||
`BufferDone(filled)` runs the OLA shift and queues output; `GetSamples(req, buf)` pulls
|
||||
from the queue. Config: `set_srate`, `set_nch`, **`set_shift(ratio)` (pitch, duration-
|
||||
preserving)**, `set_tempo(scale)` (an *independent* duration knob — Preserve uses
|
||||
`set_tempo(1.0)`), `SetQualityParameter(q)` (selects window/overlap ms from a fixed table).
|
||||
- **Per-voice instantiability / memory:** modest. `m_psbuf` is an OLA ring of `bsize·nch`
|
||||
where `bsize = window_ms · 0.001 · srate` (≈ 2205 frames at 50 ms / 44.1 kHz ≈ a few
|
||||
KB/voice), plus `m_inbuf` (one input block) and a bounded `m_queue`. `m_rsbuf` allocates
|
||||
only when `set_tempo ≠ 1` (unused in Preserve). One instance per voice is cheap in memory.
|
||||
- **RT-safety:** allocations occur in `BufferDone` — `m_psbuf.Resize` (once, when
|
||||
`bsize·nch` first sets, at a fixed quality/srate/nch — pre-warmable) and `m_queue.Add`
|
||||
(grows only until the push/pull cadence reaches steady state). **Pre-warm at voice-
|
||||
allocation** (run silence through once so `m_psbuf` sizes and `m_queue` settles); after
|
||||
that no `process`-thread allocation. No locks. **RT-viable with the pre-warm discipline.**
|
||||
- **Latency:** inherent ~half-window (initial `m_pspos = bsize/2` → ~25 ms @ 50 ms window)
|
||||
plus fill-up — a **real note-onset lag**. This is the load-bearing cost. Mitigation:
|
||||
pre-warm; and Varispeed (zero-latency) serves the tight-transient one-shot material, so the
|
||||
lag lands on sustained/loop material where least harmful. Smaller-window quality settings
|
||||
(the table goes to 3–10 ms) trade latency for more warble.
|
||||
- **Quality:** basic — this is REAPER's "SimpleWindowed" mode. Audible warble on large
|
||||
transpositions; **`set_formant_shift` is an explicit empty stub** → no formant preservation.
|
||||
Usable for loop/phrase Preserve; replaceable by route (b) if not.
|
||||
- **CPU / polyphony:** `PitchShiftBlock` is O(length) per block — a few mults + one OLA
|
||||
crossfade branch per frame, **no FFT**. Per-voice cost is modest; **N polyphonic voices
|
||||
each running one is feasible** within RT discipline. If the aggregate cost is material, a
|
||||
**Preserve-mode-specific voice cap** (below the Varispeed cap) is the pressure valve —
|
||||
flagged in Verify, set from measured per-voice budget at build.
|
||||
- **Formant-preserving / studio-grade time-stretch (elastique-class): NOT in WDL, confirmed.**
|
||||
REAPER's elastique is **licensed (zplane)**, not in the vendored tree (grep found only
|
||||
unrelated libpng/giflib string matches). Formant-correct duration-preserving repitch is
|
||||
**unavailable without a new third-party dependency** (JUCE / rubberband / signalsmith each a
|
||||
new-dependency fork with D-A weight — not proposed). Stated, not worked around.
|
||||
- **Recommendation:** the **Preserve** engine (S16-F2) is `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) is a held **Varispeed-quality** upgrade only.
|
||||
|
||||
### 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
|
||||
**amplitude** function, and both pitch engines carry the channel dimension internally — the
|
||||
**Varispeed** path is a per-frame per-channel read-rate scalar, and the **Preserve** shifter
|
||||
is **`set_nch`-aware** (one shifter instance per voice transposes all its channels together).
|
||||
So S15/S16 **compose cleanly with S7's channel dimension** rather than conflicting: S7 adds a
|
||||
channel axis to the read/mix; S15 adds an amplitude-shape axis; S16 adds a pitch-engine +
|
||||
read-rate axis; all orthogonal. **Recommended order:** **S15 before S16** (S16 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-engine mode + pitch-env params on). **S16 is
|
||||
now meaningfully heavier than the prior "just an envelope" framing** — the Preserve engine is
|
||||
a per-voice DSP object with its own RT budget, pre-warm, and possible voice-cap; treat S16's
|
||||
Preserve-engine point as the phase's next real DSP spike, not a thin add-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; the Preserve shifter is `set_nch`-driven), 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
|
||||
@@ -1748,11 +1839,17 @@ Set by Daniel on DAW-testing the S1–S6 instrument, alongside the UX-overhaul d
|
||||
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.
|
||||
(`WDL_Resampler` — sinc/linear resampler, couples duration → **Varispeed** path) and
|
||||
`vendor/WDL/WDL/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 tree. **S16
|
||||
Preserve-engine (route a) must-verify at build:** (i) **pre-warm** `WDL_SimplePitchShifter`
|
||||
at voice-allocation (run silence so `m_psbuf` sizes and `m_queue` reaches steady state) →
|
||||
**no `process`-thread `WDL_Queue::Add` growth**; (ii) measure **per-voice CPU + onset
|
||||
latency** (window·srate) against the polyphony cap; (iii) set a **Preserve-mode-specific
|
||||
voice cap** if the per-voice cost demands one. The pitch-envelope modulation is hand-rolled
|
||||
over whichever engine. If the held sinc **Varispeed**-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.
|
||||
@@ -1937,9 +2034,20 @@ a multi-capture drag over the FX button is either rejected or loads the first).
|
||||
- **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.
|
||||
- **Pitch envelope is off by default (S16).** Default-disabled → offset always 0 → the
|
||||
engine's un-modulated output → playback bit-identical to the same engine pre-envelope. A
|
||||
regression that applies pitch modulation when the envelope is off is a bug.
|
||||
- **Pitch engine is a per-zone performance choice, not a bank fact (S16).** Varispeed vs
|
||||
Preserve is per-`PerformanceZone` component state (D-B), never written to `Sample` or the
|
||||
bank. The engine default is fork S16-F1 (**lean Preserve** — Daniel's call), with a
|
||||
prominent per-zone toggle so drum/one-shot zones opt into Varispeed cheaply.
|
||||
- **Preserve engine is RT-disciplined (S16).** The `WDL_SimplePitchShifter` (or hand-rolled)
|
||||
Preserve path **pre-warms at voice-allocation** and does **no allocation in `process`** — a
|
||||
`WDL_Queue::Add` or `Resize` on the audio thread in steady state is a bug. Preserve's onset
|
||||
latency (shifter window) is an accepted property, **not** a defect; a note-onset **click or
|
||||
smear** from a cold-started (un-pre-warmed) shifter **is** a bug.
|
||||
- **`WDL_Resampler` is not a Preserve engine (S16).** It is a resampler (couples duration) —
|
||||
a held Varispeed-quality option only. Do not wire it as the duration-preserving path.
|
||||
- **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
|
||||
|
||||
@@ -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^((note−root)/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^((note−root +
|
||||
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 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. **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)
|
||||
|
||||
+110
-13
@@ -7,12 +7,20 @@ 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 (r7)**. r7 records the
|
||||
Status: framed by product-designer (2026-07-26), **revised 2026-07-26 (r8)**. r8 records
|
||||
Daniel's **duration-preserving correction** (2026-07-26, verbatim: *"isn't that ratio stuff
|
||||
going to change the playback rate? I want duration-preserving repitching"*): the ratio path is
|
||||
**varispeed** (pitch/duration coupled), so S16 is reshaped from "pitch envelope only" into a
|
||||
**pitch-engine mode — Varispeed vs Preserve — plus the pitch envelope** riding either engine.
|
||||
The WDL verdict flips: `WDL_SimplePitchShifter` (duration-preserving OLA), previously dismissed
|
||||
as the wrong tool, is **now the Preserve-engine candidate** and got a real per-voice RT
|
||||
viability assessment. Two S16 forks flagged: **S16-F1** (engine default — lean Preserve) and
|
||||
**S16-F2** (Preserve implementation — WDL shifter first, hand-rolled pure module held). See the
|
||||
r8 Addendum in §4. 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
|
||||
pitch surface swept and reported. 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
|
||||
@@ -777,6 +785,12 @@ 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)
|
||||
|
||||
> **Superseded in part by the r8 Addendum below (2026-07-26).** Daniel's duration-preserving
|
||||
> correction reshaped S16 from "pitch envelope only" into a Varispeed/Preserve pitch-engine
|
||||
> mode, and **flipped this addendum's WDL verdict** — `WDL_SimplePitchShifter` (called the
|
||||
> "wrong tool" in item 3 below) is now the Preserve-engine candidate. Read this as the r7
|
||||
> point-in-time record; the r8 Addendum carries the current S16 shape.
|
||||
|
||||
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
|
||||
@@ -829,6 +843,81 @@ off by default. Explore using WDL pitch capabilities."*
|
||||
**lean per-zone only** (the single capture is already a one-zone map), flagged because it
|
||||
touches S10's single-capture setup surface.
|
||||
|
||||
### Addendum — duration-preserving correction: S16 becomes pitch-engine modes (Daniel, 2026-07-26, r8)
|
||||
|
||||
**Correction, verbatim (Daniel, 2026-07-26):** *"isn't that ratio stuff going to change the
|
||||
playback rate? I want duration-preserving repitching."* Daniel is right about the mechanics.
|
||||
The r7 S16 spec modulated pitch by biasing the per-frame read ratio (`readPos_ += ratio_`) —
|
||||
that is **varispeed**: pitch and duration are coupled (an octave up halves the note's
|
||||
duration). Daniel wants **duration-preserving** repitch (a transposed note keeps its length).
|
||||
This reshapes S16 and **flips the r7 WDL verdict** on `WDL_SimplePitchShifter`.
|
||||
|
||||
**What changed (the reframe, then the spec):**
|
||||
|
||||
1. **The reframe — this is a mode, not a replacement.** Both behaviors are musically
|
||||
legitimate, so the answer is not "swap varispeed for preserve" but **a per-zone/per-capture
|
||||
pitch-engine mode**:
|
||||
- **Varispeed** (current path, cheap, zero-latency) — pitch/duration coupled. The
|
||||
**classic sampler / RS5K** default; right for **drums / one-shots** (pitch-down-lengthens-
|
||||
the-hit is a feature there).
|
||||
- **Preserve** (duration-preserving) — a pitch shifter transposes the output while the read
|
||||
holds the source duration. Right for **tempo-locked loops and phrases** (a transposed loop
|
||||
still lines up to the bar) — which is what captured banks skew toward (project slices).
|
||||
The pitch envelope (r7's S16 body) then rides **either** engine: under Varispeed it biases
|
||||
the read ratio (as specced); under Preserve it biases the shifter's shift amount. So the
|
||||
envelope is preserved, re-homed onto the engine seam.
|
||||
|
||||
2. **Fork S16-F1 (Daniel's call): the default engine. Lean Preserve.** Argued honestly:
|
||||
Preserve because Daniel asked for it **unprompted** (reads as his expectation) and the
|
||||
capture workflow is **loop/phrase-heavy**; but Varispeed is the **classic-sampler
|
||||
expectation**, is **cheaper + zero-latency**, is **bit-identical to today's shipped feel**,
|
||||
and is what percussive one-shots want. Recommendation: **default Preserve, prominent cheap
|
||||
per-zone toggle to Varispeed.** Daniel's call.
|
||||
|
||||
3. **WDL verdict corrected — `WDL_SimplePitchShifter` is now the right category.** Under
|
||||
"duration-preserving is the requirement," r7's dismissal ("wrong tool, duration-preserving
|
||||
OLA") inverts: **duration-preserving is exactly what we need.** A real per-voice RT
|
||||
viability assessment (from `vendor/WDL/WDL/simple_pitchshift.h`):
|
||||
- **API:** push/pull block (`GetBuffer`/`BufferDone`/`GetSamples`); `set_shift(2^(semi/12))`
|
||||
for pitch with an **independent** `set_tempo(1.0)` duration knob — pitch and duration
|
||||
separately controllable, exactly Preserve.
|
||||
- **Per-voice:** modest memory (OLA ring ≈ window·srate ≈ a few KB/voice at the 50 ms
|
||||
quality-0 window). CPU cheap (O(length), a few mults + one OLA crossfade/frame, **no
|
||||
FFT**) → **N polyphonic voices each running one is feasible** in RT discipline.
|
||||
- **Costs owned:** (i) **onset latency** ~half-window (~25 ms @ 50 ms) — the load-bearing
|
||||
cost; pre-warm at voice-allocation, and it lands on sustained/loop material (Varispeed
|
||||
serves tight one-shots); (ii) **queue-growth** allocation in `BufferDone` — settled by a
|
||||
silence pre-warm so no `process`-thread allocation in steady state; (iii) **basic
|
||||
quality** (SimpleWindowed warble on big transpositions; `set_formant_shift` is an empty
|
||||
stub → no formant preservation) — acceptable for loops, replaceable by route (b).
|
||||
- **Fork S16-F2:** **route (a)** `WDL_SimplePitchShifter` (low-cost proof) vs **route (b)** a
|
||||
hand-rolled pure `pitch_shift` OLA/granular module (house pattern, CTest-testable, full
|
||||
control). **Lean (a) first, (b) held** as the quality/latency upgrade — same
|
||||
`PitchEngine::Preserve` contract behind the seam.
|
||||
|
||||
4. **Not proposed / restated ceilings.** `WDL_Resampler` is a *resampler* (couples duration) —
|
||||
a held **Varispeed-quality** upgrade, **not** a Preserve engine. **elastique is NOT
|
||||
available** (licensed zplane, not vendored). JUCE / rubberband / signalsmith are each a
|
||||
**new-dependency fork carrying full D-A weight** (bare-VST3-no-framework is the locked D-A
|
||||
choice) — **not proposed**.
|
||||
|
||||
5. **S15 interaction (cleaner under Preserve).** Trigger's **%-length** becomes **pitch-
|
||||
independent** under Preserve (wall-clock stable under transpose — cleaner than Varispeed,
|
||||
where transposing a Trigger also scales its length); Gate's **sustain loop** contract under
|
||||
Preserve is *loop the source read, shift the output* (loop points stay source-frame facts);
|
||||
the **start point** is engine-independent (a source-frame offset). Channel-agnostic for S7
|
||||
(the shifter is `set_nch`-aware; one instance per voice carries all channels).
|
||||
|
||||
6. **RT/CPU honesty.** Preserve is **meaningfully heavier** than varispeed — a per-voice DSP
|
||||
object with its own budget, pre-warm, and a possible **Preserve-mode-specific voice cap**
|
||||
(below the Varispeed cap) if per-voice cost demands it. Put in Verify: pre-warm → no
|
||||
`process` allocation; measure per-voice CPU + onset latency against the polyphony cap. Treat
|
||||
S16's Preserve-engine point as the phase's next real DSP spike, not a thin envelope add-on.
|
||||
|
||||
**Where the spec lives:** PLAN.md §S16 (reshaped to "pitch engine modes + pitch envelope",
|
||||
with forks S16-F1/F2 and the corrected WDL finding) and the S15 × S16 interaction note;
|
||||
CONTEXT.md §Pitch engine modes — Varispeed vs Preserve + the corrected WDL surface finding.
|
||||
|
||||
---
|
||||
|
||||
## Where this landed
|
||||
@@ -878,21 +967,29 @@ 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.
|
||||
10. **Sampling modes + pitch engine → engine features** (**S15** Trigger vs Gate, **S16**
|
||||
pitch-engine modes + pitch envelope; see the "sampling modes" r7 + "duration-preserving"
|
||||
r8 Addenda 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. **S16 reshaped (r8, Daniel's duration-preserving correction):** a per-
|
||||
zone **pitch-engine mode — Varispeed** (current, cheap, pitch/duration coupled — classic
|
||||
sampler, right for drums) **vs Preserve** (duration-preserving via a per-voice pitch
|
||||
shifter — right for tempo-locked loops/phrases). Pitch envelope = per-voice AD, off by
|
||||
default, riding either engine (biases `ratio_` under Varispeed, the shift amount under
|
||||
Preserve). WDL verdict corrected: **`WDL_SimplePitchShifter` is the Preserve-engine
|
||||
candidate** (duration-preserving OLA — RT-viable per-voice with pre-warm; the load-bearing
|
||||
cost is onset latency), `WDL_Resampler` (sinc) held as a Varispeed-quality upgrade only; no
|
||||
formant-preserving/elastique in WDL. Forks: **S16-F1** (engine default — lean Preserve,
|
||||
Daniel's call), **S16-F2** (Preserve impl — WDL shifter first / hand-rolled held), plus
|
||||
S15-F1 (choke, held) / S15-F2 (param granularity, lean per-zone). Feature set settled;
|
||||
the engine default is Daniel's fork.
|
||||
|
||||
**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, **S15/S16** the Trigger-vs-Gate + pitch-envelope engine features);
|
||||
9000 UX overhaul, **S15/S16** the Trigger-vs-Gate + pitch-engine-modes 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,
|
||||
/ ingest / bank-generation / sampling-mode / pitch-engine 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.
|
||||
|
||||
Reference in New Issue
Block a user