Merge Ψ-W1-T1: a ranged item capture renders the window, with children and receives silenced for it

# Conflicts:
#	docs/TODO.md
#	src/app/CMakeLists.txt
This commit is contained in:
2026-08-01 21:25:37 -04:00
25 changed files with 969 additions and 21 deletions
+95
View File
@@ -482,3 +482,98 @@ CTest target driven by a fake `reaper_plugin_info_t`; the Media-Explorer section
registration and `ingestHandleSectionCommand` move into it and gain unit coverage; and
`ingest.cpp`'s own `command_id`+`gaccel` registration collapses onto
`action_registry::registerAction` where the shapes match.
## A multi-track TRACK capture renders one file per track and lands one of them
**Context (surfaced by Ψ-W1-T1, capture-range-exactness).** Track scope has always
rendered through `RENDER_SETTINGS &128` ("selected tracks via master"), and the
ranged item capture now joins it there. The SDK header (~3041) documents the
single-file bit `&(4<<16)` for "rendering selected items or razor edits" only, so it
does not apply to `&128`: the reading is that N selected tracks produce N files.
`capture.cpp` sets `RENDER_PATTERN` to one literal stem and detects success by
`std::filesystem::exists`, so N stems collapse onto one name and whichever file
survives lands as a successful capture carrying one track's audio.
**The wart.** `renderOffline` refuses this shape for the ranged ITEM capture
(`render_settings::isMultiTrackRangedItemRender`) because that path was newly routed
into it. Track scope with two or more tracks selected has the same exposure and is
deliberately untouched — changing a shipped action from "produces a file" to
"refuses" is a behavioral-contract change, and it was out of that track's surface.
**Intended fix.** Not proposed. Three shapes exist and the choice is a product call,
not a mechanical one: refuse (matching the item path), render each track and land N
bank entries (that is batch capture's meaning, not this action's), or sum the
selected tracks into one file (needs a summing render source `&128` does not offer).
**The constraint the fix MUST handle.** The per-track-output reading of `&128` is
INFERRED from the header's single-file wording, never observed in a DAW. Verify it
first — including what REAPER actually writes when N stems share one literal
`RENDER_PATTERN` — because the answer decides whether this is a defect at all.
**Priority / risk.** Unknown until the DAW check above runs. If the reading is right,
a silently-wrong capture on an ordinary two-track selection; if wrong, nothing.
**Done looks like.** The `&128` multi-track output shape is DAW-confirmed, and track
scope either produces defined correct output for a multi-track selection or refuses
it with a message naming the reason.
## A `SelectedItems` recipe replays against whatever items are selected then
**Context (surfaced by Ψ-W1-T1, capture-range-exactness).** `RunRecaptureFromSource`
rebuilds a `CaptureRequest` from the recorded `CaptureRecipe` and resolves its source
tracks by GUID. `renderOffline` engages `RenderTrackSelection` only when the recipe's
source mode is `SelectedTracks`, which is what makes a ranged item capture and a
track capture replay against their recorded tracks rather than the live selection.
**The wart.** A recipe whose source mode is `SelectedItems` — every pre-fix item-scope
capture, and every post-fix full-extent one — renders `&32`, which prints whatever
items happen to be selected when the replay fires. The recorded recipe therefore does
not fully determine the audio it reproduces, which is what "recapture from source"
promises.
**Intended fix.** Not proposed. The recipe stores tracks and a range; it carries no
item GUIDs, so no guard on the shell side can reconstruct the item selection from
what is recorded. Closing it means widening `CaptureRecipe` (a wire-format change with
a version rung) or re-sourcing full-extent item captures through the tracks render too,
which would drag them onto the isolation path for no gain.
**The constraint the fix MUST handle.** Widening the recipe must keep every already-
persisted recipe readable, and must not make a replay depend on items that no longer
exist — a deleted source item has to degrade to a stated refusal, not a silent
substitution.
**Priority / risk.** Pre-existing; not introduced or worsened by the range-exactness
work. Harmless when the user re-runs a recapture with the same items still selected,
wrong when they do not.
**Done looks like.** A `SelectedItems` recapture either reproduces its recorded audio
from the recipe alone, or refuses with a message naming what the recipe cannot pin
down.
## An overlapping item on the source track itself is not isolated from a ranged item capture — DECIDED, not deferred
**Context (surfaced by Ψ-W1-T1, capture-range-exactness).** The re-source to the
selected-tracks render (`&128`) needed transient upstream silencing so an item capture
did not also print folder children and receives; `render_isolation` (`UpstreamIsolation`)
covers both. A third widening exists in the same shape: a non-selected item on the
SAME track that overlaps the requested range is now audible in the render, where the
pre-fix `&32` selected-items source excluded it by construction (that source only ever
prints the selected items).
**This is a decision, not a gap.** `src/shell/capture/CLAUDE.md` states the reasoning in
full and it is not repeated here: `UpstreamIsolation`/`render_selection` silence and
select TRACKS because the recipe that replays a capture stores tracks and a range, never
item GUIDs — a mute plan keyed to today's overlapping item could not be recomputed at
replay time, so muting items would make the capture stop reproducing itself. The named
candidate (a) in `docs/PLAN.md` §Ψ-W1-T1 carried exactly this semantic edge; it was
weighed against candidate (b) (an item-bounds render with a derived start time) and (a)
shipped with the edge accepted rather than closed.
**Priority / risk.** Low in the common case (one item per track over the captured range is
the normal shape); a project with deliberately overlapping items on one track is the one
that surfaces it, and the practical mitigation is unchanged from before this track:
select/move the neighbour, or capture at track scope instead.
**Done looks like.** Nothing to do — recorded so a future reviewer does not read the
non-isolation as an oversight and re-propose closing it against the recipe's stated
tracks-and-range-only shape.