Splits Ignore into unreadable vs not-a-request and counts every verdict; the report prints only when the pass answered nobody, so its absence proves the action never ran.
12 KiB
src/shell/capture — REAPER-facing capture backends and action bodies
Scope
Everything that turns a capture request into a rendered file + populated Sample,
plus the action bodies that drive capture from REAPER's UI/action list: the two
concrete capture backends (offline render, realtime record), scope/source
resolution, the realtime in-flight state machine, single- and batch-capture
orchestration, insert-to-timeline, provenance stamping, and the shared
MediaItem*/MediaTrack* GUID-read helpers. Pure decision logic (what counts as
an orphan, how a range maps to capture units, etc.) lives in the corresponding
core/ modules this shell calls into — this directory is the REAPER API surface
only.
Invariants
This directory implements, but does not restate, the repo-wide capture precision
invariants (null test, bit-identical repeats, non-destructive, exact bounds,
relative-paths-only) and the load-bearing capture/placement separation — see root
CLAUDE.md §Precision invariants and §The load-bearing principle. Shell-specific
detail not covered there:
- The item scope's render source is window-dependent.
ResolveScopeSourceis where that is decided — it measures the selected items' extent against the resolved range (core/capture/render_window) and hands the answer tosourceModeForScopeonResolvedSource. Why, insrc/core/capture/CLAUDE.md. - A ranged item capture isolates TRACKS, not ITEMS. Routing it through the
selected-tracks source widens what the render hears, and the two widenings are
answered differently. Folder children and receives are cut for the render's
duration (
render_isolation) because they are tracks, and a recipe carrying tracks can recompute that plan at replay time. An overlapping item on the source track itself is NOT isolated: the recipe stores tracks and a range, never item GUIDs, so a mute plan over items could not be replayed and the capture would stop reproducing itself. Do not "fix" the second by muting items. renderOfflineis the one seam both a fresh capture and a recipe replay cross, which is why the refusal and both transient guards live there rather than in the action bodies — anything placed inResolveScopeSourcealone would missRunRecaptureFromSourceentirely.- FX-bypass guard ordering.
scope_resolvereads the M10 provenance-assembly inputs (track/item selection, FX-chain identity) BEFORE the FX-bypass guard neutralizes the in-scope chain — provenance must see the chain as it really is, not as capture temporarily leaves it. - Realtime capture drives off REAPER's transport across timer ticks —
capture_realtime_shellcannot block REAPER's UI for the duration of a realtime record, sobegin/tick/abortare async by construction and the temp-track + send recipe lives in the shell, not the pure core. RunInsertSelectedis the one deliberate exception to capture-never-places (seecapture_orchestratorbelow) — every other capture entry point writes only a file + index entry.
Modules
capture— two CONCRETE backends with deliberately different lifecycles (no shared interface — the formerICaptureBackendwas deleted in Q-W3, T4-26: one deriver, zero polymorphic call sites):OfflineRenderBackend(deterministic default, synchronous) andRealtimeRecordBackend(async begin/tick/abort). Input:CaptureRequest. Output: finished file + populatedSamplehanded tobank_model. It also owns the two file-side steps both backends share, in this order:collapseCapturedFileToMono(the lossless mono collapse, applied to the landed file) andstampCaptureSample, which measures the channel count off that same file so the entry and the audio cannot disagree. AndcaptureNameFor— the impure local-clock read the entry points call to build a request's label + stem, kept out of the purecore/capture/capture_namecomposition it feeds.scope_resolve(shell/capture) — scope/source resolution shared by every capture entry point (Q-W3 hoist out ofmain.cpp): razor-else-time range inference, selected-track/selected-item-owning-track collection with canonical GUIDs, and the M10 provenance-assembly inputs (read BEFORE the FX-bypass guard neutralizes the in-scope chain). Also the one place a source track's NAME is read (trackName, viaGetTrackName— chosen overP_NAMEbecause it already answers REAPER's"Track N"convention for an unnamed track), landed onResolvedSource::trackNamesparallel tosourceTracksand composed into the capture's label + stem by the purecore/capture/capture_name.render_selection(shell/capture) — the transient track selection a selected-tracks render (&128) requires, as a stack RAII guard: REAPER prints whatever tracks are selected, sorenderOfflinemakes the request's own tracks BE the selection for the render's duration and restores the user's set on every exit path. Engaged ONLY for that source mode, which leaves a stated residual: a&32selected-items render still prints whatever ITEMS the user has selected. Live captures are unaffected (that selection is the source), but a recipe replay of aSelectedItemscapture renders against whatever happens to be selected then — the recipe stores tracks and a range, never item GUIDs, so this guard cannot close it. Filed indocs/TODO.md.render_isolation(shell/capture) — the transient upstream silencing a ranged ITEM render needs, as a stack RAII guard alongside the two above: the selected-tracks source prints everything flowing INTO the track, so each direct folder child'sB_MAINSENDand each of the track's receives'B_MUTEare cut for the render and restored on every exit path. Direct children only — a grandchild reaches the track through the child that owns it. The child-set walk is pure (core/capture/track_topology).capture_orchestrator(shell/capture) — single-capture orchestration + the realtime/insert action bodies (Q-W3 hoist, T4-02):renderOffline(one offline render under the scope's FX-bypass guard),captureAndIndexOne(render + provenance stamp + bank add + tracking-ledger record, unpersisted),RunCapture/RunCaptureItemAssign,RunCaptureRealtimeTrack/RunCancelRealtime(the realtime action bodies — the in-flight state lives inrealtime_lifecycle), andRunInsertSelected(the ONE deliberate exception to capture-never-places).bake_land(shell/capture) — the EXTENSION's half of the resample chain: scans every open project tab for pendingrsbake_*requests, lands the ones belonging to the project this session has loaded, and refuses the rest withWrongProject— one undo point for the batch, each answered over its own key inside the invoking instance's synchronous action call. The per-key verdict itself is NOT this TU's: it iscore/wire's pureclassifyBakeScan, so this shell only enumerates, reads, and applies — counting every verdict into awire::BakeScanTallyas it goes, and printingwire::describeBakeScanwhen the pass answered nobody. Each key is materialized before any answer is written, so noSetProjExtStatein this action mutates a set the enumerator is still walking. It RENDERS NOTHING — the instrument already did, through its own engine in its own process, which is what makes the baked audio the sound the user approved and what keeps the voice engine out of the extension's link graph. Replace-vs-add comes fromtracking::resampleLanding; a replace keeps the entry's id and slot and never deletes the superseded file. Hash-dedup applies on the add path only, before the disk write, matchingupdateSampleInPlace's "an in-place refresh is not an insert". A refused index withdraws the bytes this call had just written — the self-cleanup carve-out from prune's deletion authority, stated inprune_fs.cpp's header.capture_batch(shell/capture) — the batch-capture family + re-capture-from-source (Q-W3 hoist, T4-02):RunBatchCaptureItems(one sample per selected item),RunBatchCaptureRazor(one sample per razor area),RunRecaptureFromSource(regenerate a provenanced sample from its recorded source's current state, bank-only). Every unit routes throughcapture_orchestratorso every precision invariant holds; persist is batched to one ext-state write per action.realtime_lifecycle(shell/capture) — the in-flight realtime-capture state machine + globals (Q-W3 hoist): the action starts it,OnTimerdrives it per tick viaDriveRealtimeCapture(a single-pointer-test idle fast path — load-bearing hot-path guardrail),CommitRealtimeResultlands a finished capture in the bank,AbortRealtimeCaptureForUnloadtears down cleanly on extension unload.capture_realtime_shell(shell/capture) — the async realtime-record backend surface (Q-W6 split of the former fatcapture.h):RealtimeRecordBackend::begin/tick/abort, transport-driven across timer ticks (a realtime record cannot block REAPER's UI for its own duration). Deliberately shares NO interface with the offline backend — the lifecycles genuinely differ (the formerICaptureBackendinterface was deleted in Q-W3, T4-26).capture_realtime_finalize(shell/capture) — the file-side half of the realtime-record shell (Q-W3, T4-08): discovers the file REAPER actually recorded, moves it into the bank, runs the Auto-tail PCM decay-scan trim, and populates the finishedSample.insert— placement viaInsertMedia. Conform-to-project-tempo is an explicit opt-in flag, never silent stretching. The mono collapse needs no change here:insert.cpppasses only a path toInsertMedia, and REAPER derives the item's channel count from the file itself — a 1-channel WAV yields a mono item for free.provenance_shell— FX-chain identity queries viaTrackFX_*/TakeFX_*APIs; feeds the pureprovenancefingerprint builder. StampsSample.provenanceon capture; ambiguous/mixed cases record nothing conservatively.track_guid— sharedMediaTrack*→ canonical GUID-string formatter; single source of truth for membership keys.item_read— the ONE place aMediaItem*is read for its canonical GUID string (itemGuid) and for the durableP_LANENAMEof the fixed lane it sits on (itemLaneName); extracted from previously-duplicateditemGuid/itemLaneNamepairs inview.cppandbank_panel.cpp— the item-read analog oftrack_guid's singleMediaTrack*→GUID-key formatter. Callers must already know the track is fixed-lane (I_FREEMODE==2) before callingitemLaneName; the pureisOnManualLanepredicate handles the non-fixed-lane case separately.
Gotchas
- This directory's governing precision invariants are the repo-wide capture
invariants in root
CLAUDE.md, not a standalone spec block here. captureandcapture_realtime_shelldeliberately share NO common interface with each other (the formerICaptureBackendwas removed) — do not reintroduce one without a real second polymorphic call site.- The selected-tracks render (
&128) is read as emitting one file per selected track — the single-file bit&(4<<16)is documented for item/razor sources only (SDK header ~3041), and that is the whole basis for the reading; it is DAW-unverified. If it holds, then sinceRENDER_PATTERNis one literal stem and success is a file-exists check, N tracks would land one track's audio as a successful capture.renderOfflinerefuses EVERY multi-track render through that source (render_settings::isMultiTrackStemRender) — the ranged item capture and the plain track capture alike, each with its own way out (render_settings::multiTrackRefusalMessage). The refusal is keyed on the render SOURCE and not on the scope, so a future caller that reaches&128inherits it. Re-opening a multi-track track capture needs the DAW check indocs/verify-track-scope-multitrack.mdto come back the other way first. - Realtime is the one capture path that accepts a multi-track selection, and it is correct to: its per-source-track sends sum in the one temp track, which is a real mix rather than a stem collapse. The offline refusal above does not apply to it.