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:
@@ -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 M8–M11. 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 M0–M11 capture roadmap and Phase D)
|
||||
|
||||
> **Separate phase namespace.** The M-numbers belong to the capture pillar
|
||||
|
||||
Reference in New Issue
Block a user