Decouple the instrument reload from VST3 activation, and make the master meter's accumulate exact

This commit is contained in:
2026-08-02 12:41:57 -04:00
parent 4b0b03d8d5
commit 5c6525fb91
17 changed files with 462 additions and 239 deletions
+39 -38
View File
@@ -50,9 +50,8 @@ own width formula, not carried over from a prior measurement. The stale geometry
when off, the lookahead when on, reported to the host's PDC. This is **routine VST3
behaviour**; the `restartComponent(kLatencyChanged)` it costs is the normal contract, and
the deactivate/reactivate the flag mandates is **accepted** — the toggle is a patch-design
gesture. The only reason the cycle is expensive at all is that **our** `setActive` re-decodes
the WAV, which is a latent improvement filed in `docs/TODO.md`, not a design constraint.
§3.1.1.
gesture. The cycle used to be expensive only because **our** `setActive` re-decoded the WAV;
Γ-W3 decoupled the two lifetimes, so it no longer does. §3.1.1.
- **The cortex limiter does not clear the bar** — §3.5. Read it, take nothing.
- **Loop gets an explicit enable on the chrome row** (Γ-F4), and the four-mark grammar
sits under it. The core finding behind the re-approach: three identical bars draw a
@@ -560,31 +559,28 @@ plugins — lookahead limiters, linear-phase EQs and oversampling processors all
REAPER handles it as a matter of course. The deactivate/reactivate is the *normal* cost of
the flag, and for a typical plugin it is cheap: `setActive` allocates and frees buffers.
**What makes it expensive here is entirely our own design, in one line.**
`ReaSamplerProcessor::setActive` is deliberately destructive in both directions
(`reasampler_processor.cpp:85-109`):
**What made it expensive here was entirely our own design, in one line** — and Γ-W3 removed
that line. `ReaSamplerProcessor::setActive` was deliberately destructive in both directions:
- `setActive(true)` calls `reloadInstrument()` (`:89-97`) — **a bridge read and a full WAV
re-decode**, plus a fresh engine. This is the expensive half, and no part of it is required
by the SDK: it is there because activation was the convenient trigger for a reload, not
because activation implies one.
- `setActive(false)` frees `live_`, `draining_` **and** the graveyard (`:98-107`), so every
sounding voice dies. The comment there explains why that is correct and must not be
softened casually: a surviving `live_` would be displaced into the drain slot on reactivate
and *"resurrect stale sustained voices as ghosts."*
- `setActive(true)` called `reloadInstrument()`**a bridge read and a full WAV re-decode**,
plus a fresh engine. That was the expensive half, and no part of it was required by the SDK:
it was there because activation was the convenient trigger for a reload, not because
activation implies one.
- `setActive(false)` frees `live_`, `draining_` **and** the graveyard, so every sounding voice
dies. That half is correct and must not be softened casually: a surviving `live_` would be
displaced into the drain slot on reactivate and *"resurrect stale sustained voices as
ghosts."*
**So the cost is ours, and it is ours to reduce.** The reduction is **decoupling the reload
from activation** — keeping the decoded `SampleData` alive across a deactivate while still
destroying voice state, which is exactly the shape `rebuildVoiceEngine`'s drain-slot swap
already implements for voice-count edits. **That is a latent improvement with a clear trigger
condition, filed in `docs/TODO.md` ("Decouple the instrument reload from VST3 activation") —
not a reason to abandon dynamic latency, and not scheduled in this phase.**
**The cost was ours, and it has been reduced (Γ-W3 — see §7.11).** The deactivate now parks the
decoded `SampleData` and the reactivate rebuilds only the voice state around it, through the
same drain-slot swap `rebuildVoiceEngine` uses for voice-count edits. An activation cycle costs
no disk read and no decode; an instance with nothing decoded still takes the full reload, which
is where the pre-v10 legacy lift lives.
**The honest cost of the toggle today, stated plainly:** every sounding note stops and the
sample is re-decoded from disk. **Daniel has accepted it** (Γ-F6): *"Toggling the limiter
killing the voices isn't a deal breaker though, the limiter will either be on or off on its
instance, toggling during playback is not a use case."* There is no fallback design and no
measurement gate.
**The honest cost of the toggle, stated plainly:** every sounding note stops. **Daniel has
accepted it** (Γ-F6): *"Toggling the limiter killing the voices isn't a deal breaker though,
the limiter will either be on or off on its instance, toggling during playback is not a use
case."* There is no fallback design and no measurement gate.
#### The standing scar, and why this is nonetheless not the forbidden change
@@ -660,10 +656,9 @@ What is in scope alongside it — and what each is actually for:
in the **not-automatable** class, and it is emphatically not the plugin's `kIsBypass`
parameter either.
- **Observe what REAPER does, and record it — as evidence, not as a gate.** Whether notes
cut, whether the re-decode is perceptible, whether transport hiccups, is DAW-observable
only. Record it in Γ-W1-T2's review because it is the trigger-condition evidence for the
`docs/TODO.md` decoupling entry. **No outcome changes the design**; Γ-F6 is closed either
way.
cut and whether transport hiccups is DAW-observable only. The re-decode half of that
question is gone (§7.11), so what remains to observe is the voice cut alone. **No outcome
changes the design**; Γ-F6 is closed either way.
### 3.2 The meter
@@ -1401,13 +1396,18 @@ squarely on `ReaSamplerProcessor::setActive`, which is deliberately destructive
directions. **Those four are hygiene against the `kIoChanged` scar (§3.1.1), not a hedge
against the flag itself** — Γ-F6 is ruled and the restart ships.
**7.11 — `setActive` conflates two lifetimes, and dynamic latency is the first feature that
makes a user notice.** Activation currently means both "the audio thread may run" and "the
decoded `SampleData` is (re)built" (`reasampler_processor.cpp:89-97`). Phase Γ does **not**
separate them — Γ-F6 accepts the cost — but the conflation is now a named, filed improvement
(`docs/TODO.md`, "Decouple the instrument reload from VST3 activation") rather than an
unremarked property. **Do not restructure `setActive` inside this phase**; its destructive
shape is deliberate and its reasoning is documented at the call site.
**7.11 — `setActive` conflated two lifetimes; it no longer does (LANDED, Γ-W3).** Activation
used to mean both "the audio thread may run" and "the decoded `SampleData` is (re)built", so
every host-driven cycle paid a bridge read and a full WAV decode. The two are now separate:
`setActive(false)` parks the decoded sample and destroys the voice state, `setActive(true)`
rebuilds the voices around the parked sample through the drain-slot swap `rebuildVoiceEngine`
already used. **This section's earlier instruction — "do not restructure `setActive` inside
this phase" — was superseded by Daniel's ruling that this track does it**; the deactivate's
destruction of voice state is still deliberate (a surviving `live_` would resurrect stale
sustained voices as ghosts) and only the PCM survives. Nothing parked routes the activation
back through the full reload, which is what keeps the pre-v10 legacy lift reachable. Γ-F6 is
untouched: dynamic latency ships and the deactivate/reactivate is still the accepted cost —
it is simply a much cheaper one.
---
@@ -1427,7 +1427,7 @@ ceiling.
| **Γ-F3** | Does the log taper raise the 2 s stage-time ceiling? | **REVERSED, same day. Ruled first "not in this phase — stays 2.0 s"; then Daniel: _"extend the stage lengths to 10s."_ The ceiling moves 2.0 → 10.0 in Γ-W1-T1.** The reversal's cause is Ruling 1: parameters now ship in-phase, so the ceiling is a one-way door that has to be walked through *before* them. | **§4.3.1** (new), §4.3; `docs/TODO.md` entry discharged |
| **Γ-F4** | Explicit loop enable? | **Yes — on the CHROME ROW.** Not a deck cell; loop is a waveform-overlay concept and has no deck. | **§6.4** (new), §6.5, §7.9 |
| **Γ-F5** | MASTER's reserved slot: one cell or two? | **One cell.** Two would spend 60 of the 82 px headroom on an unnamed control and freeze row 1 forever. | **§1.6** (new), §1.4 |
| **Γ-F6** | Is the `kLatencyChanged` deactivate/reactivate acceptable as the cost of the toggle? | **Yes — ship dynamic latency as ruled.** No constant-latency fallback, no measurement gate. *Corrected this doc's analysis: the cost is self-inflicted, not SDK-imposed.* | **§3.1.1** (rewritten), §7.10, §7.11, `docs/TODO.md` |
| **Γ-F6** | Is the `kLatencyChanged` deactivate/reactivate acceptable as the cost of the toggle? | **Yes — ship dynamic latency as ruled.** No constant-latency fallback, no measurement gate. *Corrected this doc's analysis: the cost is self-inflicted, not SDK-imposed.* | **§3.1.1** (rewritten), §7.10, §7.11; `docs/TODO.md` decoupling entry discharged in Γ-W3 |
| **Γ-F7** | VST3 parameter ORDER: signal flow, or the editor's visual rows? | **Signal flow***"signal flow order."* The frozen id numbering and the presentation index both follow the deck's own rule; the visual layout is too mobile to freeze against. | **§8.3**; `parameter-automation.md` §6.4 (argument) and §6.2 (the 44-id table) |
Three of these corrected this doc rather than confirming it, and all three corrections are
@@ -1473,7 +1473,8 @@ reintroduced:
than just counting:
1. **§3.1.1 was rewritten, not annotated.** Its prior framing — dynamic latency as exotic and
expensive — was wrong. Dynamic PDC is routine; the expense is our reload-on-activate.
expensive — was wrong. Dynamic PDC is routine; the expense was our reload-on-activate, and
Γ-W3 removed it (§7.11).
2. **The measurement gate was dropped.** Γ-W1-T2's first deliverable is the limiter, not a
spike. What remains is an *observation* recorded in review as evidence for the deferred
improvement — it gates nothing.