Files
reasampler/src/core/capture/CLAUDE.md
T
daniel 292d14d14c Prove the render bounds at the boundary they cross, and name a short render whose count is exactly a millisecond-floored window
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.
2026-08-02 14:28:03 -04:00

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_plan never sets the &4 ("stretch/loop to fit time sel") bit; kStretchToTimeSelBit is exposed precisely so a test can assert it is never present in any computed InsertMedia mode.
  • 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_settings owns the offline RENDER_* mapping; tail_control owns the panel-facing toggle/cycle/clamp/label logic sharing the same TailMode enum 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::fxBypassPlanFor selects which tracks (self / ancestors / master) get their FX bypassed for a given CaptureScope; there is no master capture scope (to capture the master, render a track instead).
  • Relative paths only, by construction. capture_paths::BankPaths separates the absolute render directory REAPER needs from the project-relative path the BankIndex stores; bankRelativeForName spells an enumerated folder entry the identical way deriveBankPaths spelled it at capture time, so the prune core's exact-string match cannot drift.
  • Project-identity transition is GUID-primary. capture_paths's classifyProjectTransition checks the minted GUID before the live ReaProject* 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_trim is retired; wav_codec is the sole owner).
  • capture_realtime (core/capture, renamed from realtime_record in Q-W3 — the Q-9 naming rider: pure module takes the stem, the shell takes the suffix, matching drag_out/drag_out_win) — the M8 realtime-record pure logic: capture scope + FX-tap point → I_RECMODE/I_RECMODE_FLAGS values, wet/dry → tap point, the recorded-file → Sample mapping, and the async record-phase state machine. Depends on bank_model for the plain Sample/SourceMode types. 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 (classifyProjectTransitionNoOp/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 through capture_paths::sanitizeStem (so a name that sanitizes to nothing files as capture), while the label keeps the source name verbatim. Stem uniqueness stays entirely makeUniqueTag's — this module never disambiguates.
  • insert_plan — the REAPER-free logic behind the insert shell (M6): computes the InsertMedia mode bitmask from an InsertOptions struct (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 when conform == None.
  • render_settings — the REAPER-free logic behind the capture action family: SourceModeRENDER_SETTINGS bit mapping, P_RAZOREDITS string parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (fxBypassPlanFor), the tail-mode → RENDER_TAILFLAG/RENDER_NORMALIZE/RENDER_TRIMEND mapping (tailRenderSettingsFor) and its realtime-window analog (realtimeRecordWindowEnd), the capture-action taxonomy table (captureActionTable) main.cpp iterates to register the CAPTURE_ITEM/CAPTURE_TRACK family, and renderSourceLabel (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), and itemExtentPrintsWindow, the predicate render_settings::sourceModeForScope consults 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) and describeBoundsDrift (the sentence the offline backend prints when RENDER_STARTPOS/RENDER_ENDPOS do not read back as they were written).
  • track_topology — the REAPER-free folder arithmetic over a project's flat I_FOLDERDEPTH delta list: directChildIndices names a folder parent's DIRECT children, the set shell/capture/render_isolation silences 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 docked bank_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 the TailSetting JSON round-trip persist stores per-project.

Gotchas

  • render_settings's RENDER_SETTINGS/RENDER_NORMALIZE/RENDER_TAILFLAG/ RENDER_TRIMEND bit values are transcribed verbatim from the SDK header (reaper_plugin_functions.h lines ~3041/~3047/~3051/~3062) — re-verify against the header before changing any bit value, per the root CLAUDE.md API-verification rule.
  • The selected-items render source (&32) cannot narrow a window — REAPER derives that render's bounds from the selected items' own extents, so RENDER_BOUNDSFLAG=0 + RENDER_STARTPOS/RENDER_ENDPOS do 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 why sourceModeForScope routes item scope to &32 only when the item extent already IS the requested window — do not re-point item scope unconditionally at &32, and do not widen the &32 branch 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. fxBypassPlanFor is keyed on CaptureScope, 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 in track_topology; the item-vs-track asymmetry behind it is in src/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. hashWavContent covers the fmt body plus the data payload, 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 a bext time 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.cpp is unaffected (it drives SetEditCurPos + InsertMedia rather 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's kDefaultManualTailMs/kManualStepMs and render_settings's kMaxTailMs/kAutoTrimThresholdDb are separate constants in separate files by design (panel-facing default/step vs. runaway-guard cap) — don't conflate them when touching either.