docs: close out Θ-W3-T2 into COMPLETED, collapse the W3 wave, and record the overlay contrast wart as open
This commit is contained in:
@@ -261,6 +261,74 @@ single derivation from the value type; `PlayParams` stays a plain copyable value
|
||||
- A filter envelope only advances while its depth is non-zero (the exact-skip at
|
||||
`modAmount == 0`), which is what keeps the at-rest path byte-identical.
|
||||
|
||||
### Θ-W3-T2 — staged-envelope-curves
|
||||
|
||||
Grows the envelope-overlay editor from an amp-only fixture into the shared graphical
|
||||
surface for all three envelopes (amp, pitch, filter): a corner radio switch per deck
|
||||
selects which envelope is overlay-active (none by default, exclusive); every sloped
|
||||
stage on every envelope (Attack/Decay/Release — Hold and Sustain stay flat) gets an
|
||||
editable curve exponent (0.1–10, 1.0 the linear neutral) via a paired inner knob dial
|
||||
and a round mid-segment overlay knot, both resolving through the one curve law in
|
||||
`src/core/util/curve_law.h`; the pitch envelope becomes AHD (Attack → Hold → Decay,
|
||||
Hold a fraction of the time remaining after Attack and Decay, so A+H+D ≤ span holds by
|
||||
construction, no clamp); AHDSR envelopes get a right-anchored release, dragged from
|
||||
its top node with the bottom-right corner fixed; and the Trigger amp/filter
|
||||
fade-in/fade-out pair is retired in favor of a Trigger AHD, consolidating what were
|
||||
two staged-shape mechanisms into one — item 8's rule (pitch always AHD; amp and filter
|
||||
AHDSR in Gate, AHD in Trigger) governs all three. The Trigger × Preserve end-of-sample
|
||||
click is fixed at its root cause: `freezeTail()` stopping the pitch shifter's writer a
|
||||
full window before the read head arrives.
|
||||
|
||||
**Open question resolved — per-mode stage-value state.** Gate and Trigger keep
|
||||
SEPARATE stored stage values, on both the amp (`PlaySeconds::adsr` +
|
||||
`PlaySeconds::trigAhd`) and the filter (`FilterSeconds::env` + `FilterSeconds::trigEnv`).
|
||||
Migration forces it: an old instance carries both an AHDSR and a fade pair, and one
|
||||
shared set cannot preserve both modes' prior sound. Cost: ~160 bytes of persisted
|
||||
state per instance, 6 additional `DeckParam` ids.
|
||||
|
||||
**Deviations from spec:**
|
||||
- **The migration exponent is FITTED, not neutral — Daniel's explicit ruling,
|
||||
resolving a spec contradiction.** PLAN.md stated both "pre-existing instances load
|
||||
at exponent 1.0" and "exponents at whatever reproduces the prior fade shape";
|
||||
those conflict, and the fix resolves toward the second, since it carries the
|
||||
migration guarantee. Attack lifts at **p = 0.6133**, decay at **q = 1.7437**; max
|
||||
deviation from the retired equal-power (sin/cos) fade shape drops from 0.2105 to
|
||||
0.0875. Every non-migrated curve still lifts to the 1.0 neutral.
|
||||
- **Item 4's fix is deliberately WIDER than spec.** The spec scoped the end-of-sample
|
||||
click fix to Trigger × Preserve; the landed fix is not mode-scoped, so Gate ×
|
||||
Preserve × source-exhaustion also now rings out (~4 ms) where it previously
|
||||
hard-cut. A held Gate note whose source runs out with no loop is cut at sustain
|
||||
level, landing on the same recycled synthetic tail — scoping the fix to Trigger
|
||||
alone would have knowingly left that click.
|
||||
- **Migration is lossy under a sample-rate mismatch** — a documented bound, not a
|
||||
bug. The retired fades were source frames; the lift divides by the project rate
|
||||
while the AHD rebuilds at decode rate, so a rate mismatch shifts migrated stage
|
||||
lengths by that ratio. Documented in the v10 version ladder
|
||||
(`component_state_io.h`) with a test.
|
||||
- **Payload version is v10.** `component_state_io.cpp` was split on the format seam
|
||||
into `component_state_io.cpp` + a new `params_payload.{h,cpp}`.
|
||||
- **New pure module:** `src/core/util/curve_law.h` — the one per-segment curve law
|
||||
(exponent domain, normalized-position→level map, the mid-segment inverse an overlay
|
||||
knot drags through, and the knob's norm↔exponent travel with an exact centre
|
||||
detent). The neutral exponent is a bit-identity. Measured cost of a non-neutral
|
||||
exponent: ~4.7 ns per evaluation, +224 ns/output frame worst case at 16 voices —
|
||||
3.1% → 4.1% of one core at 44.1 kHz.
|
||||
- **`OverlayEnv` and the overlay-selection state machine live in
|
||||
`core/instrument/ui/deck_groups`**, not the shell.
|
||||
- **The knot-creation gesture differs from spec.** Spec said dragging a segment
|
||||
*adds* a knot; the landed behavior draws the knot unconditionally on every sloped
|
||||
non-zero segment and responds to a drag within the grab radius. Daniel confirmed
|
||||
this reading stands.
|
||||
- **Loop markers moved from `AccentTertiary` to `AccentSecondary`** — they collided
|
||||
exactly with the envelope trace (RGB delta 0) in the same overlay rect. Daniel
|
||||
ruled. The palette is under active change on a separate branch, so the specific hex
|
||||
values are in flux (see `docs/TODO.md`).
|
||||
|
||||
**Still open — not closed by this track.** The envelope overlay's contrast against
|
||||
the waveform (tertiary purple, measured 1.37:1, below the 3:1 indicator floor) awaits
|
||||
Daniel's eye on a build; pinned as a flagged deviation in `tests/test_theme.cpp`. See
|
||||
`docs/TODO.md`.
|
||||
|
||||
### Ξ-W1-T1 — tracking-consolidation
|
||||
|
||||
Consolidates the provenance/usage territory into one system: the retired
|
||||
|
||||
Reference in New Issue
Block a user