Cut shell/capture comment bloat ~33% (comments only, zero code change)

This commit is contained in:
2026-07-29 20:48:59 -04:00
parent 1f24c4b095
commit 54f5f24506
22 changed files with 699 additions and 1215 deletions
+45 -90
View File
@@ -1,36 +1,18 @@
#pragma once
// capture — the REAPER-facing capture shell (CLAUDE.md §load-bearing split).
// The shared capture seam: CaptureRequest/CaptureResult (types both backends
// speak), OfflineRenderBackend, and the makeUniqueTag/stampCaptureSample helpers.
// Realtime's async begin/tick/abort surface lives in capture_realtime_shell.h.
//
// This header declares the SHARED capture seam (Q-W6 split of the former fat
// header — the realtime backend's async begin/tick/abort surface now lives in
// capture_realtime_shell.h):
// * CaptureRequest / CaptureResult — everything a capture needs and yields,
// source-mode-agnostic; the types BOTH backends speak.
// * OfflineRenderBackend — the deterministic default; a plain CONCRETE class
// (the former ICaptureBackend interface was deleted in
// Q-W3, T4-26 — it had one deriver and zero polymorphic
// call sites; every construction site instantiates the
// concrete type).
// * makeUniqueTag / stampCaptureSample — the shared file-tag mint and the shared
// finished-capture metadata stamp both backends call
// (Q-W3 riders T1-11 / T2-09).
//
// It includes bank_model (pure) to hand back a populated Sample, but NO REAPER
// headers — the .cpp is the REAPER-facing translation unit. Keeping this header
// REAPER-free lets callers (the capture orchestration TUs) depend on the seam
// without dragging the SDK into every include site.
// REAPER-free on purpose (bank_model only) so callers can depend on the seam
// without dragging the SDK into every include site; the .cpp is the REAPER TU.
#include <string>
#include <vector>
#include "core/model/bank_model.h"
#include "core/capture/render_settings.h" // TailMode (pure) — the three-state tail contract
#include "core/capture/render_settings.h" // TailMode — the three-state tail contract
// MediaTrack / ReaProject are forward-declared (like track_guid.h) so this header
// stays REAPER-free while RealtimeRecordBackend::begin can take the resolved source
// MediaTrack* to tap and stampCaptureSample can take the project handles its reads
// pin. The pointers are opaque here — never dereferenced in a pure/header context;
// only the REAPER-facing capture TUs touch them.
// Forward-declared, never dereferenced here — only the REAPER-facing .cpp touches these.
class MediaTrack;
class ReaProject;
@@ -38,71 +20,55 @@ namespace reasampler::capture {
using model::Sample;
// Audio bit-depth for the rendered wav. 32-bit float is the M3 default —
// rationale lives in capture.cpp next to the sink-config bytes.
// 32-bit float is the default; rationale lives in capture.cpp next to the sink-config bytes.
enum class WavBitDepth {
Int16,
Int24,
Float32,
};
// One capture, independent of source mode. Populated by the caller (the action
// handler in M3; the action family in M7) and consumed by a backend.
//
// M3 fills only the fields the master-mix/time-selection path needs; the rest
// are declared now so M7/M8 do not reshape the struct (they are the seam).
// One capture, independent of source mode.
struct CaptureRequest {
SourceMode sourceMode = SourceMode::MasterMix;
// Sample-accurate render bounds in project seconds. For the M3 spike these
// come straight from the time selection (GetSet_LoopTimeRange) — NO rounding.
// Sample-accurate render bounds in project seconds — NO rounding.
double startSeconds = 0.0;
double endSeconds = 0.0;
// 1.0 = fully wet, 0.0 = fully dry. All three-scope capture actions set this to 1.0 (wet).
// The field is kept as the seam for future true-dry work (M10 null test):
// true pre-FX dry offline is NOT available via RENDER_SETTINGS — it requires
// FX-bypass-around-render or the M8 realtime pre-FX path, and will be
// designed alongside the M10 null test. Also recorded on the Sample.
// 1.0 = fully wet, 0.0 = fully dry. Every current capture action sets 1.0;
// true pre-FX dry isn't available via RENDER_SETTINGS (needs FX-bypass-around-render
// or the realtime pre-FX path) so this stays a seam for that future work.
double wetDry = 1.0;
// Track GUID(s) the capture came from, when the source mode is track-scoped
// (SelectedTracks). Empty for master/items/razor. The action layer (M7)
// resolves the selection to canonical GUID strings and passes them here; the
// backend copies them onto the Sample (it does NOT itself read the selection —
// it stays source-agnostic, driven entirely by the request).
// Track GUID(s) when source mode is track-scoped (SelectedTracks); empty otherwise.
// The backend only copies these onto the Sample — it never reads selection itself.
std::vector<std::string> trackGuids;
// Render tail (docs/product/capture-tail.md §The three tail states). Default
// None: exact bounds, no added silence — the precision invariant, and the only
// mode valid for null-test / verify captures. `tailMs` is meaningful ONLY for
// TailMode::Manual (clamped to the 8 s cap by the pure mapping); Auto uses the
// 8 s cap + -72 dB trim internally, None ignores it.
// Render tail (docs/product/capture-tail.md §The three tail states). None = exact
// bounds, no added silence — the only mode valid for null-test/verify captures.
// tailMs applies only to Manual (clamped to 8s by the pure mapping); Auto uses
// the 8s cap + -72 dB trim internally, None ignores it.
TailMode tailMode = TailMode::None;
double tailMs = 0.0;
// Output format. 0 sampleRate => follow project rate (deterministic: the
// project rate is fixed for a given project).
// 0 sampleRate => follow project rate.
int sampleRate = 0;
int channelCount = 2;
WavBitDepth bitDepth = WavBitDepth::Float32;
// Human base name for the file stem; sanitized by capture_paths. The unique
// tag (disambiguator) is supplied separately by the backend caller so the
// pure naming logic stays testable.
// Sanitized by capture_paths. uniqueTag (disambiguator) is supplied by the
// backend caller so the pure naming logic stays testable.
std::string baseName = "capture";
std::string uniqueTag; // e.g. a timestamp/counter; may be empty
std::string uniqueTag;
};
// Outcome of a capture attempt. `Ok` carries the populated Sample; every failure
// is an explicit code (never a thrown exception across the REAPER boundary) so
// the action handler can log a precise reason.
// Every failure is an explicit code, never a thrown exception across the REAPER boundary.
enum class CaptureStatus {
Ok,
NoProject, // no active project to render / resolve a bank folder
EmptyRange, // start >= end: nothing to render
UnsupportedMode, // backend does not implement this source mode (M3 scope)
UnsupportedFormat, // requested bit depth has no known REAPER blob (M3: Float32 only)
UnsupportedMode, // backend does not implement this source mode
UnsupportedFormat, // requested bit depth has no known REAPER blob (Float32 only)
RenderFailed, // the render action ran but produced no output file
TransportBusy, // realtime backend: transport already playing/recording — refused
};
@@ -113,44 +79,33 @@ struct CaptureResult {
std::string message; // human-readable detail for the console log
};
// Deterministic offline-render backend. Drives the full offline source family —
// master mix / time selection, selected tracks, selected items, razor area — all
// wet-only (render_settings.h) with optional tail. The source selection + range
// are resolved by the caller (the action layer) and handed in via the
// CaptureRequest; the backend drives RENDER_* and never reads the DAW selection
// itself. SourceMode::Realtime returns UnsupportedMode (that is the M8 backend).
// Non-destructive: restores every RENDER_* setting it touches on every path.
// A plain concrete class — the former ICaptureBackend interface was deleted
// (Q-W3, T4-26): it had one deriver, zero polymorphic call sites, and the async
// realtime backend deliberately never implemented it (see SEAM CHOICE below).
// Deterministic offline-render backend: master mix / time selection / selected
// tracks / selected items / razor area, all wet-only, optional tail. Source
// selection + range are resolved by the caller and handed in via CaptureRequest —
// the backend drives RENDER_* and never reads the DAW selection itself.
// SourceMode::Realtime returns UnsupportedMode. Non-destructive: restores every
// RENDER_* setting it touches on every path. Plain concrete class — see the
// no-shared-interface note in capture_realtime_shell.h before adding one back.
class OfflineRenderBackend {
public:
CaptureResult capture(const CaptureRequest& request);
};
// --- Shared backend helpers (Q-W3 riders) ------------------------------------
// Mints the filesystem-safe disambiguating tag for one capture's file stem +
// Sample id: "<prefix><unix-epoch-seconds>-<n>" where <n> is a PER-SESSION
// MONOTONIC counter (T1-11 fix). The wall-clock second alone had a collision
// window: two captures of the same baseName within one second derived the same
// stem, so the second render silently overwrote the first file (reachable via
// batch capture driving short renders back-to-back). The counter makes every tag
// of a session distinct regardless of timing. `prefix` is the backend's family
// marker ("" offline, "rt-" realtime).
// Sample id: "<prefix><unix-epoch-seconds>-<n>", <n> a per-session monotonic
// counter. Wall-clock seconds alone collide when batch capture drives short
// renders back-to-back, silently overwriting the first file. `prefix` is the
// backend's family marker ("" offline, "rt-" realtime).
std::string makeUniqueTag(const std::string& prefix);
// Stamps the SHARED finished-capture metadata onto `s` (T2-09 dedupe — this stamp
// was copy-pasted per backend and had silently diverged): trackGuids +
// channelCount (echoed from the request), the resolved sampleRate (request rate,
// else PROJECT_SRATE read from `rateProj`; 0 stays 0 when unknown), captureTempo
// (Master_GetTempo), the capture-start time signature (TimeMap_GetTimeSigAtTime
// against `timeSigProj` — the offline path passes nullptr = active project, the
// realtime path pins the record's own project; the divergence stays caller-visible
// as this argument), the WAV-aware contentHash of the finished file at
// `absolutePath` (left empty when unreadable — the safe, confirm-eliciting
// direction), and createdTimestamp (now). The per-backend bits (id, paths, bounds,
// tier, realtime's recorded-length override) stay with each caller.
// Stamps the metadata shared by both backends onto `s`: trackGuids + channelCount
// (echoed from the request), resolved sampleRate (request rate, else PROJECT_SRATE
// from `rateProj`), captureTempo, the capture-start time signature
// (TimeMap_GetTimeSigAtTime against `timeSigProj` — offline passes nullptr for the
// active project, realtime pins the record's own project), the WAV-aware
// contentHash of `absolutePath` (left empty when unreadable), and createdTimestamp.
// Per-backend bits (id, paths, bounds, tier, realtime's length override) stay
// with each caller.
void stampCaptureSample(Sample& s, const CaptureRequest& req,
ReaProject* rateProj, ReaProject* timeSigProj,
const std::string& absolutePath);