M3: offline-render capture spike (master-mix / time-selection)

ICaptureBackend/CaptureRequest seam + OfflineRenderBackend driving snapshotted
RENDER_* with dither/normalize forced off for bit-identical output, RenderFailed
via filesystem check, and a pure capture_paths lib with tests. Spike action registered.
This commit is contained in:
2026-07-22 13:07:31 -04:00
parent 93e2783098
commit d9ad8e4adf
7 changed files with 837 additions and 26 deletions
+100
View File
@@ -0,0 +1,100 @@
#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 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.
//
// 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 <string>
#include "bank_model.h"
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. M3 renders the wet master mix (1.0);
// dry/partial routing is M7. Carried now so the Sample records it.
double wetDry = 1.0;
// 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)
RenderFailed, // the render action ran but produced no output file
};
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. M3 implements ONLY the
// TimeSelection / MasterMix case (both map to "render the master mix over the
// requested bounds"); any other source mode returns UnsupportedMode.
class OfflineRenderBackend : public ICaptureBackend {
public:
CaptureResult capture(const CaptureRequest& request) override;
};
} // namespace reasampler