Archive Milestone 8 and Milestone T1 in docs
Move completed M8 (async realtime backend — landed as selected-track post-fader tap, not master) and T1 (offline capture tail + panel toggle) from PLAN.md to COMPLETED.md; resolved open questions removed. T2 realtime tail remains.
This commit is contained in:
+126
@@ -307,3 +307,129 @@ auto-inserts into the arrange.
|
||||
- **FX-scope semantics (initial rework):** item = item/take FX only; track = item FX + the selected track's own track FX; master = full chain. For item/track, the out-of-scope chain (ancestors + master, plus the item's own track for item scope) is neutralized during the render.
|
||||
- **Master scope subsequently removed:** capture is now **two scopes — item and track only**. `CAPTURE_MASTER` and `CAPTURE_MASTER_REALTIME` are retired (to capture the master, render a track instead). The master track is still neutralized as out-of-scope chain for both item and track captures; it is a bypass target, not a capture scope. The realtime backend taps the selected track (track scope only; item realtime deferred).
|
||||
- **`FxBypassGuard` (RAII):** snapshot → neutralize (FX bypassed via `I_FXEN`; gain zeroed via `D_VOL`; pan/width/pan-law/mode set to unity via `D_PAN`/`D_WIDTH`/`D_PANLAW`/`I_PANMODE`) → render → restore. Non-destructive. This guard is the reusable mechanism M8 (realtime backend) and M10 (null-test / true dry) build on.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 8 — RealtimeRecordBackend
|
||||
**Goal:** Realtime record behind the same `ICaptureBackend`, producing identical
|
||||
bank entries. CONTEXT.md §capture (realtime), §Precision invariants.
|
||||
**Verify (in DAW):** Hidden temp track taps each selected track's own post-fader
|
||||
output via a `CreateTrackSend`; recorded file moves into the bank; **non-destructive**
|
||||
— temp track (and its sends) removed cleanly, every snapshotted track arm, time
|
||||
selection, and edit cursor restored unchanged on every terminal path.
|
||||
|
||||
- [x] Track-scope tap: a `CreateTrackSend(source, temp)` from each selected track
|
||||
into a hidden temp track (`B_MAINSEND=0`, hidden from TCP/mixer). The temp records
|
||||
its own post-fader output — capturing each source track's output **after its own FX
|
||||
and fader, before the parent/folder/master sums it** — chain-independent by
|
||||
construction. No `FxBypassGuard` needed or used. Multiple selected tracks sum in the
|
||||
temp track (matching offline track scope). Item realtime deferred (`UnsupportedMode`).
|
||||
No track selected → refused.
|
||||
- [x] Timer-driven async state machine (`begin`/`tick`/`abort` driven by `OnTimer`,
|
||||
non-blocking — REAPER's UI stays responsive across the record). `begin()` validates,
|
||||
snapshots all state, creates the temp track, routes the tap, arms, calls
|
||||
`CSurf_OnRecord`, and **returns immediately**. `tick()` (called from `OnTimer`)
|
||||
reads the transport via `GetPlayStateEx`/`GetPlayPositionEx` scoped to the record's
|
||||
own `ReaProject*` (project-switch safe), advances the pure `advanceRecordPhase`
|
||||
state machine, and on a terminal verdict stops + finalizes/restores. `abort()` is
|
||||
the force-terminate path for shutdown and project switch.
|
||||
- [x] `RealtimeCaptureState` snapshot + idempotent restore: snapshots cursor,
|
||||
time selection, and every other track's `I_RECARM`; restore() is latched
|
||||
(`restored_` flag) and safe to call from whichever terminal path fires first.
|
||||
Terminal paths: normal completion, manual stop, error, second-capture reject,
|
||||
project switch (project-scoped `OnStopButtonEx(proj_)`, never the global
|
||||
`CSurf_OnStop`), **project close** (guarded by `ValidatePtr2(nullptr, proj_,
|
||||
"ReaProject*")` — a closed project calls `dropWithoutRestore()` rather than
|
||||
touching freed pointers), and extension unload.
|
||||
- [x] `Finalizing` flush-wait before file move: after the transport stops,
|
||||
`tick()` waits for the recorded file size to be positive and stable across a tick
|
||||
before calling `finalizeRecording` (file is no longer being written by REAPER's
|
||||
audio thread). A wall-clock ceiling (steady-clock, independent of the play cursor)
|
||||
bounds both the total record duration and the flush wait separately.
|
||||
- [x] Move recorded source into bank: `recordedFilePath` discovers the take's source
|
||||
file from the temp track's first media item; `finalizeRecording` moves it into the
|
||||
bank folder (cross-volume fallback: copy+remove); populates a `Sample` via
|
||||
`sampleFromRecordedCapture`; clean teardown via `restore()` deletes the temp track
|
||||
(which REAPER uses to automatically remove every send routed into it).
|
||||
- [x] Dialog-free; realtime is inherently non-deterministic (documented, not asserted
|
||||
bit-identical); saved-project gate (refuses + prompts Save-As if unsaved, matching
|
||||
offline). Bindable **cancel** action registered. Master scope removed entirely —
|
||||
to capture the master, render a track.
|
||||
|
||||
**Notes/decisions:**
|
||||
- **Track scope only this increment.** Item realtime is deferred: item scope needs
|
||||
per-item take isolation on top of the track-output tap — a separate increment.
|
||||
- **TAP vs. FxBypassGuard.** The `CreateTrackSend` defaults to post-fader
|
||||
(`I_SENDMODE=0`) with full-stereo (`I_SRCCHAN` default): post-fader taps the source
|
||||
track after its own FX and fader/pan, before the parent sums it. The parent chain
|
||||
downstream of that branch is not in the tapped path at all — so there is nothing to
|
||||
neutralize and `FxBypassGuard` (which mutates the live chain, altering the user's
|
||||
monitoring) is deliberately not used. This also fixed the earlier silent-file bug
|
||||
from the spike, which sent FROM the master INTO a temp track (a feedback loop REAPER
|
||||
refuses, recording silence). A regular track→track send has no feedback.
|
||||
- **Project-close guard.** `abort()` gates every REAPER call on
|
||||
`ValidatePtr2(nullptr, proj_, "ReaProject*")`. A closed project already reclaimed
|
||||
its temp track, arms, and transport — `dropWithoutRestore()` latches `restored_`
|
||||
and clears `temp_` / `armSnaps_` without touching any REAPER pointer.
|
||||
- **No undo block.** The transient mutations (temp track, sends, arm, transport) are
|
||||
fully reversed by `restore()`; surfacing them as an undo point would pollute the
|
||||
user's history with an internal scaffold they cannot meaningfully undo.
|
||||
|
||||
---
|
||||
|
||||
## T1 — offline tail: auto (default) + manual override
|
||||
**Goal:** Preserve decay tails on offline captures. **Auto**: render an 8 s-capped
|
||||
tail, then auto-trim trailing silence to -72 dB via a **surgical** `RENDER_NORMALIZE`
|
||||
(only the trim-end bit, `32768`) + a derived `RENDER_TRIMEND` amplitude ratio.
|
||||
**Manual**: a fixed tail length clamped to the 8 s cap, no trim. **None** (default):
|
||||
exact bounds, byte-identical to the pre-tail capture. 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.
|
||||
|
||||
- [x] Pure layer (`render_settings.{h,cpp}`): named constants `kAutoTrimThresholdDb`
|
||||
(-72) + derived `RENDER_TRIMEND` amplitude ratio via `autoTrimEndRatio()` (≈
|
||||
0.00025119 for -72 dB, computed as `10^(dB/20)` — `std::pow` is not `constexpr`
|
||||
before C++26 so this is a function, not a constant), `kMaxTailSeconds`/`kMaxTailMs`
|
||||
(8 s); `TailMode { None, Auto, Manual }` enum; `TailRenderSettings` struct
|
||||
(tailFlag/tailMs/normalize/trimEnd); `tailRenderSettingsFor(mode, manualTailMs)`
|
||||
mapping (None = kTailFlagNone + kNormalizeDisableAll; Auto = kTailFlagCustomBounds
|
||||
+ kMaxTailMs + kNormalizeTrimEnd (32768) + autoTrimEndRatio(); Manual =
|
||||
kTailFlagCustomBounds + clamped ms + kNormalizeDisableAll); unit-tested.
|
||||
- [x] Wire the mapping into `OfflineRenderBackend` (`capture.cpp`): drives tail +
|
||||
surgical-normalize (Auto) / disable-all (Manual/None) via `GetSetProjectInfo`;
|
||||
`ScopedRenderSettings` snapshots and restores `RENDER_TRIMEND` alongside the
|
||||
existing `RENDER_*` set. `RENDER_TAILFLAG = kTailFlagCustomBounds` (1) for Auto
|
||||
and Manual — custom bounds is the always-applicable tail bit for offline captures.
|
||||
- [x] `CaptureRequest` three-state tail contract (None/Auto/Manual(ms)); default
|
||||
None (exact bounds, null-test-safe). The earlier `renderTail` bool/`tailMs` pair
|
||||
was superseded.
|
||||
- [x] Exposure: a **docked-panel footer toggle** (label "Tail: Off" / "Tail: Auto" /
|
||||
"Tail: Manual", cycles on click via `cycleTailMode`) in `bank_panel.cpp`, backed
|
||||
by the pure `tail_control` module (`TailSetting`, `cycleTailMode`,
|
||||
`clampManualMs`, `tailToggleLabel` — unit-tested). Default `TailMode::None`.
|
||||
`CAPTURE_ITEM` and `CAPTURE_TRACK` read the panel setting at fire time — **no
|
||||
per-action tail variants shipped** (the "…with tail" variants were dropped in
|
||||
favour of the toggle; null-test/verify captures use None explicitly).
|
||||
- [x] 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.)
|
||||
|
||||
**Notes/decisions:**
|
||||
- **Surgical normalize.** `kNormalizeTrimEnd = 32768` sets only the trim-ending-silence
|
||||
bit; every other postprocessing bit is clear. A fixed-threshold trailing-silence trim
|
||||
scales and fades nothing, so two identical Auto requests trim at the identical sample
|
||||
→ bit-identical repeats hold (spec §surgical normalize).
|
||||
- **`kNormalizeDisableAll = (4 << 16) = 262144`.** Used for None and Manual — the
|
||||
same disable-all value the pre-tail exact-bounds capture used.
|
||||
- **`tail_control` pure module** (`src/tail_control.{h,cpp}`): REAPER-free logic for
|
||||
the panel toggle. `kDefaultManualTailMs = 2000.0` (2 s). Fine-adjust UI (±
|
||||
click zones / scroll) is a noted follow-on; this pass ships a fixed default.
|
||||
- **Follow-ons noted, not done:** Manual fine-adjust UI; per-project persistence of
|
||||
the toggle (currently extension-session lifetime, resets to None on unload); T2
|
||||
realtime tail.
|
||||
|
||||
Reference in New Issue
Block a user