feat(capture): bindable capture family — master/tracks/items/razor (M7)

Four wet capture actions route to OfflineRenderBackend (snapshot/restore
RENDER_*, 32-bit float), produce a Sample, add to the bank, persist. Pure
render_settings maps source mode to RENDER_SETTINGS and parses P_RAZOREDITS
(tested). No arrange insertion. True dry deferred to M10.
This commit is contained in:
2026-07-23 10:01:34 -04:00
parent 4f1231ea81
commit 7b530193b2
7 changed files with 679 additions and 60 deletions
+33 -18
View File
@@ -4,10 +4,19 @@
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU
// that defines the API pointers; here they are extern (CLAUDE.md §contract).
//
// Scope (M3): ONE source mode — the time-selection master mix. Drives the
// RENDER_* project settings via GetSetProjectInfo / _String, snapshots and
// restores every setting it changes (non-destructive), triggers a render, then
// populates a Sample. It NEVER inserts into the arrange (load-bearing principle).
// Scope (M7): the full offline source family — master mix / time selection,
// selected tracks, selected items, razor area — all wet-only with optional tail.
// Drives the RENDER_* project settings via GetSetProjectInfo / _String
// (the source-selection bits come from render_settings.cpp, the pure mapping),
// snapshots and restores every setting it changes (non-destructive), triggers a
// render, then populates a Sample. It NEVER inserts into the arrange
// (load-bearing principle) — RENDER_ADDTOPROJ&1 is cleared on every path.
//
// The backend is SOURCE-AGNOSTIC: it does NOT read the DAW selection. The action
// layer (main.cpp) resolves each source mode to a concrete time range (+ track
// GUIDs for track captures) and hands it in via the CaptureRequest. This keeps
// the render-driving here and the selection-reading testable/visible up in the
// actions layer.
//
// RENDER PROGRESS WINDOW (Item 2 finding — not suppressible via stock API):
// Triggering kActionRenderUsingMostRecentSettings (42230) causes REAPER to show
@@ -30,6 +39,7 @@
#include <vector>
#include "capture_paths.h"
#include "render_settings.h"
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_EnumProjects
@@ -61,11 +71,6 @@ constexpr int kActionRenderUsingMostRecentSettings = 42230;
// ourselves for exact, unrounded bounds). Verified: SDK header line ~3042.
constexpr double kBoundsCustom = 0.0;
// RENDER_SETTINGS master-mix bit pattern. Per the SDK header (line ~3041):
// (&(1|2))==0 => master mix, &8=use render matrix. We want plain master mix:
// no stems (bits 1|2 clear), no render matrix. Value 0 = master mix, no matrix.
constexpr double kRenderSettingsMasterMix = 0.0;
// RENDER_TAILFLAG bit &1 = apply tail for custom time bounds. We clear it for
// the spike (exact bounds, no added silence — precision invariant).
constexpr double kTailFlagNone = 0.0;
@@ -226,13 +231,16 @@ std::string makeUniqueTag() {
CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
CaptureResult result;
// M3 implements only the master-mix / time-selection case. Both mean "render
// the master mix over the requested bounds".
if (request.sourceMode != SourceMode::MasterMix &&
request.sourceMode != SourceMode::TimeSelection) {
// Resolve the RENDER_SETTINGS source/processing bits for this mode + wet/dry
// (pure mapping, unit-tested in render_settings). An unsupported mode (only
// SourceMode::Realtime — that is the M8 realtime backend) is refused here so
// the offline path never silently renders the wrong thing.
const RenderSettingsChoice choice =
renderSettingsFor(request.sourceMode, request.wetDry);
if (!choice.supported) {
result.status = CaptureStatus::UnsupportedMode;
result.message = "OfflineRenderBackend (M3) supports only master-mix / "
"time-selection capture.";
result.message = "OfflineRenderBackend does not render this source mode "
"(realtime capture is the M8 backend).";
return result;
}
@@ -350,8 +358,11 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
GetSetProjectInfo(proj, "RENDER_TAILMS", 0.0, true);
}
// Master mix, no stems, no render matrix.
GetSetProjectInfo(proj, "RENDER_SETTINGS", kRenderSettingsMasterMix, true);
// Source-selection bits for this mode, from the pure render_settings mapping
// (verified against SDK header ~3041). All M7 actions are wet-only:
// master mix = 0; tracks = &128; items = &32|single-file; razor = &4096|single-file.
GetSetProjectInfo(proj, "RENDER_SETTINGS",
static_cast<double>(choice.settings), true);
// Resolve the effective sample rate. When the request carries 0 ("follow
// project"), read PROJECT_SRATE explicitly so RENDER_SRATE is set to the
@@ -448,6 +459,10 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
// Seconds are the authoritative source for the render. Do NOT add DAW-
// unverifiable PPQ resolution here — it requires a live REAPER to validate.
s.wetDry = request.wetDry;
// Track GUIDs for track-scoped captures (empty for master/items/razor). The
// caller resolved the selection to canonical GUID strings; we record them so a
// "re-capture from source" (M10) knows which tracks the sample came from.
s.trackGuids = request.trackGuids;
s.channelCount = request.channelCount;
// Store the resolved sample rate only when it is known (> 0). If the project
// never pinned a rate (PROJECT_SRATE read 0), we did not force RENDER_SRATE
@@ -465,7 +480,7 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
result.status = CaptureStatus::Ok;
result.sample = s;
result.message = "Captured master mix [" +
result.message = "Captured [" +
std::to_string(request.startSeconds) + "s, " +
std::to_string(request.endSeconds) + "s] -> " +
paths.relativePath;