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:
2026-08-01 16:51:04 -04:00
parent eb777f55e1
commit 256216d670
4 changed files with 658 additions and 255 deletions
+193 -94
View File
@@ -6,12 +6,16 @@ contour-trace defect, and a re-approach of the loop/crossfade markers.
**Status:** items AF 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 75133 % 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 75133 % 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 §§17 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 §§17 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`.