docs: close out Θ-W3-T2 into COMPLETED, collapse the W3 wave, and record the overlay contrast wart as open

This commit is contained in:
2026-07-31 13:20:33 -04:00
parent 87754477a7
commit e1e668a521
3 changed files with 97 additions and 213 deletions
+15 -213
View File
@@ -172,219 +172,21 @@ exist before it can receive a corner radio switch and inner curve dials. T1 addi
depends on W2-T1 for the filter itself: the filter is the first control set where the
latched-at-note-on delivery model fails audibly, and it is what made the defect visible.
**Two tracks, and they are SERIAL — T1 before T2. This is not a priority hint.** Θ-W4's
two tracks are disjoint and merely ordered; these two are not. T1
(`live-parameter-delivery`) changes how every playback parameter *reaches* a sounding
voice; T2 (`staged-envelope-curves`) changes what *shape* the three envelopes have. Both
land inside `envelopes.h`'s stage math: T1 has to define what happens to a stage's
counters when its duration changes mid-flight, and T2 rewrites the same level
computation to apply a per-segment curve exponent and to grow the Trigger AHD. Two
writers on those forty lines is a merge fight over the phase's hottest code.
The ordering is T1-first for one structural reason: **T2 adds parameters, T1 defines how
parameters are delivered.** Authoring T2's curve exponents, Hold fraction, and Trigger
AHD fields under the latching model means adding them, then re-plumbing every one of them
through the live path afterwards — a backfill cliff, and the exact shape of avoidable
rework. T1 first means every parameter T2 introduces is authored into the delivery
mechanism from the start. The cost of the ordering is honest and worth stating: T2 is the
wave's user-visible payload and T1 delays it by one track.
T1 has landed and specified its mid-stage rule in terms that survive T2: **hold
normalized stage position** — φ = elapsed/duration held fixed across a duration change,
then advancing at 1/newDuration (Daniel's pick among the candidates T1 laid out; see
`docs/COMPLETED.md`). It is expressed over *normalized stage position* rather than over
output level specifically so T2's exponent is a pure map of that position and composes
with the rule rather than invalidating it.
Beyond the ordering, the W3 collision is the phase's densest, which is why neither track
splits further: the envelope parameter model, the per-sample envelope evaluation, the
deck-band inner dials, and the waveform-band overlay are one system with one source of
truth (the settled S-VIEW rule: node-drag and slider-edit read/write the same model,
structurally, not via a listener chain). Splitting T2 into model and UI tracks would put
two writers on `play_params.h` + `component_state_io` and two writers on the overlay's
forward/inverse map pair. The serialization is the correct answer.
#### Θ-W3-T1 — `live-parameter-delivery`
**Θ-W3-T1 has landed** — see `docs/COMPLETED.md` for the full narrative. Continuous
playback controls (filter cutoff/Q/morph/drive/mod amount/key-track; every stage time
and level on all three envelopes) are now delivered live to sounding voices via a
seqlock-published `LiveValues` block owned at processor-instance scope, above
`LoadedInstrument`, so a drain-slot voice keeps tracking the knob (the desired
behavior). `foldLive(const PlayParams&)` is the single derivation from the value type.
Both of Daniel's ruling questions are answered and implemented: **reload tier =
Grouping B** (root note, loop span, and start frame still reload; everything else
continuous goes live), and **mid-stage rule = candidate (iv), hold normalized stage
position** (φ = elapsed/duration held fixed across a duration change, then advancing
at 1/newDuration) — expressed over normalized position specifically so T2's
per-segment curve exponent, below, composes with it. **Trigger's %-length and fades
are NOT live** — they are baked into `SampleData` at build, so a Trigger-mode instance
gets zero live amp delivery until T2 folds the fade pair into the AHD. The five
non-live exclusions (`kKeyTrack`, `kFilterVel`, `kTrigLength`, `kTrigFadeIn`,
`kTrigFadeOut`) are documented in `src/core/instrument/ui/deck_groups.h`, now their
single home.
#### Θ-W3-T2 — `staged-envelope-curves`
**Prerequisite: Θ-W3-T1 has landed** — see `docs/COMPLETED.md`. Both tracks write
`envelopes.h`'s stage math, and this track's curve exponent must be authored as a map of
the normalized stage position T1's mid-stage rule holds fixed (φ = elapsed/duration,
candidate (iv)).
**Goal.** Grow the envelope-overlay editor from an amp-only fixture into the shared
graphical surface for every envelope, give every envelope shapeable segments, fix the
overlay's layout and contrast failures, consolidate the Trigger amp fade pair into the
AHD — and, in the same region, kill the Trigger×Preserve end-of-sample click.
**Consolidates items 1, 8, 14, 4.**
**Surface boundary — owns:**
- `core/instrument/engine/` — the envelope evaluator (from W1's seam), `zone_params.h`,
`pitch_shift.{h,cpp}` (item 4's tail), the voice-render envelope and tail path.
- `core/instrument/map/``trigger_seam`, `component_state_io` (curve exponents, the
pitch Hold stage, the Trigger AHD, migration).
- `core/instrument/ui/``envelope_overlay`, `envelope_edit`, `knob_deck` (the inner
dial).
- `src/shell/instrument/` — the deck band (inner dials, radio switches) and the
waveform band's **overlay layer only** (T2's lane geometry from W2 is consumed, not
re-owned).
**Behavior — the overlay surface (item 1).**
- **Radio switch per envelope deck.** Each envelope knob-deck group (AMP ENVELOPE,
PITCH ENV, and now Filter) gains a radio switch in the corner of its deck. Selecting a
deck's radio makes *that* envelope the one displayed and editable in the graphic
waveform overlay — replacing today's always-on Amp AHDSR. The switch is exclusive:
**one overlay-active envelope at a time, or none** — no-envelope-shown is a valid
state, not an error.
- **Default overlay: none.** The editor opens with **no envelope selected**.
- **Segment curve values on all envelopes.** Amp AHDSR, Pitch AHD, and Filter AHDSR all
gain an editable curve value per *sloped* segment. The curve is exponential; the
per-segment parameter is the exponent scalar, range **0.1 to 10**. **1.0 is the linear
neutral** (y = x^1.0 is linear).
- **Which segments are sloped: every stage except Hold and Sustain.** For an AHDSR that
is Attack, Decay, Release; for the Pitch AHD, Attack and Decay. Hold is flat
everywhere and carries no curve dial.
- **Inner dial on curvable-segment knobs.** Every knob controlling a sloped, curvable
segment gains an **inner dial**: its own inner arc, its own hover accent (tertiary
purple), its own needle, its own numerical label. The inner dial controls that
segment's curve exponent; the outer knob keeps controlling the segment's time/level
value as today.
- **Curve editing in the overlay.** Dragging on a segment **adds a round mid-segment
knot** whose drag adjusts that segment's curve — the overlay is a curve-edit surface
in its own right, alongside (not instead of) the inner dial. Same one-model rule as
the existing draggable nodes: knot-drag and inner dial cannot diverge.
- **Overlay recolor.** Envelope segments change from secondary blue to **tertiary
purple** — secondary blue contrasts poorly against the primary green waveform behind
it. Re-verify the tertiary-purple-on-green pair against `theme`'s contrast-floor
assertions.
- **Pre-existing instances load at exponent 1.0** — the linear neutral — so their
audible envelope behavior is unchanged.
**Behavior — layout policy and the pitch AHD (item 8).**
- **The release segment anchors to the right edge** of the overlay. Release is dragged
from its **top node** (the node joining sustain to release) instead of the bottom
corner; the bottom corner — the envelope's end point — becomes **fixed/anchored**, not
draggable. Today's failure is that with little or no release the sustain portion
occupies only a small stretch and the whole figure reads off-center.
- **Spec change: the pitch envelope becomes AHD** — Attack → Hold → Decay. **Hold ranges
0 to 100% of the time remaining after Attack and Decay** (100% of
`sample length (attack + decay)`). At 100% Hold fills all remaining time; at 0% it
takes none.
- **The A + H + D ≤ sample-length bound holds by construction, not by a clamp.** Hold is
expressed as a fraction of what is left, so the sum cannot overflow. Do not add a
separate clamp; none is needed.
- **Scope rule — split on the sustain stage.** The 1:1-overlay property and the
combined-time bound apply to **all envelopes that do NOT have a sustain stage**;
envelopes **with** a sustain stage get the right-anchored-release policy instead. The
two policies **coexist rather than merge**. Daniel's rationale: the 1:1 mapping only
makes sense for trigger, not gated envelopes. **The pitch envelope's D is not a
release, so it is not right-anchored.**
**Behavior — mode-driven envelope shape (item 14).**
- **Trigger mode: fades out, AHD in.** In Trigger mode the amp envelope becomes an AHD
per item 8's definition. The fade-in-length and fade-out-length controls **go away in
Trigger mode**; Attack and Decay carry those roles. Daniel's motive is stated:
reducing code for the same job — one staged-envelope design covering what is currently
two mechanisms.
- **The Trigger AHD's sloped segments get the full curve treatment** — exponent 0.110,
inner dials, mid-segment overlay knots, tertiary-purple rendering.
- **Gate mode unchanged** — the amp envelope stays the AHDSR with the right-anchored
release.
- **The filter envelope follows the same rule.** Stated once, governing all three:
**pitch is always AHD (1:1 overlay); amp and filter are AHDSR in Gate
(right-anchored release) and AHD in Trigger (1:1 overlay).** The rule selects by the
envelope's **current shape under the active playback mode**, not by which processor it
modulates. No new rule is needed — item 8's rule already decides every case.
- **Pre-existing instances reopen sounding identical.** A Trigger instance's prior
fade-in/fade-out contour must be reproduced by the loaded AHD. The evident mapping
(Attack ← fade-in, Decay ← fade-out, Hold ← the full remainder, exponents at whatever
reproduces the prior fade shape) is a verify-at-implementation detail. **A prior zero
fade-out is Decay = 0** — the abrupt end stays representable, so nothing the old
controls could express is lost.
**Behavior — the Trigger×Preserve click (item 4).**
- In Trigger mode — polyphonic or monophonic alike — with the pitch engine in
**Preserve mode only**, the end of the sample produces an audible click. The scoping is
the useful part of the report and is recorded as given: Trigger × Preserve ×
end-of-sample; Varispeed is not implicated.
- Expected: a Trigger one-shot in Preserve ends silently — no click or discontinuity at
the sample end, in either voice mode.
- **Why it is in this track and not earlier:** the defect sits in the region the
fade-out currently governs, and this track retires that mechanism. Fixing it in a
prior wave means fixing it, then re-verifying it under the surviving mechanism
anyway — and the only other instrument-side track before this one owns the whole
engine. The gate below is the *post-consolidation* gate.
**Acceptance criteria.**
- Each envelope deck shows a corner radio switch; activating one puts that envelope in
the overlay, editable there, and the overlay tracks the switch immediately. At most one
envelope is overlay-active; with none active the overlay draws no envelope.
- The editor opens with no envelope overlay-active.
- Every curvable-segment knob shows the inner dial (inner arc, tertiary-purple hover
accent, needle, numeric label); sweeping it 0.1 → 10 visibly reshapes the overlay
segment and **audibly** reshapes the envelope on played notes. Hold and Sustain knobs
carry no inner dial.
- Dragging on an overlay segment adds a round mid-segment knot; dragging the knot adjusts
that segment's curve, and the segment's inner dial reflects the same value.
- Overlay envelope segments render in tertiary purple and are clearly legible against the
primary green waveform.
- With release at zero or minimum, the sustain segment extends to (near) the right edge —
the overlay reads full-width, not bunched left. Dragging the sustain→release top node
adjusts release; the bottom-right corner is fixed and not draggable.
- Envelopes with a sustain stage follow that anchoring policy; the pitch envelope's Decay
is **not** right-anchored.
- The pitch envelope plays and displays three stages — Attack, Hold, Decay — with Hold
spanning 0100% of the time remaining after Attack and Decay. **No combination of A, H,
D exceeds the sample length, and no clamp fires, because none exists.**
- The overlay of any sustain-less envelope is 1:1 with the waveform's time axis: a stage
boundary at N seconds sits over the waveform at N seconds.
- In Trigger mode the amp deck shows Attack / Hold / Decay — **no fade-in or fade-out
control anywhere in Trigger mode** — with curve inner dials on Attack and Decay and
none on Hold.
- Switching between Gate and Trigger switches the amp *and* filter envelope surfaces
(deck and overlay) between AHDSR and AHD accordingly.
- The Trigger amp AHD behaves stage-for-stage like the pitch AHD — Hold semantics, curve
treatment, and overlay mapping match. (The observable proxy for the consolidation
motive: one staged-envelope design, several consumers.)
- **Migration:** a project saved before this change reopens sounding identical — every
curve at exponent 1.0, and a Trigger instance's prior fade contour (including a zero
fade-out's abrupt end) audibly reproduced by the loaded AHD.
- **Item 4's gate, under the surviving mechanism:** a Trigger-mode note played in
Preserve, in both Poly and Mono, ends with no audible click at the sample end —
verified by ear *and* by inspecting the rendered output for a terminal discontinuity.
Varispeed playback is unchanged.
**Open questions.**
- **Stage-value state across the Gate/Trigger switch — shared or per-mode? [propose]**
The Gate AHDSR and Trigger AHD share stage names (A, H, D); whether they share *values*
(one envelope whose S and R fall away in Trigger) or keep per-mode state (item 3's
"save but inactive" dual-state precedent) is unspecified — and the question covers the
**filter envelope too**. Migration leans per-mode for the amp: an old instance carries
both its AHDSR values and its Trigger fade values, and a shared-value model cannot
preserve both modes' prior sound at once. The filter is new and carries no migration
weight either way; matching the amp's answer is the natural default. Resolve with a
proposal at review — the sound-identical gate must hold for whichever mode a saved
instance plays in.
**Both tracks have landed** — Θ-W3-T1 (`live-parameter-delivery`) and Θ-W3-T2
(`staged-envelope-curves`) — see `docs/COMPLETED.md` for the full narrative of each.
Between them: every continuous playback control (filter cutoff/Q/morph/drive/mod
amount/key-track, every stage time and level on all three envelopes) now reaches a
sounding voice live instead of latching at note-on, and the envelope-overlay editor
grew from an amp-only fixture into the shared graphical surface for all three
envelopes — a corner radio switch per deck (none active by default), curve-shapeable
segments on every sloped stage (0.110 exponent, an inner dial paired with an overlay
knot), the release-right-anchored AHDSR layout against the pitch envelope's 1:1 AHD,
and the Trigger amp/filter fade pair folded into a Trigger AHD, consolidating what
were two staged-shape mechanisms into one. The Trigger × Preserve end-of-sample click
is fixed; the landed fix is wider than scoped, also ringing out Gate × Preserve ×
source-exhaustion, previously a hard cut. The ordering was deliberately serial — T1's
live-delivery mechanism landed first so T2's new curve exponents and Trigger AHD
fields were authored directly into it rather than backfilled afterward.
---