292d14d14c
No truncation exists on our side of that boundary, so the read-back is the only evidence available for whether REAPER kept the window — and it fires on every tail mode, where only None was ever judged.
10 KiB
10 KiB
src/core/capture — pure logic behind the capture pillar
Scope
Pure, REAPER-free logic behind the capture pillar: path arithmetic, the RIFF/WAV
codec, render-settings/FX-scope/tail-mode mapping, InsertMedia mode-bit
computation, the realtime-record state machine, and batch-capture planning.
Does not include: the REAPER-bound capture backends themselves
(shell/capture), the docked panel's tail-toggle window/click-handling
(shell/panel), or the InsertMedia call/undo-block mechanics
(shell/capture's insert.cpp).
Invariants
The repo-wide precision invariants (null test, bit-identical repeats,
non-destructive, exact bounds, relative-paths-only, capture FX scope) are
authoritative in root CLAUDE.md — reference them, don't re-copy them.
Detail specific to these pure modules:
- No silent time-stretch, made checkable.
insert_plannever sets the &4 ("stretch/loop to fit time sel") bit;kStretchToTimeSelBitis exposed precisely so a test can assert it is never present in any computedInsertMediamode. - Tail is a three-state mode (
docs/product/capture-tail.md), not a per-action variant: None (exact bounds, byte-identical, the only mode for null-test/verify captures), Auto (generous 8 s tail then trim trailing silence to -72 dB surgical normalize), Manual (fixed length, clamped to the 8 s cap, no trim).render_settingsowns the offline RENDER_* mapping;tail_controlowns the panel-facing toggle/cycle/clamp/label logic sharing the sameTailModeenum and the same 8 s / -72 dB constants (single source of truth — do not hardcode a second copy in either module). - Capture FX scope is enforced via FX-bypass + gain-neutralize, not a render
bit.
render_settings::fxBypassPlanForselects which tracks (self / ancestors / master) get their FX bypassed for a givenCaptureScope; there is no master capture scope (to capture the master, render a track instead). - Relative paths only, by construction.
capture_paths::BankPathsseparates the absolute render directory REAPER needs from the project-relative path theBankIndexstores;bankRelativeForNamespells an enumerated folder entry the identical wayderiveBankPathsspelled it at capture time, so the prune core's exact-string match cannot drift. - Project-identity transition is GUID-primary.
capture_paths'sclassifyProjectTransitionchecks the minted GUID before the liveReaProject*object, specifically because REAPER can recycle a closed project's pointer address onto an unrelated project.
Modules
wav_codec— chunk walker + layout parse + float32 build + size-field patch + the lossless mono collapse + content hashes; the single pure RIFF/WAV owner (wav_trimis retired;wav_codecis the sole owner).capture_realtime(core/capture, renamed fromrealtime_recordin Q-W3 — the Q-9 naming rider: pure module takes the stem, the shell takes the suffix, matchingdrag_out/drag_out_win) — the M8 realtime-record pure logic: capture scope + FX-tap point →I_RECMODE/I_RECMODE_FLAGSvalues, wet/dry → tap point, the recorded-file →Samplemapping, and the async record-phase state machine. Depends onbank_modelfor the plainSample/SourceModetypes. The transport/temp-track/send recipe lives in the shell (shell/capture/capture_realtime_shell.cpp+capture_realtime_finalize.cpp).batch_capture— pure batch-capture planner: maps source ranges to capture units and aggregates results.capture_paths— the REAPER-free path arithmetic behind offline capture: bank-subfolder + unique-filename derivation (deriveBankPaths, forward-slash form, no filesystem touch), the absolute-render-dir vs. project-relative-index-path split (BankPaths), the persist-side inverse (resolveBankFile,projectDirOfRpp), the Save-As bank-relocation plan (deriveRelocationPlan), and the GUID-primary project-identity classifier (classifyProjectTransition→NoOp/Load/SaveAsRelocate) the persist-poll timer drives.capture_name— the REAPER-free composition of one capture's label + file-stem base from its source-track name(s), a local-calendar discriminator (MM-DD HHMM, from the shell's clock read), and an optional batch ordinal. The label and the stem deliberately diverge: the stem still passes throughcapture_paths::sanitizeStem(so a name that sanitizes to nothing files ascapture), while the label keeps the source name verbatim. Stem uniqueness stays entirelymakeUniqueTag's — this module never disambiguates.insert_plan— the REAPER-free logic behind theinsertshell (M6): computes theInsertMediamodebitmask from anInsertOptionsstruct (placement target, tempo-conform ratio, preserve-pitch flag), guaranteeing the &4 stretch-to-time-selection bit is never set and that no tempo bits are set whenconform == None.render_settings— the REAPER-free logic behind the capture action family:SourceMode→RENDER_SETTINGSbit mapping,P_RAZOREDITSstring parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (fxBypassPlanFor), the tail-mode →RENDER_TAILFLAG/RENDER_NORMALIZE/RENDER_TRIMENDmapping (tailRenderSettingsFor) and its realtime-window analog (realtimeRecordWindowEnd), the capture-action taxonomy table (captureActionTable)main.cppiterates to register the CAPTURE_ITEM/CAPTURE_TRACK family, andrenderSourceLabel(the source named in the offline backend's bounds refusal).render_window— the REAPER-free frame arithmetic behind exact capture bounds:frameCountFor(the frame count a project-time window occupies at the project rate — the number the offline backend checks the rendered file against before landing it, so a render that printed something other than the window is refused rather than banked),renderHonoredBounds(the gate's verdict and the sole home of its one-frame tolerance, which is empirical rather than proven — the header states which renderer models it covers and which it does not), anditemExtentPrintsWindow, the predicaterender_settings::sourceModeForScopeconsults to decide whether REAPER's selected-items render source can express a requested window at all. It also owns the two short-render diagnostics:msFlooredEndFrameCount(the frames a window holds with its end floored to the millisecond — the shape two live short renders matched, quoted by the refusal as a count coincidence and nothing more) anddescribeBoundsDrift(the sentence the offline backend prints whenRENDER_STARTPOS/RENDER_ENDPOSdo not read back as they were written).track_topology— the REAPER-free folder arithmetic over a project's flatI_FOLDERDEPTHdelta list:directChildIndicesnames a folder parent's DIRECT children, the setshell/capture/render_isolationsilences so a ranged item capture does not print its track's children. Grandchildren are excluded by construction — they reach the parent only through the child that owns them.tail_control— the REAPER-free logic behind the dockedbank_panel's tail-mode toggle: the cycle order (None → Auto → Manual → None), the Manual-length clamp/scroll-wheel fine-adjust (clampManualMs/adjustManualMs, 250 ms/notch, 2000 ms default), the toggle's label text (e.g. "Tail: Manual 2.0s"), and theTailSettingJSON round-trip persist stores per-project.
Gotchas
render_settings'sRENDER_SETTINGS/RENDER_NORMALIZE/RENDER_TAILFLAG/RENDER_TRIMENDbit values are transcribed verbatim from the SDK header (reaper_plugin_functions.hlines ~3041/~3047/~3051/~3062) — re-verify against the header before changing any bit value, per the rootCLAUDE.mdAPI-verification rule.- The selected-items render source (
&32) cannot narrow a window — REAPER derives that render's bounds from the selected items' own extents, soRENDER_BOUNDSFLAG=0+RENDER_STARTPOS/RENDER_ENDPOSdo not constrain it. This is an inference from the observed defect (a time selection inside a long item captured the whole item), NOT a header-confirmed fact. It is whysourceModeForScoperoutes item scope to&32only when the item extent already IS the requested window — do not re-point item scope unconditionally at&32, and do not widen the&32branch to windows it cannot express. This is the one home for that inference; the sites that act on it point here rather than restating it. - The re-source changes the CONTENT, not the FX scope.
fxBypassPlanForis keyed onCaptureScope, so a ranged item capture still hears take/item FX only — but the selected-tracks source prints everything upstream of the track. The shell answers that with a transient silencing (shell/capture/render_isolation) whose child-set walk lives here intrack_topology; the item-vs-track asymmetry behind it is insrc/shell/capture/CLAUDE.md. kRenderPreFaderStems(&8192) is deliberately not used — REAPER offline render has no true pre-FX "dry" bit; FX scoping is done entirely by the FX-bypass-around-render mechanism, never by a render bit.- The mono collapse changes a capture's content identity, by design.
hashWavContentcovers thefmtbody plus thedatapayload, and the collapse rewrites both — so a collapsed capture does NOT hash-dedup against a stereo twin of the same audio already in the bank. Accepted: the predicate is deterministic over deterministic bytes, so repeats of the same request still dedup against each other, which is what the bit-identical-repeats invariant actually asks for. Do not "fix" this by hashing pre-collapse — that would make two entries with different audio layouts share one identity. - The collapse's minimal rebuild also drops
bext/iXML/LIST — a source-position consequence, not only a hashing one. REAPER's renderer writes abexttime reference, and REAPER's own import paths can position an item at that BWF timestamp, so a collapsed capture loses it while a declined (non-collapsed) capture from the same action keeps it — two captures from one action behave differently on re-import.shell/capture/insert.cppis unaffected (it drivesSetEditCurPos+InsertMediarather than reading BWF), so this is not a defect in the shipped insert path. Accepted, not verified against a DAW re-import:[verify — DAW]. tail_control'skDefaultManualTailMs/kManualStepMsandrender_settings'skMaxTailMs/kAutoTrimThresholdDbare separate constants in separate files by design (panel-facing default/step vs. runaway-guard cap) — don't conflate them when touching either.