Q-W3: main.cpp → pointers+entry+dispatch via 4 capture hoists; one pure wav_codec RIFF owner; ICaptureBackend deleted; capture_realtime rename + finalize split; shared stampCaptureSample; makeUniqueTag gains monotonic counter (fixes same-second batch collisions). 60/60 green.
This commit is contained in:
+70
-41
@@ -1,19 +1,23 @@
|
||||
#pragma once
|
||||
#include "core/namespaces.h"
|
||||
// capture — the REAPER-facing capture shell (CLAUDE.md §load-bearing split).
|
||||
//
|
||||
// This header declares the capture *seam* the later milestones fill:
|
||||
// * CaptureRequest — everything a capture needs, source-mode-agnostic.
|
||||
// * ICaptureBackend — the SYNCHRONOUS interface OfflineRenderBackend implements
|
||||
// (headless, immediate, returns a finished Sample).
|
||||
// * OfflineRenderBackend — the deterministic default; drives the offline scopes.
|
||||
// * 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).
|
||||
// * RealtimeRecordBackend — the ASYNC realtime seam (begin/tick/abort), driven
|
||||
// across timer ticks; deliberately NOT an ICaptureBackend
|
||||
// across timer ticks; a genuinely different lifecycle
|
||||
// (see the SEAM CHOICE note at its declaration).
|
||||
// * 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 (main.cpp, future actions.cpp) depend on the seam
|
||||
// REAPER-free lets callers (the capture orchestration TUs) depend on the seam
|
||||
// without dragging the SDK into every include site.
|
||||
|
||||
#include <memory>
|
||||
@@ -23,13 +27,17 @@
|
||||
#include "core/model/bank_model.h"
|
||||
#include "core/capture/render_settings.h" // TailMode (pure) — the three-state tail contract
|
||||
|
||||
// MediaTrack is forward-declared (like track_guid.h) so this header stays
|
||||
// REAPER-free while RealtimeRecordBackend::begin can take the resolved source
|
||||
// MediaTrack* to tap. The pointers are opaque here — never dereferenced in a
|
||||
// pure/header context; only the REAPER-facing capture_realtime.cpp touches them.
|
||||
// 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.
|
||||
class MediaTrack;
|
||||
class ReaProject;
|
||||
|
||||
namespace reasampler {
|
||||
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.
|
||||
@@ -106,27 +114,48 @@ struct CaptureResult {
|
||||
std::string message; // human-readable detail for the console log
|
||||
};
|
||||
|
||||
// The capture seam. One method: run a request, return a populated Sample (or a
|
||||
// failure code). Backends are non-destructive — they must restore any global
|
||||
// state they touch before returning (OfflineRenderBackend snapshots/restores the
|
||||
// RENDER_* project settings).
|
||||
class ICaptureBackend {
|
||||
public:
|
||||
virtual ~ICaptureBackend() = default;
|
||||
virtual CaptureResult capture(const CaptureRequest& request) = 0;
|
||||
};
|
||||
|
||||
// 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).
|
||||
class OfflineRenderBackend : public ICaptureBackend {
|
||||
// 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).
|
||||
class OfflineRenderBackend {
|
||||
public:
|
||||
CaptureResult capture(const CaptureRequest& request) override;
|
||||
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).
|
||||
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.
|
||||
void stampCaptureSample(Sample& s, const CaptureRequest& req,
|
||||
ReaProject* rateProj, ReaProject* timeSigProj,
|
||||
const std::string& absolutePath);
|
||||
|
||||
// --- Realtime-record backend: the ASYNC seam ---------------------------------
|
||||
//
|
||||
// A realtime record is inherently asynchronous: CSurf_OnRecord starts the transport
|
||||
@@ -137,16 +166,17 @@ public:
|
||||
// from the same OnTimer that runs session.poll()) advances the in-flight record and
|
||||
// reports when it is done.
|
||||
//
|
||||
// SEAM CHOICE (surfaced): RealtimeRecordBackend deliberately does NOT implement the
|
||||
// synchronous ICaptureBackend — that interface returns a finished Sample from one
|
||||
// call, which no longer fits a record that spans ticks. The two backends have
|
||||
// genuinely different lifecycles (offline is headless + immediate; realtime is
|
||||
// transport-driven + async), so forcing a shared async interface would make offline
|
||||
// fake a lifecycle it does not have (its tick() would always be Done on the first
|
||||
// call — dead code / an LSP smell). Offline stays synchronous and unchanged; the
|
||||
// realtime backend owns this small bespoke async seam, driven by exactly one caller
|
||||
// (main.cpp's OnTimer). This is the split-sync/async fork, chosen over a unified
|
||||
// async interface for that reason.
|
||||
// SEAM CHOICE (surfaced): the two backends deliberately share NO interface. The
|
||||
// lifecycles are genuinely different (offline is headless + immediate — one
|
||||
// synchronous capture() call returns a finished Sample; realtime is
|
||||
// transport-driven + async — begin/tick/abort across timer ticks), so a shared
|
||||
// interface would make offline fake a lifecycle it does not have (its tick()
|
||||
// would always be Done on the first call — dead code / an LSP smell). Offline
|
||||
// stays synchronous; the realtime backend owns this small bespoke async seam,
|
||||
// driven by exactly one caller (the timer-driven realtime_lifecycle). This is the
|
||||
// split-sync/async fork, chosen over a unified async interface for that reason.
|
||||
// (The old synchronous ICaptureBackend interface over OfflineRenderBackend was
|
||||
// deleted in Q-W3 — T4-26: one deriver, zero polymorphic call sites.)
|
||||
|
||||
// One tick's verdict from the in-flight record.
|
||||
enum class RealtimeTickStatus {
|
||||
@@ -163,9 +193,8 @@ struct RealtimeTickResult {
|
||||
// The opaque in-flight capture state. Owns the snapshot of everything to restore
|
||||
// (temp track + its receive sends from the source tracks, other tracks' I_RECARM,
|
||||
// transport, edit cursor, time selection) and the record's own project handle.
|
||||
// Defined in
|
||||
// capture_realtime.cpp; the header stays REAPER-free (no MediaTrack*/ReaProject*
|
||||
// leaks here) by holding it behind a forward-declared type + unique_ptr.
|
||||
// Defined in capture_realtime_shell.cpp; the header stays REAPER-free (nothing is
|
||||
// dereferenced here) by holding it behind a forward-declared type + unique_ptr.
|
||||
//
|
||||
// restore()/teardown is idempotent and lives ON THIS OBJECT (not a function-scope
|
||||
// RAII guard) because the record spans ticks — no single stack frame outlives it.
|
||||
@@ -173,10 +202,10 @@ struct RealtimeTickResult {
|
||||
// funnels through the same single restore, safe to call once from whichever fires.
|
||||
class RealtimeCaptureState;
|
||||
|
||||
// Out-of-line deleter so callers (main.cpp) can own a unique_ptr to the opaque
|
||||
// RealtimeCaptureState WITHOUT its full (REAPER-typed) definition — the delete is
|
||||
// compiled in capture_realtime.cpp where the type is complete, keeping this header
|
||||
// REAPER-free (load-bearing split).
|
||||
// Out-of-line deleter so callers (realtime_lifecycle) can own a unique_ptr to the
|
||||
// opaque RealtimeCaptureState WITHOUT its full (REAPER-typed) definition — the
|
||||
// delete is compiled in capture_realtime_shell.cpp where the type is complete,
|
||||
// keeping this header REAPER-free (load-bearing split).
|
||||
struct RealtimeCaptureStateDeleter {
|
||||
void operator()(RealtimeCaptureState* p) const noexcept;
|
||||
};
|
||||
@@ -232,4 +261,4 @@ public:
|
||||
RealtimeTickResult abort(RealtimeCaptureState& state);
|
||||
};
|
||||
|
||||
} // namespace reasampler
|
||||
} // namespace reasampler::capture
|
||||
|
||||
Reference in New Issue
Block a user