docs: spec capture-tail feature (Milestone T)

Add docs/product/capture-tail.md (authoritative spec) and Milestone T in PLAN.md:
offline auto-trim (-72 dB) + manual tail modes, surgical RENDER_NORMALIZE
(trim-end-only), realtime PCM-decay-scan follow-on, invariant interactions,
and DAW-confirm items.
This commit is contained in:
2026-07-23 15:38:16 -04:00
parent ed9b5c6ad7
commit 3791e6c119
2 changed files with 468 additions and 0 deletions
+69
View File
@@ -82,6 +82,75 @@ landed milestone.
---
# Milestone T — capture tail (rider on the offline render path)
> **Rider, not a new pillar.** Tail preservation wires into the already-shipped
> offline `OfflineRenderBackend` (M3/M7) — no new backend, no new render trigger.
> It takes a **T** tag (not an M-number) because it is an enhancement to landed
> capture, sequenced independently of M8M11. Authoritative spec:
> **`docs/product/capture-tail.md`** (full `RENDER_*` values, the surgical
> `RENDER_NORMALIZE`, the realtime parallel path, invariant interactions,
> acceptance criteria, DAW-confirm items). Parameters set by Daniel: auto-trim
> threshold **-72 dB**, max-tail cap **8 s**. When a point lands, doc-keeper moves
> it to `COMPLETED.md`.
## T1 — offline tail: auto (default) + manual override
**Goal:** Preserve decay tails on offline captures. **Auto** (default): render an
8 s-capped tail, then auto-trim trailing silence to -72 dB via a **surgical**
`RENDER_NORMALIZE` (only the trim-end bit set) + `RENDER_TRIMEND`. **Manual**: a
fixed tail length (clamped to the 8 s cap), no trim, keeping today's disable-all
normalize. Tail is opt-in; **None** stays byte-identical to today. See
`docs/product/capture-tail.md` §The offline path.
**Verify (in DAW):** A range ending mid-reverb + Auto tail ends at the -72 dB decay
point (not a hard 8 s, not the range end); a non-decaying signal caps at range + 8 s;
**two identical Auto requests are byte-identical** (deterministic trim); a
TailMode::None capture is byte-identical to the pre-tail exact-bounds capture;
Manual(N ms) yields range + N ms untrimmed, with N clamped to 8000; `ScopedRenderSettings`
restores `RENDER_NORMALIZE` and every touched setting on every path.
- [ ] Pure layer (`render_settings.{h,cpp}`): named constants `kAutoTrimThresholdDb`
(-72) + derived `RENDER_TRIMEND` ratio (≈0.00025119), `kMaxTailSeconds`/`kMaxTailMs`
(8 s); a `TailMode { None, Auto, Manual }` → (`RENDER_TAILFLAG`/`RENDER_TAILMS`/
`RENDER_NORMALIZE`/`RENDER_TRIMEND`) mapping + the manual-tail clamp; unit-tested.
- [ ] Wire the mapping into `OfflineRenderBackend` (`capture.cpp`): drive the tail +
surgical-normalize (Auto) / disable-all (Manual/None) values; snapshot/restore
`RENDER_TRIMEND` alongside the existing `RENDER_*` set. `RENDER_TAILFLAG = 1`
unconditionally (custom bounds — not per range type).
- [ ] Replace/extend `CaptureRequest.renderTail`(bool)/`tailMs` with the three-state
tail contract (None/Auto/Manual(ms)); default None (exact bounds, null-test-safe).
- [ ] DAW-confirm: `RENDER_TRIMEND` amplitude curve (0.00025119 ≈ -72 dB); trim-end-only
normalize (32768) does not engage fades/normalize/pad; trim never eats pre-`ENDPOS`
body. (See spec §Open questions / DAW-confirm.)
## T2 — realtime tail (follow-on to T1)
**Goal:** The parallel tail path for the M8 realtime backend, which does not drive
`RENDER_*`: record an 8 s-capped tail window past the range end, then **trim in a
PCM decay-scan** to the -72 dB point (Manual = record fixed tail, skip the scan).
See `docs/product/capture-tail.md` §The realtime path.
**Verify (in DAW):** A realtime Auto capture of a decaying source records ≥ the range
then trims at the -72 dB decay point (± inherent realtime tolerance); realtime tail is
**not** asserted bit-identical (documented non-determinism).
**Depends on:** T1, M8.
- [ ] Record `[start, end + clamp(tail, 8 s)]` (extend the record time selection in
`capture_realtime.cpp`); Manual skips the scan, Auto proceeds to it.
- [ ] Pure decay-scan helper alongside `peaks`: `lastFrameAboveThreshold(interleaved,
channels, frames, linearThreshold) -> frameIndex` (backward scan, per-frame max-abs
across channels, no fold); unit-tested with a synthetic decaying ramp. (Spec §realtime
path option (a) — recommended over bending `computeEnvelope`.)
- [ ] Realtime shell: read the recorded wav PCM into a float buffer, find the trim
frame, rewrite the file truncated (new I/O the backend does not do today).
## Milestone T open questions
- **Auto as the shipped-action default?** Whether `CAPTURE_ITEM`/`CAPTURE_TRACK`/
`CAPTURE_MASTER` (currently all TailMode::None) flip to Auto, gain a "…with tail"
variant, or take a modifier. Product call for Daniel; **leaning** paired variant /
toggle over silently changing the exact-bounds default. Not blocking T1 (the
request-level three-state contract is independent). (touches `render_settings.cpp`
action table + `actions`.)
---
# Phase B — Multi-bank (parallel to the M0M11 capture roadmap and Phase D)
> **Separate phase namespace.** The M-numbers belong to the capture pillar