diff --git a/docs/COMPLETED.md b/docs/COMPLETED.md index fba8466..b57fbee 100644 --- a/docs/COMPLETED.md +++ b/docs/COMPLETED.md @@ -1249,6 +1249,51 @@ limiter. Until that lands this is a recorded, known limitation, not an oversight **Neither track has been verified in a running DAW; both are asserted in CTest only.** +### Γ-W3-T3 — bake-prints-limiter + +The bake's master stage now prints the limiter as well as the gain multiply, closing the +audible gap Γ-W3-T2 recorded and left open: a capture baked with the limiter engaged +returns limited audio rather than unlimited audio. `renderBake` +(`core/instrument/bake/bake_render.cpp`) instantiates its own `Limiter` — the same +bake-only-engine precedent its `VoiceEngine` already set — never linked into +`reaper_reasampler`; `src/app/CMakeLists.txt`'s exclusion comment names the limiter +alongside `sampler_core`/`pitch_shift`/the filter, so the extension's link graph gains no +new edge. + +**The lookahead needed compensation, which was open at spec time.** The limiter delays its +output by `kLimiterLookaheadSeconds` (0.002 s = 96 samples at 48 kHz), so the render's +buffers carry `renderFrames() + flushFrames` frames, the extra span fed silence rather than +more rendered audio, and the capture is read out starting at `leadInFrames + flushFrames` +instead of `leadInFrames` alone — the file is the same frames it would be bypassed, not the +same capture shifted 2 ms late. + +**A sequencing trap, recorded inline at the call site.** `Limiter::prepare()` ends by +calling `reset()`, which snaps to whatever the enable target already is, so the render calls +`setEnabled(true)` before `prepare()`. Reversed, the limiter would take its live-engage path +instead — `process()`'s prime-then-fade — muting and then fading in the first ~12 ms of +every capture (the delay-line prime plus `kLimiterMuteSeconds`, per `limiter.h`). + +**The bypassed path is unchanged.** `test_bake_render.cpp` asserts bypass ≡ engaged +bit-for-bit under the ceiling (a ramp fixture at unity gain, verified against the source +sample for sample too) and separately confirms the printed-limiter path holds the ceiling +and stays stereo-linked under a DC fixture driven well past it; repeat bakes stay +bit-identical with the limiter engaged as well, since `renderBake` builds a fresh `Limiter` +per call and `prepare()` zeroes every one of its state fields. + +**Double-limiting is a named boundary, not a defect** (`bake/CLAUDE.md`): a printed capture +replayed through an engaged limiter is limited twice. The post-bake reset ordinarily +prevents it, since `limiterEnabled` is not on the survive list. + +**`bake/CLAUDE.md`'s invariant is corrected alongside the code.** The text Γ-W3-T2 left in +place ("the limiter is not printed") is replaced with "the whole chain is printed — voice, +master gain, then the limiter, in the processor's own order," and the track's three +`[propose at review]` open questions are answered inline in the same section: the bake +instantiates its own `Limiter`; the lookahead does need in-render compensation, exactly the +above; and yes, this track also corrects the invariant text rather than leaving it to a +later pass. + +**Not verified in a running DAW — CTest-asserted only.** + ### Γ-W4-T1 — vst3-parameter-set The instrument now reports its automatable parameters to the host: 44 of 44 issue, under a diff --git a/docs/PLAN.md b/docs/PLAN.md index fc5ce24..dcd3219 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -597,6 +597,9 @@ system's is in **`docs/product/parameter-automation.md` §§6–10**. Read §1.2 table) and §7 (collisions) before dispatching any track here — every number in this phase is derived there, and `docs/TODO.md`'s old deck-rework geometry is superseded. +**All four waves have landed — Phase Γ is complete.** W1 through W4 each carry their own +landed note below; see `docs/COMPLETED.md` for every track's full narrative. + **Fork state — SEVEN ruled, ONE OF THEM LATER REVERSED, NONE OPEN.** Indexed at spec §8, folded into the tracks below: - **Γ-F1** — `kEditorMinHeight` stays **680**. @@ -1013,9 +1016,9 @@ than re-derived. **T2 depended on Phase Ξ** — `Ξ-W2-T1 (resample-bake-chain) first, the phase's only external gate — see `docs/COMPLETED.md` for the full narrative of each landed track. -**Two tracks have landed** — Γ-W3-T1 (`deck-reflow`) and Γ-W3-T2 (`bake-reset-amendment`) — see -`docs/COMPLETED.md` for the full narrative of each. **A third track is now open and has not -run:** Γ-W3-T3 (`bake-prints-limiter`), added below on Daniel's ruling of 2026-08-02. +**All three tracks have landed** — Γ-W3-T1 (`deck-reflow`), Γ-W3-T2 +(`bake-reset-amendment`), and Γ-W3-T3 (`bake-prints-limiter`) — see `docs/COMPLETED.md` for +the full narrative of each. **None of the three tracks takes a payload rung.** T1 was layout only; T2 changed a reset list, not a format; T3 changes what the render's audio contains, not what is stored. @@ -1066,56 +1069,24 @@ field-by-field assertions over two independently-dialled fixtures, never struct mutation-verified spot-check sweep confirming both fixtures actually moved every asserted field off its default. -**One invariant correction:** `bake/CLAUDE.md` had claimed the whole signal chain prints, master -gain included. It doesn't — the render's gain multiply is the only master-stage value it -prints; the limiter runs in the processor's block, off the bake path entirely. - -**Outstanding, not closed by this track.** A capture baked with the limiter engaged comes back -unlimited — a real audible gap, and Daniel has ruled that a future track will change the bake to -print the limiter. **That track is now Γ-W3-T3, below.** +**One invariant correction, at the time this track landed:** `bake/CLAUDE.md` had claimed the +whole signal chain prints, master gain included. It didn't yet — the render's gain multiply was +the only master-stage value it printed; the limiter ran in the processor's block, off the bake +path entirely, so a capture baked with the limiter engaged came back unlimited. **Γ-W3-T3 +(below) has since closed that gap** — the limiter is printed too now, and `bake/CLAUDE.md`'s +invariant text is corrected again to match. **Neither track has been verified in a running DAW; both are asserted in CTest only.** #### Γ-W3-T3 — `bake-prints-limiter` -**Not started. Opened by Daniel's ruling, 2026-08-02.** - -**Goal.** Print the limiter through the bake's master stage, so a capture baked with the -limiter engaged returns limited audio rather than unlimited audio. - -**Why this exists.** Γ-W3-T2's own finding disproved the premise -`docs/product/instrument-control-surface.md` §3.4's reset classification rested on: -`renderBake` (`core/instrument/bake/bake_render.cpp`) prints only a flat master-gain multiply, -and the limiter (`core/instrument/engine/limiter`) runs in the processor's `process()` block, -off the bake path entirely. Until this track lands, this is a recorded, known limitation — see -`docs/COMPLETED.md`'s Γ-W3-T2 entry — not an oversight. - -**Consolidates:** nothing from the seventeen. A correction, on the same footing as Γ-W3-T2 (see -"Work in this plan that is not one of the seventeen"). - -**Spec:** none yet written. This ruling postdates §3.4 and has no product-doc section of its -own; §3.4 is superseded on this one point, which a future scoping pass of this track should -correct there as well as here. - -**Surface boundary — likely, not yet confirmed against a full scoping pass:** owns -`core/instrument/bake/bake_render` (the gain-multiply step, extended to also run the signal -through a limiter), consuming `core/instrument/engine/limiter` — not owned, not modified. Does -not own the processor's live block, the limiter DSP itself, the parameter surface, or -`bake_reset` (the limiter-enable reset classification is already Γ-W3-T2's, landed). - -**Open questions — none of this is ruled yet, only the goal is:** -- **[propose at review]** Whether the bake instantiates its own `Limiter` — mirroring - `renderBake`'s existing bake-only `VoiceEngine`, off the audio thread, never linked into - `reaper_reasampler` — or reaches the limiter's settled behavior some other way. The - bake-only-engine precedent (`bake/CLAUDE.md`) argues for the former. -- **[propose at review]** Whether the limiter's lookahead needs any accommodation in an - offline, non-realtime render — the processor's `getLatencySamples()` PDC report exists for - the live block, and a bake is not on that clock, so this may be a non-issue; it has not been - checked. -- **[propose at review]** Whether this track also corrects `bake/CLAUDE.md`'s invariant text - ("the limiter is not [printed]") alongside the code, once scoped in full. -- **No [Daniel] question on the goal itself** — the ruling above is the goal; what is open is - the mechanism, not whether to do it. +**Landed** — see `docs/COMPLETED.md` for the full narrative. The bake's master stage now +prints the limiter as well as the gain multiply: `renderBake` instantiates its own `Limiter` +(the bake-only-engine precedent, never linked into `reaper_reasampler`), the lookahead is +compensated inside the render (an extra `flushFrames` of silence past the window, the capture +read out at `leadInFrames + flushFrames`), and `bake/CLAUDE.md`'s invariant text is corrected +alongside the code. All three of the track's `[propose at review]` open questions are +answered in that entry. ---