feat(capture): M8 async realtime-record backend (master scope)
Timer-driven realtime capture behind ICaptureBackend (begin/tick/abort via OnTimer, non-blocking). Records master into a hidden temp track, moved to the bank non-destructively with idempotent restore across every terminal path. Pure phase machine unit-tested. Track/item deferred; realtime is non-deterministic.
This commit is contained in:
+108
-4
@@ -3,16 +3,19 @@
|
||||
//
|
||||
// This header declares the capture *seam* the later milestones fill:
|
||||
// * CaptureRequest — everything a capture needs, source-mode-agnostic.
|
||||
// * ICaptureBackend — the one interface behind which OfflineRenderBackend
|
||||
// (M3, here) and RealtimeRecordBackend (M8) both sit.
|
||||
// * OfflineRenderBackend — the deterministic default; M3 implements ONLY the
|
||||
// time-selection master-mix case.
|
||||
// * ICaptureBackend — the SYNCHRONOUS interface OfflineRenderBackend implements
|
||||
// (headless, immediate, returns a finished Sample).
|
||||
// * OfflineRenderBackend — the deterministic default; drives the offline scopes.
|
||||
// * RealtimeRecordBackend — the ASYNC realtime seam (begin/tick/abort), driven
|
||||
// across timer ticks; deliberately NOT an ICaptureBackend
|
||||
// (see the SEAM CHOICE note at its declaration).
|
||||
//
|
||||
// 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
|
||||
// without dragging the SDK into every include site.
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -83,6 +86,7 @@ enum class CaptureStatus {
|
||||
UnsupportedMode, // backend does not implement this source mode (M3 scope)
|
||||
UnsupportedFormat, // requested bit depth has no known REAPER blob (M3: Float32 only)
|
||||
RenderFailed, // the render action ran but produced no output file
|
||||
TransportBusy, // realtime backend: transport already playing/recording — refused
|
||||
};
|
||||
|
||||
struct CaptureResult {
|
||||
@@ -112,4 +116,104 @@ public:
|
||||
CaptureResult capture(const CaptureRequest& request) override;
|
||||
};
|
||||
|
||||
// --- Realtime-record backend: the ASYNC seam ---------------------------------
|
||||
//
|
||||
// A realtime record is inherently asynchronous: CSurf_OnRecord starts the transport
|
||||
// on REAPER's audio thread and returns immediately — it does NOT block until the
|
||||
// range completes, which takes (end - start) wall-clock seconds. Blocking the main
|
||||
// thread for that duration freezes REAPER's UI, so the realtime backend is DRIVEN
|
||||
// ACROSS TIMER TICKS instead: begin() starts and returns at once; tick() (called
|
||||
// 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.
|
||||
|
||||
// One tick's verdict from the in-flight record.
|
||||
enum class RealtimeTickStatus {
|
||||
InProgress, // still recording — call tick() again next timer tick
|
||||
Done, // finished (range end reached, or the user stopped) — `result` is set
|
||||
Failed, // an error tore the capture down — `result.message` explains
|
||||
};
|
||||
|
||||
struct RealtimeTickResult {
|
||||
RealtimeTickStatus status = RealtimeTickStatus::InProgress;
|
||||
CaptureResult result; // meaningful only when status == Done or Failed
|
||||
};
|
||||
|
||||
// The opaque in-flight capture state. Owns the snapshot of everything to restore
|
||||
// (temp track, other tracks' I_RECARM, master send/routing, 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.
|
||||
//
|
||||
// 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.
|
||||
// Every terminal path (normal completion, user stop, error, project switch, unload)
|
||||
// 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).
|
||||
struct RealtimeCaptureStateDeleter {
|
||||
void operator()(RealtimeCaptureState* p) const noexcept;
|
||||
};
|
||||
using RealtimeCaptureHandle =
|
||||
std::unique_ptr<RealtimeCaptureState, RealtimeCaptureStateDeleter>;
|
||||
|
||||
// Realtime-record backend — captures by RECORDING in realtime (transport-driven)
|
||||
// into a hidden temp track, then moves the recorded file into the bank as a Sample.
|
||||
// For sources offline render cannot do (hardware, performed FX) and as the true
|
||||
// pre-FX-dry path (I_RECMODE_FLAGS &3==1 — the only pre-FX tap in the SDK; offline
|
||||
// render has none). Dialog-free: never invokes the offline-render progress window.
|
||||
//
|
||||
// Non-bit-identical by nature (it is realtime); offline stays the deterministic
|
||||
// default. Non-destructive across EVERY terminal path — the review gate — which is
|
||||
// harder here than offline because the record spans ticks: the snapshot + restore
|
||||
// live on RealtimeCaptureState, not a function-scope RAII destructor.
|
||||
//
|
||||
// SCOPE (this increment): MASTER scope only — records the master-mix output, which
|
||||
// does not need the FxBypassGuard scope isolation (the whole chain is in scope).
|
||||
// Track/Item scopes are a genuine routing fork surfaced to Daniel, NOT silently
|
||||
// built (see capture_realtime.cpp §FORK). A Track/Item request is refused.
|
||||
class RealtimeRecordBackend {
|
||||
public:
|
||||
// Starts a realtime record: validates the request (master scope, non-empty
|
||||
// range, active + saved project, transport idle), snapshots all state to
|
||||
// restore, creates the hidden temp track, routes the master send, arms, and
|
||||
// CSurf_OnRecord — then returns IMMEDIATELY (no wait, no UI block). On success
|
||||
// the returned unique_ptr owns the in-flight state; drive it with tick(). On a
|
||||
// validation/setup failure returns nullptr and fills `outFailure` with the
|
||||
// CaptureStatus + message (nothing was left mutated — begin() restores on its
|
||||
// own failure paths).
|
||||
RealtimeCaptureHandle begin(const CaptureRequest& request,
|
||||
CaptureResult& outFailure);
|
||||
|
||||
// Advances the in-flight record one tick. Reads the transport (bound to the
|
||||
// record's OWN project handle so a project switch cannot confuse it), and on a
|
||||
// terminal verdict stops the transport, finalizes the recorded file into the
|
||||
// bank Sample (Done) or reports the failure (Failed), then restores ALL
|
||||
// snapshotted state. Returns InProgress while the record is still running.
|
||||
// After Done/Failed the state is spent — the caller drops the unique_ptr.
|
||||
RealtimeTickResult tick(RealtimeCaptureState& state);
|
||||
|
||||
// Force-terminate an in-flight record NOW without waiting for the range end:
|
||||
// stops the transport, finalizes whatever was captured (best effort) or abandons
|
||||
// it, and restores ALL snapshotted state. For the shutdown / project-switch
|
||||
// paths (extension unload, a new project became active) where the record must
|
||||
// not leak a temp track / armed track / altered transport into the user's
|
||||
// project. Idempotent — safe even if a prior tick already tore the state down.
|
||||
RealtimeTickResult abort(RealtimeCaptureState& state);
|
||||
};
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
Reference in New Issue
Block a user