docs: close Γ-F6 and resequence Phase Γ into three waves
Dynamic latency ships as ruled; the restart cost is re-attributed to our own setActive and filed in TODO. The reflow splits canvas (W1-T4) from arrangement (W3-T1); preserve-time-stretch moves to W1-T5.
This commit is contained in:
@@ -6,12 +6,16 @@ contour-trace defect, and a re-approach of the loop/crossfade markers.
|
||||
|
||||
**Status:** items A–F below are **SETTLED (Daniel, 2026-08-01)** from a direct interview;
|
||||
this doc records them, works out the design detail they imply, and states the arithmetic.
|
||||
**The five forks this doc opened (Γ-F1…Γ-F5) were all ruled on by Daniel on 2026-08-01**;
|
||||
their rulings are folded into the sections they affect and the rulings themselves are
|
||||
recorded in §8. §8 also carries **one new fork, Γ-F6**, which the Γ-F2 ruling surfaced from
|
||||
the vendored SDK and which Daniel did not have in front of him. **The forward-looking VST3
|
||||
automation parameter system is deliberately NOT in this phase** — it has its own doc,
|
||||
`docs/product/parameter-automation.md`.
|
||||
**All six forks this doc opened (Γ-F1…Γ-F6) are ruled (Daniel, 2026-08-01)**; their rulings
|
||||
are folded into the sections they affect and indexed in §8. **Nothing in this phase awaits a
|
||||
Daniel answer.** **The forward-looking VST3 automation parameter system is deliberately NOT
|
||||
in this phase** — it has its own doc, `docs/product/parameter-automation.md`.
|
||||
|
||||
> **Γ-F6's ruling corrected this document's analysis, not merely its recommendation.** §3.1.1
|
||||
> previously framed dynamic latency reporting as exotic and expensive. It is neither: it is
|
||||
> routine for VST3 instruments and REAPER handles it as a matter of course. What is expensive
|
||||
> here is **self-inflicted** — this plugin's own `setActive` — and therefore ours to reduce.
|
||||
> §3.1.1 has been rewritten accordingly, not merely annotated with the ruling.
|
||||
|
||||
Every geometry number below was re-derived from `src/core/instrument/ui/knob_deck.cpp`'s
|
||||
own width formula, not carried over from a prior measurement. The stale geometry block in
|
||||
@@ -41,11 +45,13 @@ own width formula, not carried over from a prior measurement. The stale geometry
|
||||
toggle in the caption corner, gain knob upper-left, a full-double-height stereo peak
|
||||
meter down the right, an averted-clip bubble, and a **one-cell** reserved lower-left
|
||||
slot (Γ-F5).
|
||||
- **The limiter is a lookahead design with DYNAMIC reported latency** (Γ-F2): zero when
|
||||
off, the lookahead when on, reported to the host's PDC. That buys a transparent
|
||||
true-peak limiter and costs a `restartComponent(kLatencyChanged)` on the toggle — which
|
||||
the SDK defines as a host **deactivate/reactivate**, not a re-tap. §3.1.1 is the whole
|
||||
of that cost and how it is contained.
|
||||
- **The limiter is a lookahead design with DYNAMIC reported latency** (Γ-F2, Γ-F6): zero
|
||||
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.
|
||||
- **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
|
||||
@@ -112,6 +118,16 @@ waveform band at the floor = 680 − 90 (chrome) − 4 − 4 − 8 − 216
|
||||
|
||||
**1190 × 680, against a 1280 × 720 ceiling — 90 px of width headroom, 40 px of height.**
|
||||
|
||||
> **Who lands which half.** The floor, the three budget constants it is derived from
|
||||
> (row block 1020 · MASTER 142 · ceiling 1280) and each group's row membership land in
|
||||
> **Γ-W1-T4**, in wave 1, so the rest of the phase is authored at the final window. The
|
||||
> arrangement *inside* that budget — the justification law, the gutters, the tie-line,
|
||||
> MASTER's interior — is **Γ-W3-T1**, because every one of those measures a descriptor that
|
||||
> does not exist until Γ-W2-T1 and Γ-W3-T1 create it. **Row 1's natural width does not fit
|
||||
> the 1020 block until Γ-W3-T1**: it is 1030 today, +42 from PITCH/RATE, −92 from FILTER's
|
||||
> `Band|Notch` caption move, = 980. Row 2's 876 already fits. `docs/PLAN.md` at Γ-W1-T4
|
||||
> states the seam and the interim layout in full.
|
||||
|
||||
Three corrections to the arithmetic in the brief, all small and all in our favour:
|
||||
|
||||
1. **MASTER at 142, not ~236.** A 236-wide MASTER puts the floor at exactly 1280 — the
|
||||
@@ -304,7 +320,10 @@ resampler that couples duration) — never wire it as the duration-preserving pa
|
||||
Preserve, Rate is *supposed* to change duration; a Preserve implementation that resamples
|
||||
the read rate and cancels the resulting pitch shift in the shifter is an explicit
|
||||
duration control, not a covert Preserve path. The invariant forbids using a resampler *as*
|
||||
the pitch-preserving mechanism, and that prohibition stands.
|
||||
the pitch-preserving mechanism, and that prohibition stands. **As of the 2026-08-01
|
||||
resequencing this paragraph describes a contingency, not the shipping path** — the real
|
||||
stretcher lands ahead of Rate (§2.5), so nothing composes a resampled read with a cancelling
|
||||
shift unless that contingency is taken.
|
||||
|
||||
### 2.3 Rate is latched at note-on — and the reason matters more than the rule
|
||||
|
||||
@@ -357,10 +376,25 @@ Preserve it is an addend to a shift amount the pitch envelope already modulates.
|
||||
### 2.5 The Preserve time-stretcher — quality bar, not algorithm
|
||||
|
||||
**Preserve mode has no pitch-preserving time-stretch DSP today.** `pitch_shift` is a
|
||||
correlation-aligned SOLA *pitch* shifter; composing it with a resampled read yields a
|
||||
working stretch, and that composition is the **interim path** Γ-W2-T1 ships so Rate is a
|
||||
complete feature the day it lands. A **real stretcher, written from established
|
||||
state-of-the-art literature**, follows as its own track.
|
||||
correlation-aligned SOLA *pitch* shifter. A **real stretcher, written from established
|
||||
state-of-the-art literature**, is its own track.
|
||||
|
||||
**Sequencing — changed 2026-08-01 (Daniel), and it retires an implementation.** This doc
|
||||
originally put the stretcher *after* Rate and had Γ-W2-T1 ship an **interim** composition —
|
||||
a resampled read with the resulting pitch change cancelled in the SOLA shifter — so Rate
|
||||
would be complete the day it landed. **That is reversed.** The stretcher has zero dependency
|
||||
on any UI work and is the phase's longest pole, so it runs from the start of the phase
|
||||
(Γ-W1-T5) and Rate lands onto it (Γ-W2-T1). Two consequences:
|
||||
|
||||
- **The interim composition is not built.** It only ever existed to be deleted; skipping a
|
||||
disposable implementation is the win. It survives in this document as the **named
|
||||
contingency** if the stretcher's gate slips past the point Rate is ready to dispatch — see
|
||||
Γ-W2-T1's open questions. Taking it is an escalation to Daniel, not an engineer's call.
|
||||
- **The quality reference changes.** There is no interim path to A/B against. The honest
|
||||
reference is **varispeed playback at the equivalent ratio** — same duration, pitch shifted —
|
||||
which answers "what does preserving pitch cost" and needs nothing built to serve it. The
|
||||
null case is unchanged and is now stronger: ratio 1.0 with no shift must be **bit-identical
|
||||
to the shipped Preserve read**, a baseline that exists rather than one that was invented.
|
||||
|
||||
**Do not pick an algorithm in this document.** The constraints:
|
||||
|
||||
@@ -380,12 +414,12 @@ state-of-the-art literature**, follows as its own track.
|
||||
latency by priming the ring with the actual upcoming source. **A stretcher that
|
||||
reintroduces an onset delay or a first-frame smear is a regression, not a trade-off.**
|
||||
- **Quality bar.** No audible metallic/phasey artefacting on sustained tonal material at
|
||||
±6 st and 75–133 % rate; no smearing of transient material at 50 %/200 % worse than the
|
||||
interim resample+SOLA path; the null case (rate 100 %, no shift) must be **bit-identical
|
||||
to the un-stretched read**.
|
||||
±6 st and 75–133 % ratio; no smearing of transient material at 50 %/200 % worse than
|
||||
varispeed at the equivalent ratio; the null case (ratio 1.0, no shift) must be
|
||||
**bit-identical to the shipped Preserve read**.
|
||||
- **Gate.** Measure and report before the algorithm is final: per-voice CPU at 32 voices,
|
||||
added latency (must be zero at the onset), and A/B recordings against the interim path on
|
||||
three material classes (one-shot, tonal sustain, full-mix bounce).
|
||||
added latency (must be zero at the onset), and A/B recordings on three material classes
|
||||
(one-shot, tonal sustain, full-mix bounce).
|
||||
|
||||
---
|
||||
|
||||
@@ -433,7 +467,7 @@ Behaviour:
|
||||
- **Gain reduction is published per block** for the bubble indicator (§3.3).
|
||||
- **Lookahead, with DYNAMIC reported latency** — §3.1.1.
|
||||
|
||||
### 3.1.1 Lookahead and dynamic latency (Γ-F2, ruled by Daniel 2026-08-01)
|
||||
### 3.1.1 Lookahead and dynamic latency (Γ-F2 and Γ-F6, both ruled by Daniel 2026-08-01)
|
||||
|
||||
**Settled: the limiter has lookahead, and the plugin reports latency dynamically.**
|
||||
Daniel's reasoning, verbatim intent: true-peak detection needs oversampling and a
|
||||
@@ -452,9 +486,15 @@ This overrides the zero-lookahead recommendation this doc previously carried.
|
||||
SDK default of **0**. This track is the first latency reporting the instrument has ever
|
||||
done, so there is no existing behaviour to preserve, only a new contract to get right.
|
||||
|
||||
#### What the vendored SDK actually says, and why it is more expensive than it looks
|
||||
#### What the vendored SDK says — and where the cost actually comes from
|
||||
|
||||
Two facts read directly out of `vendor/vst3sdk`, both load-bearing:
|
||||
> **This section previously argued that the SDK's requirements made dynamic latency
|
||||
> expensive. That framing was wrong and Daniel corrected it** (Γ-F6): *"you have to have
|
||||
> missed something, I used plenty of VST3s inside of REAPER that report PDC
|
||||
> dynamically."* He is right. The corrected analysis follows; the SDK quotes are unchanged
|
||||
> because the quotes were never the problem — the attribution of the cost was.
|
||||
|
||||
Two facts read directly out of `vendor/vst3sdk`:
|
||||
|
||||
> `pluginterfaces/vst/ivstaudioprocessor.h:293-299` — *"If during the use of the plug-in
|
||||
> this latency change, the plug-in has to inform the host by using
|
||||
@@ -466,21 +506,36 @@ Two facts read directly out of `vendor/vst3sdk`, both load-bearing:
|
||||
> to deactivate and reactivate the plug-in**, then afterwards the host could ask for the
|
||||
> current latency."*
|
||||
|
||||
The second is the sharp one. `kLatencyChanged` is not a "re-read the number" flag — the
|
||||
SDK defines it as a **deactivate/reactivate cycle**. And in *this* plugin,
|
||||
**Both describe the ordinary contract.** Dynamic latency reporting is routine for VST3
|
||||
plugins — lookahead limiters, linear-phase EQs and oversampling processors all do it — and
|
||||
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`):
|
||||
|
||||
- `setActive(false)` frees `live_`, `draining_`, **and** the graveyard — *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)` calls `reloadInstrument()` — a bridge read, a **WAV re-decode**, and a
|
||||
fresh engine.
|
||||
- `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."*
|
||||
|
||||
**So the honest cost of the toggle is: every sounding note stops, and the sample is
|
||||
re-decoded from disk.** That is a materially heavier consequence than "a brief click," and
|
||||
it is the reason §8's new fork Γ-F6 exists rather than this being fully closed.
|
||||
**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 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 standing scar, and why this is nonetheless not the forbidden change
|
||||
|
||||
@@ -516,11 +571,16 @@ reason the following are acceptance criteria and not suggestions:
|
||||
user clicking the toggle repeatedly produces one restart per settled state, not one per
|
||||
click.
|
||||
|
||||
#### Flipping the toggle during playback — the product decision
|
||||
#### Flipping the toggle during playback — settled twice over
|
||||
|
||||
**Ruling (mine, not deferred): the toggle applies immediately, the restart is requested
|
||||
immediately, and the resulting interruption is accepted and documented. It is NOT deferred
|
||||
to a transport boundary.** Three reasons, in order of weight:
|
||||
**Daniel's ruling (Γ-F6) is the outer one: this is not a use case, and it is not to be
|
||||
designed for.** *"The limiter will either be on or off on its instance, toggling during
|
||||
playback is not a use case."* Nothing below is a mitigation for an accepted cost; what
|
||||
survives is either ordinary hygiene or an ordinary quality measure.
|
||||
|
||||
**The inner ruling stands unchanged: the toggle applies immediately, the restart is requested
|
||||
immediately, and it is NOT deferred to a transport boundary.** Three reasons, in order of
|
||||
weight:
|
||||
|
||||
1. **A deferred restart is a silent lie.** If the limiter's audio engages now but the
|
||||
reported latency lands at the next transport stop, the plugin is misaligned by the
|
||||
@@ -533,25 +593,28 @@ to a transport boundary.** Three reasons, in order of weight:
|
||||
transport stopped is the common editing case; a transport-boundary deferral would mean
|
||||
the restart never lands at all in that case, which is the worst outcome of the three.
|
||||
|
||||
The mitigations that make this acceptable rather than merely defensible:
|
||||
What is in scope alongside it — and what each is actually for:
|
||||
|
||||
- **The limiter's own output has no hard step.** Within the plugin, the engage/disengage is
|
||||
covered by a short (≤ 10 ms) equal-gain crossfade between the pre- and post-toggle paths,
|
||||
so whatever the host does around it, we do not emit a discontinuity of our own making.
|
||||
- **A short (≤ 10 ms) equal-gain crossfade over the engage/disengage. Kept as a QUALITY
|
||||
measure, not as a mitigation.** A limiter engaging is a gain-path change, and this codebase
|
||||
already ramps every gain-path change (`kGainRampSeconds`, `ValueRamp`); a plugin that steps
|
||||
its gain path clicks whether or not a restart is pending. It also earns its keep for a
|
||||
reason that has nothing to do with the restart: **the host, not the plugin, decides when to
|
||||
act on the request**, so our own transition must be clean in the window before it does.
|
||||
- **The toggle is framed as a patch-design control, not a performance control.** It is set
|
||||
once while building a sound. The editor should not encourage flipping it while playing,
|
||||
and nothing in the UI should make it a per-take gesture.
|
||||
and nothing in the UI should make it a per-take gesture. Γ-F6's ruling *is* this framing.
|
||||
- **The limiter enable is explicitly NOT automatable.** This is the load-bearing
|
||||
consequence and it must be recorded where the parameter work will read it: an automation
|
||||
lane toggling a latency-changing parameter would request a host deactivate/reactivate on
|
||||
every flip. See `docs/product/parameter-automation.md` §3.8 — the limiter enable belongs
|
||||
in the **not-automatable** class, and it is emphatically not the plugin's `kIsBypass`
|
||||
parameter either.
|
||||
- **The interruption is verified in REAPER, and its severity recorded.** The SDK mandates
|
||||
the deactivate/reactivate; *what REAPER actually does with it* — whether sounding notes
|
||||
cut, whether the re-decode is perceptible, whether transport hiccups — is DAW-verifiable
|
||||
only. That verification is W1-T2's first deliverable, and its outcome is what closes
|
||||
Γ-F6.
|
||||
- **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.
|
||||
|
||||
### 3.2 The meter
|
||||
|
||||
@@ -1175,15 +1238,24 @@ override, no `kLatencyChanged`, and no `restartComponent` call site exists anywh
|
||||
latency reporting. There is therefore no existing behaviour to preserve — but §3.1.1's four
|
||||
verification requirements bind, because the deactivate/reactivate the flag mandates lands
|
||||
squarely on `ReaSamplerProcessor::setActive`, which is deliberately destructive in both
|
||||
directions.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 8. Forks — five ruled, one open
|
||||
## 8. Forks — all six ruled, none open
|
||||
|
||||
### 8.1 Ruled by Daniel, 2026-08-01
|
||||
|
||||
All five forks this doc opened are closed. The rulings are folded into the sections that
|
||||
Every fork this doc opened is closed. The rulings are folded into the sections that
|
||||
depend on them; this table is the index, not a second copy of the reasoning.
|
||||
|
||||
| Fork | Question | Ruling | Where it landed |
|
||||
@@ -1193,9 +1265,10 @@ depend on them; this table is the index, not a second copy of the reasoning.
|
||||
| **Γ-F3** | Does the log taper raise the 2 s stage-time ceiling? | **Not in this phase — stays 2.0 s.** The 10 s ambition is preserved as a `docs/TODO.md` entry with its rationale. | §4.3, `docs/TODO.md` |
|
||||
| **Γ-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 90 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` |
|
||||
|
||||
Two of these corrected this doc rather than confirming it, and both corrections are worth
|
||||
remembering as pattern:
|
||||
Three of these corrected this doc rather than confirming it, and all three corrections are
|
||||
worth remembering as pattern:
|
||||
|
||||
- **Γ-F2 inverted the recommendation.** The zero-lookahead pitch weighed "monitoring latency
|
||||
on every instance" against limiter transparency — but that trade only existed under the
|
||||
@@ -1206,68 +1279,94 @@ remembering as pattern:
|
||||
belonged to a deck. It does not — nothing about loop belongs to a deck — and once that is
|
||||
seen, the chrome row is obvious and free. **The reframe was the answer; the fork as posed
|
||||
had no good option in it.**
|
||||
- **Γ-F6 was a mis-attributed cost.** The fork was posed as "the SDK mandates an expensive
|
||||
cycle — is that acceptable?", with a constant-latency fallback and a measurement gate
|
||||
attached. Daniel's answer — *"you have to have missed something, I used plenty of VST3s
|
||||
inside of REAPER that report PDC dynamically"* — located the error correctly: the SDK
|
||||
mandates an *ordinary* cycle, and everything expensive about it is in **our** `setActive`.
|
||||
The right move was never a fallback; it was to name the self-inflicted cost, accept it now,
|
||||
and file its reduction. **Before presenting a platform constraint as a fork, check whether
|
||||
the constraint is the platform's or ours** — the two want completely different answers, one
|
||||
a product decision and the other a deferred improvement.
|
||||
|
||||
### 8.2 Open — one fork, surfaced by the Γ-F2 ruling
|
||||
### 8.2 Closed — Γ-F6, and what its closure changed
|
||||
|
||||
**Fork Γ-F6 — `kLatencyChanged` mandates a host deactivate/reactivate. Is that acceptable
|
||||
as the cost of the limiter toggle?**
|
||||
**Fork Γ-F6 asked: `kLatencyChanged` mandates a host deactivate/reactivate. Is that
|
||||
acceptable as the cost of the limiter toggle?**
|
||||
|
||||
This was not visible when Γ-F2 was posed and Daniel did not have it in front of him. The
|
||||
vendored SDK (`pluginterfaces/vst/ivsteditcontroller.h:105-108`) defines the flag as: *"The
|
||||
host has to deactivate and reactivate the plug-in."* In this plugin,
|
||||
`ReaSamplerProcessor::setActive` is destructive in both directions
|
||||
(`reasampler_processor.cpp:85-109`) — deactivate frees every sounding voice, reactivate
|
||||
re-decodes the WAV from disk. **So flipping the limiter cuts held notes and reloads the
|
||||
sample.** Full detail in §3.1.1.
|
||||
**Ruled: yes — ship it (Daniel, 2026-08-01).** *"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."*
|
||||
|
||||
- **(a) Ship it, accept the cut.** The limiter is a patch-design control set once while
|
||||
building a sound, not a per-take gesture; the enable is classified **not automatable** so
|
||||
nothing can flip it at rate; the plugin's own output is crossfaded so *we* emit no step.
|
||||
Cost: a user who flips it mid-audition loses the note they were holding.
|
||||
- **(b) Constant reported latency** — the delay line engaged whenever the limiter design
|
||||
ships, on or off, so the toggle never changes latency and never restarts. Cost: exactly
|
||||
the thing Daniel's ruling rejected — every instance pays the lookahead in live monitoring
|
||||
whether or not the limiter is used. Named here as the pre-agreed fallback, not as a
|
||||
re-litigation.
|
||||
- **(c) Zero-lookahead** — the original §8 recommendation. Closed; do not reopen it here.
|
||||
The two alternatives the fork carried are **closed, not shelved**, and neither is to be
|
||||
reintroduced:
|
||||
|
||||
**Recommendation: (a), gated on a DAW measurement.** Ship the dynamic-latency design as
|
||||
ruled, and make **W1-T2's first deliverable a verification spike in REAPER**: flip the
|
||||
limiter with notes held, during playback and while stopped, and record what actually
|
||||
happens — whether notes cut, whether the re-decode is perceptible, whether transport
|
||||
hiccups. If the observed behaviour is as ugly as the SDK's worst case allows, **(b) is the
|
||||
pre-agreed fallback and needs one word from Daniel, not a redesign** — the DSP is identical
|
||||
either way and only the latency-reporting predicate changes.
|
||||
- **Constant reported latency** (the delay line engaged whether or not the limiter is on) —
|
||||
rejected. It is exactly what Γ-F2's ruling refused: every instance paying the lookahead in
|
||||
live monitoring whether or not the limiter is used.
|
||||
- **Zero lookahead** — closed at Γ-F2. Do not reopen it here.
|
||||
|
||||
*This is the only open fork in the phase.* Nothing in §§1–7 is awaiting a Daniel answer.
|
||||
**What the closure changed beyond the ruling**, and why this fork is worth reading rather
|
||||
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.
|
||||
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.
|
||||
3. **The ≤ 10 ms crossfade survives, reclassified.** It is a quality measure on a gain-path
|
||||
change, in line with every other ramp in this codebase, not a mitigation for an accepted
|
||||
interruption.
|
||||
4. **The verification requirements survive unchanged**, because they were always about the
|
||||
`kIoChanged` scar (a dual-mono capture panned hard right by a prior mid-session
|
||||
`restartComponent`), not about this flag.
|
||||
5. **The reduction is filed**, with a trigger condition, in `docs/TODO.md`.
|
||||
|
||||
*No fork in this phase is open. Nothing in §§1–7 awaits a Daniel answer.*
|
||||
|
||||
---
|
||||
|
||||
## 9. Build shape
|
||||
|
||||
Sequenced into `docs/PLAN.md` as **Phase Γ** (worktree slug prefix `pg-`), four waves:
|
||||
Sequenced into `docs/PLAN.md` as **Phase Γ** (worktree slug prefix `pg-`), **three waves**
|
||||
(resequenced by Daniel, 2026-08-01 — see below):
|
||||
|
||||
```
|
||||
Γ-W1 Foundations [3 tracks, disjoint by surface]
|
||||
Γ-W1 Foundations [5 tracks, disjoint by surface]
|
||||
T1 knob-interaction-law ............ item D (editor input + deck_values tapers)
|
||||
T2 master-bus-audio ................ item C (pure limiter + meter ballistics +
|
||||
processor + LATENCY REPORTING)
|
||||
T3 contour-trace-curves ............ item E (waveform painter)
|
||||
T4 editor-floor-and-row-law ........ item B's CANVAS half
|
||||
(floor + budget constants + row predicate)
|
||||
T5 preserve-time-stretch ........... item A's engine half [measure-and-report gate]
|
||||
Γ-W2 New controls, and the overlay's marks [2 tracks]
|
||||
T1 pitch-rate-deck ................. item A (params + engine + deck descriptor)
|
||||
T2 loop-crossfade-ux ............... item F (waveform painter + pure marker geometry
|
||||
+ the chrome-row loop enable)
|
||||
Γ-W3 The reflow [1 track]
|
||||
T1 deck-reflow ..................... item B + C's UI half
|
||||
Γ-W4 Preserve time-stretch [1 track]
|
||||
T1 preserve-time-stretch ........... item A's quality half [measure-and-report gate]
|
||||
T1 deck-reflow ..................... item B's ARRANGEMENT half + C's UI half
|
||||
```
|
||||
|
||||
**Two resequencing decisions, both Daniel's (2026-08-01).**
|
||||
|
||||
1. **Item B splits: canvas early, arrangement late.** The window floor, the width budget it
|
||||
derives from, and each group's row membership land in W1-T4 so every other UI track is
|
||||
drawn, tested and judged at the final 1190 × 680 window. The two-row layout itself stays in
|
||||
W3-T1, because it can only be measured once the final PITCH/RATE and MASTER descriptors
|
||||
exist. The exact seam — what W1-T4 can assert, what it cannot, and what the editor looks
|
||||
like in between — is in `docs/PLAN.md` at Γ-W1-T4.
|
||||
2. **The stretcher moved last → first** (W4-T1 → W1-T5). Longest pole, zero UI dependency.
|
||||
It inverts its relationship with Rate: prerequisite, not successor, which retires the
|
||||
interim resample-and-cancel path unbuilt (§2.5).
|
||||
|
||||
The wave boundaries are collision boundaries, not preferences: `deck_values.cpp` is written
|
||||
by W1-T1 then W2-T1; `editor_paint_waveform.cpp` by W1-T3 then W2-T2; the deck descriptors
|
||||
by W2-T1 then W3-T1; and **one params-payload version bump per wave, owned by one track**
|
||||
(W1-T2 takes v14 for the limiter flag, W2-T1 takes v15 for rate + pitch offset) so no two
|
||||
tracks contend for the format ladder. The Γ-F4 ruling adds a chrome surface to W2-T2 and one
|
||||
named shared file inside W2 — **§7.9**, which restates the wave's disjointness claim rather
|
||||
than repeating it. Full track specs, dependencies and acceptance criteria are in
|
||||
`docs/PLAN.md`.
|
||||
by W1-T1 then W2-T1; `editor_paint_waveform.cpp` by W1-T3 then W2-T2; `deck_groups.cpp` by
|
||||
W1-T4 (the row predicate) then W2-T1 (the descriptor) then W3-T1 (the row consumption);
|
||||
`voice.cpp` by W1-T5 then W2-T1; and **one params-payload version bump per wave, owned by one
|
||||
track** (W1-T2 takes v14 for the limiter flag, W2-T1 takes v15 for rate + pitch offset) — the
|
||||
two new W1 tracks take **no rung at all**, so the ladder is unchanged by the resequencing.
|
||||
Two shared files are named rather than discovered at merge:
|
||||
`core/instrument/engine/CMakeLists.txt` inside W1 (T2 | T5) and `editor_session.cpp` inside
|
||||
W2 (T1 | T2, **§7.9**) — both textual adjacency, not semantic contention. Full track specs,
|
||||
dependencies and acceptance criteria are in `docs/PLAN.md`.
|
||||
|
||||
Reference in New Issue
Block a user