From 3791e6c119bf9cf28dc01299c714408f7b950359 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Thu, 23 Jul 2026 15:38:16 -0400 Subject: [PATCH] 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. --- PLAN.md | 69 ++++++ docs/product/capture-tail.md | 399 +++++++++++++++++++++++++++++++++++ 2 files changed, 468 insertions(+) create mode 100644 docs/product/capture-tail.md diff --git a/PLAN.md b/PLAN.md index 5234ed4..fccacd7 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 diff --git a/docs/product/capture-tail.md b/docs/product/capture-tail.md new file mode 100644 index 0000000..138f431 --- /dev/null +++ b/docs/product/capture-tail.md @@ -0,0 +1,399 @@ +# Capture tail — spec + +Authoritative spec for the **capture-tail** feature: preserving reverb/release +tails that decay past the end of a capture range. The tickable milestone lives in +`PLAN.md` (Milestone T); this doc holds the full technical detail **and** the +product framing. + +> **Why this doc carries the technical spec (not `CONTEXT.md`).** Every other +> pillar (capture M0–M11, Design View, Multi-bank) keeps its authoritative +> technical spec as a `CONTEXT.md §` section and its *why* in a `docs/product/` +> note. Capture-tail is a rider on the already-shipped offline-render path +> (M3/M7), not a standalone pillar, and it is being specced without reopening +> `CONTEXT.md`. So the authoritative detail lands **here**, house-styled to match +> the CONTEXT specs; when the tail work lands, doc-keeper may fold the invariant +> deltas into `CONTEXT.md §Precision invariants` as landed history. Same standing +> discipline applies: **verify every REAPER API name/flag against +> `vendor/reaper-sdk/sdk/reaper_plugin_functions.h` before use** — the flag values +> below are transcribed from that header (line numbers cited) and are not guesses. + +Status: specced (2026-07-23), aligned with Daniel. Parameters set by Daniel: +**auto-trim silence threshold = -72 dB**, **max-tail cap = 8 s**. Open items and +DAW-confirm targets are at the bottom. + +--- + +## Goal + +A reverb or release tail that rings out past a capture range's end should be able +to land in the capture — cleanly, without the user hand-measuring where the decay +falls silent, and without ever bloating an exact-bounds capture with silence it did +not ask for. + +Two modes: + +- **Automatic (default).** Render a *generous* tail (the 8 s cap), then auto-trim + the trailing silence to -72 dB, so the capture ends where the tail naturally + decays. No user input. +- **Manual override.** A fixed tail length, no trim — explicit control for the user + who wants exactly N seconds of tail (e.g. a rhythmic reverb throw held to a bar). + +The tail is **opt-in** in both modes. A capture with no tail requested is exact +bounds, byte-for-byte, unchanged from today — this is load-bearing for the null +test (see Invariant interactions). + +--- + +## The offline path (the primary path) + +The offline `OfflineRenderBackend` (`src/capture.cpp`) already drives every +`RENDER_*` setting through `GetSetProjectInfo` behind a `ScopedRenderSettings` +snapshot/restore, forces dither and all normalize-postprocessing off, and renders +32-bit float. The tail wires into that existing path — no new render trigger, no +new backend. + +### Bounds are always custom — so the tail bit is always `&1` + +The backend renders with `RENDER_BOUNDSFLAG = 0` (custom time bounds) for **every** +scope and every range type: it sets `RENDER_STARTPOS` / `RENDER_ENDPOS` explicitly +from the request's exact seconds (`capture.cpp` ~L352–354). It does **not** use the +time-selection / selected-items / regions bounds modes. + +`RENDER_TAILFLAG` is a bitmask keyed to the **bounds mode**, not the capture range +type (header line 3047): + +``` +RENDER_TAILFLAG : &1=custom time bounds, &2=entire project, &4=time selection, + &8=all project markers/regions, &16=selected media items, + &32=selected project markers/regions +``` + +Because we always render in custom-time-bounds mode, **the only tail bit that ever +applies is `&1`**. There is no per-range-type tail-flag decision to make — a razor +capture, a time-selection capture, and an item capture are all custom-bounds +renders under the hood, so all three take `RENDER_TAILFLAG = 1`. + +> **Correction to the framing brief.** The brief asked us to pick a +> `RENDER_TAILFLAG` bit *per capture range type* (time selection vs. razor vs. item) +> and flagged `&32` as "markers/regions." The header (line 3047) says `&32` = +> *selected project regions* and `&8` = *all markers/regions* — but neither matters: +> our renders are all `RENDER_BOUNDSFLAG = 0`, so the tail bit is `&1` unconditionally. +> The existing `kTailFlagCustomBounds = 1.0` constant in `capture.cpp` (~L80) is +> already correct; the field wiring is what's missing. + +### Mode 1 — Automatic (default): generous tail + auto-trim to -72 dB + +Set, in addition to the exact `STARTPOS`/`ENDPOS` already driven: + +| Setting | Value | Meaning / header ref | +|---|---|---| +| `RENDER_TAILFLAG` | `1` | apply tail for custom time bounds (line 3047, `&1`) | +| `RENDER_TAILMS` | `8000` | the 8 s cap, in ms (line 3048) | +| `RENDER_NORMALIZE` | `32768` | **only** the trim-ending-silence bit (line 3051, `&32768`) | +| `RENDER_TRIMEND` | `≈ 0.000251` | -72 dB threshold (line 3062; scaling below) | + +The render produces up to 8 s of tail past `ENDPOS`, then REAPER trims the trailing +silence back to the -72 dB threshold, so the file ends where the decay crosses -72 +dB. Non-decaying signal (a sustained pad, a loop) never falls below -72 dB, so the +8 s cap is what stops it — that is the cap's whole job (runaway guard). + +**The surgical `RENDER_NORMALIZE` — this is the subtle part.** Today the backend +forces `RENDER_NORMALIZE = 4<<16 = 262144` (`kNormalizeDisableAll`, `capture.cpp` +~L92, L403): *disable all render postprocessing*. That single "disable all" bit +masks out normalization, brickwall, fades, pad, and trim in one move. + +Auto-trim needs the trim-ending-silence bit **on** — which means we can no longer +use the disable-all bit (they are semantically opposed; disable-all suppresses trim +along with everything else). The replacement is **surgical**: set *only* the +trim-end bit and leave every other postprocessing bit clear. + +``` +RENDER_NORMALIZE = 32768 // ONLY &32768 (trim ending silence). Everything else OFF: + // &1 normalization -> clear + // &64 brickwall limit -> clear + // &512 fade-in / &1024 fade-out -> clear + // &16384 trim starting silence -> clear + // &(1<<16)/&(2<<16) pad start/end -> clear +``` + +**Why this stays deterministic and un-coloring — the thing the disable-all was +protecting.** The disable-all bit existed to guarantee no normalize/brickwall/fade +touches the signal (those are level-dependent and would break bit-identical repeats +and the null test). A **fixed-threshold trailing-silence trim does none of that**: +it does not scale, limit, fade, or reshape any sample — it only *chooses where the +file ends* by finding the last sample above a fixed -72 dB threshold. Every retained +sample is bit-identical to a no-trim render of the same tail. Because the threshold +is fixed (not derived from the signal's own level, the way normalize is), two +identical requests trim at the identical sample → **bit-identical repeats hold, and +the trimmed region is exactly the region that was below -72 dB anyway**. The trim is +a boundary decision, not a signal transform. So the surgical normalize reintroduces +**none** of the coloring the disable-all was guarding against — it only re-enables +the one bit that is a pure boundary operation. + +**`RENDER_TRIMEND` threshold scaling.** The header (line 3062) documents +`RENDER_TRIMEND` as a linear amplitude ratio: "0.5 means -6.02 dB." So the value is +`10^(dB/20)`. For -72 dB: + +``` +RENDER_TRIMEND = 10^(-72/20) = 10^(-3.6) ≈ 0.00025119 +``` + +Store this as a derived constant from the named -72 dB (see Named constants), not a +magic float — deriving it keeps the dB the single source of truth and lets a future +config change the dB without hand-recomputing the ratio. + +> **DAW-confirm.** `RENDER_TRIMEND`'s scaling is documented as amplitude ratio +> (matching `RENDER_NORMALIZE_TARGET` / `RENDER_BRICKWALL`, both "0.5 = -6.02 dB"). +> Confirm in a live REAPER that `0.00025119` trims at ≈-72 dB (not, say, -72 dB +> interpreted on a different curve). Cheap to verify: render a decaying reverb tail +> and inspect where the file ends. + +### Mode 2 — Manual override: fixed tail, no trim + +The existing (currently unwired) `CaptureRequest.renderTail` / `tailMs` fields +(`capture.h` ~L63–64) drive this directly: + +| Setting | Value | +|---|---| +| `RENDER_TAILFLAG` | `1` | +| `RENDER_TAILMS` | `request.tailMs` (clamped to the 8 s cap — see below) | +| `RENDER_NORMALIZE` | `262144` (`kNormalizeDisableAll`, unchanged) | +| `RENDER_TRIMEND` | not set / irrelevant (trim bit is clear) | + +Fixed tail = no trim, so manual mode keeps the disable-all normalize exactly as +today's no-tail path does. The file is exactly `[start, end + tailMs]` of rendered +audio, unprocessed. **Clamp `tailMs` to the 8 s cap** even in manual mode — the cap +is a runaway guard against a non-decaying signal rendering forever, and that risk +exists whether the tail length came from the auto default or an explicit request. +(If a user genuinely needs > 8 s of held tail, that is a reason to revisit the cap +as configurable — noted below — not to let a single request uncap it.) + +### The three tail states, unified + +The request already has `renderTail: bool` + `tailMs: double`. The auto-trim mode +adds a third state, so the wiring is a small enum, not a bool: + +- **None** (default for null-test / verify captures, and the current three-scope + action defaults): `RENDER_TAILFLAG = 0`, `RENDER_TAILMS = 0`, normalize = + disable-all. Exact bounds. Byte-identical to today. +- **Auto** (the new user-facing default for tail-on captures): tailFlag `1`, + tailMs `8000`, normalize `32768` (surgical trim), trimEnd `0.00025119`. +- **Manual(ms)**: tailFlag `1`, tailMs `clamp(ms, 8000)`, normalize `262144` + (disable-all), no trim. + +Recommended shape: replace `bool renderTail` with a `TailMode { None, Auto, +Manual }` and keep `tailMs` meaningful only for `Manual`. (Implementation detail +for staff-engineer; the three states above are the contract.) The pure +range/settings decisions — clamp, mode→(`RENDER_*` values) mapping — belong in +`render_settings.{h,cpp}` next to `renderSettingsFor`, so they are unit-tested +outside the DAW exactly like the source-bit mapping is today; the backend just +applies the returned values. + +--- + +## The realtime path (parallel, follow-on) + +The M8 `RealtimeRecordBackend` (`src/capture_realtime.cpp`) does **not** drive +`RENDER_*` at all — it records the master send into a hidden temp track over a time +selection. So none of the offline tail machinery reaches it. It needs a **parallel** +tail path, and it is explicitly a **follow-on to the offline tail** (offline lands +first; realtime tail is a later increment). + +**Recipe:** + +1. **Record a generous tail window.** Extend the recorded range end by the 8 s cap: + set the record time selection to `[start, end + 8 s]` instead of `[start, end]` + (`capture_realtime.cpp` ~L481–482, where `re = request.endSeconds` today). The + transport runs the extra 8 s and the temp track captures the decaying tail. +2. **PCM decay-scan trim.** After the file flushes (the existing deferred-finalize + flush wait), read the recorded PCM and scan **backward** from the end to find the + last frame whose absolute level crosses **-72 dB**; truncate the file at that + frame (rounded to a frame boundary, all channels). If no frame in the tail window + exceeds -72 dB after the range end, trim back to the original `end`. If the signal + never falls below -72 dB within the 8 s window, keep the full window (the cap did + its job). +3. **Manual override** mirrors offline: record `[start, end + clamp(ms, 8 s)]` and + **skip** the decay-scan (fixed tail, no trim). + +**Reuse `peaks` for the scan — but note the gap.** `peaks::computeEnvelope` +(`src/peaks.h`) already computes per-channel min/max over interleaved float PCM +without folding channels — the right level primitive. But it needs *frames in +memory*, and it computes an envelope over bins, not a "last frame above threshold" +index. Two honest options for staff-engineer, surfaced not pre-decided: + +- **(a) A thin new pure helper** alongside `peaks`: `lastFrameAboveThreshold(interleaved, + channelCount, frameCount, linearThreshold) -> frameIndex`, scanning backward, + taking the max abs across channels per frame (no fold — just the per-frame peak + used for the threshold test). Pure, unit-testable with a synthetic decaying ramp, + mirrors the `peaks` discipline. **Recommended** — it is ~15 lines and exactly the + operation; bending `computeEnvelope` (bin-oriented) to answer a boundary question + is a worse fit. +- **(b) Reuse `computeEnvelope`** at a fine bin resolution and walk the bins + backward for the last bin whose |min|/|max| exceeds threshold, then trim at that + bin's frame span. Coarser (bin-granular, not frame-exact) and re-purposes a + thumbnail tool for a trim decision — not recommended, but avoids a new symbol. + +The **file read + truncate** is REAPER-facing (it lives in the realtime shell, not +pure) — reading the recorded wav's PCM into a float buffer and rewriting it +truncated. That is new I/O the realtime backend does not do today (it only +moves/renames the file). Flagged as real work, not a wiring change. + +**Realtime is non-deterministic regardless.** M8 is already documented as +non-bit-identical by nature (it is a live record). The decay-scan trim does not +change that: even the trim *index* can vary run-to-run because the recorded samples +vary. This is fine and expected — the realtime tail is a convenience, not a +precision path. The **offline auto-trim is the deterministic one**; realtime is not +held to bit-identical repeats. + +--- + +## Invariant interactions (state these plainly) + +- **Opt-in beyond the region.** The tail only ever adds audio past the range end + when a tail is explicitly requested (Auto or Manual). This is exactly the existing + invariant: *"no added silence unless a tail is explicitly requested"* + (`CONTEXT.md §Precision invariants`, `CLAUDE.md §Exact bounds`). Auto-trim + strengthens it — the tail added is decay, not silence, and the silence past the + decay is trimmed off. +- **The null test uses NO tail.** The null-test / verify capture (M10) and any + bit-identical-repeat verification must run **TailMode::None** — exact bounds. A + dry no-tail capture re-inserted at its source position must null against the + source; a tail would extend the file past the source region and break the null. + **The tail must be OFF for null-test and verification captures** — this is a hard + rule, not a default. (The three-scope capture *actions* may default to a tail once + this ships; the *verify* action never does.) +- **Determinism holds for offline.** The offline auto-trim path is deterministic: + fixed threshold + fixed cap + a boundary-only trim = two identical requests + produce bit-identical files (§surgical normalize argument above). Realtime is + inherently non-deterministic and is not held to this. +- **FX-scope bypass composes correctly — the tail is the right decay per scope.** + The tail render still goes through `FxBypassGuard` for track/item scopes + (`render_settings.h §FxBypassPlan`, `CLAUDE.md §Capture FX scope`). This is + exactly what we want: the tail is the *in-scope FX decay*. + - **Master scope:** whole chain in scope → the tail is the full master-chain + reverb/release decay. Correct. + - **Track scope:** the track's own FX are in scope, ancestors/master bypassed → + the tail is **the track's own reverb/delay decay**, not the parent bus's. A track + with a reverb plugin captures that reverb's tail; a track feeding a folder reverb + does **not** capture the folder reverb's tail (that send is out of scope — and + note the pre-existing send-isolation caveat in `PLAN.md §Open questions`, which + the tail inherits unchanged, does not worsen). Correct and consistent. + - **Item scope:** item/take FX only, self-track + ancestors + master bypassed → + the tail is the **item/take FX decay only**. An item with a take reverb captures + its tail; the track's reverb does not ring into it. Correct. + + In every scope the captured tail is precisely the decay of the FX that scope + hears — the FX-scope invariant already guarantees this, and the tail just lets that + decay finish instead of being cut at the range end. + +--- + +## Named constants + +Both are named constants, defined once in the pure layer (`render_settings.h`, +next to the render-bit constants), so the offline and realtime paths share one +source of truth: + +```cpp +// Auto-trim trailing-silence threshold. -72 dB is quiet enough that the trimmed +// region is inaudible decay, loud enough to not chase a reverb's infinite noise +// floor. Daniel-set. +inline constexpr double kAutoTrimThresholdDb = -72.0; +// Derived linear amplitude ratio for RENDER_TRIMEND (header line 3062: "0.5 = -6dB", +// i.e. 10^(dB/20)). Do not hardcode the ratio — derive it so the dB stays the source +// of truth. ≈ 0.00025119. +// (constexpr pow is C++26; until then compute once at first use or precompute with +// a comment showing the arithmetic — implementation detail.) + +// Max tail rendered/recorded past the range end. The runaway guard: a non-decaying +// or looping signal never crosses the trim threshold, so this caps the render. +// Daniel-set. +inline constexpr double kMaxTailSeconds = 8.0; +inline constexpr double kMaxTailMs = 8000.0; +``` + +**Configurable later?** Both are fixed constants now (YAGNI — no user has asked to +tune them, and a precision tool benefits from predictable defaults). Two realistic +futures to leave room for, not build: + +- **-72 dB threshold** → a per-capture or global "tail trim floor" setting, if users + find -72 dB too aggressive (cuts a long reverb early) or too lax (leaves audible + hiss). Low likelihood; the value is deliberately conservative. +- **8 s cap** → a global "max tail" ceiling, if someone captures long orchestral or + ambient tails that genuinely exceed 8 s. More likely than the threshold to be + raised. Keeping it a single named constant makes promoting it to a setting a + one-line change plus a UI affordance. + +Neither is in scope now; both are single-constant seams so promotion is cheap. + +--- + +## Acceptance criteria + +**Offline — automatic (default):** + +- A capture of a range ending mid-reverb, with Auto tail, produces a file whose + audio extends past the range end and ends where the reverb decays below -72 dB + (not at a hard 8 s, and not at the range end). +- A non-decaying signal (sustained pad / loop) with Auto tail produces a file capped + at exactly range + 8 s (the cap fired; nothing trimmed). +- **Bit-identical repeats hold under Auto tail**: two identical Auto-tail requests + produce byte-identical files (the trim is deterministic). +- A capture with **TailMode::None** is byte-identical to the pre-tail exact-bounds + capture of the same range (no regression to the existing path). + +**Offline — manual override:** + +- A Manual(N ms) capture produces a file of exactly range + N ms of rendered audio, + untrimmed, for `N ≤ 8000`. +- A Manual request with `N > 8000` is clamped to 8000 ms. + +**FX-scope composition:** + +- Track-scope Auto capture of a track with its own reverb captures that reverb's + tail; the same track's parent-bus reverb does **not** ring into the tail. +- Item-scope Auto capture captures take-FX decay only. + +**Realtime (follow-on):** + +- A realtime Auto capture of a decaying source records ≥ the range then trims the + file at the -72 dB decay point (± the inherent realtime tolerance). +- Realtime tail is **not** asserted bit-identical (documented non-determinism). + +**Invariants (regression gate):** + +- The null-test / verify capture runs TailMode::None and still nulls to silence. +- `ScopedRenderSettings` restores `RENDER_NORMALIZE` (and every touched setting) to + the user's prior value on every path, including the new surgical-normalize path — + the user's project render config is untouched after a tail capture. + +--- + +## Open questions / DAW-confirm items + +- **`RENDER_TRIMEND` curve (DAW-confirm).** Confirm `0.00025119` trims at ≈-72 dB in + a live REAPER (documented as amplitude ratio, line 3062 — matches + `RENDER_NORMALIZE_TARGET` / `RENDER_BRICKWALL`, but verify against a real decaying + render). Highest-value confirm — the whole auto mode rides on it. +- **Surgical-normalize interaction (DAW-confirm).** Confirm `RENDER_NORMALIZE = + 32768` (only trim-end) trims trailing silence **without** engaging any normalize / + fade / pad behavior — i.e. the other bits being clear genuinely means "off," not + "default on." The header bit layout says so (line 3051); confirm empirically that + a trim-end-only render does not, e.g., apply a default fade-out. +- **Trim vs. exact end boundary.** Confirm the trim never eats into audio *before* + the range end — i.e. `RENDER_TRIMEND` trims only the rendered **tail's** trailing + silence, never the pre-`ENDPOS` body. (It should: trim-end operates on the file's + trailing edge, and the tail is appended after `ENDPOS`. But a range that itself + ends in near-silence before a loud transient is the edge case to check the trim + doesn't over-eat.) +- **Auto as the action default?** Should the three shipped capture actions + (`CAPTURE_ITEM` / `CAPTURE_TRACK` / `CAPTURE_MASTER`, currently all TailMode::None + per `render_settings.cpp §captureActionTable`) flip to Auto tail by default once + this ships, or should tail be a separate action variant / a modifier? Product call + for Daniel. **Leaning:** a per-action-family toggle or a paired "…​with tail" + variant rather than silently changing the existing actions' behavior — the current + exact-bounds default is a documented contract and some captures (chops, wavetable + grabs) want no tail. Not blocking the offline implementation; the request-level + contract (three tail states) is independent of which action sets which. +- **Realtime tail sequencing.** Confirmed a **follow-on** to the offline tail — do + not block offline on it. Filed as a separate PLAN point.