docs: collapse Phase Gamma Wave 2 to its landed record, and correct five spec claims the implementation disproved
This commit is contained in:
@@ -344,16 +344,16 @@ shift unless that contingency is taken.
|
||||
**Settled: Rate is latched at note-on for this phase (not live on sustaining voices).**
|
||||
Two consequences the implementation must get right:
|
||||
|
||||
**It is a latch, not a reload.** `isLiveDeckParam` is currently a binary predicate whose
|
||||
`false` branch routes an edit to a **full reload** (bridge read, WAV re-decode, fresh
|
||||
**It is a latch, not a reload.** `deckParamCommit` was a binary predicate whose
|
||||
`false` branch routed an edit to a **full reload** (bridge read, WAV re-decode, fresh
|
||||
engine) or an engine rebuild. Routing a swept knob down that path is unacceptable. Rate is
|
||||
therefore a **third commit class**: *published into the live block like any live parameter,
|
||||
but read only by `snapLive` at note-on and never by `applyLive` on a sounding voice.* The
|
||||
mechanism already exists — the invariant "A fresh note SNAPS, a sounding one holds φ" is
|
||||
exactly this split — but the *classification* does not.
|
||||
exactly this split — but the *classification* did not, until this phase widened it.
|
||||
|
||||
> **Where this is recorded.** `core/instrument/CLAUDE.md` states that *"which controls are
|
||||
> live is ONE decision, recorded in ONE place"* — `isLiveDeckParam` / `liveCommitFor` in
|
||||
> live is ONE decision, recorded in ONE place"* — `deckParamCommit` / `liveCommitFor` in
|
||||
> `ui/deck_groups`. Phase Γ widens that one decision from two states to three
|
||||
> (`Live` / `NoteOnLatched` / `Reload`) rather than adding a second predicate elsewhere.
|
||||
> This is also precisely the seam the automation work needs — see
|
||||
@@ -374,12 +374,23 @@ Preserve it is an addend to a shift amount the pitch envelope already modulates.
|
||||
|
||||
### 2.4 Rate scaling — what "scales with rate" means, concretely
|
||||
|
||||
- **Loop points scale with rate.** The loop is a pair of *source-frame* facts. Under
|
||||
Varispeed the read increment changes and the loop is traversed proportionally faster —
|
||||
scaling is automatic and the stored frames are untouched. Under Preserve the read
|
||||
advances at `rate ×` the source rate, so the loop's wall-clock period scales by `1/rate`
|
||||
while its source-frame span is unchanged. **In neither mode are the stored loop frames
|
||||
rewritten**; the marks on the waveform do not move when Rate moves.
|
||||
- **Loop points scale with rate under Varispeed; under Preserve, the loop's *traversal*
|
||||
scales and its audible period does not.** The loop is a pair of *source-frame* facts.
|
||||
Under Varispeed the read increment changes and the loop is traversed proportionally
|
||||
faster — scaling is automatic, the stored frames are untouched, and the audible period
|
||||
scales by `1/rate` along with everything else the voice plays. **Under Preserve this is
|
||||
the opposite of what the Varispeed case suggests, and the obvious extension of it is
|
||||
wrong** — which is why an engineer measured this before writing code against it rather
|
||||
than inferring it from the Varispeed case above. What scales with rate under Preserve is
|
||||
the loop's *traversal* — how fast the source is consumed (the feed-side witness is
|
||||
`testPreserveStretchLoopsTheSourceSpan`) — not its audible period: holding the source's
|
||||
period constant while its duration changes is what Preserve *is*. **Measured** (Debug
|
||||
build, one machine): with a ring long enough to hold the whole loop, the rendered
|
||||
sawtooth period is ~3999.9 output frames at rate 0.5, 1.0, and 2.0 alike; at shorter
|
||||
rings, where splice cadence intrudes instead of the design property being isolated, the
|
||||
same fixture measured 3064 and 4130 frames at rate 0.5 — never the 8000 a scaling period
|
||||
would give either. **In neither mode are the stored loop frames rewritten**; the marks on
|
||||
the waveform do not move when Rate moves.
|
||||
- **Contours scale with rate.** A drawn contour is a pure function of *normalized* sample
|
||||
position (`core/instrument/CLAUDE.md`: "Normalized is what makes a contour
|
||||
length-independent"), so it follows the read head by construction. **The staged
|
||||
@@ -1042,13 +1053,20 @@ being a bare orphan rectangle and becomes the same kind of object as every other
|
||||
|
||||
| Mark | Ink | Cap | Line | Label |
|
||||
|---|---|---|---|---|
|
||||
| **Start** | `accent/primary` | solid **right-pointing triangle** (a play flag — it points into the material that will play) | solid | `START`, right of the line |
|
||||
| **Start** | `overlay/trace` | solid **right-pointing triangle** (a play flag — it points into the material that will play) | solid | `START`, right of the line |
|
||||
| **Loop start** | `accent/secondary` | **L-cap opening right** | solid | `LOOP`, right of the line |
|
||||
| **Loop end** | `accent/secondary` | **L-cap opening left** | solid | `END`, left of the line |
|
||||
| **Crossfade** | `accent/secondary`, reduced alpha | **ramp cap** — a small right triangle whose hypotenuse rises left→right, drawing the fade-in shape | **dashed** — a soft boundary, not a hard one | `XFADE`, left of the line |
|
||||
|
||||
Start is the only `accent/primary` mark in the band, because it is the only one that is
|
||||
always in effect (both Gate and Trigger). The loop pair's opposed L-caps read as `[ … ]`
|
||||
Start draws in `overlay/trace`, not `accent/primary`: `accent/primary` **is** the waveform
|
||||
fill, so a primary START would measure 1:1 against the material it marks. `overlay/trace`
|
||||
measures 3.071:1 against the fill and 3.065:1 against `bg/base`, clearing the 3:1 non-text
|
||||
floor on both — provably optimal, since `core/ui/CLAUDE.md`'s two-neighbour rule derives
|
||||
`sqrt(9.41) ≈ 3.07` as the ceiling any single value can hold against both neighbours at
|
||||
once. Start is still the only mark always in effect (both Gate and Trigger), but that is no
|
||||
longer what its ink says, now that `overlay/trace` is shared with the envelope trace: the
|
||||
distinction is carried by shape instead — a straight full-height column under a solid
|
||||
triangle cap, never a curve. The loop pair's opposed L-caps read as `[ … ]`
|
||||
without needing to be explained. All four caps use primitives already in the kit
|
||||
(axis-aligned fills, AA-restroked triangles per `visual-design-language.md` §8).
|
||||
|
||||
@@ -1088,17 +1106,23 @@ cosmetic gain.
|
||||
> already-accepted failure worse.** The edge wedge leaves the loop fill's peak alpha at
|
||||
> 0.20 exactly as today, so the pair is untouched.
|
||||
- **The ingredient draws as a ghost.** `[loopStart − crossfade, loopStart)` — the material
|
||||
actually being mixed in — draws the **mirror** wedge (growing right-to-left, peaking at
|
||||
`loopStart`) at half alpha, outside the loop fill. It carries no handle. **At rest it is a
|
||||
hairline dashed outline; it fills in on hover or drag of the crossfade handle** — a hover
|
||||
state in the sense §3.3 of the visual language means, revealing the relationship only when
|
||||
the user is asking about it.
|
||||
actually being mixed in — draws the **same ramp** the audible wedge draws (growing
|
||||
left-to-right, peaking at `loopStart`) at half alpha, outside the loop fill — not a
|
||||
mirror of it: the incoming tap's weight at ingredient frame `loopStart − crossfade + k` is
|
||||
the same `crossfadeWeight` as audible frame `loopEnd − crossfade + k`, so both spans carry
|
||||
the identical ramp, which is exactly why one `crossfadeWedgeHeight` function draws both.
|
||||
It carries no handle. **At rest it is a hairline dashed outline; it fills in on hover or
|
||||
drag of the crossfade handle** — a hover state in the sense §3.3 of the visual language
|
||||
means, revealing the relationship only when the user is asking about it.
|
||||
- **This makes the clamp self-explanatory.** The hard clamp is
|
||||
`crossfade ≤ min(start, loopLength)` (`loop_span.h:19`, and its "no material ahead of the
|
||||
loop" reasoning in `engine/loop/CLAUDE.md`). With the ghost drawn, **the fade stops
|
||||
growing exactly when the ghost's left edge reaches the START mark or the LOOP mark** — the
|
||||
user sees the reason instead of hitting an invisible wall. That is the single best payoff
|
||||
in this design and it costs nothing extra.
|
||||
`crossfade ≤ min(loopStart, loopLength)` (`loop_span.h:19` — `maxCrossfade(loopStart,
|
||||
loopEnd − loopStart)`; `start` there names `loopStart`, not the START mark — and its "no
|
||||
material ahead of the loop" reasoning in `engine/loop/CLAUDE.md`). With the ghost drawn,
|
||||
**each half of the clamp is now visible, on a different mark:** the ghost's left edge
|
||||
reaches frame 0 — the overlay's own left edge, not a mark — exactly at the `loopStart`
|
||||
bound, and the audible wedge's left edge reaches the LOOP mark exactly at the `loopLength`
|
||||
bound. The user sees why the fade stopped growing instead of hitting an invisible wall.
|
||||
That is the single best payoff in this design and it costs nothing extra.
|
||||
|
||||
**(d) The off-state and the Trigger state get words, not just alpha.**
|
||||
|
||||
@@ -1309,7 +1333,7 @@ the floor without touching cell metrics, because the **group inventory and its r
|
||||
assignment** now also drive it. Restate as: *the deck's cell metrics AND its group/row
|
||||
composition both drive `kEditorMinWidth`; none of the three may move alone.*
|
||||
|
||||
**7.5 — `isLiveDeckParam` becomes three-valued.** See §2.3. The exhaustive switch must
|
||||
**7.5 — `deckParamCommit` becomes three-valued.** See §2.3. The exhaustive switch must
|
||||
classify the two new `DeckParam`s or fail to compile — which is exactly what it is designed
|
||||
to do, and which is why the two new parameters are cheap to add *now*.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user