# 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 (`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 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: `SourceMode` → `RENDER_SETTINGS` bit mapping, `P_RAZOREDITS` string parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (`fxBypassPlanFor`), the one bounds mode a capture hands its window over on (`kRenderBoundsTimeSelection`) and the tail bit paired with it (`kTailFlagTimeSelection`), 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 one short-render diagnostic: `msFlooredEndFrameCount` (the frames a window holds with its end floored to the millisecond — the shape two live short renders matched on the retired custom-bounds mode, quoted by a refusal as a count coincidence and nothing more) and `isOnMillisecondGrid`, the whole-millisecond tolerance that count depends on.
- `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`.
- **The custom-time-bounds field floors the render window to the millisecond; the
  time selection does not.** Both observations and why only one bounds mode is
  reachable: `render_settings.h`'s `kRenderBoundsTimeSelection` — the one narrative
  home; this bullet is a pointer, not a retelling. Do not reintroduce
  `RENDER_BOUNDSFLAG=0`.
- `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.
