docs: collapse Phase Gamma Wave 1 to its landed record, and correct T7's superseded status
This commit is contained in:
+75
-577
@@ -843,604 +843,98 @@ exact interim layout; do not "fix" it in a track that does not own it.
|
||||
|
||||
### Γ-W1 — Foundations
|
||||
|
||||
**Depends on:** nothing in this phase. **Six tracks, disjoint by surface** — re-verified
|
||||
against this membership rather than carried over from the four-wave shape. The phase's track
|
||||
numbering runs to T7; **T6 landed within this wave but has no entry in this document**:
|
||||
**Depends on:** nothing in this phase.
|
||||
|
||||
| Track | Owns |
|
||||
|---|---|
|
||||
| **T1** `knob-interaction-law` | a **new pure taper module** under `core/instrument/ui/`, `ui/deck_values`, `ui/envelope_overlay` + `ui/envelope_edit` (the AHDSR schematic axis and its drag inverse), `ui/param_slider`, the three `shell/instrument/editor_input_*` drag paths, `editor_controls.cpp`'s `envClampBounds` only, the shared modifier helper in `editor_internal.h` |
|
||||
| **T2** `master-bus-audio` | new pure `engine/limiter` + `engine/meter_ballistics`, `shell/instrument/reasampler_processor` + `processor_state`, `map/component_state_io` + `params_payload` (**the wave's payload rung**) |
|
||||
| **T3** `contour-trace-curves` | `shell/instrument/editor_paint_waveform.cpp`'s staged trace + a **new pure** tessellation module |
|
||||
| **T4** `editor-floor-and-row-law` | `ui/sample_bands.h` (the floor), `ui/knob_deck.h` (budget constants + two invalidated header notes), `ui/deck_groups` (the row predicate **only**), five test fixtures |
|
||||
| **T5** `preserve-time-stretch` | `engine/pitch_shift` + a new pure stretcher module, `engine/voice.{h,cpp}`'s Preserve read path |
|
||||
| **T7** `psola-preserve` | a **new pure module** `engine/period_detect` (two-pass YIN, with its own `period_detect_tests` target), `pitch_shift`'s jump geometry, the load-time hook in `map/sample_map`'s `buildSampleData`, `voice`'s note-on |
|
||||
|
||||
**Two shared files in the wave, named rather than discovered at merge.**
|
||||
`src/core/instrument/engine/CMakeLists.txt` — T2 declares two new pure libraries and their
|
||||
test targets there, T5 declares one. **And, newly, `src/core/instrument/ui/CMakeLists.txt`** —
|
||||
T1 declares the taper module and its test target, T3 declares the tessellation module and
|
||||
its. All four are append-only additions in separate blocks — **textual merge adjacency, not
|
||||
semantic contention.** Whichever lands second rebases.
|
||||
|
||||
**Three near-misses that are avoided by construction, and must stay avoided.**
|
||||
|
||||
(a) **T3's tessellation helper lands in a NEW pure module — explicitly NOT
|
||||
`ui/envelope_overlay`, which T1 now owns**, and not in `editor_internal.h`, which T1 is also
|
||||
editing. The prior wording offered `envelope_overlay` as an option; Ruling 2 removed it,
|
||||
because T1's schematic-axis work rewrites that module's whole time→x map. This still
|
||||
satisfies the phase's geometry-stays-pure criterion, so it costs nothing.
|
||||
|
||||
(b) **T1 and T3 are disjoint by file but coupled by data, and the coupling has a stated
|
||||
resolution.** T1 owns where an AHDSR's vertices *land*; T3 owns the stroke *between*
|
||||
vertices. T3's tessellation is over φ across a segment's pixel span, so the tapered axis
|
||||
changes nothing about the curve it draws — **but T3's tests must assert against the returned
|
||||
vertices, not against absolute pixel literals**, or they break when T1 lands. Whichever
|
||||
track lands second rebases; expressing T3's assertions relatively makes that rebase free.
|
||||
|
||||
(c) **T4 touches `deck_groups` but adds only the new row predicate**; it does not touch
|
||||
`sampleDeckGroups`, which W2-T1 and W3-T1 own in later waves, and it does not touch
|
||||
`deck_values`, which is T1's.
|
||||
|
||||
**Two consumption boundaries worth stating, because they look like collisions and are not.**
|
||||
T1 **consumes** `engine/master_gain`'s dB taper for its whole-dB snap and does not edit it;
|
||||
T2 does not touch it either. And T2's payload rung is the wave's only format change —
|
||||
T1's taper and ceiling changes are persistence-neutral by construction (the payload stores
|
||||
raw engine doubles).
|
||||
|
||||
**Both of the phase's DSP unknowns are in this wave** — T2's limiter and T5's stretcher. That
|
||||
is deliberate: they are the two tracks whose gate can fail, and failing in wave 1 is
|
||||
recoverable in a way that failing in the last wave is not.
|
||||
**All seven tracks have landed** — Γ-W1-T1 (`knob-interaction-law`), Γ-W1-T2
|
||||
(`master-bus-audio`), Γ-W1-T3 (`contour-trace-curves`), Γ-W1-T4 (`editor-floor-and-row-law`),
|
||||
Γ-W1-T5 (`preserve-time-stretch`), Γ-W1-T6 (`exhaustive-switch gate on pure libraries`), and
|
||||
Γ-W1-T7 (`psola-preserve`) — see `docs/COMPLETED.md` for the full narrative of each. **T6 has
|
||||
no subsection below**, matching this plan's original choice not to give it one; its record in
|
||||
`docs/COMPLETED.md` is reconstructed from the CMake change and its enforcement call site
|
||||
rather than from a spec section here.
|
||||
|
||||
#### Γ-W1-T1 — `knob-interaction-law`
|
||||
|
||||
**Goal.** One consistent, unit-category-driven interaction and taper rule across every
|
||||
variable control, **over a stage-time range raised 2 s → 10 s**, landed **before** any new
|
||||
control is added so the new ones are authored into it rather than retro-fitted — and before
|
||||
any parameter is declared, so the law is what the host is handed rather than something the
|
||||
host has to be reconciled with later.
|
||||
|
||||
**Spec:** `docs/product/instrument-control-surface.md` §4, **§4.3.1 (the 10 s ceiling and the
|
||||
overlay-legibility design — new, read it before scoping this track)**, and
|
||||
`docs/product/parameter-automation.md` §8 (the one-way-door sweep this track discharges).
|
||||
|
||||
**Surface boundary — owns:** a **new pure taper module** under `core/instrument/ui/` (the
|
||||
ms/semitone/exponent maps, extracted so they have one home),
|
||||
`core/instrument/ui/deck_values` (the bindings, the snap-unit table, `resetDeckParam`),
|
||||
`core/instrument/ui/envelope_overlay` (the ceiling constant **and** the AHDSR schematic
|
||||
axis) and `core/instrument/ui/envelope_edit` (its drag inverse),
|
||||
`core/instrument/ui/param_slider` (the drag law), `shell/instrument/editor_input_*`
|
||||
(modifier read + re-anchor), `shell/instrument/editor_controls.cpp`'s `envClampBounds`
|
||||
**only** (it reads `kEnvTimeMaxSeconds`), and the modifier-reading helper the three input
|
||||
paths share. **Does not own** any deck descriptor, any parameter, the waveform painter, or
|
||||
`engine/master_gain` (consumed, not edited).
|
||||
|
||||
**Why this track does NOT split, asked and answered.** Ruling 2 makes it materially bigger —
|
||||
tapers, modifiers, re-anchor, reset bypass, the ceiling, and the overlay's schematic scale.
|
||||
Two splits were considered and both are **serial, not parallel**, so neither buys any
|
||||
concurrency: an *interaction* half (modifiers, snap, re-anchor) needs the *domain* half's
|
||||
taper and snap-unit table to exist first; and a standalone *overlay-axis* track needs the
|
||||
taper module and the final ceiling before it can define a stage's slot width. Splitting
|
||||
would therefore cost a wave and gain nothing, while putting the single most
|
||||
identity-critical function in the phase across a wave boundary — the same function the host
|
||||
will normalize against three waves later. **The seam that matters is internal and is a
|
||||
deliverable: the taper is extracted into its own pure module**, which is what makes "the
|
||||
taper IS the host-facing normalization" structurally true rather than a comment. The
|
||||
~600-line ceiling is a per-file bar, and the extraction is what keeps every file under it.
|
||||
|
||||
**Behavior.**
|
||||
- **Shift snaps to whole numbers in the control's displayed unit**; **Ctrl scales the drag by
|
||||
0.05**; **Shift+Ctrl = Shift wins** (Ctrl is ignored — with an integer-quantized output a
|
||||
finer drag yields the same sequence, so this is identity, not a compromise).
|
||||
- **Snap unit by category:** ms knobs → whole ms; semitone knobs (incl. Rate, when it
|
||||
arrives) → whole semitones; percent/fraction knobs → whole percent; the 12 curve-exponent
|
||||
inner dials → whole numbers (which puts 1.0, the linear neutral, one snap away); master
|
||||
gain → whole dB; already-integer controls unchanged. Full table in the spec §4.2.
|
||||
- **Mid-drag modifier transitions re-anchor** — on every press *and* release during an active
|
||||
drag, the current value becomes the anchor value and the current cursor position the anchor
|
||||
position. The value is continuous across the transition; only the rate changes. Without
|
||||
this the grab-anchored absolute drag (`kKnobDragRangePixels = 128`) jumps by
|
||||
`(1 − 0.05) ×` the accumulated delta.
|
||||
- **Millisecond knobs become log-scaled.** Exactly 0 s at norm 0 and exactly
|
||||
`kEnvTimeMaxSeconds` at norm 1, monotone throughout; **10 ms lands within 0.12–0.20 of
|
||||
travel and 100 ms within 0.42–0.52**.
|
||||
- **The stage-time ceiling moves 2.0 s → 10.0 s** (Daniel, reversing Γ-F3):
|
||||
`kGateStageMaxSeconds` (`envelope_overlay.h:85`) and, through it, `kEnvTimeMaxSeconds`
|
||||
(`deck_values.h:22`). **The two move together or not at all** — `deck_values.h` reads the
|
||||
overlay's constant rather than restating it precisely so they cannot drift
|
||||
(`deck_values.h:19-22`). The taper's landmarks above are fit against the **new** ceiling,
|
||||
which is why the ceiling cannot be a follow-up: fitting the taper twice is the only other
|
||||
way to get there.
|
||||
- **`resetDeckParam`'s bypass becomes MANDATORY rather than merely required-anyway.**
|
||||
`deck_values.h:42-46` records that exact default recovery depends on the ceiling being a
|
||||
power of two; **2.0 is, 10.0 is not**, and the log taper compounds it. Nothing here may be
|
||||
"simplified" back into a norm round-trip under any circumstance.
|
||||
- **NEW, and the sharpest requirement in the track: every default must have an EXACT
|
||||
normalized preimage under its taper.** `ParameterInfo::defaultNormalizedValue` (Γ-W4-T1)
|
||||
is normalized, so a host's reset-to-default arrives as `toPlain(defaultNorm)` — and **the
|
||||
host has no `resetDeckParam` bypass to use**. The bypass fixes the editor's reset and
|
||||
cannot fix the host's; only exactness in the map itself makes the two land on the same
|
||||
value. This binds the taper's *shape*, so it belongs here and cannot be handed forward.
|
||||
Master gain's unity (≈ 0.714 norm) is the case where a hair off is audible.
|
||||
- **The AHDSR overlay's schematic axis becomes the taper — the ceiling's real cost, and it
|
||||
is design work, not a constant change.** Each of the four timed stages gets an equal slot
|
||||
and today maps seconds across it linearly (`gatePxPerSecond`,
|
||||
`envelope_overlay.cpp:33-34`). At 2 s a 30 ms attack is 1.5 % of its stage's domain; **at
|
||||
10 s it is 0.3 %, under a pixel at the floor width.** The fix: a stage's slot width becomes
|
||||
`slotPx × taperNorm(seconds)` instead of `slotPx × seconds / ceiling`, so a node's position
|
||||
within its slot **is** its knob's needle position. Legibility becomes ceiling-independent by
|
||||
construction; the one-model invariant gets stronger rather than strained; and **the drawn
|
||||
curve is unaffected**, because the taper decides only where a stage's end node lands while φ
|
||||
still runs linearly across the stage's pixel span — so Γ-W1-T3's φ^p trace composes with it
|
||||
rather than fighting it. **The AHD policy is untouched**: an AHD maps 1:1 onto the
|
||||
waveform's own PCM-aligned time axis and stays linear in seconds. Two alternatives
|
||||
(content-fit auto-scale; a minimum drawn stage width) were considered and rejected — spec
|
||||
§4.3.1 names why, and neither is to be reintroduced as a "simplification."
|
||||
- **`envelope_edit`'s drag inverse must remain the EXACT inverse of the draw.** Both read the
|
||||
same taper module; a node dragged to a pixel and the knob's value at that pixel are one
|
||||
number, not two that agree.
|
||||
- **The taper is EXTRACTED into its own pure module**, with its own `<module>_tests` target,
|
||||
because it now has three consumers in two different dependency layers: `deck_values` (which
|
||||
sits above `envelope_overlay`), `envelope_overlay`/`envelope_edit` (which sit below it),
|
||||
and — from Γ-W4-T1 — the host. Leaving it inside `deck_values` would force an inverted
|
||||
include edge. **Do not solve that by copying the map.**
|
||||
- **Semitone knobs become log2/centre-expanded.** Symmetric, exactly 0 at centre, exactly
|
||||
±`kPitchDepthMaxSemis` at the ends, monotone; **±7 st reached at 50–58 % of each
|
||||
half-travel**.
|
||||
- **`resetDeckParam` bypasses the taper** — it writes the default value directly instead of
|
||||
round-tripping through `norm → value`. This *removes* the power-of-two dependency the
|
||||
header currently documents rather than working around it; that comment
|
||||
(`deck_values.h:42-46`) becomes wrong and must be rewritten.
|
||||
- **Scope is the parameter, not the widget.** Deck knobs (outer ring and inner dial),
|
||||
envelope stage nodes and curve knots all honour it — they are surfaces onto one model, and
|
||||
a snap on one but not the others is a divergence. **Waveform markers are explicitly
|
||||
excluded**: they carry a shipped zero-crossing snap on the same modifier space and their
|
||||
domain is frames.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- Every taper change is verified **persistence-neutral**: a project saved before the change
|
||||
reopens with bit-identical stored values and identical audio; only needle angles move.
|
||||
- Holding Shift mid-drag on each unit category lands the documented whole unit; releasing it
|
||||
does not jump the value.
|
||||
- Holding and releasing Ctrl mid-drag is continuous — no step at either transition.
|
||||
- Double-clicking any knob (outer ring and inner dial independently) lands **exactly** on its
|
||||
default at every taper, verified against a default-constructed `PlaySeconds` rather than a
|
||||
round trip.
|
||||
- The log/log2 landmark positions above are asserted in the **taper module's** own tests, and
|
||||
hold at the **10 s** ceiling — the fit is against the new ceiling, not the old one.
|
||||
- **Every default round-trips exactly through `norm → value`**, asserted per unit category
|
||||
against a default-constructed `PlaySeconds` and against `master_gain`'s unity. This is the
|
||||
criterion Γ-W4-T1 will declare `defaultNormalizedValue` from; it fails here, not there.
|
||||
- **A stage time of several seconds is reachable by hand with no loss of resolution below
|
||||
100 ms**, and `kEnvTimeMaxSeconds == kGateStageMaxSeconds` is asserted, not assumed.
|
||||
- **A project saved at the 2 s ceiling reloads with identical stored seconds and identical
|
||||
audio** — the ceiling change is persistence-neutral for the same reason the taper is.
|
||||
- **The AHDSR overlay reads legibly at both ends of the new range**: a default 3 ms attack is
|
||||
a visible, grabbable node at the floor width, and a 10 s decay still lands its end node at
|
||||
its slot's edge. Assert the node separation, then judge the result by eye in the DAW.
|
||||
- **The overlay's drag inverse is the exact inverse of its draw** at the tapered axis —
|
||||
`nodeAtPoint` / `resolveNodeDrag` and `buildEnvelopePolyline` round-trip.
|
||||
- **The AHD 1:1 policy is unchanged**, asserted: a sustain-less envelope's x-axis stays
|
||||
wall-clock over the waveform.
|
||||
- **The taper module is pure, CTest-covered, and is the ONLY definition of each map** — a
|
||||
grep finds no second copy in `deck_values`, `envelope_overlay`, or the shell.
|
||||
- **The filter's four `*Norm` controls are untouched by the taper pass** — cutoff, Q, morph
|
||||
and drive are already wire-frozen in payload v9; a regression baseline proves their audio
|
||||
is unchanged.
|
||||
- One shared modifier-read helper serves all drag surfaces; no second modifier grammar exists.
|
||||
|
||||
**Open questions.**
|
||||
- **No [Daniel] questions.** Fork **Γ-F3 is REVERSED**: the ceiling moves to **10.0 s, in this
|
||||
track.** Daniel's *"a horrifically long decay with tight exp"* is the case it serves, and
|
||||
the `docs/TODO.md` entry that carried it is discharged rather than deferred again. **The
|
||||
reversal's cause is Ruling 1** — a range endpoint is host-facing normalization, free to
|
||||
move now and permanently expensive after Γ-W4-T1. Both of the prerequisites the deferred
|
||||
entry named are in this track anyway: the log taper (which is what makes a higher ceiling
|
||||
usable at the low end rather than unusable) and the reset bypass (which retires the
|
||||
power-of-two dependency — 2.0 is a power of two, 10.0 is not). The engineer should know the
|
||||
reset bypass is now doing triple duty and must not be "simplified" back into a norm
|
||||
round-trip under any circumstance.
|
||||
- **[propose at review]** the exact shape of the taper, subject to the landmark bounds **and**
|
||||
the exact-default-preimage requirement. Those two together are tighter than either alone,
|
||||
and the second is easy to satisfy by accident and easy to lose in a refactor — **assert it,
|
||||
do not observe it.**
|
||||
- **[propose at review]** whether the tapered schematic axis wants a visible tick or
|
||||
gradation cue, now that it is no longer linear in time. The plan's lean is **no** — the ms
|
||||
labels carry the number and the editor's no-decoration policy stands — but a reader who
|
||||
finds the axis illegible in the DAW should say so rather than silently adding one.
|
||||
**Landed** — see `docs/COMPLETED.md` for the full narrative. One consistent interaction and
|
||||
taper law across every variable control, landed before Rate/Pitch or any VST3 parameter
|
||||
existed so both are authored into it rather than retrofitted. The taper is extracted into
|
||||
its own pure module, `core/instrument/ui/param_taper` — the one home three consumers read
|
||||
(the knob's needle, the AHDSR overlay's schematic axis and its drag inverse, and — from a
|
||||
later wave — the VST3 host's `toPlain`/`toNormalized`). Shift snaps to whole units in the
|
||||
control's displayed category; Ctrl scales the drag by 0.05; Shift+Ctrl resolves to Shift; a
|
||||
mid-drag modifier transition re-anchors value and cursor position. **The stage-time ceiling
|
||||
moves 2.0 s → 10.0 s** (`kGateStageMaxSeconds`/`kEnvTimeMaxSeconds`, moved together so they
|
||||
cannot drift), reversing Γ-F3 on Daniel's later ruling, and every default now has an exact
|
||||
normalized preimage under its own taper — the requirement Γ-W4-T1's `defaultNormalizedValue`
|
||||
depends on, since a host's reset-to-default has no `resetDeckParam` bypass to fall back on.
|
||||
The filter's four `*Norm` controls stay untouched (wire-frozen in payload v9); the change is
|
||||
persistence-neutral throughout.
|
||||
|
||||
#### Γ-W1-T2 — `master-bus-audio`
|
||||
|
||||
**Goal.** The master limiter and the meter's **audio and publication halves**, plus **the
|
||||
plugin's first latency reporting** — no editor drawing. Landing the audio ahead of the deck is
|
||||
what lets Γ-W3 draw against real published state instead of a stub.
|
||||
|
||||
**Spec:** `docs/product/instrument-control-surface.md` §3.1, **§3.1.1 (latency — read this
|
||||
first; it was rewritten when Γ-F6 closed, so an older reading of it is wrong)**, §3.2–3.3,
|
||||
§3.5, §7.10, §7.11, §8.2.
|
||||
|
||||
**Surface boundary — owns:** a new pure limiter module and a new pure meter-ballistics
|
||||
module under `core/instrument/engine/` (each with its own `<module>_tests` target),
|
||||
`shell/instrument/reasampler_processor` (the chain, the published block state, **and the
|
||||
`getLatencySamples` / `restartComponent(kLatencyChanged)` path**), and **the phase's FIRST
|
||||
params-payload rung** (the limiter enable flag). **Does not own** MASTER's deck geometry or
|
||||
any drawing — that is Γ-W3-T1. **This track spends that rung: `kParamsPayloadVersion == 15`**
|
||||
(`map/component_state_io.h:170`), confirmed on this branch as of 2026-08-01 — the full ladder
|
||||
(spent / next-free / reserved) is stated in the phase summary's resequencing note near the end
|
||||
of this phase's ASCII block.
|
||||
|
||||
**Behavior.**
|
||||
- **Chain:** `voice mixer → master gain (existing ramped multiply) → limiter (bypassable) →
|
||||
output bus`, with the meter tapped at the **bus output, post-limiter**.
|
||||
- **Limiter: a single toggle, no configurable controls.** Baked ceiling **−0.3 dBTP**.
|
||||
Default **off**. **No makeup gain, ever, of any kind** — transparent at rest.
|
||||
Stereo-linked detection (max |L|,|R| drives one gain) so the image is not moved.
|
||||
- **True-peak detection is sidechain-only** — an oversampled detector in the sidechain, never
|
||||
oversampling the signal path. Factor is the engineer's call under the measure gate.
|
||||
- **Lookahead, with DYNAMIC reported latency (Γ-F2, ruled).** `getLatencySamples()` returns
|
||||
**0** when the limiter is off and **the lookahead in samples** when it is on; the toggle
|
||||
calls `IComponentHandler::restartComponent(kLatencyChanged)`. **None of this exists today** —
|
||||
there is no `getLatencySamples` override, no `kLatencyChanged`, and no `restartComponent`
|
||||
call site anywhere in `src/`; the plugin ships the SDK default of 0. This track introduces
|
||||
the plugin's first latency reporting.
|
||||
- **The restart is routine; the fencing is against a standing scar, not against the flag.**
|
||||
Dynamic latency reporting is ordinary VST3-instrument behaviour and REAPER handles it as a
|
||||
matter of course. The SDK's deactivate/reactivate requirement
|
||||
(`pluginterfaces/vst/ivsteditcontroller.h:105-108`) is the normal contract. **What makes the
|
||||
cycle expensive here is this plugin's own `setActive`** — reactivate calls
|
||||
`reloadInstrument()`, a bridge read plus a full WAV re-decode
|
||||
(`reasampler_processor.cpp:89-97`), where a typical plugin only allocates buffers; deactivate
|
||||
frees `live_`/`draining_`/graveyard (`:98-107`) for a documented reason (ghost sustained
|
||||
voices). **Γ-F6 is ruled: ship it — the toggle is a patch-design gesture, not a
|
||||
during-playback one.** Do **not** build a constant-reported-latency fallback and do **not**
|
||||
gate the deliverable on a measurement. The reduction of that self-inflicted cost is filed in
|
||||
`docs/TODO.md` ("Decouple the instrument reload from VST3 activation") with its trigger
|
||||
condition; it is out of scope here. The four requirements below survive as engineering
|
||||
hygiene against the `kIoChanged` scar, and all four are acceptance criteria:
|
||||
1. **Verify the whole call sequence against the vendored Steinberg SDK** before writing it,
|
||||
including the ordering rule that the new latency is what `getLatencySamples` returns
|
||||
*after* `setActive(true)` — so **the reported value must derive from persisted state, not
|
||||
from a transient the deactivate clears.**
|
||||
2. **Prove the restart does not disturb the output bus arrangement.** The output stays one
|
||||
permanently-stereo bus, never renegotiated.
|
||||
3. **Ship a regression test in the spirit of `testDualMonoStereoSampleRendersCentered`** —
|
||||
a dual-mono capture rendered across a limiter toggle stays centered, L ≡ R.
|
||||
4. **`restartComponent` is never called from `process()`.** Main/UI thread only, and
|
||||
coalesced so repeated clicks produce one restart per settled state.
|
||||
**This is NOT the change `reasampler_processor.cpp:66-68` forbids.** That warning is against
|
||||
reintroducing per-mode **bus** renegotiation (`kIoChanged` class), which panned a dual-mono
|
||||
capture hard right in the host's pin re-routing; `kLatencyChanged` is a different flag and the
|
||||
bus is untouched. But the precedent — mid-session `restartComponent` in this plugin has
|
||||
already shipped one real regression — is exactly why (2) and (3) are non-negotiable.
|
||||
- **Flipping the toggle during playback: apply immediately, do NOT defer to a transport
|
||||
boundary** (product ruling, spec §3.1.1). A deferred restart leaves the plugin misaligned by
|
||||
the lookahead with no visible cue, which is worse than a visible interruption; and the host,
|
||||
not the plugin, schedules the deactivate/reactivate anyway. **Daniel has accepted the
|
||||
interruption outright** (Γ-F6) — it is not a case to design for. Two things remain in scope,
|
||||
and neither is a mitigation for it:
|
||||
- **A short (≤ 10 ms) equal-gain crossfade over the engage/disengage.** Kept as a *quality*
|
||||
measure, not a mitigation: a limiter engaging is a gain-path change, and this codebase
|
||||
already ramps every gain-path change (`kGainRampSeconds`, `ValueRamp`). It also earns its
|
||||
keep independently of the restart, because **we do not control when the host acts on the
|
||||
request** — our own transition must be clean in the window before it does.
|
||||
- **The limiter enable is classified NOT automatable**
|
||||
(`docs/product/parameter-automation.md` §3.8) so nothing can flip it at rate. It is also
|
||||
**not** the plugin's `kIsBypass` parameter.
|
||||
- **Per block the processor publishes, as relaxed atomics:** per-channel peak `max|x|`, a
|
||||
latched clip flag, and the block's maximum gain reduction. **No dB conversion, no
|
||||
ballistics, no hold timers on the audio thread** — the UI converts and runs ballistics from
|
||||
block peaks and elapsed time. This widens the existing advisory-peak pattern
|
||||
(`reasampler_processor.h:109-113`), which is not reusable as-is.
|
||||
- **Meter ballistics (pure, unit-tested):** instantaneous rise; **fall 20 dB/s**; peak-hold
|
||||
latched at the running max, **held 1.5 s**, then falling at the same rate; scale **linear in
|
||||
dB over −60…+6 dBFS**; clip latches at block peak ≥ 0 dBFS and is cleared on request.
|
||||
- **The `ComponentState` payload rung** appends the limiter flag as a strict suffix on the
|
||||
existing discipline; the preceding version's blob is a strict prefix and lifts to bypassed.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- **With the limiter bypassed the rendered output is byte-identical to the pre-change build**,
|
||||
asserted by a regression baseline, not by ear.
|
||||
- With the limiter engaged, no output sample exceeds the ceiling on program material that
|
||||
exceeds it by up to +12 dB; with it bypassed and gain driven, the output does exceed
|
||||
0 dBFS (proving the toggle is doing the work).
|
||||
- **Nothing is louder at rest with the limiter on.** A signal that never reaches the threshold
|
||||
is bit-identical engaged and bypassed.
|
||||
- No allocation, no lock, no transcendental on the per-sample path; the measure-and-report
|
||||
gate reports per-voice-block CPU with the limiter engaged at 32 voices.
|
||||
- The meter-ballistics module is pure and CTest-covered: rise, 20 dB/s fall, 1.5 s hold, clip
|
||||
latch/clear, and the dB↔pixel map are all asserted without a host.
|
||||
- A project saved before this change reopens with the limiter bypassed and sounding identical.
|
||||
- **`getLatencySamples()` returns exactly 0 with the limiter off**, and the lookahead in
|
||||
samples with it on — asserted against the persisted flag, and correct across a
|
||||
deactivate/reactivate cycle.
|
||||
- **A dual-mono capture rendered across a limiter toggle stays centered** (L ≡ R), and the
|
||||
output bus arrangement after a latency-change restart is identical to before it.
|
||||
- **The plugin emits no hard step at the toggle** — the engage/disengage crossfade is asserted
|
||||
on a rendered signal, not judged by ear.
|
||||
|
||||
**Open questions.**
|
||||
- **No [Daniel] questions. Fork Γ-F6 is ruled** — dynamic latency ships as specced, the
|
||||
deactivate/reactivate is accepted, and there is no fallback design and no measurement gate.
|
||||
Do not reintroduce either; the constant-reported-latency option is closed, not shelved.
|
||||
- **[verify]** `temp_cortex/` has already been assessed and **rejected** (spec §3.5) — do not
|
||||
re-litigate it, and do not transplant from it.
|
||||
- **[record, not a gate]** While the limiter is in REAPER under your hand, note what the
|
||||
restart actually costs — do notes cut, is the re-decode perceptible, does transport hiccup —
|
||||
and record it in this track's review. It is **not** a gate on shipping and no outcome changes
|
||||
the design; it is the trigger-condition evidence for the `docs/TODO.md` entry "Decouple the
|
||||
instrument reload from VST3 activation," which is where that cost gets reduced if it ever
|
||||
matters. Do **not** restructure `setActive` here: its destructive shape is deliberate and its
|
||||
reasoning (ghost sustained voices on reactivate) is documented at the call site.
|
||||
**Landed** — see `docs/COMPLETED.md` for the full narrative. The master bus: a bypassable
|
||||
true-peak limiter (baked −0.3 dBTP ceiling, default off, no makeup gain, stereo-linked,
|
||||
sidechain-only oversampled detection), the meter's audio and publication half, and the
|
||||
plugin's first latency report — `getLatencySamples()` returns 0 with the limiter off and the
|
||||
lookahead in samples with it on, driving `restartComponent(kLatencyChanged)` on toggle. New
|
||||
pure modules `core/instrument/engine/limiter` and `engine/meter_ballistics`. **Spent the
|
||||
phase's first payload rung: `kParamsPayloadVersion` reaches 15**, appending the limiter
|
||||
enable flag as a strict suffix.
|
||||
|
||||
#### Γ-W1-T3 — `contour-trace-curves`
|
||||
|
||||
**Goal.** Staged envelope segments draw as the curve their exponent defines, so the
|
||||
mid-segment knot stops floating off its own trace.
|
||||
|
||||
**Spec:** `docs/product/instrument-control-surface.md` §5.
|
||||
|
||||
**Surface boundary — owns:** `shell/instrument/editor_paint_waveform.cpp`'s staged-envelope
|
||||
trace and a **new pure tessellation module** for it. **Does not own** the loop/crossfade
|
||||
marks (Γ-W2-T2), `envelope_overlay`'s vertex model, or the drawn-EG (spline) trace.
|
||||
|
||||
**The helper's home is now constrained, not a choice.** `ui/envelope_overlay` was previously
|
||||
offered as a candidate home for the tessellation helper; **Γ-W1-T1 now owns that module**
|
||||
(the AHDSR schematic axis, per Ruling 2), so the helper lands in a **new** pure module under
|
||||
`core/instrument/ui/`. T1 also owns where an AHDSR's vertices land — this track owns only the
|
||||
stroke *between* vertices, and tessellates over φ across a segment's pixel span, so the
|
||||
tapered axis changes nothing about the curve drawn. **Express this track's assertions against
|
||||
the returned vertices, not against absolute pixel literals**, and the rebase onto T1 is free.
|
||||
|
||||
**Behavior.** The defect is verified: `editor_paint_waveform.cpp:218` drops knots
|
||||
(`if (v.knot) continue;`) and joins the remaining vertices with straight strokes, and
|
||||
`curveMap` is never called in the paint path even though the exponent is in scope at `:211`.
|
||||
Knot *positioning* already honours the exponent via `curveMidLevel`
|
||||
(`envelope_overlay.cpp:94-105`) — that divergence is the visible symptom. The fix draws each
|
||||
sloped stage through **the same `curveMap` the audio uses**, so trace and sound cannot
|
||||
diverge; tessellation approach is the engineer's call.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- **At every exponent the knot's centre lies on the trace, within 1 px** — the reported defect,
|
||||
stated as the gate.
|
||||
- **At exponent 1.0 the segment is visually identical to today's straight line.**
|
||||
- No visible faceting at the widest segment the canvas can produce; a fixed low tessellation
|
||||
count is not acceptable at full width.
|
||||
- All three envelopes, both play modes, all sloped stages (attack/decay/release) — one paint
|
||||
path, one fix.
|
||||
- The established trace grammar is unchanged: one weight, `kEnvTracePx = 2.0`, through the
|
||||
analytic stroker. Both overlay layout policies (AHDSR right-anchored schematic, AHD 1:1)
|
||||
are honoured unchanged. The spline overlay's own trace is untouched.
|
||||
- **Audio is unchanged** — this is a drawing defect only; a regression baseline proves it.
|
||||
**Landed** — see `docs/COMPLETED.md` for the full narrative. Staged envelope segments now
|
||||
draw as the curve their exponent defines, closing the defect where the mid-segment knot
|
||||
floated off its own trace. A new pure module, `curve_tessellate`, draws every sloped stage
|
||||
through the same curve the audio's evaluators use, so the drawn stage and the sound it makes
|
||||
cannot diverge. All three envelopes, both play modes, every sloped stage, share the one fix;
|
||||
audio is unchanged.
|
||||
|
||||
#### Γ-W1-T4 — `editor-floor-and-row-law`
|
||||
|
||||
**Goal.** Commit the **canvas** — the window floor, the width budget it is derived from, and
|
||||
the row every deck group belongs to — so every other UI track in the phase is drawn, tested and
|
||||
judged at the final window size. The **arrangement** inside that canvas is Γ-W3-T1's.
|
||||
|
||||
**Spec:** `docs/product/instrument-control-surface.md` §1.1 (the two categories), §1.2 (the
|
||||
floor arithmetic block), §1.6 (the headroom ledger), §7.1 and §7.4 (the two invalidated
|
||||
`knob_deck.h` notes).
|
||||
|
||||
**Surface boundary — owns:** `core/instrument/ui/sample_bands.h` (`kEditorMinWidth`),
|
||||
`core/instrument/ui/knob_deck.h` (the declared budget constants and the two invalidated header
|
||||
notes), `core/instrument/ui/deck_groups.{h,cpp}` (**the new row predicate only**), and the five
|
||||
test fixtures that read the floor — `test_sample_bands.cpp`, `test_deck_groups.cpp`,
|
||||
`test_knob_deck.cpp`, `test_sample_chrome.cpp`, `test_keyboard_strip.cpp`. **Does not own**
|
||||
`layoutDeck` / `deckRowCount` / `deckHeight` behaviour, the justification law, any descriptor,
|
||||
MASTER's inventory or interior, any painter, or any parameter. It changes **no drawing code at
|
||||
all.**
|
||||
|
||||
**Behavior — what it commits.**
|
||||
- **`kEditorMinWidth` 980 → 1190. `kEditorMinHeight` stays 680** (Γ-F1).
|
||||
- **Three declared budget constants in `knob_deck.h`:** the row block both rows will justify
|
||||
inside (**1020**), the right-anchored spanning deck's reserved width (**MASTER 142**), and
|
||||
the ceiling (**1280**). These are *declarations of budget*, not measurements — nothing
|
||||
computes them from a descriptor, and Γ-W3-T1's job is to prove its content fits inside them.
|
||||
- **The floor is derived, not asserted as a literal.** `1020 + kDeckGroupGap(12) + 142 +
|
||||
2·kPad(8) = 1190`. `kEditorMinWidth` stays a literal in `sample_bands.h` — **do not add an
|
||||
include edge from `sample_bands` to `knob_deck`**, which would invert the allocator's
|
||||
deliberate independence from the deck (it takes `deckHeight` as a *parameter* for exactly
|
||||
that reason). The identity is asserted in `test_deck_groups.cpp`, which already includes
|
||||
both headers. This is the Θ-W6-T1 derived-floor precedent, landed once and never rewritten.
|
||||
- **Row membership becomes a property of the group id:** `DeckRow { Sound, Contour, Spanning }`
|
||||
+ `deckRowFor(DeckGroupId)` in `deck_groups`, an **exhaustive switch** on the
|
||||
`isLiveDeckParam` discipline, so a future group is a compile error rather than a silent
|
||||
default. Partition: **Sound** = PITCH/RATE, FILTER, VELOCITY, VOICE; **Contour** = PITCH ENV,
|
||||
FILTER ENV, AMP ENVELOPE; **Spanning** = MASTER. **Nothing consumes it until Γ-W3-T1** — that
|
||||
is the seam, and it is why the predicate is safe to land now: **membership is a property of
|
||||
the group, width is a property of the descriptor**, and only the widths are still moving.
|
||||
- **Γ adds no new deck group**, so no later track amends this predicate.
|
||||
|
||||
**The seam, stated as what this track can and cannot assert.**
|
||||
|
||||
*Can assert today:*
|
||||
- The derived floor identity above, and `kEditorMinWidth ≤ 1280` with **90 px** of headroom.
|
||||
- `kEditorMinHeight == 680`, asserted so no later track drifts Γ-F1's ruling.
|
||||
- `deckRowFor` is total over `DeckGroupId` and yields exactly the partition above.
|
||||
- **Row 2's natural width already fits the block, in both play modes:**
|
||||
252 + 312 + 312 = **876 ≤ 1020**, leaving both its gutters ≥ `kDeckGroupGap`. Mode-stable
|
||||
because FILTER ENV's and AMP's reserve slots hold them at 312 in Gate and Trigger alike.
|
||||
- **MASTER's reserve is not yet spent:** `deckGroupWidth(MASTER) == 72 ≤ 142`.
|
||||
- At the floor, deck band **216** and waveform band **358** — the reflow's 112 px arrives here,
|
||||
two waves early (see the interim layout below).
|
||||
|
||||
*Cannot assert yet, and must not force:*
|
||||
- **Row 1's natural width does not fit the block.** Today it is PITCH 150 + FILTER 524 +
|
||||
VELOCITY 192 + VOICE 164 = **1030**, against the 1020 block. The 50 px deficit is exactly
|
||||
what the two descriptor changes buy: PITCH → PITCH/RATE **+42** (Γ-W2-T1) and FILTER's
|
||||
`Band|Notch` moving to the caption corner **−92** (Γ-W3-T1), netting **980**. Record the
|
||||
target and the two contributions as a test comment; **assert the fit in Γ-W3-T1, and do not
|
||||
pre-empt either descriptor change to close it early.**
|
||||
- Gutter distribution, the filter tie-line at x = 636, flush outer edges, MASTER's interior and
|
||||
its meter — all Γ-W3-T1. Every one of them measures a descriptor that does not exist yet.
|
||||
|
||||
**The interim editor, stated exactly so it is not filed as a defect.** At the new floor,
|
||||
`availWidth = 1190 − 2·kPad = 1174`, and the **unchanged** greedy whole-group wrap packs:
|
||||
|
||||
```
|
||||
row 1 PITCH 150 · PITCH ENV 252 · FILTER 524 = 950 used, 224 px ragged right
|
||||
row 2 FILTER ENV 312 · AMP 312 · VELOCITY 192 ·
|
||||
VOICE 164 · MASTER 72 = 1100 used, 74 px ragged right
|
||||
```
|
||||
|
||||
**Two rows, not three** — so the deck band is already 216 and the waveform already draws at its
|
||||
final 358 px, in both Gate and Trigger. After Γ-W2-T1 lands PITCH/RATE the pack is row 1 = 992,
|
||||
row 2 unchanged; still two rows. The composition is wrong in exactly the way the reflow exists
|
||||
to fix — PITCH ENV sits up with the sound decks, VOICE and MASTER sit down with the envelopes,
|
||||
MASTER is still a single-height 72 px box, and both rows are left-packed with dead space at the
|
||||
right. **Worse than today in composition, better in proportion.** That is the accepted
|
||||
transitional state for the rest of the phase.
|
||||
|
||||
**Do not convert the two-row interim into a claim.** It is a coincidence of the greedy wrap at
|
||||
exactly this width, not a guarantee — which is precisely why Γ-W3-T1's criterion is "two rows
|
||||
**by construction**, asserted against the group inventory, not observed as a wrap outcome."
|
||||
`testDeckFitsInsideTheEnforcedMinimumWindow` currently asserts `deckRowCount == 3`; relax it to
|
||||
an **upper bound** (`<= 2`), which is a real regression canary throughout the interim and is
|
||||
subsumed by Γ-W3-T1's exact claim. An exact `== 2` here is acceptable only with a comment
|
||||
naming it as a wrap outcome the reflow replaces.
|
||||
|
||||
**Acceptance criteria.**
|
||||
- **The floor is 1190 × 680, reached by a derived test over the three budget constants**, not
|
||||
by a literal — and the derivation is the one Γ-W3-T1 later reads rather than a second copy.
|
||||
- **Headroom is exactly 90 px** against the 1280 ceiling, asserted.
|
||||
- `deckRowFor` is exhaustive over `DeckGroupId`; adding a group without classifying it fails to
|
||||
compile.
|
||||
- Row 2's natural width and MASTER's unspent reserve are asserted, in **both** play modes.
|
||||
- **All five floor-reading test fixtures pass at the new floor** — including the chrome row,
|
||||
whose title slot gets *more* room at 1190, not less.
|
||||
- **No drawing code changes, no descriptor changes, no parameter changes, no audio change.**
|
||||
A regression baseline proves the last of those trivially.
|
||||
- The two invalidated `knob_deck.h` notes (§7.1's fourteen-pixel headroom figure, §7.4's
|
||||
cells-and-floor pairing) are **re-derived against the new floor, not deleted** — §7.4's
|
||||
restatement is *the deck's cell metrics AND its group/row composition both drive
|
||||
`kEditorMinWidth`; none of the three may move alone.*
|
||||
|
||||
**Open questions.** **No [Daniel] questions.** **[propose at review]** whether the three budget
|
||||
constants belong in `knob_deck.h` (the deck owns the row block and the spanning-deck reserve)
|
||||
or in `sample_bands.h` (the allocator owns the floor they derive). The plan's lean is
|
||||
`knob_deck.h` with the identity in the test, because it adds no include edge; either is
|
||||
defensible, but the *derivation must live in exactly one place*.
|
||||
**Landed** — see `docs/COMPLETED.md` for the full narrative. Commits the editor's canvas
|
||||
ahead of the rest of the phase's UI work: `kEditorMinWidth` moves 980 → 1190
|
||||
(`kEditorMinHeight` stays 680, Γ-F1), derived from three budget constants — the row block
|
||||
(1020), MASTER's reserved width (142), and the 1280 ceiling (`kEditorCeilingWidth`, relocated
|
||||
into `sample_bands.h`) — leaving 90 px of headroom. Row membership becomes a property of the
|
||||
group id via an exhaustive `deckRowFor(DeckGroupId)` switch (Sound / Contour / Spanning),
|
||||
consumed by no one yet — **that consumption, and the fit inside the 1020 block, is
|
||||
Γ-W3-T1's** to assert. No drawing code, descriptor, parameter, or audio changed in this
|
||||
track.
|
||||
|
||||
#### Γ-W1-T5 — `preserve-time-stretch`
|
||||
|
||||
**Goal.** A real pitch-preserving time-stretcher for Preserve mode, written from established
|
||||
state-of-the-art literature — landed **before** the control that drives it, so Rate ships onto a
|
||||
finished engine rather than onto a disposable stand-in.
|
||||
|
||||
**Spec:** `docs/product/instrument-control-surface.md` §2.5.
|
||||
|
||||
**Moved from Γ-W4-T1 (Daniel, 2026-08-01).** It is the longest pole in the phase and has zero
|
||||
dependency on any UI work. **The consequence is the interesting one: it inverts the
|
||||
relationship with Rate.** Under the old order the stretcher was Rate's quality upgrade and
|
||||
Γ-W2-T1 shipped an interim resample-and-cancel path to make Rate complete on day one; under
|
||||
this order the stretcher is Rate's **prerequisite** and **the interim path is not built at
|
||||
all.** Skipping a stand-in that was only ever going to be deleted is the win; see Γ-W2-T1's
|
||||
named contingency for what happens if this track's gate slips.
|
||||
|
||||
**Precedent for landing a DSP module ahead of its consumer:** Θ-W1-T3 (`filter-dsp-port`)
|
||||
landed the filter DSP as a standalone pure module a wave before Θ-W2-T1 wired it into the voice
|
||||
path, for the same reason — the unknown is the DSP, not the wiring.
|
||||
|
||||
**Surface boundary — owns:** `core/instrument/engine/pitch_shift` and whatever new pure module
|
||||
the stretcher needs (each with its own `<module>_tests` target), plus `voice.{h,cpp}`'s Preserve
|
||||
read path. **Does not own** any parameter, any UI, the varispeed path, or the deck. It adds no
|
||||
`ComponentState` field and takes **no rung of the payload ladder**.
|
||||
|
||||
**Behavior and constraints.** The algorithm is **the engineer's call under a
|
||||
measure-and-report gate — this plan deliberately names none.** The constraints:
|
||||
- **The stretch ratio is an argument, not a parameter.** Nothing publishes a non-unity ratio
|
||||
until Γ-W2-T1's Rate knob does. Until then the Preserve read path runs at ratio 1.0 and must
|
||||
be **bit-identical to the shipped Preserve read** — a stronger and cheaper regression gate
|
||||
than the old plan's A/B-against-an-interim-path, because the baseline is a build that exists.
|
||||
- **CPU stance (Daniel, verbatim intent):** *"we should be efficient but accept the cost of
|
||||
high-quality algorithm choices. It's 2026, most people's computers can handle audio with
|
||||
ease. Just don't be wasteful."*
|
||||
- **RT-safe:** no allocation, no I/O, no lock in `process()`; buffers sized at voice
|
||||
allocation or at the off-audio-thread reload, on `pitch_shift`'s existing pre-warm
|
||||
precedent.
|
||||
- **Per-voice state, holding up at the 32-voice ceiling.** The gate is 32 simultaneous
|
||||
Preserve voices at 50 % and 200 %, not one voice at 100 %.
|
||||
- **No new third-party dependency** (`pitch_shift`'s standing property).
|
||||
- **No dispatch on the per-sample path** — concrete, inlineable types; no `IStretcher`.
|
||||
- **Onset behaviour is a regression surface.** GA2 eliminated Preserve's ~25 ms onset 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.**
|
||||
|
||||
**Acceptance criteria.**
|
||||
- **Ratio 1.0 with no shift is bit-identical to the shipped Preserve read**, asserted by a
|
||||
regression baseline — the null case, and the criterion that makes landing this ahead of Rate
|
||||
safe.
|
||||
- Preserve speaks on frame 0 — no added onset latency, no first-frame smear, in any
|
||||
ratio/shift combination.
|
||||
- No audible metallic or phasey artefacting on sustained tonal material at ±6 st and
|
||||
75–133 % ratio; transient material at 50 % / 200 % is no worse smeared than **varispeed
|
||||
playback at the equivalent ratio** — the honest "what does preserving pitch cost" reference,
|
||||
and the one that needs **no disposable implementation built to serve the comparison.**
|
||||
- The Gate sustain-loop contract is unchanged: **loop the source, shift the output** — loop
|
||||
points remain source-frame facts.
|
||||
- **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 on three material classes (one-shot,
|
||||
tonal sustain, full-mix bounce). Report to Daniel; the choice is not final until he has heard
|
||||
the A/Bs.
|
||||
|
||||
**Open questions.** **[propose, with a measurement step]** the algorithm family itself.
|
||||
**[verify]** that `core/instrument/CLAUDE.md`'s *"`WDL_Resampler` is not a Preserve engine —
|
||||
never wire it as the duration-preserving path"* is honoured: under Preserve, Rate legitimately
|
||||
changes duration, so a resampled read is an explicit duration control — but the
|
||||
*pitch-preserving* mechanism must not be a resampler. **No [Daniel] questions.**
|
||||
**Landed** — see `docs/COMPLETED.md` for the full narrative. A real pitch-preserving
|
||||
time-stretcher for Preserve mode, moved up from a later wave (Daniel, 2026-08-01) so Rate
|
||||
ships onto a finished engine instead of a disposable stand-in — the interim
|
||||
resample-and-cancel path that had been planned for Γ-W2-T1 was not built at all. New
|
||||
header-only pure module `time_stretch` alongside `pitch_shift`'s existing shift-ratio
|
||||
control; rate 1.0 is exactly one source frame per output frame with no residue, keeping the
|
||||
unity-ratio Preserve read bit-identical to the pre-stretch engine. No new third-party
|
||||
dependency, no allocation/lock in `process()`, no per-sample dispatch.
|
||||
|
||||
---
|
||||
|
||||
#### Γ-W1-T7 — `psola-preserve`
|
||||
|
||||
**Goal.** Preserve's splices become pitch-synchronous: the source's fundamental period is
|
||||
detected offline at sample load, cached, and the splice jump becomes a whole number of that
|
||||
period, so an aligned landing point exists by construction rather than being searched for.
|
||||
**Landed** — see `docs/COMPLETED.md` for the full narrative, including the corrected closure
|
||||
status below. Preserve's splices become pitch-synchronous: a new pure module,
|
||||
`core/instrument/engine/period_detect` (two-pass YIN), estimates the source's fundamental
|
||||
period once at load; `pitch_shift`'s splice jump becomes the multiple of that period nearest
|
||||
the fixed window, so an aligned landing point exists by construction. Detection runs off the
|
||||
audio thread by link graph — `sampler_core` does not link `period_detect` — and an unknown
|
||||
period restores the fixed-window geometry byte for byte. A period is derived from the audio
|
||||
at load, so it is cache, not state: no `ComponentState` field, no payload rung. It gates the
|
||||
Rate control (Γ-W2-T1) on the plan's own stated principle that Rate must not ship before its
|
||||
Preserve engine.
|
||||
|
||||
**Origin — Daniel's ruling, 2026-08-01, after Γ-W1-T5 landed.** T5's measurement pass showed
|
||||
the shipped fixed 50 ms OLA window with a ±window/4 correlation search could only reach
|
||||
landings spanning `[¾w, 1¼w]` — a 1.5:1 span that cannot contain a whole number of periods for
|
||||
low material. A 30 Hz tone (1470 frames at 44.1 kHz) had no phase-aligned landing at all. A
|
||||
second, distinct failure mode: splices recur every `window/|rate − shift|` frames and fail
|
||||
when that interval is shorter than the output period. Three cheaper options were offered and
|
||||
declined: widening `maxLag` to `window/2` (fixes the geometry, not the cadence); sizing the
|
||||
window from the note's known fundamental at note-on (fixes both, but trusts root-note
|
||||
tagging); enlarging the window to ~200 ms (fixes both, smears transients — the OLA crossfade
|
||||
is `window/4`).
|
||||
|
||||
**Why this is a new track and not an amendment to Γ-W1-T5.** It touches the sample-load /
|
||||
analysis path, which is outside T5's stated surface boundary (`pitch_shift`, the stretcher
|
||||
module, `voice`'s Preserve read path).
|
||||
|
||||
**Why PSOLA is affordable here.** ReaSampler is a sampler, so the source is fixed and fully
|
||||
known at load. Detection runs once during the reload that already happens, entirely off the
|
||||
audio thread — the usual real-time objection to PSOLA does not apply.
|
||||
|
||||
**Surface boundary — owns:** a new pure module `core/instrument/engine/period_detect`
|
||||
(two-pass YIN, with its own `period_detect_tests` target), `pitch_shift`'s jump geometry, the
|
||||
load-time hook in `map/sample_map`'s `buildSampleData`, and `voice`'s note-on. Adds no
|
||||
`ComponentState` field and takes **no rung of the payload ladder** — a detected period is
|
||||
derived from the audio, so it is cache, not state.
|
||||
|
||||
**Behavior and constraints.**
|
||||
- **Enforcement is by link graph, not by convention.** The detector runs off the audio thread
|
||||
because `sampler_core` does not link `period_detect` — no translation unit on the render path
|
||||
can name `detectPeriod`. An unknown period restores the fixed-window geometry byte for byte.
|
||||
- **Dependency it discharges:** it gates the Rate control (Γ-W2-T1) on the plan's own stated
|
||||
principle that Rate must not ship before its Preserve engine.
|
||||
|
||||
**Status — landed, with open findings.**
|
||||
- The **geometry** failure mode is closed and asserted.
|
||||
- The **cadence** failure mode is **not** closed. It was re-characterized rather than fixed:
|
||||
the previously-headlined 7–21 % artifact-energy readings turned out to be ~95 % the
|
||||
measurement's own spectral leakage, leaving a real excess of 0.23–0.51 %. The track asserted
|
||||
no-regression there rather than claiming an improvement.
|
||||
|
||||
**Open questions.**
|
||||
- **Unresolved review findings, not a design fork.** A later review of the follow-up fold
|
||||
returned unresolved Major findings; remediation has not yet been dispatched. The
|
||||
findings themselves live in the review, not here.
|
||||
**Both failure modes this track set out to close are now closed.** The **geometry** failure
|
||||
mode (no phase-aligned landing existing inside the search window for low material) closed at
|
||||
the original merge. The **cadence** failure mode — splices recurring faster than the output
|
||||
period — was left open at that point, with unresolved review findings from a later review of
|
||||
a follow-up fold; three remediation commits have since landed and a re-review confirmed the
|
||||
earlier findings closed. The closing measurement (one-machine, Debug-build) is recorded in
|
||||
`docs/COMPLETED.md`, not restated here.
|
||||
|
||||
---
|
||||
|
||||
@@ -4354,13 +3848,17 @@ Phase Ξ — The resample loop (W1 concurrency-safe with Θ from Θ-W
|
||||
T1 capture-signal-popup .................... 15 (popup abandoned; window derives)
|
||||
|
||||
Phase Γ — The instrument's control surface (none of the seventeen; ends with VST3 params)
|
||||
W1 Foundations [5 tracks, disjoint by surface]
|
||||
W1 Foundations — landed [seven tracks, disjoint by surface]
|
||||
T1 knob-interaction-law ....... modifiers + ONE taper module + reset bypass
|
||||
+ 10 s ceiling + AHDSR schematic axis [Ruling 2]
|
||||
T2 master-bus-audio ........... limiter + meter ballistics + dynamic PDC [rung 1]
|
||||
T3 contour-trace-curves ....... staged traces draw curved, knot on its trace
|
||||
T4 editor-floor-and-row-law ... floor 1190x680 + budget constants + row predicate
|
||||
T5 preserve-time-stretch ...... real stretcher [measure-and-report gate]
|
||||
T6 exhaustive-switch gate on pure libraries ... /we4062, -Werror=switch on
|
||||
pure libraries [no PLAN entry — see COMPLETED.md]
|
||||
T7 psola-preserve ............. PSOLA-aligned splice jump; period_detect
|
||||
[cadence closure — see COMPLETED.md]
|
||||
W2 New controls, and the overlay's marks [2 tracks]
|
||||
T1 pitch-rate-deck ............ Rate + Pitch, Varisp/Presrv compounding [rung 2]
|
||||
T2 loop-crossfade-ux .......... four-mark grammar; fade painted where it is heard
|
||||
|
||||
Reference in New Issue
Block a user