4ef41cf705
Capture is now item + track only (master removed as a scope; still bypassed as out-of-scope chain). Realtime records the selected track's own output via per-track post-fader sends into a hidden temp track, fixing the silent file.
231 lines
12 KiB
C++
231 lines
12 KiB
C++
#pragma once
|
|
// 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.
|
|
// * 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>
|
|
|
|
#include "bank_model.h"
|
|
|
|
// 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.
|
|
class MediaTrack;
|
|
|
|
namespace reasampler {
|
|
|
|
// 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.
|
|
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).
|
|
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.
|
|
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.
|
|
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).
|
|
std::vector<std::string> trackGuids;
|
|
|
|
// Render tail. Default OFF for the spike (exact bounds, no added silence —
|
|
// precision invariant). M7 makes this bindable.
|
|
bool renderTail = false;
|
|
double tailMs = 0.0;
|
|
|
|
// Output format. 0 sampleRate => follow project rate (deterministic: the
|
|
// project rate is fixed for a given project).
|
|
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.
|
|
std::string baseName = "capture";
|
|
std::string uniqueTag; // e.g. a timestamp/counter; may be empty
|
|
};
|
|
|
|
// 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.
|
|
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)
|
|
RenderFailed, // the render action ran but produced no output file
|
|
TransportBusy, // realtime backend: transport already playing/recording — refused
|
|
};
|
|
|
|
struct CaptureResult {
|
|
CaptureStatus status = CaptureStatus::RenderFailed;
|
|
Sample sample; // valid only when status == Ok
|
|
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 {
|
|
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 + 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.
|
|
//
|
|
// 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): TRACK scope only — records the selected track's OWN
|
|
// output (item + that track's own FX + its own fader/pan, PRE-parent), matching
|
|
// offline's track scope. This needs NO FxBypassGuard: a send tapping a track's
|
|
// output is naturally PRE-parent (the parent has not summed it yet), so the tap is
|
|
// chain-independent by construction. Item realtime is deferred (UnsupportedMode).
|
|
class RealtimeRecordBackend {
|
|
public:
|
|
// Starts a realtime record: validates the request (track scope, non-empty range,
|
|
// at least one source track, active + saved project, transport idle), snapshots
|
|
// all state to restore, creates the hidden temp track, routes a send FROM each
|
|
// source track INTO the temp track, arms, and CSurf_OnRecord — then returns
|
|
// IMMEDIATELY (no wait, no UI block). `sourceTracks` are the selected tracks to
|
|
// tap (resolved by the action layer — the CaptureRequest itself stays REAPER-free,
|
|
// carrying only the provenance GUIDs). 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,
|
|
const std::vector<MediaTrack*>& sourceTracks,
|
|
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
|