docs: close out Theta-W4-T1 into COMPLETED, collapse the T1 spec, and record the frames-not-ms crossfade ruling
This commit is contained in:
@@ -410,3 +410,53 @@ a state by color alone.
|
||||
Full detail — the two-neighbour contrast rule, the WCAG threshold correction, and the
|
||||
accepted below-floor pairs — lives in `src/core/ui/CLAUDE.md` and
|
||||
`docs/product/visual-design-language.md` §4 Direction B; not duplicated here.
|
||||
|
||||
### Θ-W4-T1 — gate-loop-sustain
|
||||
|
||||
Establishes loop points as a usable feature and makes a Gate-mode loop function as the
|
||||
sustain — indefinite playback until note-off, with a crossfaded seam. The regression
|
||||
half resolved as **present but unreachable, not removed**: nothing in any capture path
|
||||
ever wrote `Sample::loop`, so every capture opened with `hasLoop == false`; the ghost
|
||||
default parked `loopStart` at frame 0 directly under the start marker, where
|
||||
`markerAtPoint`'s first-in-draw-order tie-break made the handle ungrabbable; and no
|
||||
crossfade existed at all. Fixed by moving the ghost span to `defaultLoopBounds` (last
|
||||
quarter of the sample, both handles clear), making a collapsed span the explicit OFF
|
||||
gesture, and adding a parameterized crossfade.
|
||||
|
||||
New pure module `src/core/instrument/engine/loop/` (`loop_span`, its own CMake target,
|
||||
its own `CLAUDE.md`, `loop_span_tests`) holds `resolveLoop`, `defaultLoopBounds`,
|
||||
`maxCrossfade`, `crossfadeWeight`, `lerpSource`, `crossfadedSource`. Params payload
|
||||
bumped to **v11** (`kParamsLoopVersion`), appended at the tail; slot 12 is reserved for
|
||||
Θ-W4-T2.
|
||||
|
||||
**Open questions resolved:**
|
||||
- **Crossfade units and range.** Stored in source FRAMES, not ms — deliberately against
|
||||
the plan's ms lean, because `sample_map.h`'s rule keeps source-timeline quantities in
|
||||
source frames and the seconds path is documented lossy under a sample-rate mismatch.
|
||||
Default 0 frames (a hard seam, which is what makes the migration bar hold by
|
||||
construction); range is the derived `[0, min(loopStart, loopEnd − loopStart)]`.
|
||||
- **Editing surface.** The waveform markers, plus a new `markerHandleRect` top-strip
|
||||
grab tab (top 10px, hit-tested before the full-height marker columns) so markers
|
||||
sharing a frame stay independently grabbable — a general fix for the tie-break
|
||||
defect, not a crossfade special case.
|
||||
- **Crossfade shape.** Settled during implementation, not specified in the source doc:
|
||||
linear, not equal-power (correlated taps one loop length apart; no transcendental on
|
||||
the per-sample path), with a decorrelated full-mix/stem exception recorded in the
|
||||
module's own `CLAUDE.md`.
|
||||
|
||||
**Deviations from spec / code review:**
|
||||
- Code review found one Major: the crossfade normalizer left an avoidable residual
|
||||
seam discontinuity, and the module's own `CLAUDE.md` had enshrined that limitation as
|
||||
a mathematical impossibility. Remediated — `crossfadeWeight` now normalizes over
|
||||
`crossfade − 1` so the last rendered frame lands exactly on the incoming tap, the
|
||||
false invariant was corrected, and the seam test now asserts against the material's
|
||||
natural one-frame step rather than a proportionality band. Six review minors were
|
||||
also fixed.
|
||||
- `voice.h` sits at ~650 lines after `lerpSource`/`crossfadedSource` moved out to
|
||||
`loop_span.h` — still over the ~600-line ceiling under the standing documented
|
||||
hot-path exception.
|
||||
|
||||
**Left open by this track, deferred to Daniel (not defects):** whether the seam sounds
|
||||
smooth on real material, whether the top-strip tab is discoverable, and the LICE
|
||||
rendering of the tab and crossfade fill. Also open: whether the crossfade default
|
||||
should stay 0 (a smooth seam becomes opt-in).
|
||||
|
||||
Reference in New Issue
Block a user