Refuse every multi-track selected-tracks render, both scopes; make a failed mono collapse observable

This commit is contained in:
2026-08-01 22:40:54 -04:00
parent 6e6f4a6a15
commit f69c4bf6bf
14 changed files with 366 additions and 114 deletions
+28 -27
View File
@@ -511,39 +511,40 @@ registration and `ingestHandleSectionCommand` move into it and gain unit coverag
`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
## The `&128` multi-track output shape is still DAW-unobserved, and a refusal now rests on it
**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.
**Context.** The multi-track TRACK capture no longer lands one track's audio under an
`Ok`: `renderOffline` refuses every selected-tracks render covering more than one track,
both scopes, naming the way out (`render_settings::isMultiTrackStemRender` /
`multiTrackRefusalMessage`). What did NOT change is the evidence: the per-track-output
reading of `&128` is still INFERRED from the SDK header documenting the single-file bit
`&(4<<16)` for item/razor sources only. It has never been observed in a DAW.
**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.
**The wart.** The refusal is therefore as unverified as the defect it closes. If REAPER
in fact sums a multi-track `&128` render into the single literal `RENDER_PATTERN`, the
refusal costs a working capture — a user who selects two tracks and captures gets a
message where a correct summed file used to land.
**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).
**Intended fix.** Run the observation in `docs/verify-track-scope-multitrack.md` §3 (a
hand-driven Render dialog, source "selected tracks via master", one literal filename, two
tracks selected — then count the files REAPER writes). If it comes back "one summed
file", the refusal is over-strict for the TRACK scope and should be narrowed back; the
ITEM-scope half stays regardless, since a per-item-track render is not the sum the user
asked for either way. If it comes back "one file per track", nothing to do and the
inference is retired into fact.
**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.
**The constraint the fix MUST handle.** Narrowing the refusal must keep the ITEM scope
refusing, must keep `renderOffline` the single seam (so a recipe replay cannot diverge
from a fresh capture), and must not re-open the collapse for any caller that reaches
`&128` later — the predicate is keyed on the render source precisely so new callers
inherit it.
**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.
**Priority / risk.** Low and bounded either way: the current behavior refuses rather than
lands wrong audio, so the cost of being wrong here is a refused capture, not a bad one.
**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.
**Done looks like.** The `&128` multi-track output shape is DAW-observed and written into
`src/shell/capture/CLAUDE.md` as fact rather than inference, and the refusal is either
kept as-is or narrowed to the item scope with that observation cited.
## A `SelectedItems` recipe replays against whatever items are selected then
+137
View File
@@ -0,0 +1,137 @@
# DAW verification — track-scope capture over a multi-track selection
What a DAW pass must establish for the multi-track track capture, and the exact numbers
or strings to read off. Nothing below can be closed by a unit test: every item depends on
what REAPER actually does with a render request.
**Build to use.** Release, installed into `UserPlugins/`, REAPER restarted — extensions
load at startup only. Set the docked panel's tail toggle to **None** before every cell;
Auto adds an 8 s window and Manual a fixed one, and both would invalidate the frame-count
readings.
**Project to use.** One saved project, project sample rate pinned to 48000. Two audio
tracks, `A` and `B`, each holding one item at least 30 s long, with *audibly different*
content (a tone on `A`, a drum loop on `B`). One folder track `F` with `A` and `B` as its
children, used only in §5.
---
## 1. The regression floor — single-track track capture is unchanged
Select **track `A` only**. Make a time selection from **10.000 s to 12.000 s**. Run
*ReaSampler: capture selected track(s)*.
Read off:
- A file appears in the project's bank folder, and one new card appears on the panel.
- The card's length reads **2.000 s**; its frame count is **96000** (`round(12.0 × 48000)
round(10.0 × 48000)`). The backend refuses the capture with `BoundsMismatch` if the
render is more than one frame off that, so a landed capture already proves the number
to ±1 — what you are confirming here is that it landed at all.
- The REAPER console shows **no** `ReaSampler capture failed:` line.
- Track `A` is still the only selected track afterwards.
Repeat with a **razor area** on `A` over the same 1012 s span and no time selection: same
three readings.
**This is the byte-identical floor.** If either cell now refuses, the change is wrong —
the refusal must fire only above one track.
## 2. The defect cell — two selected tracks now refuse
Select **`A` and `B` together**. Time selection 10.00012.000 s. Run *capture selected
track(s)*.
Read off:
- The console prints exactly:
`ReaSampler capture failed: A track capture renders the selected tracks through the
master, and more than one track cannot land as a single file. Capture one track at a
time, or route them into a folder/bus track and capture that (a folder's own output is
its children summed).`
- **No** new card on the panel, and **no** new `.wav` in the bank folder (check the folder
directly — a stray file with nothing indexing it would mean the refusal fired too late).
- `A` and `B` are both still selected, both still unmuted, and neither track's fader, pan,
or FX-bypass state changed. The refusal returns before any guard is constructed, so
there should be nothing to restore — this reading is what confirms that.
Repeat with a **razor area spanning both tracks** and no time selection: identical
readings. Note that the track *selection* is what the refusal counts — a razor over two
tracks with only `A` selected is a one-track capture and must still succeed (§1).
## 3. The decisive observation — what `&128` actually writes
**This is the one that retires an inference, and it is the reason `docs/TODO.md` still
carries an entry.** The refusal in §2 rests on reading the SDK header's single-file bit
`&(4<<16)` as applying to item/razor sources only, never to `&128` — so N selected tracks
are believed to produce N files. That has never been observed.
Drive REAPER's own Render dialog by hand, with the extension out of the loop:
1. Select `A` and `B`.
2. File → Render. **Source:** *Selected tracks (stems)* — the dialog wording for `&128`.
**Bounds:** *Custom time range*, 10.000 to 12.000 s.
3. **File name:** a literal stem with **no wildcards at all** — e.g. `stemprobe`. Clear
`$track` / `$item` / anything else from the pattern; the extension writes exactly one
literal stem, so the probe must too.
4. Render to an empty scratch folder.
Read off — **the file count in that folder**:
- **Two files** (however REAPER disambiguated them, or one file that visibly got
overwritten): the inference holds, the §2 refusal is correct, and the `docs/TODO.md`
entry can be closed by writing this observation into `src/shell/capture/CLAUDE.md` as
fact.
- **One file containing `A` and `B` summed** (confirm by ear, or by nulling it against a
master render of the same range with only `A` and `B` unmuted): the inference is wrong,
the §2 refusal costs a working capture, and the track-scope half should be narrowed back
per the `docs/TODO.md` entry. The item-scope half stays either way.
Also record **what REAPER named the files** — that decides whether a future correct
multi-track capture could ever be built on this source at all.
## 4. Recapture replays the same answer
Take a **single-track** track capture that carries provenance (capture a range on `A`
whose source item is itself a bank sample, so `detectParent` fires), select its card, and
run *re-capture from source*. It must regenerate — same audio, same 96000 frames.
Then construct the multi-track case: a recorded recipe whose `trackGuids` names two
tracks. The reachable way to get one is to have captured it before this change; if no such
entry exists in any project, record that this cell was **not exercised** rather than
inventing one. When it is exercised, read off:
- `ReaSampler re-capture failed:` followed by the **same** message text as §2.
- The bank entry is untouched — same file, same hash, same card.
## 5. The way out actually works
Route `A` and `B` into folder `F`. Select **`F` only**, time selection 10.00012.000 s,
capture track scope.
Read off: one card, 2.000 s, and the audio contains **both** `A` and `B`. This is what the
refusal message tells the user to do, so it has to be true.
## 6. Realtime still accepts a multi-track selection
Select `A` and `B`. Run *ReaSampler: capture selected track(s) in realtime* over the same
range. Read off: **one** card, and its audio contains both tracks. Realtime taps each
source track with a send into one temp track, so it sums where the offline render cannot —
the divergence from §2 is deliberate and this cell is what confirms it is real.
## 7. Mono collapse — what is and is not reachable
Capture a range on a track whose content is dead-center (a mono source panned center, or
a duplicated-channel file). Read off:
- The panel card reports **1 channel**.
- The console shows **no** `the lossless mono collapse ... landed intact, as captured.`
line.
**Not DAW-reachable:** the collapse's *failure* branch. It fires only if the captured file
cannot be read, or its temporary rewrite cannot be written or renamed, inside the same
call that just rendered the file — there is no manual way to inject that fault between the
render and the rename. The branch is covered only at its reporting seam
(`tests/test_wav_codec.cpp`, `testCollapseOutcomesReportDistinctly`), and its console line
has never been seen in a running REAPER. If you ever do see it, the capture in question is
intact and correctly measured — only the size win was lost.