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:
@@ -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.
|
||||
Reference in New Issue
Block a user