Cut shell/capture comment bloat ~33% (comments only, zero code change)
This commit is contained in:
+109
-261
@@ -1,38 +1,22 @@
|
|||||||
// capture.cpp — REAPER-facing offline-render backend (OfflineRenderBackend) plus
|
// REAPER-facing offline-render backend (OfflineRenderBackend) plus the shared
|
||||||
// the shared backend helpers (makeUniqueTag / stampCaptureSample — Q-W3 riders).
|
// backend helpers (makeUniqueTag / stampCaptureSample).
|
||||||
//
|
//
|
||||||
// Compiled into the reaper_reasampler MODULE. Includes
|
// Includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is
|
||||||
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU
|
// the one TU that defines the API pointers; here they are extern.
|
||||||
// that defines the API pointers; here they are extern (CLAUDE.md §contract).
|
|
||||||
//
|
//
|
||||||
// Renders a CaptureRequest's source over its requested range. The full three-scope
|
// Drives the RENDER_* project settings via GetSetProjectInfo/_String (source-
|
||||||
// capture family (item / track / master, each over a razor-else-time range) is
|
// selection bits come from the pure render_settings mapping), snapshots and
|
||||||
// driven here — all wet-only with optional tail. FX scope is enforced by the
|
// restores every setting it changes, triggers a render, then populates a Sample.
|
||||||
// caller (via FX-bypass-around-render / FxBypassGuard) before invoking capture;
|
// Source-agnostic: never reads the DAW selection itself, only the CaptureRequest
|
||||||
// this backend is source-agnostic and does not itself read the DAW selection.
|
// the caller resolved. RENDER_ADDTOPROJ&1 is cleared on every path — never
|
||||||
// Drives the RENDER_* project settings via GetSetProjectInfo / _String
|
// inserts into the arrange.
|
||||||
// (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
|
// RENDER PROGRESS WINDOW: triggering kActionRenderUsingMostRecentSettings (42230)
|
||||||
// layer (main.cpp) resolves each source mode to a concrete time range (+ track
|
// shows REAPER's offline-render progress dialog for the render's duration; no
|
||||||
// GUIDs for track captures) and hands it in via the CaptureRequest. This keeps
|
// RENDER_SETTINGS bit or GetSetProjectInfo desc suppresses it — inherent to
|
||||||
// the render-driving here and the selection-reading testable/visible up in the
|
// REAPER's offline render path. The dialog-free alternative is the realtime-
|
||||||
// actions layer.
|
// record backend, which captures the master bus to a temp track during playback
|
||||||
//
|
// and never invokes the offline render pipeline.
|
||||||
// RENDER PROGRESS WINDOW (Item 2 finding — not suppressible via stock API):
|
|
||||||
// Triggering kActionRenderUsingMostRecentSettings (42230) causes REAPER to show
|
|
||||||
// its offline-render progress dialog (progress bar + waveform view) for the
|
|
||||||
// duration of the render. The RENDER_SETTINGS bits documented in
|
|
||||||
// reaper_plugin_functions.h (line ~3041) contain no "no-dialog", "headless", or
|
|
||||||
// "suppress-progress-window" flag. No GetSetProjectInfo desc documents such a
|
|
||||||
// flag either. There is no stock, header-verifiable mechanism to prevent REAPER
|
|
||||||
// from showing this UI for an offline file render triggered via Main_OnCommand.
|
|
||||||
// This is inherent to REAPER's offline render path. The dialog-free alternative
|
|
||||||
// is the realtime-record backend (M8), which captures the master bus output to a
|
|
||||||
// temp track during playback and never invokes the offline render pipeline.
|
|
||||||
|
|
||||||
#include "shell/capture/capture.h"
|
#include "shell/capture/capture.h"
|
||||||
|
|
||||||
@@ -64,72 +48,50 @@ namespace reasampler::capture {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// --- Render command / setting constants -------------------------------------
|
// The no-dialog render is the built-in action "File: Render project, using the
|
||||||
//
|
// most recent render settings" — command id 42230. Stock main action id, not in
|
||||||
// DAW-ONLY ASSUMPTION (open question, CONTEXT.md §Open questions): the no-dialog
|
// reaper_plugin_functions.h, confirmed against a running REAPER. Renders
|
||||||
// render is triggered by the built-in action "File: Render project, using the
|
// headlessly using whatever RENDER_* settings are currently on the project —
|
||||||
// most recent render settings" — command id 42230. This is a stock REAPER main
|
// why we set them all explicitly first.
|
||||||
// action id, NOT part of reaper_plugin_functions.h, so it CANNOT be verified
|
|
||||||
// against the SDK header; it must be confirmed in a running REAPER. It renders
|
|
||||||
// headlessly (no dialog) using whatever RENDER_* settings are currently on the
|
|
||||||
// project — which is exactly why we set them all explicitly first.
|
|
||||||
constexpr int kActionRenderUsingMostRecentSettings = 42230;
|
constexpr int kActionRenderUsingMostRecentSettings = 42230;
|
||||||
|
|
||||||
// RENDER_BOUNDSFLAG value 0 = custom time bounds (we set STARTPOS/ENDPOS
|
// RENDER_BOUNDSFLAG 0 = custom time bounds (we set STARTPOS/ENDPOS ourselves
|
||||||
// ourselves for exact, unrounded bounds). Verified: SDK header line ~3042.
|
// for exact, unrounded bounds). SDK header ~3042.
|
||||||
constexpr double kBoundsCustom = 0.0;
|
constexpr double kBoundsCustom = 0.0;
|
||||||
|
|
||||||
// RENDER_TAILFLAG / RENDER_TAILMS / RENDER_NORMALIZE / RENDER_TRIMEND for the tail
|
// RENDER_TAILFLAG/TAILMS/NORMALIZE/TRIMEND are driven from the pure
|
||||||
// are driven from the pure tailRenderSettingsFor mapping (render_settings.h),
|
// tailRenderSettingsFor mapping (render_settings.h) in the tail-driving block below.
|
||||||
// unit-tested outside the DAW. See the tail-driving block in capture() below.
|
|
||||||
|
|
||||||
// RENDER_DITHER disable-all: &16 = disable all dither/noise-shaping.
|
// RENDER_DITHER &16 = disable all dither/noise-shaping (SDK header ~3050).
|
||||||
// Verified: SDK header line ~3050: "&16=disable all".
|
// Float32 doesn't need dither, but an enabled project dither setting would
|
||||||
// Float-32 output does not need dither, but if the user's project has dither
|
// otherwise apply and break bit-identical repeats. Force off.
|
||||||
// enabled the render would obey it, breaking bit-identical repeats. Force off.
|
|
||||||
constexpr double kDitherDisableAll = 16.0;
|
constexpr double kDitherDisableAll = 16.0;
|
||||||
|
|
||||||
// --- WAV render sink configuration ------------------------------------------
|
// 32-bit IEEE float: lossless, needs no dither, so identical inputs render
|
||||||
|
// bit-identically and a dry capture nulls exactly against its source. 16/24-bit
|
||||||
|
// int paths need dither for correctness, which is nondeterministic.
|
||||||
//
|
//
|
||||||
// FORMAT CHOICE (CONTEXT.md open question — surfaced for Daniel to confirm):
|
// GetSetProjectInfo_String("RENDER_FORMAT", ...) takes the BASE64-ENCODED sink
|
||||||
// 32-bit IEEE float. Rationale: float is lossless and needs NO dither, so
|
// config, not raw bytes (SDK header ~3114) — raw bytes are silently rejected and
|
||||||
// identical inputs render bit-identically (enables the M10 null test) and a dry
|
// REAPER falls back to its project default format.
|
||||||
// capture nulls exactly against its source. 16/24-bit int paths require dither
|
|
||||||
// for correctness, which is nondeterministic — unacceptable for a precision tool.
|
|
||||||
//
|
//
|
||||||
// API FACT (SDK header line ~3114): GetSetProjectInfo_String("RENDER_FORMAT", ...)
|
// Ground truth captured from a live REAPER set to WAV/32-bit float. Decodes to
|
||||||
// uses the BASE64-ENCODED string form of the sink config — NOT raw binary bytes.
|
// 7 bytes: "evaw" (WAV fourcc, LE) + 0x20 (float bit-depth) + 0x00 0x00 (flags).
|
||||||
// Writing raw bytes causes REAPER to silently reject the value and fall back to
|
|
||||||
// the project's default render format (typically 16-bit/44.1 kHz). This was the
|
|
||||||
// confirmed root cause of the M3 offline-capture regression.
|
|
||||||
//
|
|
||||||
// GROUND TRUTH: base64 string captured from a live REAPER configured to
|
|
||||||
// WAV / 32-bit float. Decodes to 7 bytes: 65 76 61 77 20 00 00
|
|
||||||
// = "evaw" (WAV fourcc, little-endian) + 0x20 (=32, the float bit-depth field)
|
|
||||||
// + 0x00 0x00 (flags: little-endian, no BWF/loop metadata).
|
|
||||||
constexpr const char* kRenderFormatWavFloat32 = "ZXZhdyAAAA==";
|
constexpr const char* kRenderFormatWavFloat32 = "ZXZhdyAAAA==";
|
||||||
|
|
||||||
// Int16 / Int24 blob strings are NOT implemented in M3 — their byte encoding
|
// Int16/Int24 blobs aren't implemented — no live-captured ground truth exists;
|
||||||
// was not captured from a live REAPER and must not be guessed. If M7+ adds
|
// do not guess the encoding. Returns nullptr for unsupported depths.
|
||||||
// them, capture the ground-truth base64 from a running REAPER first.
|
|
||||||
//
|
|
||||||
// Returns nullptr for unsupported depths.
|
|
||||||
const char* wavSinkConfigBase64(WavBitDepth depth) {
|
const char* wavSinkConfigBase64(WavBitDepth depth) {
|
||||||
switch (depth) {
|
switch (depth) {
|
||||||
case WavBitDepth::Float32: return kRenderFormatWavFloat32;
|
case WavBitDepth::Float32: return kRenderFormatWavFloat32;
|
||||||
case WavBitDepth::Int16: return nullptr; // M7+: capture ground-truth blob first
|
case WavBitDepth::Int16: return nullptr; // capture ground-truth blob first
|
||||||
case WavBitDepth::Int24: return nullptr; // M7+: capture ground-truth blob first
|
case WavBitDepth::Int24: return nullptr; // capture ground-truth blob first
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- RENDER_* snapshot / restore --------------------------------------------
|
// RENDER_* settings are project-GLOBAL; snapshot every value we touch and
|
||||||
//
|
// restore on the way out via RAII so early returns can't leak a half-restored state.
|
||||||
// The RENDER_* settings are project-GLOBAL: clobbering them would destroy the
|
|
||||||
// user's render configuration. We snapshot every value we are about to change,
|
|
||||||
// then restore all of them in the reverse order on the way out (non-destructive
|
|
||||||
// invariant). Modeled as a small RAII guard so early returns cannot leak a
|
|
||||||
// half-restored state.
|
|
||||||
struct RenderSettingsSnapshot {
|
struct RenderSettingsSnapshot {
|
||||||
ReaProject* proj = nullptr;
|
ReaProject* proj = nullptr;
|
||||||
|
|
||||||
@@ -191,8 +153,6 @@ void snapshotRenderSettings(RenderSettingsSnapshot& s, ReaProject* proj) {
|
|||||||
|
|
||||||
void restoreRenderSettings(const RenderSettingsSnapshot& s) {
|
void restoreRenderSettings(const RenderSettingsSnapshot& s) {
|
||||||
if (!s.captured) return;
|
if (!s.captured) return;
|
||||||
// Restore strings first, then numerics — order is not load-bearing since the
|
|
||||||
// fields are independent, but we mirror snapshot order for readability.
|
|
||||||
setProjString(s.proj, "RENDER_FILE", s.renderFile);
|
setProjString(s.proj, "RENDER_FILE", s.renderFile);
|
||||||
setProjString(s.proj, "RENDER_PATTERN", s.renderPattern);
|
setProjString(s.proj, "RENDER_PATTERN", s.renderPattern);
|
||||||
setProjString(s.proj, "RENDER_FORMAT", s.renderFormat);
|
setProjString(s.proj, "RENDER_FORMAT", s.renderFormat);
|
||||||
@@ -221,30 +181,16 @@ struct ScopedRenderSettings {
|
|||||||
ScopedRenderSettings& operator=(const ScopedRenderSettings&) = delete;
|
ScopedRenderSettings& operator=(const ScopedRenderSettings&) = delete;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Whole-file reads go through the shared core/util readFileBytes (Q-W1, T2-03):
|
|
||||||
// empty on any I/O failure (the caller then leaves contentHash empty — the safe,
|
|
||||||
// confirm-eliciting direction for an unreadable file).
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// --- Shared backend helpers (Q-W3 riders — see capture.h) --------------------
|
|
||||||
|
|
||||||
std::string makeUniqueTag(const std::string& prefix) {
|
std::string makeUniqueTag(const std::string& prefix) {
|
||||||
// Timestamp + PER-SESSION MONOTONIC counter (T1-11 fix). The timestamp alone
|
// Timestamp + per-session monotonic counter: the timestamp alone has one-second
|
||||||
// had one-second resolution: two captures of the same baseName within the same
|
// resolution, so two captures of the same baseName within a second (batch
|
||||||
// wall-clock second derived the same file stem, so the second render silently
|
// capture) collided on file stem and Sample id. This varies the file NAME, not
|
||||||
// overwrote the first file and minted two Samples with colliding ids —
|
// the audio bytes — bit-identical-repeat is about identical content per request.
|
||||||
// reachable in practice via batch capture. The counter (shared across both
|
// Residual: the counter resets per-process, so a same-second collision across
|
||||||
// backends — this is the one definition both call) makes every tag of a
|
// two REAPER instances (or a mid-session reload) remains theoretically possible;
|
||||||
// session distinct regardless of timing. NOTE: the tag varies the file NAME,
|
// scoped deliberately to the reachable single-process case.
|
||||||
// not the audio bytes — bit-identical-repeat is about identical *content* for
|
|
||||||
// identical requests; two deliberate captures naturally live in two files.
|
|
||||||
// RESIDUAL (Q-W3 review follow-up): the counter is per-process, starting over
|
|
||||||
// at 0 on every REAPER launch/extension reload, so two separate REAPER
|
|
||||||
// instances (or a reload mid-session) can still mint the same timestamp+counter
|
|
||||||
// pair in the same wall-clock second — a same-second cross-process collision
|
|
||||||
// remains theoretically possible. Scoped to per-session deliberately: this fix
|
|
||||||
// targets the reachable-in-practice single-process batch-capture case above.
|
|
||||||
static std::atomic<unsigned long long> counter{0};
|
static std::atomic<unsigned long long> counter{0};
|
||||||
const std::time_t now = std::time(nullptr);
|
const std::time_t now = std::time(nullptr);
|
||||||
return prefix + std::to_string(static_cast<long long>(now)) + "-" +
|
return prefix + std::to_string(static_cast<long long>(now)) + "-" +
|
||||||
@@ -259,26 +205,19 @@ void stampCaptureSample(Sample& s, const CaptureRequest& req,
|
|||||||
s.trackGuids = req.trackGuids;
|
s.trackGuids = req.trackGuids;
|
||||||
s.channelCount = req.channelCount;
|
s.channelCount = req.channelCount;
|
||||||
|
|
||||||
// Resolved sample rate: the request's pinned rate, else PROJECT_SRATE read
|
// PROJECT_SRATE can read 0 on a project that never pinned a rate — stays 0
|
||||||
// from the caller's project handle. PROJECT_SRATE can read 0 on a project that
|
// (honest "unknown") rather than a bogus literal.
|
||||||
// never explicitly pinned a rate — the value stays 0 (the Sample zero-value)
|
|
||||||
// rather than a bogus literal (the honest "unknown" both backends shared).
|
|
||||||
s.sampleRate = (req.sampleRate > 0)
|
s.sampleRate = (req.sampleRate > 0)
|
||||||
? req.sampleRate
|
? req.sampleRate
|
||||||
: static_cast<int>(GetSetProjectInfo(rateProj, "PROJECT_SRATE", 0.0, false));
|
: static_cast<int>(GetSetProjectInfo(rateProj, "PROJECT_SRATE", 0.0, false));
|
||||||
|
|
||||||
s.captureTempo = Master_GetTempo(); // BPM at capture time (verified ~4651)
|
s.captureTempo = Master_GetTempo(); // BPM at capture time
|
||||||
|
|
||||||
// Time signature at the capture's START time (L7 F1 stamp). TimeMap_GetTimeSigAtTime
|
// Time signature effective at the capture's START time, so a sample captured
|
||||||
// (verified reaper_plugin_functions.h:7130 — void(ReaProject*, double time,
|
// under 3/4 keeps a 3/4 read-out even if the project later switches to 4/4.
|
||||||
// int* numOut, int* denomOut, double* tempoOut)) reads the meter effective at
|
// `timeSigProj` is the caller's project pin — offline passes nullptr (active
|
||||||
// that project time, so a sample captured under 3/4 keeps a 3/4 read-out even
|
// project); realtime pins the record's own project. tempoOut is ignored —
|
||||||
// if the project later switches to 4/4. `timeSigProj` is the CALLER's project
|
// captureTempo already carries it.
|
||||||
// pin — offline passes nullptr (the active project); realtime pins the record's
|
|
||||||
// own project (the T2-09 divergence, kept caller-visible as this argument).
|
|
||||||
// tempoOut is ignored — captureTempo already carries the master tempo. Leaves
|
|
||||||
// 0/0 (unstamped) if the API is somehow unavailable; the formatter renders a
|
|
||||||
// blank musical read-out.
|
|
||||||
{
|
{
|
||||||
int tsNum = 0, tsDenom = 0;
|
int tsNum = 0, tsDenom = 0;
|
||||||
double tsTempo = 0.0;
|
double tsTempo = 0.0;
|
||||||
@@ -287,14 +226,10 @@ void stampCaptureSample(Sample& s, const CaptureRequest& req,
|
|||||||
s.captureTimeSigDenom = tsDenom;
|
s.captureTimeSigDenom = tsDenom;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content hash: WAV-aware FNV-1a over the finished file's fmt+data chunks so
|
// hashWavContent (not raw hashBytes) skips render-varying metadata chunks
|
||||||
// hashReferencedElsewhere can identify copies in other banks and suppress the
|
// (bext timestamp, iXML, LIST/INFO) so identical audio from two renders/records
|
||||||
// last-reference confirm when another bank still holds the same file. Using
|
// collapses to the same hash, letting dedup find copies across banks.
|
||||||
// hashWavContent (not the raw hashBytes) skips render-varying metadata chunks
|
// Unreadable file leaves contentHash empty (bank_model treats "" as non-dedup).
|
||||||
// (bext origination timestamp, iXML, LIST/INFO, etc.) so two renders/records of
|
|
||||||
// identical audio collapse to the same hash. Best-effort: an unreadable file
|
|
||||||
// leaves contentHash empty — the safe, confirm-eliciting direction (bank_model
|
|
||||||
// treats "" as non-participating in dedup).
|
|
||||||
{
|
{
|
||||||
const std::vector<std::uint8_t> fileBytes = util::readFileBytes(absolutePath);
|
const std::vector<std::uint8_t> fileBytes = util::readFileBytes(absolutePath);
|
||||||
if (!fileBytes.empty()) {
|
if (!fileBytes.empty()) {
|
||||||
@@ -308,16 +243,14 @@ void stampCaptureSample(Sample& s, const CaptureRequest& req,
|
|||||||
CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||||
CaptureResult result;
|
CaptureResult result;
|
||||||
|
|
||||||
// Resolve the RENDER_SETTINGS source/processing bits for this mode + wet/dry
|
// SourceMode::Realtime is refused here — that's the realtime backend's job —
|
||||||
// (pure mapping, unit-tested in render_settings). An unsupported mode (only
|
// so the offline path never silently renders the wrong thing.
|
||||||
// SourceMode::Realtime — that is the M8 realtime backend) is refused here so
|
|
||||||
// the offline path never silently renders the wrong thing.
|
|
||||||
const RenderSettingsChoice choice =
|
const RenderSettingsChoice choice =
|
||||||
renderSettingsFor(request.sourceMode, request.wetDry);
|
renderSettingsFor(request.sourceMode, request.wetDry);
|
||||||
if (!choice.supported) {
|
if (!choice.supported) {
|
||||||
result.status = CaptureStatus::UnsupportedMode;
|
result.status = CaptureStatus::UnsupportedMode;
|
||||||
result.message = "OfflineRenderBackend does not render this source mode "
|
result.message = "OfflineRenderBackend does not render this source mode "
|
||||||
"(realtime capture is the M8 backend).";
|
"(realtime capture is the realtime backend).";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,8 +261,7 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current project (idx -1 == the active project tab). Verified: SDK header
|
// idx -1 == the active project tab.
|
||||||
// line ~1264, EnumProjects(int idx, char*, int).
|
|
||||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||||
if (!proj) {
|
if (!proj) {
|
||||||
result.status = CaptureStatus::NoProject;
|
result.status = CaptureStatus::NoProject;
|
||||||
@@ -337,131 +269,81 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the project directory from the .rpp file path.
|
// Unsaved-project detection via EnumProjects(-1, buf, bufsz): the .rpp path
|
||||||
|
// out-param is empty for a project that has never been saved — a reliable
|
||||||
|
// unsaved sentinel. NOT GetProjectPathEx: that returns the recording path, not
|
||||||
|
// the .rpp location, and is never empty even when unsaved (the original bug —
|
||||||
|
// captures landed in REAPER's default media location instead of by the .rpp).
|
||||||
//
|
//
|
||||||
// Unsaved-project detection: we use EnumProjects(-1, buf, bufsz) to read
|
// Flow: read .rpp path; if empty, Main_SaveProject(proj, true) prompts
|
||||||
// the project's .rpp filename. Per SDK header line ~1262:
|
// Save-As and blocks until dismissed; re-read; if still empty (cancelled),
|
||||||
// EnumProjects(int idx, char* projfnOutOptional, int sz)
|
// refuse with NoProject and write nothing.
|
||||||
// "idx=-1 for current project, projfn can be NULL if not interested in filename."
|
|
||||||
// The out-parameter is the full path to the .rpp file, and is EMPTY for a
|
|
||||||
// project that has never been saved — making it a reliable unsaved sentinel.
|
|
||||||
//
|
|
||||||
// WHY NOT GetProjectPathEx: that function returns the project *recording path*
|
|
||||||
// (SDK header line ~2548: "Get the project recording path."), NOT the .rpp
|
|
||||||
// location. For an unsaved project it returns REAPER's default media/recording
|
|
||||||
// directory — never empty — so it cannot detect the unsaved state. Using it
|
|
||||||
// caused the original bug: the guard never fired, and captures landed in
|
|
||||||
// REAPER's default media location rather than alongside the .rpp.
|
|
||||||
//
|
|
||||||
// WHY NOT GetProjectPathEx for the saved-project dir: even for a saved project,
|
|
||||||
// GetProjectPathEx returns the recording path (which may be a media subfolder),
|
|
||||||
// not the .rpp parent directory. We need the .rpp parent so reasampler_bank/
|
|
||||||
// sits alongside the .rpp and travels with the project.
|
|
||||||
//
|
|
||||||
// FLOW:
|
|
||||||
// 1. Read .rpp path via EnumProjects(-1, buf, bufsz).
|
|
||||||
// 2. If non-empty (saved) -> derive project dir as parent of the .rpp.
|
|
||||||
// 3. If empty (unsaved) -> Main_SaveProject(proj, true) prompts Save-As.
|
|
||||||
// Re-read. If now non-empty -> proceed. If still empty (user cancelled) ->
|
|
||||||
// refuse CaptureStatus::NoProject, write nothing.
|
|
||||||
//
|
|
||||||
// DAW-ONLY ASSUMPTION: Main_SaveProject(proj, true) opens a Save/Save-As
|
|
||||||
// dialog and blocks until the user dismisses it. "true" = forceSaveAsIn.
|
|
||||||
// Verified SDK header line ~4599:
|
|
||||||
// void Main_SaveProject(ReaProject* proj, bool forceSaveAsInOptional)
|
|
||||||
// The blocking behaviour and dialog appearance can only be confirmed in a
|
|
||||||
// running REAPER.
|
|
||||||
auto readRppPath = [&]() -> std::string {
|
auto readRppPath = [&]() -> std::string {
|
||||||
std::vector<char> buf(4096, '\0');
|
std::vector<char> buf(4096, '\0');
|
||||||
// EnumProjects(-1, ...) returns the active project and writes the .rpp
|
|
||||||
// path into buf. We already have the ReaProject* from the earlier call
|
|
||||||
// (nullptr-checked above), but calling EnumProjects again is the only
|
|
||||||
// stock, header-documented way to read the .rpp filename.
|
|
||||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||||
return std::string(buf.data());
|
return std::string(buf.data());
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string rppPath = readRppPath();
|
std::string rppPath = readRppPath();
|
||||||
if (rppPath.empty()) {
|
if (rppPath.empty()) {
|
||||||
// Project is unsaved. Prompt the user to choose a save location.
|
|
||||||
Main_SaveProject(proj, true);
|
Main_SaveProject(proj, true);
|
||||||
// Re-read: non-empty if the user confirmed, still empty if cancelled.
|
|
||||||
rppPath = readRppPath();
|
rppPath = readRppPath();
|
||||||
}
|
}
|
||||||
if (rppPath.empty()) {
|
if (rppPath.empty()) {
|
||||||
// User cancelled the save dialog — refuse, write nothing.
|
|
||||||
result.status = CaptureStatus::NoProject;
|
result.status = CaptureStatus::NoProject;
|
||||||
result.message = "Project must be saved before capture — nothing captured.";
|
result.message = "Project must be saved before capture — nothing captured.";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive the project directory as the parent folder of the .rpp file.
|
// Project dir = parent of the .rpp; forward-slash-normalized so the rest of
|
||||||
// std::filesystem::path handles both forward- and back-slash paths; .parent_path()
|
// the capture pipeline (deriveBankPaths, RENDER_FILE) sees a clean path.
|
||||||
// gives the containing directory. Convert to forward-slash string so the rest
|
|
||||||
// of the capture pipeline (deriveBankPaths, RENDER_FILE) sees a clean path.
|
|
||||||
const std::string projectDir = [&]() -> std::string {
|
const std::string projectDir = [&]() -> std::string {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
std::string dir = fs::path(rppPath).parent_path().string();
|
std::string dir = fs::path(rppPath).parent_path().string();
|
||||||
// normalizeSlashes is in capture_paths (pure); replicate the transform
|
|
||||||
// inline here to avoid a cross-module dependency for a one-liner.
|
|
||||||
for (char& c : dir) { if (c == '\\') c = '/'; }
|
for (char& c : dir) { if (c == '\\') c = '/'; }
|
||||||
// Strip a single trailing slash (defensive; parent_path usually omits it).
|
|
||||||
if (dir.size() > 1 && dir.back() == '/') dir.pop_back();
|
if (dir.size() > 1 && dir.back() == '/') dir.pop_back();
|
||||||
return dir;
|
return dir;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
// Compute the unique tag ONCE so the file stem and Sample.id carry the same
|
// Compute the tag ONCE — calling makeUniqueTag() twice would let the file
|
||||||
// tag. Calling makeUniqueTag() twice would yield different values (the counter
|
// stem and Sample.id diverge (the counter advances per call).
|
||||||
// advances per call — bug: id and filename diverge).
|
|
||||||
const std::string uniqueTag = makeUniqueTag("");
|
const std::string uniqueTag = makeUniqueTag("");
|
||||||
const BankPaths paths =
|
const BankPaths paths =
|
||||||
deriveBankPaths(projectDir, request.baseName, uniqueTag);
|
deriveBankPaths(projectDir, request.baseName, uniqueTag);
|
||||||
|
|
||||||
// Snapshot + auto-restore ALL render settings we are about to touch.
|
|
||||||
ScopedRenderSettings guard(proj);
|
ScopedRenderSettings guard(proj);
|
||||||
|
|
||||||
// --- Drive the render settings (exact, deterministic) -------------------
|
|
||||||
// Custom time bounds so the rendered length equals the requested range with
|
// Custom time bounds so the rendered length equals the requested range with
|
||||||
// NO rounding and NO added silence (unless a tail was explicitly requested).
|
// NO rounding and NO added silence (unless a tail was explicitly requested).
|
||||||
GetSetProjectInfo(proj, "RENDER_BOUNDSFLAG", kBoundsCustom, true);
|
GetSetProjectInfo(proj, "RENDER_BOUNDSFLAG", kBoundsCustom, true);
|
||||||
GetSetProjectInfo(proj, "RENDER_STARTPOS", request.startSeconds, true);
|
GetSetProjectInfo(proj, "RENDER_STARTPOS", request.startSeconds, true);
|
||||||
GetSetProjectInfo(proj, "RENDER_ENDPOS", request.endSeconds, true);
|
GetSetProjectInfo(proj, "RENDER_ENDPOS", request.endSeconds, true);
|
||||||
|
|
||||||
// Tail: TAILFLAG / TAILMS / NORMALIZE / TRIMEND all come from the pure mapping
|
// TAILFLAG/TAILMS/NORMALIZE/TRIMEND from the pure mapping: None -> exact
|
||||||
// (render_settings.h, unit-tested). None -> exact bounds + disable-all normalize
|
// bounds + disable-all normalize; Auto -> 8s tail + surgical trim-end
|
||||||
// (byte-identical to the pre-tail path); Auto -> 8 s tail + surgical trim-end
|
// normalize + -72 dB TRIMEND; Manual -> clamped fixed tail + disable-all, no
|
||||||
// normalize + -72 dB TRIMEND; Manual -> clamped fixed tail + disable-all, no trim.
|
// trim. NORMALIZE is driven here (not the determinism block below) so the
|
||||||
// RENDER_NORMALIZE is driven HERE from the mapping (not the determinism block
|
// Auto surgical value isn't clobbered.
|
||||||
// below) so the Auto surgical value is not clobbered — the snapshot guard restores
|
|
||||||
// the user's original RENDER_NORMALIZE / RENDER_TRIMEND on every exit path.
|
|
||||||
const TailRenderSettings tail =
|
const TailRenderSettings tail =
|
||||||
tailRenderSettingsFor(request.tailMode, request.tailMs);
|
tailRenderSettingsFor(request.tailMode, request.tailMs);
|
||||||
GetSetProjectInfo(proj, "RENDER_TAILFLAG",
|
GetSetProjectInfo(proj, "RENDER_TAILFLAG",
|
||||||
static_cast<double>(tail.tailFlag), true);
|
static_cast<double>(tail.tailFlag), true);
|
||||||
GetSetProjectInfo(proj, "RENDER_TAILMS", tail.tailMs, true);
|
GetSetProjectInfo(proj, "RENDER_TAILMS", tail.tailMs, true);
|
||||||
|
|
||||||
// Source-selection bits for this mode, from the pure render_settings mapping
|
// Source-selection bits for this mode (SDK header ~3041), all wet-only:
|
||||||
// (verified against SDK header ~3041). All M7 actions are wet-only:
|
|
||||||
// master mix = 0; tracks = &128; items = &32|single-file; razor = &4096|single-file.
|
// master mix = 0; tracks = &128; items = &32|single-file; razor = &4096|single-file.
|
||||||
GetSetProjectInfo(proj, "RENDER_SETTINGS",
|
GetSetProjectInfo(proj, "RENDER_SETTINGS",
|
||||||
static_cast<double>(choice.settings), true);
|
static_cast<double>(choice.settings), true);
|
||||||
|
|
||||||
// Resolve the effective sample rate. When the request carries 0 ("follow
|
// request 0 = "follow project"; PROJECT_SRATE is still readable via
|
||||||
// project"), read PROJECT_SRATE explicitly so RENDER_SRATE is set to the
|
// GetSetProjectInfo even when PROJECT_SRATE_USE is clear.
|
||||||
// actual value — not left as 0 for REAPER to interpret. SDK header line ~3064:
|
|
||||||
// PROJECT_SRATE = sample rate (ignored unless PROJECT_SRATE_USE set); the
|
|
||||||
// value is still readable via GetSetProjectInfo even when _USE is clear.
|
|
||||||
const int effectiveSampleRate = (request.sampleRate > 0)
|
const int effectiveSampleRate = (request.sampleRate > 0)
|
||||||
? request.sampleRate
|
? request.sampleRate
|
||||||
: static_cast<int>(GetSetProjectInfo(proj, "PROJECT_SRATE", 0.0, false));
|
: static_cast<int>(GetSetProjectInfo(proj, "PROJECT_SRATE", 0.0, false));
|
||||||
|
|
||||||
// Pin RENDER_SRATE only when the resolved rate is known (> 0). PROJECT_SRATE
|
// Only pin RENDER_SRATE when known (>0) — a brand-new project can read 0 for
|
||||||
// can read 0 on a project that has never explicitly pinned a sample rate (e.g.
|
// PROJECT_SRATE, and forcing RENDER_SRATE=0 would be a bogus literal; leave
|
||||||
// brand-new projects before the user has visited the project settings). Forcing
|
// it unset so REAPER follows its own project-rate default.
|
||||||
// RENDER_SRATE = 0 would re-introduce the "0 as literal" trap we fixed by
|
|
||||||
// moving away from blind passthrough. When the rate is unknown, leave
|
|
||||||
// RENDER_SRATE unset so REAPER follows its own project-rate default — which is
|
|
||||||
// correct behaviour for that project — rather than pinning a bogus 0.
|
|
||||||
if (effectiveSampleRate > 0) {
|
if (effectiveSampleRate > 0) {
|
||||||
GetSetProjectInfo(proj, "RENDER_SRATE",
|
GetSetProjectInfo(proj, "RENDER_SRATE",
|
||||||
static_cast<double>(effectiveSampleRate), true);
|
static_cast<double>(effectiveSampleRate), true);
|
||||||
@@ -469,100 +351,67 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|||||||
GetSetProjectInfo(proj, "RENDER_CHANNELS",
|
GetSetProjectInfo(proj, "RENDER_CHANNELS",
|
||||||
static_cast<double>(request.channelCount), true);
|
static_cast<double>(request.channelCount), true);
|
||||||
|
|
||||||
// Load-bearing principle: do NOT add the rendered file to the project as an
|
// Load-bearing: never add the rendered file to the project as an item.
|
||||||
// item. Clearing RENDER_ADDTOPROJ&1 keeps capture out of the arrange.
|
|
||||||
GetSetProjectInfo(proj, "RENDER_ADDTOPROJ", 0.0, true);
|
GetSetProjectInfo(proj, "RENDER_ADDTOPROJ", 0.0, true);
|
||||||
|
|
||||||
// Determinism: disable dither so identical inputs produce bit-identical files
|
|
||||||
// and a dry capture nulls to silence. RENDER_DITHER &16 = disable all dither/
|
|
||||||
// noise-shaping (SDK header line ~3050). Snapshotted above; restored by the guard.
|
|
||||||
GetSetProjectInfo(proj, "RENDER_DITHER", kDitherDisableAll, true);
|
GetSetProjectInfo(proj, "RENDER_DITHER", kDitherDisableAll, true);
|
||||||
|
|
||||||
// RENDER_NORMALIZE + RENDER_TRIMEND come from the tail mapping (above). None /
|
// None/Manual -> disable-all (byte-identical to pre-tail); Auto -> surgical
|
||||||
// Manual -> disable-all (byte-identical to the pre-tail path); Auto -> surgical
|
// trim-end (only &32768) + -72 dB TRIMEND — a fixed-threshold trailing-silence
|
||||||
// trim-end (only &32768) + the -72 dB TRIMEND. A fixed-threshold trailing-silence
|
// trim scales/limits/fades nothing, so Auto stays deterministic. TRIMEND is
|
||||||
// trim scales/limits/fades nothing, so Auto stays deterministic and un-coloring
|
// only consulted when the trim bit is set but written unconditionally for clarity.
|
||||||
// (spec §surgical normalize). TRIMEND is only consulted when the trim bit is set,
|
|
||||||
// but we write it unconditionally (harmless when clear) so the value is explicit.
|
|
||||||
GetSetProjectInfo(proj, "RENDER_NORMALIZE",
|
GetSetProjectInfo(proj, "RENDER_NORMALIZE",
|
||||||
static_cast<double>(tail.normalize), true);
|
static_cast<double>(tail.normalize), true);
|
||||||
GetSetProjectInfo(proj, "RENDER_TRIMEND", tail.trimEnd, true);
|
GetSetProjectInfo(proj, "RENDER_TRIMEND", tail.trimEnd, true);
|
||||||
|
|
||||||
// Output location: directory (RENDER_FILE) + file stem (RENDER_PATTERN).
|
|
||||||
// RENDER_PATTERN with no wildcards is a literal stem; REAPER appends the
|
// RENDER_PATTERN with no wildcards is a literal stem; REAPER appends the
|
||||||
// format extension. Use paths.fileStem — capture_paths owns the .wav suffix
|
// format extension. paths.fileStem already owns the .wav suffix knowledge.
|
||||||
// knowledge; re-stripping here would duplicate that coupling.
|
|
||||||
setProjString(proj, "RENDER_FILE", paths.absoluteDir);
|
setProjString(proj, "RENDER_FILE", paths.absoluteDir);
|
||||||
setProjString(proj, "RENDER_PATTERN", paths.fileStem);
|
setProjString(proj, "RENDER_PATTERN", paths.fileStem);
|
||||||
|
|
||||||
// Pin the WAV format using the ground-truth base64 blob for the chosen depth.
|
// Int16/Int24 have no captured ground-truth blob — fail explicitly rather
|
||||||
// Int16/Int24 are not implemented (no live-captured blob) — fail explicitly
|
// than silently mis-render at the wrong bit depth.
|
||||||
// rather than silently mis-render at the wrong bit depth.
|
|
||||||
const char* fmtBase64 = wavSinkConfigBase64(request.bitDepth);
|
const char* fmtBase64 = wavSinkConfigBase64(request.bitDepth);
|
||||||
if (!fmtBase64) {
|
if (!fmtBase64) {
|
||||||
result.status = CaptureStatus::UnsupportedFormat;
|
result.status = CaptureStatus::UnsupportedFormat;
|
||||||
result.message = "Requested bit depth has no verified RENDER_FORMAT blob "
|
result.message = "Requested bit depth has no verified RENDER_FORMAT blob "
|
||||||
"(M3 supports Float32 only; Int16/Int24 are M7+).";
|
"(Float32 only; Int16/Int24 not yet supported).";
|
||||||
return result;
|
return result;
|
||||||
// guard's dtor restores every RENDER_* setting here.
|
|
||||||
}
|
}
|
||||||
setProjString(proj, "RENDER_FORMAT", fmtBase64);
|
setProjString(proj, "RENDER_FORMAT", fmtBase64);
|
||||||
|
|
||||||
// --- Trigger the render -------------------------------------------------
|
|
||||||
// DAW-ONLY ASSUMPTION (see kActionRenderUsingMostRecentSettings): this runs
|
|
||||||
// the render synchronously on the current build. REAPER will show its
|
|
||||||
// offline-render progress window for the duration (see file-top comment —
|
|
||||||
// the progress UI is not suppressible via stock API).
|
|
||||||
Main_OnCommand(kActionRenderUsingMostRecentSettings, 0);
|
Main_OnCommand(kActionRenderUsingMostRecentSettings, 0);
|
||||||
|
|
||||||
// --- Verify the output file exists ---------------------------------------
|
// Main_OnCommand returns void, so a failed render is silent — stat the
|
||||||
// Main_OnCommand returns void, so a failed render is silent. Stat the
|
// expected output path to detect it.
|
||||||
// expected output path; if the file does not exist the render failed.
|
|
||||||
// Note: std::filesystem is used only in this REAPER-facing .cpp — the pure
|
|
||||||
// libs (capture_paths, bank_model) remain filesystem-free.
|
|
||||||
const std::string expectedPath = paths.absoluteDir + "/" + paths.fileName;
|
const std::string expectedPath = paths.absoluteDir + "/" + paths.fileName;
|
||||||
if (!std::filesystem::exists(expectedPath)) {
|
if (!std::filesystem::exists(expectedPath)) {
|
||||||
result.status = CaptureStatus::RenderFailed;
|
result.status = CaptureStatus::RenderFailed;
|
||||||
result.message = "Render produced no output file (expected: " +
|
result.message = "Render produced no output file (expected: " +
|
||||||
expectedPath + "). Check the REAPER console for errors.";
|
expectedPath + "). Check the REAPER console for errors.";
|
||||||
return result;
|
return result;
|
||||||
// guard's dtor restores every RENDER_* setting here.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Populate the Sample -------------------------------------------------
|
// Record the request's own bounds (exact) rather than re-measuring the file.
|
||||||
// We record the request's own bounds (exact) rather than re-measuring the
|
|
||||||
// file, so the Sample's range is precisely what was asked for.
|
|
||||||
Sample s;
|
Sample s;
|
||||||
// Use the same uniqueTag that named the file — calling makeUniqueTag() again
|
// Same uniqueTag that named the file — calling makeUniqueTag() again could
|
||||||
// here would risk a different timestamp if a second boundary crosses between
|
// yield a different value and desync Sample.id from the file name.
|
||||||
// the two calls, making Sample.id inconsistent with the file name.
|
|
||||||
s.id = "cap-" + uniqueTag + "-" + paths.fileName;
|
s.id = "cap-" + uniqueTag + "-" + paths.fileName;
|
||||||
s.displayName = request.baseName;
|
s.displayName = request.baseName;
|
||||||
s.relativePath = paths.relativePath; // project-relative (invariant)
|
s.relativePath = paths.relativePath; // project-relative (invariant)
|
||||||
s.sourceMode = request.sourceMode;
|
s.sourceMode = request.sourceMode;
|
||||||
s.sourceRange.startSeconds = request.startSeconds;
|
s.sourceRange.startSeconds = request.startSeconds;
|
||||||
s.sourceRange.endSeconds = request.endSeconds;
|
s.sourceRange.endSeconds = request.endSeconds;
|
||||||
// DEFERRED (M6/M7): startPpq, endPpq, and lengthBeats are left at 0.
|
// startPpq/endPpq/lengthBeats left at 0 — PPQ mapping is a placement-time
|
||||||
// PPQ mapping via TimeMap2_timeToBeats is a musical-placement concern for the
|
// concern; seconds are the authoritative source for the render and we don't
|
||||||
// insert milestone; the model refuses to re-derive one bound from the other.
|
// re-derive one bound from the other.
|
||||||
// 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;
|
s.wetDry = request.wetDry;
|
||||||
s.lengthSeconds = request.endSeconds - request.startSeconds;
|
s.lengthSeconds = request.endSeconds - request.startSeconds;
|
||||||
s.tier = model::Tier::Scratch; // captures land in scratch by default
|
s.tier = model::Tier::Scratch;
|
||||||
// The SHARED finished-capture stamp (T2-09 dedupe): trackGuids + channelCount
|
|
||||||
// (request echo), resolved sampleRate (request rate else PROJECT_SRATE(proj) —
|
|
||||||
// 0 stays 0 when the project never pinned a rate; we did not force RENDER_SRATE
|
|
||||||
// either, so the render ran at REAPER's default), captureTempo, the capture-
|
|
||||||
// start time signature (timeSigProj = nullptr => the active project — matching
|
|
||||||
// the Master_GetTempo read, which is also active-project), the WAV-aware
|
|
||||||
// contentHash of the rendered file, and createdTimestamp.
|
|
||||||
stampCaptureSample(s, request, proj, /*timeSigProj=*/nullptr, expectedPath);
|
stampCaptureSample(s, request, proj, /*timeSigProj=*/nullptr, expectedPath);
|
||||||
// Phase S seam fields (rootNote / loop) left empty (D-B). An offline render of a
|
// rootNote/loop left empty — a master/track/time-selection render isn't a
|
||||||
// master mix / track / time-selection is not a single played note, so no root
|
// single played note, so no root note is derivable; loop points are set
|
||||||
// note is derivable here — we do NOT guess one. Loop points are set later by an
|
// later by an explicit user action.
|
||||||
// explicit user action, not at capture. Leaving them empty is the honest default;
|
|
||||||
// the instrument (Phase S) treats an absent root note as "not a pitched sample".
|
|
||||||
|
|
||||||
result.status = CaptureStatus::Ok;
|
result.status = CaptureStatus::Ok;
|
||||||
result.sample = s;
|
result.sample = s;
|
||||||
@@ -571,7 +420,6 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|||||||
std::to_string(request.endSeconds) + "s] -> " +
|
std::to_string(request.endSeconds) + "s] -> " +
|
||||||
paths.relativePath;
|
paths.relativePath;
|
||||||
return result;
|
return result;
|
||||||
// guard's dtor restores every RENDER_* setting here.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace reasampler::capture
|
} // namespace reasampler::capture
|
||||||
|
|||||||
+45
-90
@@ -1,36 +1,18 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// capture — the REAPER-facing capture shell (CLAUDE.md §load-bearing split).
|
// The shared capture seam: CaptureRequest/CaptureResult (types both backends
|
||||||
|
// speak), OfflineRenderBackend, and the makeUniqueTag/stampCaptureSample helpers.
|
||||||
|
// Realtime's async begin/tick/abort surface lives in capture_realtime_shell.h.
|
||||||
//
|
//
|
||||||
// This header declares the SHARED capture seam (Q-W6 split of the former fat
|
// REAPER-free on purpose (bank_model only) so callers can depend on the seam
|
||||||
// header — the realtime backend's async begin/tick/abort surface now lives in
|
// without dragging the SDK into every include site; the .cpp is the REAPER TU.
|
||||||
// capture_realtime_shell.h):
|
|
||||||
// * CaptureRequest / CaptureResult — everything a capture needs and yields,
|
|
||||||
// source-mode-agnostic; the types BOTH backends speak.
|
|
||||||
// * 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).
|
|
||||||
// * 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 (the capture orchestration TUs) depend on the seam
|
|
||||||
// without dragging the SDK into every include site.
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "core/model/bank_model.h"
|
#include "core/model/bank_model.h"
|
||||||
#include "core/capture/render_settings.h" // TailMode (pure) — the three-state tail contract
|
#include "core/capture/render_settings.h" // TailMode — the three-state tail contract
|
||||||
|
|
||||||
// MediaTrack / ReaProject are forward-declared (like track_guid.h) so this header
|
// Forward-declared, never dereferenced here — only the REAPER-facing .cpp touches these.
|
||||||
// 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 MediaTrack;
|
||||||
class ReaProject;
|
class ReaProject;
|
||||||
|
|
||||||
@@ -38,71 +20,55 @@ namespace reasampler::capture {
|
|||||||
|
|
||||||
using model::Sample;
|
using model::Sample;
|
||||||
|
|
||||||
// Audio bit-depth for the rendered wav. 32-bit float is the M3 default —
|
// 32-bit float is the default; rationale lives in capture.cpp next to the sink-config bytes.
|
||||||
// rationale lives in capture.cpp next to the sink-config bytes.
|
|
||||||
enum class WavBitDepth {
|
enum class WavBitDepth {
|
||||||
Int16,
|
Int16,
|
||||||
Int24,
|
Int24,
|
||||||
Float32,
|
Float32,
|
||||||
};
|
};
|
||||||
|
|
||||||
// One capture, independent of source mode. Populated by the caller (the action
|
// One capture, independent of source mode.
|
||||||
// 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 {
|
struct CaptureRequest {
|
||||||
SourceMode sourceMode = SourceMode::MasterMix;
|
SourceMode sourceMode = SourceMode::MasterMix;
|
||||||
|
|
||||||
// Sample-accurate render bounds in project seconds. For the M3 spike these
|
// Sample-accurate render bounds in project seconds — NO rounding.
|
||||||
// come straight from the time selection (GetSet_LoopTimeRange) — NO rounding.
|
|
||||||
double startSeconds = 0.0;
|
double startSeconds = 0.0;
|
||||||
double endSeconds = 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).
|
// 1.0 = fully wet, 0.0 = fully dry. Every current capture action sets 1.0;
|
||||||
// The field is kept as the seam for future true-dry work (M10 null test):
|
// true pre-FX dry isn't available via RENDER_SETTINGS (needs FX-bypass-around-render
|
||||||
// true pre-FX dry offline is NOT available via RENDER_SETTINGS — it requires
|
// or the realtime pre-FX path) so this stays a seam for that future work.
|
||||||
// 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;
|
double wetDry = 1.0;
|
||||||
|
|
||||||
// Track GUID(s) the capture came from, when the source mode is track-scoped
|
// Track GUID(s) when source mode is track-scoped (SelectedTracks); empty otherwise.
|
||||||
// (SelectedTracks). Empty for master/items/razor. The action layer (M7)
|
// The backend only copies these onto the Sample — it never reads selection itself.
|
||||||
// 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;
|
std::vector<std::string> trackGuids;
|
||||||
|
|
||||||
// Render tail (docs/product/capture-tail.md §The three tail states). Default
|
// Render tail (docs/product/capture-tail.md §The three tail states). None = exact
|
||||||
// None: exact bounds, no added silence — the precision invariant, and the only
|
// bounds, no added silence — the only mode valid for null-test/verify captures.
|
||||||
// mode valid for null-test / verify captures. `tailMs` is meaningful ONLY for
|
// tailMs applies only to Manual (clamped to 8s by the pure mapping); Auto uses
|
||||||
// TailMode::Manual (clamped to the 8 s cap by the pure mapping); Auto uses the
|
// the 8s cap + -72 dB trim internally, None ignores it.
|
||||||
// 8 s cap + -72 dB trim internally, None ignores it.
|
|
||||||
TailMode tailMode = TailMode::None;
|
TailMode tailMode = TailMode::None;
|
||||||
double tailMs = 0.0;
|
double tailMs = 0.0;
|
||||||
|
|
||||||
// Output format. 0 sampleRate => follow project rate (deterministic: the
|
// 0 sampleRate => follow project rate.
|
||||||
// project rate is fixed for a given project).
|
|
||||||
int sampleRate = 0;
|
int sampleRate = 0;
|
||||||
int channelCount = 2;
|
int channelCount = 2;
|
||||||
WavBitDepth bitDepth = WavBitDepth::Float32;
|
WavBitDepth bitDepth = WavBitDepth::Float32;
|
||||||
|
|
||||||
// Human base name for the file stem; sanitized by capture_paths. The unique
|
// Sanitized by capture_paths. uniqueTag (disambiguator) is supplied by the
|
||||||
// tag (disambiguator) is supplied separately by the backend caller so the
|
// backend caller so the pure naming logic stays testable.
|
||||||
// pure naming logic stays testable.
|
|
||||||
std::string baseName = "capture";
|
std::string baseName = "capture";
|
||||||
std::string uniqueTag; // e.g. a timestamp/counter; may be empty
|
std::string uniqueTag;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Outcome of a capture attempt. `Ok` carries the populated Sample; every failure
|
// Every failure is an explicit code, never a thrown exception across the REAPER boundary.
|
||||||
// is an explicit code (never a thrown exception across the REAPER boundary) so
|
|
||||||
// the action handler can log a precise reason.
|
|
||||||
enum class CaptureStatus {
|
enum class CaptureStatus {
|
||||||
Ok,
|
Ok,
|
||||||
NoProject, // no active project to render / resolve a bank folder
|
NoProject, // no active project to render / resolve a bank folder
|
||||||
EmptyRange, // start >= end: nothing to render
|
EmptyRange, // start >= end: nothing to render
|
||||||
UnsupportedMode, // backend does not implement this source mode (M3 scope)
|
UnsupportedMode, // backend does not implement this source mode
|
||||||
UnsupportedFormat, // requested bit depth has no known REAPER blob (M3: Float32 only)
|
UnsupportedFormat, // requested bit depth has no known REAPER blob (Float32 only)
|
||||||
RenderFailed, // the render action ran but produced no output file
|
RenderFailed, // the render action ran but produced no output file
|
||||||
TransportBusy, // realtime backend: transport already playing/recording — refused
|
TransportBusy, // realtime backend: transport already playing/recording — refused
|
||||||
};
|
};
|
||||||
@@ -113,44 +79,33 @@ struct CaptureResult {
|
|||||||
std::string message; // human-readable detail for the console log
|
std::string message; // human-readable detail for the console log
|
||||||
};
|
};
|
||||||
|
|
||||||
// Deterministic offline-render backend. Drives the full offline source family —
|
// Deterministic offline-render backend: master mix / time selection / selected
|
||||||
// master mix / time selection, selected tracks, selected items, razor area — all
|
// tracks / selected items / razor area, all wet-only, optional tail. Source
|
||||||
// wet-only (render_settings.h) with optional tail. The source selection + range
|
// selection + range are resolved by the caller and handed in via CaptureRequest —
|
||||||
// are resolved by the caller (the action layer) and handed in via the
|
// the backend drives RENDER_* and never reads the DAW selection itself.
|
||||||
// CaptureRequest; the backend drives RENDER_* and never reads the DAW selection
|
// SourceMode::Realtime returns UnsupportedMode. Non-destructive: restores every
|
||||||
// itself. SourceMode::Realtime returns UnsupportedMode (that is the M8 backend).
|
// RENDER_* setting it touches on every path. Plain concrete class — see the
|
||||||
// Non-destructive: restores every RENDER_* setting it touches on every path.
|
// no-shared-interface note in capture_realtime_shell.h before adding one back.
|
||||||
// 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 {
|
class OfflineRenderBackend {
|
||||||
public:
|
public:
|
||||||
CaptureResult capture(const CaptureRequest& request);
|
CaptureResult capture(const CaptureRequest& request);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Shared backend helpers (Q-W3 riders) ------------------------------------
|
|
||||||
|
|
||||||
// Mints the filesystem-safe disambiguating tag for one capture's file stem +
|
// 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
|
// Sample id: "<prefix><unix-epoch-seconds>-<n>", <n> a per-session monotonic
|
||||||
// MONOTONIC counter (T1-11 fix). The wall-clock second alone had a collision
|
// counter. Wall-clock seconds alone collide when batch capture drives short
|
||||||
// window: two captures of the same baseName within one second derived the same
|
// renders back-to-back, silently overwriting the first file. `prefix` is the
|
||||||
// stem, so the second render silently overwrote the first file (reachable via
|
// backend's family marker ("" offline, "rt-" realtime).
|
||||||
// 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);
|
std::string makeUniqueTag(const std::string& prefix);
|
||||||
|
|
||||||
// Stamps the SHARED finished-capture metadata onto `s` (T2-09 dedupe — this stamp
|
// Stamps the metadata shared by both backends onto `s`: trackGuids + channelCount
|
||||||
// was copy-pasted per backend and had silently diverged): trackGuids +
|
// (echoed from the request), resolved sampleRate (request rate, else PROJECT_SRATE
|
||||||
// channelCount (echoed from the request), the resolved sampleRate (request rate,
|
// from `rateProj`), captureTempo, the capture-start time signature
|
||||||
// else PROJECT_SRATE read from `rateProj`; 0 stays 0 when unknown), captureTempo
|
// (TimeMap_GetTimeSigAtTime against `timeSigProj` — offline passes nullptr for the
|
||||||
// (Master_GetTempo), the capture-start time signature (TimeMap_GetTimeSigAtTime
|
// active project, realtime pins the record's own project), the WAV-aware
|
||||||
// against `timeSigProj` — the offline path passes nullptr = active project, the
|
// contentHash of `absolutePath` (left empty when unreadable), and createdTimestamp.
|
||||||
// realtime path pins the record's own project; the divergence stays caller-visible
|
// Per-backend bits (id, paths, bounds, tier, realtime's length override) stay
|
||||||
// as this argument), the WAV-aware contentHash of the finished file at
|
// with each caller.
|
||||||
// `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,
|
void stampCaptureSample(Sample& s, const CaptureRequest& req,
|
||||||
ReaProject* rateProj, ReaProject* timeSigProj,
|
ReaProject* rateProj, ReaProject* timeSigProj,
|
||||||
const std::string& absolutePath);
|
const std::string& absolutePath);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
// capture_batch.cpp — the M11 batch-capture family + the M10 re-capture-from-source
|
// capture_batch.cpp — the batch-capture family + re-capture-from-source. See the
|
||||||
// action (Q-W3 hoist out of main.cpp; the code moved verbatim, the session threaded
|
// header.
|
||||||
// as a parameter). See the header.
|
|
||||||
//
|
//
|
||||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
|
||||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
||||||
// pointers; here they are extern (CLAUDE.md §contract).
|
// pointers; here they are extern.
|
||||||
|
|
||||||
#include "shell/capture/capture_batch.h"
|
#include "shell/capture/capture_batch.h"
|
||||||
|
|
||||||
@@ -50,28 +49,21 @@
|
|||||||
|
|
||||||
namespace reasampler::capture {
|
namespace reasampler::capture {
|
||||||
|
|
||||||
// --- M11: batch capture (per selected item / per razor area) ----------------
|
// One action fires N captures — one sample per selected item (item scope) or per
|
||||||
|
// razor area (track scope, each area's own range). Each unit routes through
|
||||||
|
// captureAndIndexOne so every precision invariant holds; nothing lands in the
|
||||||
|
// arrange (load-bearing principle).
|
||||||
//
|
//
|
||||||
// One action fires N captures — one bank sample per selected item (item scope) or per
|
// Each unit's baseName carries its ordinal ("item-1", "item-2", ...) so two units
|
||||||
// razor area (track scope, each area's own range). Each individual capture honors every
|
// in one batch never share a stem, and makeUniqueTag's per-session monotonic
|
||||||
// precision invariant via captureAndIndexOne (exact bounds, non-destructive FX/fader/pan
|
// counter keeps same-second units across batches from colliding too.
|
||||||
// neutralize, relative paths, channel preservation) and M10 provenance stamping applies
|
|
||||||
// per capture where its detection rule matches. The load-bearing principle holds: each
|
|
||||||
// unit writes a file + a bank index entry ONLY; nothing lands in the arrange.
|
|
||||||
//
|
|
||||||
// Per-unit FILE NAMING: each unit's baseName carries its ordinal ("item-1",
|
|
||||||
// "item-2", ...) so two units are never asked to write the same stem within one
|
|
||||||
// batch, and the shared makeUniqueTag now appends a per-session monotonic counter
|
|
||||||
// (T1-11 fix) so even same-second units across batches cannot collide.
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// RAII snapshot/restore of the project's media-item selection. Batch item capture must
|
// RAII snapshot/restore of the item selection. Batch item capture must transiently
|
||||||
// transiently select exactly one item per render (RENDER_SETTINGS &32 renders whatever is
|
// select exactly one item per render (RENDER_SETTINGS &32 renders whatever is
|
||||||
// selected); the user's ORIGINAL selection must be restored on EVERY exit path — including
|
// selected); the original selection is restored on every exit path — including a
|
||||||
// a mid-batch failure or early return — because selection restoration is part of the
|
// mid-batch failure — as part of the non-destructive invariant.
|
||||||
// non-destructive invariant. Snapshot on construct (the currently-selected item set),
|
|
||||||
// restore on destruct (deselect everything, then re-select exactly the snapshot).
|
|
||||||
class ItemSelectionGuard
|
class ItemSelectionGuard
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -85,9 +77,8 @@ public:
|
|||||||
|
|
||||||
~ItemSelectionGuard()
|
~ItemSelectionGuard()
|
||||||
{
|
{
|
||||||
// Deselect every item in the project, then re-select the snapshot — restoring the
|
// Deselect everything first (not just currently-selected) so any transient
|
||||||
// exact original set regardless of what the batch selected in between. Iterate ALL
|
// selection is cleared, then re-select exactly the snapshot.
|
||||||
// items (not just the currently-selected) so any transient selection is cleared.
|
|
||||||
const int total = CountMediaItems(nullptr);
|
const int total = CountMediaItems(nullptr);
|
||||||
for (int i = 0; i < total; ++i)
|
for (int i = 0; i < total; ++i)
|
||||||
if (MediaItem* it = GetMediaItem(nullptr, i))
|
if (MediaItem* it = GetMediaItem(nullptr, i))
|
||||||
@@ -104,9 +95,8 @@ private:
|
|||||||
std::vector<MediaItem*> selected_;
|
std::vector<MediaItem*> selected_;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Selects exactly `item` (deselect-all then select-one) so the offline render's
|
// Deselect-all then select-one so the offline render's &32 bit captures exactly
|
||||||
// selected-items bit (&32) captures a single item. Used inside the batch loop under the
|
// this item. Called inside ItemSelectionGuard, which restores the original selection.
|
||||||
// ItemSelectionGuard, which restores the user's original selection afterward.
|
|
||||||
void selectOnlyItem(MediaItem* item)
|
void selectOnlyItem(MediaItem* item)
|
||||||
{
|
{
|
||||||
const int total = CountMediaItems(nullptr);
|
const int total = CountMediaItems(nullptr);
|
||||||
@@ -115,9 +105,8 @@ void selectOnlyItem(MediaItem* item)
|
|||||||
SetMediaItemSelected(it, it == item);
|
SetMediaItemSelected(it, it == item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collects every track's razor AUDIO areas as (owning track, range) pairs, preserving
|
// Collects every track's razor areas as (owning track, range) pairs, track order
|
||||||
// track order then area order — the batch analog of resolveRazorRange, which unions them.
|
// then area order. Read-only — never clears the razor selection.
|
||||||
// Read-only (never clears the razor selection). Reuses the pure parseRazorEdits parser.
|
|
||||||
std::vector<std::pair<MediaTrack*, RazorRange>> collectRazorAreas()
|
std::vector<std::pair<MediaTrack*, RazorRange>> collectRazorAreas()
|
||||||
{
|
{
|
||||||
std::vector<std::pair<MediaTrack*, RazorRange>> areas;
|
std::vector<std::pair<MediaTrack*, RazorRange>> areas;
|
||||||
@@ -135,10 +124,9 @@ std::vector<std::pair<MediaTrack*, RazorRange>> collectRazorAreas()
|
|||||||
return areas;
|
return areas;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RAII snapshot/restore of the project's TRACK selection. Batch razor capture must
|
// Mirror of ItemSelectionGuard for track selection: batch razor capture transiently
|
||||||
// transiently select exactly the area's owning track per render (track scope's &128 bit
|
// selects the area's owning track per render (&128 renders selected tracks), restoring
|
||||||
// renders whatever TRACKS are selected); the user's original track selection is restored
|
// the original selection on every exit path (non-destructive invariant).
|
||||||
// on EVERY exit path (part of the non-destructive invariant). Mirror of ItemSelectionGuard.
|
|
||||||
class TrackSelectionGuard
|
class TrackSelectionGuard
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -170,16 +158,13 @@ private:
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// Batch item capture: one bank sample per SELECTED item, item scope. Snapshots the
|
// One sample per selected item. Snapshots the selection (RAII-restored), transiently
|
||||||
// selection (RAII restore on every path), then for each selected item transiently selects
|
// selects each item in turn, renders its exact range under item-scope FX neutralize,
|
||||||
// only it, renders its exact [pos, pos+len] range under item-scope FX neutralize, adds the
|
// and persists once at the end for the whole batch.
|
||||||
// Sample, and records a per-unit verdict. Persists ONCE at the end (one ext-state write for
|
|
||||||
// the whole batch). Reports a mixed-result summary (explicit-action response — allowed).
|
|
||||||
void RunBatchCaptureItems(ReaSamplerSession& session)
|
void RunBatchCaptureItems(ReaSamplerSession& session)
|
||||||
{
|
{
|
||||||
// Read the selected items up front (pointers stay valid — batch mutates only selection
|
// Read bounds + owning track now, while the full selection is live and before any
|
||||||
// flags, never adds/removes items). Also capture each item's exact bounds and owning
|
// transient re-selection (batch only mutates selection flags, never adds/removes items).
|
||||||
// track NOW, while the full selection is live, before any transient re-selection.
|
|
||||||
struct ItemUnit { MediaItem* item; MediaTrack* track; double start; double end; };
|
struct ItemUnit { MediaItem* item; MediaTrack* track; double start; double end; };
|
||||||
std::vector<ItemUnit> itemUnits;
|
std::vector<ItemUnit> itemUnits;
|
||||||
{
|
{
|
||||||
@@ -201,8 +186,8 @@ void RunBatchCaptureItems(ReaSamplerSession& session)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plan the exact source ranges -> validated, ordinal-assigned units (pure). Empty/
|
// Plan exact ranges -> validated, ordinal-assigned units; zero-length items are
|
||||||
// inverted item ranges (a zero-length item) are dropped here so no stray render runs.
|
// dropped here so no stray render runs.
|
||||||
std::vector<BatchRange> ranges;
|
std::vector<BatchRange> ranges;
|
||||||
ranges.reserve(itemUnits.size());
|
ranges.reserve(itemUnits.size());
|
||||||
for (const ItemUnit& u : itemUnits)
|
for (const ItemUnit& u : itemUnits)
|
||||||
@@ -212,19 +197,17 @@ void RunBatchCaptureItems(ReaSamplerSession& session)
|
|||||||
BatchOutcome outcome;
|
BatchOutcome outcome;
|
||||||
bool anyAdded = false;
|
bool anyAdded = false;
|
||||||
{
|
{
|
||||||
// Restore the user's ORIGINAL item selection on every exit path (incl. early
|
// selGuard restores the original item selection on every exit path.
|
||||||
// return / mid-batch failure) — non-destructive invariant.
|
|
||||||
ItemSelectionGuard selGuard;
|
ItemSelectionGuard selGuard;
|
||||||
|
|
||||||
// The plan and itemUnits are parallel over the KEPT units. Walk itemUnits, but only
|
// plan and itemUnits are parallel over kept units; skip dropped ranges in lockstep.
|
||||||
// for those whose range survived planning (same drop rule), matching by ordinal.
|
|
||||||
std::size_t planIdx = 0;
|
std::size_t planIdx = 0;
|
||||||
for (const ItemUnit& u : itemUnits)
|
for (const ItemUnit& u : itemUnits)
|
||||||
{
|
{
|
||||||
if (!(u.end > u.start)) continue; // dropped by planCaptureUnits — skip in lockstep
|
if (!(u.end > u.start)) continue; // dropped by planCaptureUnits — skip in lockstep
|
||||||
const CaptureUnit& unit = plan[planIdx++];
|
const CaptureUnit& unit = plan[planIdx++];
|
||||||
|
|
||||||
// Transiently select ONLY this item so the item-scope render captures exactly it.
|
// select only this item so the item-scope render captures exactly it.
|
||||||
selectOnlyItem(u.item);
|
selectOnlyItem(u.item);
|
||||||
|
|
||||||
ResolvedSource src;
|
ResolvedSource src;
|
||||||
@@ -245,9 +228,9 @@ void RunBatchCaptureItems(ReaSamplerSession& session)
|
|||||||
}
|
}
|
||||||
} // selGuard restores the original selection here, on every path
|
} // selGuard restores the original selection here, on every path
|
||||||
|
|
||||||
// Persist ONCE for the whole batch (one ext-state write) — only if something landed.
|
// One ext-state write for the whole batch, only if something landed. The generation
|
||||||
// S9: one bump for the whole batch (coalesced) — the counter is monotonic, not per-sample,
|
// bump is monotonic, so one increment past the last-seen value triggers reload in
|
||||||
// so a single increment past the last-seen value is enough to trigger one instance reload.
|
// any listening instance.
|
||||||
if (anyAdded) {
|
if (anyAdded) {
|
||||||
session.bumpBankGeneration();
|
session.bumpBankGeneration();
|
||||||
session.saveToActiveProject();
|
session.saveToActiveProject();
|
||||||
@@ -256,12 +239,10 @@ void RunBatchCaptureItems(ReaSamplerSession& session)
|
|||||||
ShowConsoleMsg((outcome.summaryLine("item") + "\n").c_str());
|
ShowConsoleMsg((outcome.summaryLine("item") + "\n").c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch razor capture: one bank sample per razor AREA, track scope over that area's own
|
// One sample per razor area, track scope over that area's own range. Track scope
|
||||||
// range (the area's owning track is the source track). Track scope renders the selected
|
// renders selected tracks via master (&128), so each unit selects only its owning
|
||||||
// TRACKS via master (&128), so each unit transiently selects ONLY its owning track
|
// track under TrackSelectionGuard; the razor selection itself is read-only. Persists
|
||||||
// (SetOnlyTrackSelected) under the TrackSelectionGuard, which restores the user's original
|
// once at the end.
|
||||||
// track selection on every path. The razor selection itself is read-only and left intact.
|
|
||||||
// Persists ONCE at the end. Reports a mixed-result summary.
|
|
||||||
void RunBatchCaptureRazor(ReaSamplerSession& session)
|
void RunBatchCaptureRazor(ReaSamplerSession& session)
|
||||||
{
|
{
|
||||||
const std::vector<std::pair<MediaTrack*, RazorRange>> areas = collectRazorAreas();
|
const std::vector<std::pair<MediaTrack*, RazorRange>> areas = collectRazorAreas();
|
||||||
@@ -280,7 +261,7 @@ void RunBatchCaptureRazor(ReaSamplerSession& session)
|
|||||||
BatchOutcome outcome;
|
BatchOutcome outcome;
|
||||||
bool anyAdded = false;
|
bool anyAdded = false;
|
||||||
{
|
{
|
||||||
// Restore the user's ORIGINAL track selection on every exit path.
|
// selGuard restores the original track selection on every exit path.
|
||||||
TrackSelectionGuard selGuard;
|
TrackSelectionGuard selGuard;
|
||||||
|
|
||||||
std::size_t planIdx = 0;
|
std::size_t planIdx = 0;
|
||||||
@@ -290,8 +271,7 @@ void RunBatchCaptureRazor(ReaSamplerSession& session)
|
|||||||
const CaptureUnit& unit = plan[planIdx++];
|
const CaptureUnit& unit = plan[planIdx++];
|
||||||
MediaTrack* tr = a.first;
|
MediaTrack* tr = a.first;
|
||||||
|
|
||||||
// Transiently select ONLY this track so the track-scope render (&128) captures
|
// select only this track so track-scope render (&128) captures it via master.
|
||||||
// exactly it via master (over the custom time bounds we set per unit).
|
|
||||||
SetOnlyTrackSelected(tr);
|
SetOnlyTrackSelected(tr);
|
||||||
|
|
||||||
ResolvedSource src;
|
ResolvedSource src;
|
||||||
@@ -312,7 +292,7 @@ void RunBatchCaptureRazor(ReaSamplerSession& session)
|
|||||||
}
|
}
|
||||||
} // selGuard restores the original track selection here, on every path
|
} // selGuard restores the original track selection here, on every path
|
||||||
|
|
||||||
// S9: one coalesced bump for the whole razor batch (see the item-batch note above).
|
// One coalesced generation bump for the whole batch (see the item-batch note above).
|
||||||
if (anyAdded) {
|
if (anyAdded) {
|
||||||
session.bumpBankGeneration();
|
session.bumpBankGeneration();
|
||||||
session.saveToActiveProject();
|
session.saveToActiveProject();
|
||||||
@@ -321,25 +301,15 @@ void RunBatchCaptureRazor(ReaSamplerSession& session)
|
|||||||
ShowConsoleMsg((outcome.summaryLine("razor area") + "\n").c_str());
|
ShowConsoleMsg((outcome.summaryLine("razor area") + "\n").c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- M10: re-capture from source --------------------------------------------
|
// Regenerates a provenanced sample's file from its recorded source's current state
|
||||||
|
// and updates the bank Sample in place. Bank-only — never calls InsertMedia; the
|
||||||
|
// user re-places manually if they want the new version on the timeline.
|
||||||
|
// Non-destructive to the source (FxBypassGuard snapshot/restore via renderOffline).
|
||||||
//
|
//
|
||||||
// Regenerates a PROVENANCED bank sample's file from its recorded source's CURRENT
|
// Failure modes are explicit and reported, each a no-op: no provenance, unparseable
|
||||||
// state, then updates the bank Sample IN PLACE. BANK-ONLY — it renders a file and
|
// fingerprint, a missing recorded source track, or a failed render. On success, if
|
||||||
// refreshes the index entry; it NEVER calls InsertMedia / touches the timeline (the
|
// the source FX chain drifted since capture, the user is told — the re-capture still
|
||||||
// load-bearing capture-never-places line, structurally visible: this function has no
|
// reflects the source as it is now (drift is detected, not frozen against).
|
||||||
// insert path at all). Non-destructive to the source (FxBypassGuard snapshot/restore
|
|
||||||
// via renderOffline). Fork P2=a: refresh the bank entry only; the user re-places
|
|
||||||
// manually if they want the new version on the timeline.
|
|
||||||
//
|
|
||||||
// Failure modes are handled explicitly and reported to the user (a direct response
|
|
||||||
// to an explicit action is allowed by the console policy):
|
|
||||||
// * the selected sample has no provenance (not a resample) -> reported, no-op.
|
|
||||||
// * the recorded fingerprint is unparseable (legacy/corrupt) -> reported, no-op.
|
|
||||||
// * the recorded source track(s) no longer exist -> reported, no-op.
|
|
||||||
// * the render itself fails to satisfy the recorded request -> reported, no-op.
|
|
||||||
// On success, if the source FX chain drifted since capture (recorded vs current
|
|
||||||
// identity differ) the user is told — the re-capture still reflects the source AS IT
|
|
||||||
// IS NOW (P1=a: the fingerprint detects drift, it does not freeze the source).
|
|
||||||
void RunRecaptureFromSource(ReaSamplerSession& session)
|
void RunRecaptureFromSource(ReaSamplerSession& session)
|
||||||
{
|
{
|
||||||
const std::vector<std::string> selected = bankPanelSelectedSampleIds();
|
const std::vector<std::string> selected = bankPanelSelectedSampleIds();
|
||||||
@@ -371,8 +341,8 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the recorded capture recipe from the fingerprint. A legacy / corrupt
|
// Parse the recorded recipe; legacy/corrupt fingerprints fail gracefully, never
|
||||||
// string fails gracefully — never a partial re-capture.
|
// a partial re-capture.
|
||||||
const std::string recordedParentId = orig->provenance->parentSampleId;
|
const std::string recordedParentId = orig->provenance->parentSampleId;
|
||||||
const std::string recordedFingerprint = orig->provenance->fxChainSnapshot;
|
const std::string recordedFingerprint = orig->provenance->fxChainSnapshot;
|
||||||
const std::optional<model::CaptureRecipe> recipe =
|
const std::optional<model::CaptureRecipe> recipe =
|
||||||
@@ -384,8 +354,7 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the recorded source track GUID(s) to live tracks. Any missing track is a
|
// Missing recorded track = hard failure; never silently re-capture a different source.
|
||||||
// hard failure — we will not silently re-capture a different source.
|
|
||||||
std::vector<MediaTrack*> sourceTracks;
|
std::vector<MediaTrack*> sourceTracks;
|
||||||
for (const std::string& g : recipe->trackGuids)
|
for (const std::string& g : recipe->trackGuids)
|
||||||
{
|
{
|
||||||
@@ -400,8 +369,7 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
|
|||||||
}
|
}
|
||||||
if (sourceTracks.empty())
|
if (sourceTracks.empty())
|
||||||
{
|
{
|
||||||
// The recipe recorded no source tracks (e.g. an item-scope capture whose source
|
// e.g. an item-scope capture with no track-scoped source — nothing to resolve.
|
||||||
// tracks were not track-scoped). Without a resolvable source we cannot re-run.
|
|
||||||
ShowConsoleMsg("ReaSampler re-capture: no resolvable recorded source for this "
|
ShowConsoleMsg("ReaSampler re-capture: no resolvable recorded source for this "
|
||||||
"sample; cannot re-capture from source.\n");
|
"sample; cannot re-capture from source.\n");
|
||||||
return;
|
return;
|
||||||
@@ -411,10 +379,9 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
|
|||||||
recipe->scope == model::ProvenanceScope::Item ? CaptureScope::Item
|
recipe->scope == model::ProvenanceScope::Item ? CaptureScope::Item
|
||||||
: CaptureScope::Track;
|
: CaptureScope::Track;
|
||||||
|
|
||||||
// Rebuild the capture request verbatim from the recorded recipe — the SAME request,
|
// Rebuild the request verbatim from the recorded recipe, re-run against the
|
||||||
// re-run against the source's CURRENT state (P1=a). Exact bounds, tail, rate,
|
// source's current state: an unchanged source reproduces a byte-identical file
|
||||||
// channels, bit depth all match the original so an unchanged source produces a
|
// (bit-identical-repeats invariant).
|
||||||
// byte-identical file (bit-identical-repeats invariant, consumed as a feature).
|
|
||||||
CaptureRequest req;
|
CaptureRequest req;
|
||||||
req.sourceMode = static_cast<SourceMode>(recipe->sourceMode);
|
req.sourceMode = static_cast<SourceMode>(recipe->sourceMode);
|
||||||
req.startSeconds = recipe->startSeconds;
|
req.startSeconds = recipe->startSeconds;
|
||||||
@@ -428,10 +395,9 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
|
|||||||
req.baseName = orig->displayName.empty() ? "recapture" : orig->displayName;
|
req.baseName = orig->displayName.empty() ? "recapture" : orig->displayName;
|
||||||
req.trackGuids = recipe->trackGuids;
|
req.trackGuids = recipe->trackGuids;
|
||||||
|
|
||||||
// Read the CURRENT source FX-chain identity BEFORE the render bypasses it, to
|
// Read the current FX-chain identity BEFORE the render bypasses it, for drift
|
||||||
// compare against the recorded identity for drift reporting. Mirror the same
|
// comparison against the recorded identity (item scope via TakeFX_*, track scope
|
||||||
// scope split as buildCaptureProvenance: item scope reads take FX via TakeFX_*;
|
// via TrackFX_*).
|
||||||
// track scope reads the track FX chain via TrackFX_*.
|
|
||||||
std::string currentIdentity;
|
std::string currentIdentity;
|
||||||
if (scope == CaptureScope::Item) {
|
if (scope == CaptureScope::Item) {
|
||||||
const int n = CountSelectedMediaItems(nullptr);
|
const int n = CountSelectedMediaItems(nullptr);
|
||||||
@@ -459,11 +425,10 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the Sample IN PLACE: keep its identity (id) and its provenance thread
|
// Update in place: keep identity (id) + provenance parent, adopt the regenerated
|
||||||
// (same parent + a REFRESHED fingerprint reflecting the source as re-captured), but
|
// file's path/hash/length/rate/timestamp, and rebuild the fingerprint with the
|
||||||
// adopt the regenerated file's path / hash / length / rate / timestamp. The
|
// current FX identity so the next re-capture measures drift from here, not the
|
||||||
// fingerprint is rebuilt from the recipe with the CURRENT FX identity so a
|
// original.
|
||||||
// subsequent re-capture measures drift from this point, not the original.
|
|
||||||
model::CaptureRecipe refreshed = *recipe;
|
model::CaptureRecipe refreshed = *recipe;
|
||||||
refreshed.fxChainIdentity = currentIdentity;
|
refreshed.fxChainIdentity = currentIdentity;
|
||||||
|
|
||||||
@@ -476,33 +441,29 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
|
|||||||
updated.sampleRate = res.sample.sampleRate;
|
updated.sampleRate = res.sample.sampleRate;
|
||||||
updated.lengthSeconds = res.sample.lengthSeconds;
|
updated.lengthSeconds = res.sample.lengthSeconds;
|
||||||
updated.captureTempo = res.sample.captureTempo;
|
updated.captureTempo = res.sample.captureTempo;
|
||||||
updated.captureTimeSigNum = res.sample.captureTimeSigNum; // L7 F1: refresh meter stamp
|
updated.captureTimeSigNum = res.sample.captureTimeSigNum; // refresh meter stamp
|
||||||
updated.captureTimeSigDenom = res.sample.captureTimeSigDenom; // to the re-capture's meter
|
updated.captureTimeSigDenom = res.sample.captureTimeSigDenom; // to the re-capture's meter
|
||||||
updated.trackGuids = res.sample.trackGuids;
|
updated.trackGuids = res.sample.trackGuids;
|
||||||
updated.createdTimestamp = res.sample.createdTimestamp;
|
updated.createdTimestamp = res.sample.createdTimestamp;
|
||||||
// NOTE: levels, clipped, and lengthBeats are carried from the original (via the
|
// levels/clipped/lengthBeats carry from *orig — the offline backend doesn't
|
||||||
// *orig copy above) because the offline backend does not populate them today
|
// populate them; refresh from res.sample here if that ever changes.
|
||||||
// (res.sample leaves them at defaults). If a later milestone populates these
|
|
||||||
// fields at capture time, refresh them here from res.sample instead.
|
|
||||||
model::Provenance prov;
|
model::Provenance prov;
|
||||||
prov.parentSampleId = recordedParentId;
|
prov.parentSampleId = recordedParentId;
|
||||||
prov.fxChainSnapshot = model::buildFingerprint(refreshed);
|
prov.fxChainSnapshot = model::buildFingerprint(refreshed);
|
||||||
updated.provenance = prov;
|
updated.provenance = prov;
|
||||||
|
|
||||||
// Single batched undo point around the in-place bank mutation (mirrors the bank
|
// One batched undo point around the in-place mutation; index-only ext-state,
|
||||||
// action family's R-B pattern). The mutation is index-only ext-state; the render
|
// nothing placed on the timeline.
|
||||||
// wrote a new file but placed nothing on the timeline.
|
|
||||||
Undo_BeginBlock2(nullptr);
|
Undo_BeginBlock2(nullptr);
|
||||||
const bool changed = session.book().updateSampleInPlace(sampleId, updated);
|
const bool changed = session.book().updateSampleInPlace(sampleId, updated);
|
||||||
if (changed)
|
if (changed)
|
||||||
{
|
{
|
||||||
// Record the regenerated file in the owned manifest (a new file the tool wrote);
|
// Record the regenerated file in the owned manifest; the superseded file
|
||||||
// the superseded old file becomes an orphan reclaimed by Phase R prune.
|
// becomes an orphan for prune to reclaim.
|
||||||
session.owned().add(updated.relativePath);
|
session.owned().add(updated.relativePath);
|
||||||
// S9: re-capture-in-place regenerates the SAME id's audio — the exact case the
|
// Regenerating the same id's audio is exactly why instances need the generation
|
||||||
// hands-free refresh exists for (an instance referencing this id keeps playing the
|
// bump — they'd otherwise keep playing stale audio until reload. Bumped inside
|
||||||
// OLD audio until it reloads). Bump inside the undo block so undo rolls back the
|
// the undo block so undo rolls back the generation with the rest of the blob.
|
||||||
// generation with the rest of the blob.
|
|
||||||
session.bumpBankGeneration();
|
session.bumpBankGeneration();
|
||||||
const bool persisted = session.saveToActiveProject(); // book + manifest + generation + MarkProjectDirty
|
const bool persisted = session.saveToActiveProject(); // book + manifest + generation + MarkProjectDirty
|
||||||
Undo_EndBlock2(nullptr, persisted ? "ReaSampler: re-capture from source" : "",
|
Undo_EndBlock2(nullptr, persisted ? "ReaSampler: re-capture from source" : "",
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// capture_batch — the batch-capture family + re-capture-from-source (Q-W3 hoist
|
// The batch-capture family + re-capture-from-source: RunBatchCaptureItems (one
|
||||||
// out of main.cpp; the fourth hoist, T4-02 — recapture is planner-driven like
|
// sample per selected item), RunBatchCaptureRazor (one sample per razor area),
|
||||||
// batch and shares the RAII selection-guard machinery, so it belongs here, not
|
// RunRecaptureFromSource (regenerate a provenanced sample from its recorded
|
||||||
// with the single-shot path). Owns:
|
// source's current state, bank-only, in place). Every unit routes through
|
||||||
// * RunBatchCaptureItems — one bank sample per SELECTED item (item scope), the
|
// capture_orchestrator so every precision invariant holds; persist is batched to
|
||||||
// user's item selection snapshot/restored on every path (ItemSelectionGuard);
|
// one ext-state write per action.
|
||||||
// * RunBatchCaptureRazor — one bank sample per razor AREA (track scope over the
|
|
||||||
// area's own range), the user's track selection snapshot/restored on every
|
|
||||||
// path (TrackSelectionGuard);
|
|
||||||
// * RunRecaptureFromSource — regenerate a PROVENANCED bank sample from its
|
|
||||||
// recorded source's CURRENT state, updating the Sample in place. BANK-ONLY.
|
|
||||||
//
|
//
|
||||||
// Every unit honors every precision invariant via capture_orchestrator's
|
// The .cpp includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT
|
||||||
// captureAndIndexOne / renderOffline (exact bounds, non-destructive neutralize,
|
// (main.cpp owns the API pointers).
|
||||||
// relative paths); nothing here ever touches the arrange/timeline (load-bearing
|
|
||||||
// principle). Persist is batched: ONE ext-state write per action.
|
|
||||||
//
|
|
||||||
// REAPER-facing: the .cpp includes reaper_plugin_functions.h WITHOUT
|
|
||||||
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers — CLAUDE.md §contract).
|
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
class ReaSamplerSession;
|
class ReaSamplerSession;
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
// capture_orchestrator.cpp — the single-capture orchestration + realtime/insert
|
// See capture_orchestrator.h. FxBypassGuard lives here as a stack RAII object —
|
||||||
// action bodies (Q-W3 hoist out of main.cpp; the code moved verbatim, the session
|
// it must restore on every exit path of exactly one render call.
|
||||||
// threaded as a parameter). See the header. FxBypassGuard lives here as a STACK
|
|
||||||
// RAII object (precision-invariant-critical — it must restore on every exit path
|
|
||||||
// of exactly one render call).
|
|
||||||
//
|
//
|
||||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
// Includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is
|
||||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
// the one TU that defines the API pointers; here they are extern.
|
||||||
// pointers; here they are extern (CLAUDE.md §contract).
|
|
||||||
|
|
||||||
#include "shell/capture/capture_orchestrator.h"
|
#include "shell/capture/capture_orchestrator.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// capture_orchestrator — the single-capture orchestration + the realtime/insert
|
// Single-capture orchestration + the realtime/insert action bodies: renderOffline
|
||||||
// action bodies (Q-W3 hoist out of main.cpp, T4-02). Owns:
|
// (one offline render under the scope's FxBypassGuard, shared by single-shot/
|
||||||
// * renderOffline — ONE offline render under the scope's FxBypassGuard (the
|
// batch/recapture), captureAndIndexOne (render + provenance + bank add +
|
||||||
// stack-RAII out-of-scope FX/fader/pan neutralize, defined in the .cpp —
|
// owned-manifest record, unpersisted), RunCapture/RunCaptureItemAssign,
|
||||||
// precision-invariant-critical, shared by single-shot / batch / recapture);
|
// RunCaptureRealtimeTrack/RunCancelRealtime (in-flight state lives in
|
||||||
// * captureAndIndexOne — render + provenance stamp + bank add + owned-manifest
|
// realtime_lifecycle), and RunInsertSelected — the one deliberate exception to
|
||||||
// record, WITHOUT persisting (single-shot persists right after; batch persists
|
// capture-never-places.
|
||||||
// once at the end);
|
|
||||||
// * RunCapture / RunCaptureItemAssign — the bindable single-capture actions;
|
|
||||||
// * RunCaptureRealtimeTrack / RunCancelRealtime — the realtime action bodies
|
|
||||||
// (the in-flight state itself lives in realtime_lifecycle);
|
|
||||||
// * RunInsertSelected — the M6 placement action body (the INTENDED, explicit
|
|
||||||
// placement path — the one deliberate exception to capture-never-places).
|
|
||||||
//
|
//
|
||||||
// The session is threaded explicitly (no hidden module state): main.cpp's dispatch
|
// The session is threaded explicitly; no capture path here calls InsertMedia
|
||||||
// passes its ReaSamplerSession. The load-bearing principle holds structurally —
|
// or touches the timeline except RunInsertSelected, on purpose, via the insert shell.
|
||||||
// no capture path here calls InsertMedia or touches the arrange/timeline; only
|
|
||||||
// RunInsertSelected places, on purpose, via the insert shell.
|
|
||||||
//
|
//
|
||||||
// REAPER-facing: the .cpp includes reaper_plugin_functions.h WITHOUT
|
// The .cpp includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT
|
||||||
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers — CLAUDE.md §contract).
|
// (main.cpp owns the API pointers).
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -34,17 +26,17 @@ class ReaSamplerSession;
|
|||||||
namespace reasampler::capture {
|
namespace reasampler::capture {
|
||||||
|
|
||||||
// Renders one CaptureRequest through the offline backend under the scope's
|
// Renders one CaptureRequest through the offline backend under the scope's
|
||||||
// FX-bypass guard, returning the backend's CaptureResult. Shared by RunCapture and
|
// FX-bypass guard. Shared by RunCapture and RunRecaptureFromSource so the
|
||||||
// RunRecaptureFromSource so the FX-scope neutralize + render recipe lives in ONE
|
// FX-scope neutralize + render recipe lives in one place. Non-destructive;
|
||||||
// place. Non-destructive; touches no timeline item — it writes a file only.
|
// writes a file only.
|
||||||
CaptureResult renderOffline(CaptureScope scope,
|
CaptureResult renderOffline(CaptureScope scope,
|
||||||
const std::vector<MediaTrack*>& sourceTracks,
|
const std::vector<MediaTrack*>& sourceTracks,
|
||||||
const CaptureRequest& req);
|
const CaptureRequest& req);
|
||||||
|
|
||||||
// Renders ONE capture request under the scope's FX-bypass guard, stamps provenance,
|
// Renders one capture request, stamps provenance, adds the Sample to the
|
||||||
// and adds the resulting Sample to the ACTIVE bank + records the created file in
|
// active bank + owned-file manifest — without persisting (batch persists once
|
||||||
// the owned-file manifest — WITHOUT persisting. On success, res.sample.id carries
|
// at the end). res.sample.id carries the landed bank-index id (fresh add or
|
||||||
// the LANDED bank-index id (fresh add or hash-dedup collapse target — S8).
|
// hash-dedup collapse target).
|
||||||
CaptureResult captureAndIndexOne(ReaSamplerSession& session,
|
CaptureResult captureAndIndexOne(ReaSamplerSession& session,
|
||||||
CaptureScope scope,
|
CaptureScope scope,
|
||||||
const ResolvedSource& src,
|
const ResolvedSource& src,
|
||||||
@@ -53,21 +45,21 @@ CaptureResult captureAndIndexOne(ReaSamplerSession& session,
|
|||||||
double endSeconds);
|
double endSeconds);
|
||||||
|
|
||||||
// Runs one capture-action-table row: resolve, render + add + record, persist +
|
// Runs one capture-action-table row: resolve, render + add + record, persist +
|
||||||
// mark dirty. Returns the landed bank-index id ("" on failure/no-op) — the S8
|
// mark dirty. Returns the landed bank-index id ("" on failure/no-op) — the
|
||||||
// capture+assign path consumes it; the plain capture actions ignore it.
|
// arrange-ingest capture+assign path consumes it; plain capture actions ignore it.
|
||||||
std::string RunCapture(ReaSamplerSession& session, const CaptureActionDef& def);
|
std::string RunCapture(ReaSamplerSession& session, const CaptureActionDef& def);
|
||||||
|
|
||||||
// S8 arrange ingest: Item-scope capture into the active bank + assignment-request
|
// Item-scope capture into the active bank + assignment-request write, in one
|
||||||
// write, in one undo block.
|
// undo block.
|
||||||
void RunCaptureItemAssign(ReaSamplerSession& session);
|
void RunCaptureItemAssign(ReaSamplerSession& session);
|
||||||
|
|
||||||
// STARTS the realtime track capture (async, timer-driven — the in-flight state is
|
// Starts the realtime track capture (async, timer-driven — in-flight state is
|
||||||
// realtime_lifecycle's; OnTimer drives it) / cancels the in-flight one.
|
// realtime_lifecycle's) / cancels the in-flight one.
|
||||||
void RunCaptureRealtimeTrack(ReaSamplerSession& session);
|
void RunCaptureRealtimeTrack(ReaSamplerSession& session);
|
||||||
void RunCancelRealtime(ReaSamplerSession& session);
|
void RunCancelRealtime(ReaSamplerSession& session);
|
||||||
|
|
||||||
// Runs the M6 insert: place the bank panel's selected sample(s) at the edit cursor
|
// Places the bank panel's selected sample(s) at the edit cursor via the
|
||||||
// via the insert shell. `conform` selects the explicit opt-in tempo-match variant.
|
// insert shell. `conform` selects the explicit opt-in tempo-match variant.
|
||||||
void RunInsertSelected(ReaSamplerSession& session, bool conform);
|
void RunInsertSelected(ReaSamplerSession& session, bool conform);
|
||||||
|
|
||||||
} // namespace reasampler::capture
|
} // namespace reasampler::capture
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
// capture_realtime_finalize.cpp — the FILE-SIDE half of the realtime-record shell
|
// capture_realtime_finalize.cpp — recorded-file discovery, move-into-bank, the
|
||||||
// (Q-W3, T4-08 split): recorded-file discovery, move-into-bank, the Auto-tail PCM
|
// Auto-tail decay-scan trim, and finished-Sample population. See the header.
|
||||||
// decay-scan trim, and the finished-Sample population. See the header. The async
|
|
||||||
// record lifecycle lives in capture_realtime_shell.cpp.
|
|
||||||
//
|
//
|
||||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
|
||||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
||||||
// pointers; here they are extern (CLAUDE.md §contract).
|
// pointers; here they are extern.
|
||||||
|
|
||||||
#include "shell/capture/capture_realtime_finalize.h"
|
#include "shell/capture/capture_realtime_finalize.h"
|
||||||
|
|
||||||
@@ -19,7 +17,7 @@
|
|||||||
#include "core/capture/capture_realtime.h" // RecordedCapture, sampleFromRecordedCapture
|
#include "core/capture/capture_realtime.h" // RecordedCapture, sampleFromRecordedCapture
|
||||||
#include "core/capture/render_settings.h" // autoTrimEndRatio
|
#include "core/capture/render_settings.h" // autoTrimEndRatio
|
||||||
#include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames, planWavTruncate, patchU32LE
|
#include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames, planWavTruncate, patchU32LE
|
||||||
#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03)
|
#include "core/util/file_bytes.h" // shared whole-file loader
|
||||||
|
|
||||||
#define REAPERAPI_MINIMAL
|
#define REAPERAPI_MINIMAL
|
||||||
#define REAPERAPI_WANT_GetTrackNumMediaItems
|
#define REAPERAPI_WANT_GetTrackNumMediaItems
|
||||||
@@ -39,26 +37,21 @@ std::string normSlashes(std::string s) {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// Auto-mode PCM decay-scan trim (docs/product/capture-tail.md §realtime): after the
|
||||||
// §TAIL — Auto-mode PCM decay-scan trim (docs/product/capture-tail.md §realtime)
|
// recorded file is moved into the bank (the file we OWN, never the project), scan
|
||||||
// ============================================================================
|
// the tail (frames after the original range end) backward for the last frame above
|
||||||
// After the recorded file is stable and moved into the bank (the file we OWN — never
|
// -72 dB and truncate there. Rules:
|
||||||
// the project), Auto mode trims the trailing decay: read the WAV, scan the tail
|
// * no tail frame above -72 dB -> trim back to the range end
|
||||||
// region (frames AFTER the original range end) backward for the last frame above
|
// * signal never drops below -72 dB -> keep the full window (cap did its job)
|
||||||
// -72 dB, and truncate the file there. Rules (spec):
|
// * otherwise -> trim one frame past the last audible
|
||||||
// * no frame in the tail window above -72 dB -> trim back to the original range end
|
|
||||||
// * signal never falls below -72 dB in window -> keep the full window (cap did its job)
|
|
||||||
// * otherwise -> trim one frame past the last audible
|
|
||||||
//
|
//
|
||||||
// Returns the trimmed length in SECONDS (for the Sample), or a negative value to
|
// Returns the trimmed length in seconds, or negative for "no trim applied". Any
|
||||||
// signal "no trim applied" (caller keeps the pre-trim length). Best-effort and
|
// unreadable/unknown/short file skips the trim rather than risk corrupting the
|
||||||
// non-fatal: any unreadable/unknown/short file skips the trim (keeps the full window)
|
// capture — this is a convenience path, not a correctness one.
|
||||||
// rather than risk corrupting the capture — realtime tail is a convenience path.
|
|
||||||
//
|
//
|
||||||
// FORMAT / FLUSH ASSUMPTIONS (DAW-verify): the recorded file is a canonical 32-bit
|
// Assumes the recorded file is a canonical 32-bit float WAV, fully flushed/closed
|
||||||
// float WAV (REAPER project record format — the manual procedure sets it) and is fully
|
// before this runs (tick()'s Finalizing size-stable wait guarantees that on the
|
||||||
// flushed/closed before this runs (the tick() Finalizing size-stable wait guarantees
|
// normal path; abort()'s best-effort finalize can race it).
|
||||||
// that for the normal path; abort()'s best-effort finalize races it, documented).
|
|
||||||
double trimAutoTailInPlace(const std::string& path,
|
double trimAutoTailInPlace(const std::string& path,
|
||||||
double rangeStartSeconds,
|
double rangeStartSeconds,
|
||||||
double rangeEndSeconds) {
|
double rangeEndSeconds) {
|
||||||
@@ -73,21 +66,18 @@ double trimAutoTailInPlace(const std::string& path,
|
|||||||
const std::size_t totalFrames = layout.frameCount();
|
const std::size_t totalFrames = layout.frameCount();
|
||||||
if (totalFrames == 0) return kNoTrim;
|
if (totalFrames == 0) return kNoTrim;
|
||||||
|
|
||||||
// The original range end as a frame index within the file (frame 0 == start). Use
|
// Range end as a frame index (frame 0 == start), using the file's own sample
|
||||||
// the FILE's own sample rate (authoritative) — the request rate may be 0 (=follow
|
// rate (authoritative — the request rate may be 0 = follow project). Clamped to
|
||||||
// project). Clamp to the file so a rounding overshoot cannot exceed it.
|
// the file so a rounding overshoot cannot exceed it.
|
||||||
const double rangeSeconds = rangeEndSeconds - rangeStartSeconds;
|
const double rangeSeconds = rangeEndSeconds - rangeStartSeconds;
|
||||||
if (rangeSeconds <= 0.0) return kNoTrim;
|
if (rangeSeconds <= 0.0) return kNoTrim;
|
||||||
std::size_t rangeEndFrame = static_cast<std::size_t>(
|
std::size_t rangeEndFrame = static_cast<std::size_t>(
|
||||||
rangeSeconds * static_cast<double>(layout.sampleRate) + 0.5);
|
rangeSeconds * static_cast<double>(layout.sampleRate) + 0.5);
|
||||||
if (rangeEndFrame > totalFrames) rangeEndFrame = totalFrames;
|
if (rangeEndFrame > totalFrames) rangeEndFrame = totalFrames;
|
||||||
|
|
||||||
// Nothing recorded past the range end (the tail window was empty) -> nothing to
|
if (rangeEndFrame >= totalFrames) return kNoTrim; // tail window was empty
|
||||||
// trim; keep as-is. (Shouldn't happen for Auto, but total by construction.)
|
|
||||||
if (rangeEndFrame >= totalFrames) return kNoTrim;
|
|
||||||
|
|
||||||
// Scan ONLY the tail region (frames after the original range end). The trim never
|
// Scan only the tail region — the trim never eats into the range body.
|
||||||
// eats into the range body — the scan starts at rangeEndFrame.
|
|
||||||
const std::size_t tailFrames = totalFrames - rangeEndFrame;
|
const std::size_t tailFrames = totalFrames - rangeEndFrame;
|
||||||
const std::vector<AudioSample> tailPcm =
|
const std::vector<AudioSample> tailPcm =
|
||||||
extractFloatFrames(bytes, layout, rangeEndFrame, tailFrames);
|
extractFloatFrames(bytes, layout, rangeEndFrame, tailFrames);
|
||||||
@@ -97,11 +87,10 @@ double trimAutoTailInPlace(const std::string& path,
|
|||||||
const std::size_t lastAbove = audio::lastFrameAboveThreshold(
|
const std::size_t lastAbove = audio::lastFrameAboveThreshold(
|
||||||
tailPcm, layout.channelCount, tailFrames, threshold);
|
tailPcm, layout.channelCount, tailFrames, threshold);
|
||||||
|
|
||||||
// keptFrames: the total frame count the trimmed file retains.
|
// keptFrames: the trimmed file's total frame count. No audible tail frame -> trim
|
||||||
// no audible tail frame -> trim back to the range end (rangeEndFrame frames)
|
// back to rangeEndFrame; an audible frame at idx -> keep through that frame. The
|
||||||
// an audible frame at idx -> keep range body + up to and including that frame
|
// "never drops below threshold" case falls out naturally: lastAbove is the final
|
||||||
// The "signal never falls below threshold" case falls out naturally: lastAbove is
|
// tail frame, so keptFrames == totalFrames.
|
||||||
// the final tail frame, so keptFrames == totalFrames (the full window is kept).
|
|
||||||
std::size_t keptFrames;
|
std::size_t keptFrames;
|
||||||
if (lastAbove == audio::kNoFrameAboveThreshold) {
|
if (lastAbove == audio::kNoFrameAboveThreshold) {
|
||||||
keptFrames = rangeEndFrame;
|
keptFrames = rangeEndFrame;
|
||||||
@@ -113,21 +102,17 @@ double trimAutoTailInPlace(const std::string& path,
|
|||||||
const WavTruncatePlan plan = planWavTruncate(layout, keptFrames);
|
const WavTruncatePlan plan = planWavTruncate(layout, keptFrames);
|
||||||
if (!plan.valid) return kNoTrim;
|
if (!plan.valid) return kNoTrim;
|
||||||
|
|
||||||
// Patch the RIFF + data size fields in the in-memory buffer so they describe the
|
// Patch RIFF + data size fields to the kept frame count (wav_codec's patch
|
||||||
// kept frame count (wav_codec's patch primitive — the one RIFF owner), then
|
// primitive — the one RIFF owner), then rewrite the file as exactly the first
|
||||||
// rewrite the file as exactly the first newFileByteLength bytes (header +
|
// newFileByteLength bytes. A single truncating write avoids a separate resize
|
||||||
// patched sizes + retained PCM). A single truncating write is the simplest
|
// step and any partial-write window where on-disk sizes and length disagree.
|
||||||
// correct truncate — no separate resize step, no partial-write window where the
|
|
||||||
// on-disk sizes and length disagree. The result is a valid, playable WAV of the
|
|
||||||
// kept frames (verified by the wav_codec re-parse test).
|
|
||||||
patchU32LE(bytes, plan.dataSizeFieldOffset, plan.newDataSize);
|
patchU32LE(bytes, plan.dataSizeFieldOffset, plan.newDataSize);
|
||||||
patchU32LE(bytes, plan.riffSizeFieldOffset, plan.newRiffSize);
|
patchU32LE(bytes, plan.riffSizeFieldOffset, plan.newRiffSize);
|
||||||
|
|
||||||
// NOTE (DAW-verify, best-effort): a truncating write that fails MID-write (a full
|
// A mid-write failure (full disk, yanked drive) would leave a short file while we
|
||||||
// disk, a yanked drive) would leave a short file while we return kNoTrim, so the
|
// return kNoTrim, overstating the Sample length. Vanishingly unlikely for a
|
||||||
// Sample length would overstate the file. Vanishingly unlikely for a just-recorded
|
// just-recorded local file, and this is a convenience path, so a temp-file+
|
||||||
// local bank file, and realtime tail is a convenience path, so a temp-file+atomic-
|
// atomic-rename isn't warranted; flagged rather than built.
|
||||||
// rename is not warranted here; flagged rather than built.
|
|
||||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||||
if (!out) return kNoTrim; // could not reopen to rewrite — leave the full file
|
if (!out) return kNoTrim; // could not reopen to rewrite — leave the full file
|
||||||
out.write(reinterpret_cast<const char*>(bytes.data()),
|
out.write(reinterpret_cast<const char*>(bytes.data()),
|
||||||
@@ -190,12 +175,8 @@ CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
|
|||||||
std::filesystem::remove(recorded, rmEc); // best-effort
|
std::filesystem::remove(recorded, rmEc); // best-effort
|
||||||
}
|
}
|
||||||
|
|
||||||
// TAIL (Auto): trim the trailing decay of the recorded window in place — on the
|
// Only Auto trims; None recorded exact bounds and Manual is a fixed window
|
||||||
// BANK file we now own (destPath), never the project. Best-effort: an unreadable /
|
// (spec §The realtime path).
|
||||||
// unknown-format / short file skips the trim (keeps the full window) rather than
|
|
||||||
// corrupt the capture. Only Auto trims; None recorded exact bounds and Manual is a
|
|
||||||
// fixed window (spec §The realtime path). Returns the trimmed length in seconds,
|
|
||||||
// or < 0 for "no trim applied".
|
|
||||||
double trimmedLenSeconds = -1.0;
|
double trimmedLenSeconds = -1.0;
|
||||||
if (request.tailMode == TailMode::Auto) {
|
if (request.tailMode == TailMode::Auto) {
|
||||||
trimmedLenSeconds = trimAutoTailInPlace(destPath,
|
trimmedLenSeconds = trimAutoTailInPlace(destPath,
|
||||||
@@ -203,7 +184,7 @@ CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
|
|||||||
request.endSeconds);
|
request.endSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pure recorded-capture -> Sample mapping (identity, bounds echo, tier).
|
// Pure recorded-capture -> Sample mapping (identity, bounds echo, tier).
|
||||||
RecordedCapture cap;
|
RecordedCapture cap;
|
||||||
cap.relativePath = paths.relativePath;
|
cap.relativePath = paths.relativePath;
|
||||||
cap.uniqueTag = uniqueTag;
|
cap.uniqueTag = uniqueTag;
|
||||||
@@ -218,23 +199,16 @@ CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
|
|||||||
result.status = CaptureStatus::Ok;
|
result.status = CaptureStatus::Ok;
|
||||||
result.sample = sampleFromRecordedCapture(cap);
|
result.sample = sampleFromRecordedCapture(cap);
|
||||||
|
|
||||||
// The SHARED finished-capture stamp (T2-09 dedupe): trackGuids + channelCount
|
// Shared finished-capture stamp. timeSigProj = proj: the realtime path pins the
|
||||||
// (request echo), resolved sampleRate (request rate else PROJECT_SRATE — read
|
// record's own project (offline reads the active project instead) — the
|
||||||
// against the record's OWN project), captureTempo, the capture-start time
|
// divergence is kept caller-visible here.
|
||||||
// signature (timeSigProj = proj: the realtime path PINS the record's own
|
|
||||||
// project — the divergence from offline's active-project read, kept
|
|
||||||
// caller-visible here), the WAV-aware contentHash of the (possibly trimmed)
|
|
||||||
// bank file, and createdTimestamp.
|
|
||||||
stampCaptureSample(result.sample, request, /*rateProj=*/proj,
|
stampCaptureSample(result.sample, request, /*rateProj=*/proj,
|
||||||
/*timeSigProj=*/proj, destPath);
|
/*timeSigProj=*/proj, destPath);
|
||||||
|
|
||||||
// The recorded file's true length differs from the request range when a tail was
|
// The recorded length differs from the request range when a tail was recorded,
|
||||||
// recorded, so the Sample length must reflect the FILE, not the range:
|
// so lengthSeconds must reflect the file, not the range: trimmed length if Auto
|
||||||
// Auto with a trim applied -> the trimmed length trimAutoTailInPlace returned.
|
// trimmed, else the full recorded window (recordWindowEnd - start; equals the
|
||||||
// Auto with no trim, or Manual -> the full recorded window (end - start).
|
// exact range when tailMode is None).
|
||||||
// None -> the exact range (unchanged; recordWindowEnd == endSeconds).
|
|
||||||
// sampleFromRecordedCapture already set lengthSeconds = end - start; override it
|
|
||||||
// to the recorded/trimmed length so downstream (thumbnail, placement) matches disk.
|
|
||||||
if (trimmedLenSeconds >= 0.0) {
|
if (trimmedLenSeconds >= 0.0) {
|
||||||
result.sample.lengthSeconds = trimmedLenSeconds;
|
result.sample.lengthSeconds = trimmedLenSeconds;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// capture_realtime_finalize — the FILE-SIDE half of the realtime-record shell
|
// The file-side half of the realtime-record shell: discovers the file REAPER
|
||||||
// (Q-W3, T4-08 split riding the Q-9 rename): discovering the file REAPER actually
|
// actually recorded, moves it into the bank, runs the Auto-tail decay-scan trim,
|
||||||
// recorded, moving it into the bank, the Auto-tail PCM decay-scan trim, and the
|
// and populates the finished Sample. The async record lifecycle (state
|
||||||
// finished-Sample population. The async record LIFECYCLE (state snapshot/restore,
|
// snapshot/restore, begin/tick/abort) lives in capture_realtime_shell.cpp; this
|
||||||
// begin/tick/abort) lives in capture_realtime_shell.cpp; this half talks to
|
// half talks to wav_codec and the filesystem, not the transport.
|
||||||
// wav_codec and the filesystem, not to the transport.
|
|
||||||
//
|
//
|
||||||
// REAPER-facing: the .cpp includes reaper_plugin_functions.h WITHOUT
|
// The .cpp includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT
|
||||||
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers — CLAUDE.md §contract).
|
// (main.cpp owns the API pointers). MediaTrack/ReaProject are forward-declared
|
||||||
// MediaTrack / ReaProject are forward-declared (via capture.h) so this header
|
// (via capture.h) so this header stays SDK-lite.
|
||||||
// stays SDK-lite.
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -18,21 +16,18 @@
|
|||||||
|
|
||||||
namespace reasampler::capture {
|
namespace reasampler::capture {
|
||||||
|
|
||||||
// Discovers the file REAPER actually recorded onto the temp track: the first media
|
// The first media item's active take's source file on the temp track, forward-
|
||||||
// item's active take's source file, forward-slashed. Empty string if nothing was
|
// slashed; empty if nothing was recorded. Also used by the lifecycle's flush wait
|
||||||
// recorded (no item / take / source). Also used by the lifecycle's flush wait
|
|
||||||
// (size-stable check) before finalize runs.
|
// (size-stable check) before finalize runs.
|
||||||
std::string recordedFilePath(MediaTrack* temp);
|
std::string recordedFilePath(MediaTrack* temp);
|
||||||
|
|
||||||
// Builds a CaptureResult for a finalized recording: discover the recorded file,
|
// Discovers the recorded file, moves it into the bank at `paths`, Auto-trims the
|
||||||
// move it into the bank at `paths`, Auto-trim the tail decay in place when the
|
// tail decay in place when requested, and populates the Sample (project reads
|
||||||
// request asks for it, and populate the Sample (pure sampleFromRecordedCapture +
|
// pinned to `proj`, the record's own project). Does NOT restore any snapshotted
|
||||||
// the shared stampCaptureSample — both project reads pinned to `proj`, the
|
// state — the caller restores unconditionally afterward, even on a finalize
|
||||||
// record's OWN project). Returns Ok + Sample on success, or a RenderFailed result.
|
// failure, so finalize and restore stay separate steps. `recordWindowEnd` is the
|
||||||
// Does NOT restore any snapshotted state — the caller restores unconditionally
|
// recorded window end in project seconds (>= request.endSeconds when a tail was
|
||||||
// afterward (finalize + restore are separate steps so a finalize failure still
|
// recorded).
|
||||||
// restores). `recordWindowEnd` is the recorded window end in project seconds
|
|
||||||
// (>= request.endSeconds when a tail was recorded) — the untrimmed-length source.
|
|
||||||
CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
|
CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
|
||||||
const CaptureRequest& request,
|
const CaptureRequest& request,
|
||||||
const BankPaths& paths,
|
const BankPaths& paths,
|
||||||
|
|||||||
@@ -1,78 +1,49 @@
|
|||||||
// capture_realtime_shell.cpp — REAPER-facing realtime-record backend
|
// REAPER-facing realtime-record backend (RealtimeRecordBackend): the async record
|
||||||
// (RealtimeRecordBackend): the ASYNC record LIFECYCLE — state snapshot/restore +
|
// lifecycle — state snapshot/restore + begin/tick/abort. The file-side half
|
||||||
// begin/tick/abort. (Renamed from capture_realtime.cpp in Q-W3 — the Q-9 naming
|
// (recorded-file discovery, move-into-bank, Auto-tail trim, Sample population)
|
||||||
// rider: the PURE module owns the capture_realtime stem, this shell takes the
|
// lives in capture_realtime_finalize.cpp.
|
||||||
// suffix, matching drag_out ↔ drag_out_win.) The FILE-SIDE half — recorded-file
|
|
||||||
// discovery, move-into-bank, Auto-tail trim, Sample population — lives in
|
|
||||||
// capture_realtime_finalize.cpp (T4-08 split).
|
|
||||||
//
|
//
|
||||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
// Includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is
|
||||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
// the one TU that defines the API pointers; here they are extern.
|
||||||
// pointers; here they are extern (CLAUDE.md §contract).
|
|
||||||
//
|
//
|
||||||
// Captures the requested scope over the requested range by RECORDING in realtime
|
// Captures the requested scope by recording in realtime into a hidden temp
|
||||||
// (transport-driven) into a hidden temp track, then moves the recorded file into
|
// track, then moves the recorded file into the bank as a Sample —
|
||||||
// the bank as a Sample — non-destructively. This increment implements the TRACK
|
// non-destructively. TRACK scope only this increment (the selected track's own
|
||||||
// scope only (records the selected track's own output). Item realtime is deferred
|
// output); item realtime is deferred (UnsupportedMode) rather than half-built.
|
||||||
// (UnsupportedMode) rather than silently half-built.
|
|
||||||
//
|
//
|
||||||
// ============================================================================
|
// ASYNC: a realtime record takes (end - start) wall-clock seconds; blocking the
|
||||||
// §ASYNC — timer-driven, no UI block (M8 rework — Daniel: "do it right")
|
// main thread for that long freezes REAPER's UI. So it's driven across timer
|
||||||
// ============================================================================
|
// ticks: begin() validates, snapshots all state to restore, creates the temp
|
||||||
// A realtime record takes (end - start) wall-clock seconds. The earlier spike ran a
|
// track, routes the source-track tap, arms, CSurf_OnRecord, and returns
|
||||||
// bounded MAIN-THREAD wait for the transport to reach the range end — which FREEZES
|
// immediately; tick() (from OnTimer, same tick as session.poll()) reads the
|
||||||
// REAPER's UI for the whole record. That is gone. The record is now driven across
|
// transport and on a terminal verdict stops + finalizes/aborts + restores
|
||||||
// timer ticks:
|
// everything; abort() force-terminates (shutdown/project switch) + restores.
|
||||||
// begin() — validate, snapshot ALL state to restore, create the temp track,
|
|
||||||
// route the source-track tap, arm, CSurf_OnRecord, RETURN IMMEDIATELY.
|
|
||||||
// tick() — (from OnTimer, the same tick as session.poll()) read the transport,
|
|
||||||
// and on a terminal verdict stop + finalize/abort + RESTORE everything.
|
|
||||||
// abort() — force-terminate now (shutdown / project switch) + RESTORE everything.
|
|
||||||
//
|
//
|
||||||
// The snapshot + restore live on RealtimeCaptureState (below), NOT a function-scope
|
// The snapshot + restore live on RealtimeCaptureState, not a function-scope RAII
|
||||||
// RAII guard — because the record spans ticks, no single stack frame outlives it.
|
// guard, because the record spans ticks — no single stack frame outlives it.
|
||||||
// restore() is idempotent (a restored_ latch): every terminal path — normal
|
// restore() is idempotent: every terminal path (completion, user stop, error,
|
||||||
// completion, user stop, error, second-capture reject, project switch, unload —
|
// project switch, unload) funnels through the same restore. The pure record-mode
|
||||||
// funnels through the SAME single restore, safe to call once from whichever fires.
|
// bookkeeping, recorded-file->Sample mapping, and completion state machine
|
||||||
// The pure record-mode bookkeeping, the recorded-file->Sample mapping, and the
|
// (advanceRecordPhase) live in core/capture/capture_realtime (unit-tested
|
||||||
// completion state machine (advanceRecordPhase) all live in the pure
|
// outside the DAW); this TU owns only the REAPER-bound lifecycle recipe.
|
||||||
// core/capture/capture_realtime.{h,cpp} (unit-tested outside the DAW). This TU
|
|
||||||
// owns only the REAPER-bound lifecycle recipe.
|
|
||||||
//
|
//
|
||||||
// ============================================================================
|
// TAP: the hidden temp track receives a send FROM each selected source track
|
||||||
// §TAP — track-output tap (selected track's own output, PRE-parent)
|
// (CreateTrackSend(source, temp)) and records its own output (B_MAINSEND=0, so
|
||||||
// ============================================================================
|
// it never sums back into the master — no feedback, no monitoring double).
|
||||||
// The recipe: the hidden temp track RECEIVES a send FROM each selected source track
|
// Multiple selected tracks sum in the one temp track, matching how offline
|
||||||
// (CreateTrackSend(source, temp)). The temp track records its OWN output
|
// track scope handles a multi-track selection.
|
||||||
// (I_RECMODE 3/6, latency-compensated) with B_MAINSEND=0 (it does NOT sum back into
|
|
||||||
// the master — no feedback, no monitoring double). Multiple selected tracks each get
|
|
||||||
// a send into the one temp track, so their outputs SUM in the temp track — matching
|
|
||||||
// how offline track scope handles a multi-track selection.
|
|
||||||
//
|
//
|
||||||
// WHY THIS FAITHFULLY CAPTURES THE TRACK'S OUTPUT — and why NO FxBypassGuard:
|
// Why this needs no FxBypassGuard: CreateTrackSend defaults to I_SENDMODE=0
|
||||||
// A CreateTrackSend defaults to I_SENDMODE=0 (post-fader) with I_SRCCHAN=0
|
// (post-fader), which taps the source track after its own FX/fader/pan — its
|
||||||
// (channel offset 0, (srcchan>>10)==0 => full stereo — SDK ~3302/3304). Post-fader
|
// own output — but before the parent/folder/master sums it. The tap is
|
||||||
// taps the source track AFTER its own FX and AFTER its own fader/pan — i.e. exactly
|
// chain-independent by construction: there's nothing downstream of the branch
|
||||||
// the track's OWN OUTPUT — but BEFORE the parent/folder/master sums it. The send is
|
// point to neutralize. (An earlier spike sent FROM the master, which REAPER
|
||||||
// a branch off the signal at the track's output stage; the parent chain downstream
|
// refuses as a feedback loop and silently recorded nothing — a regular
|
||||||
// of that branch is not in the tapped path AT ALL. So the tap is chain-independent
|
// track->track send has no such loop.)
|
||||||
// BY CONSTRUCTION: there is nothing to neutralize, and FxBypassGuard (which mutates
|
|
||||||
// the live chain, altering the user's monitoring) is deliberately NOT used. This is
|
|
||||||
// the realtime analogue of offline track scope (item + the track's own FX + its own
|
|
||||||
// fader/pan; parent/folder/master excluded), reached without touching any live FX.
|
|
||||||
//
|
//
|
||||||
// This ALSO fixes the earlier silent-file bug: that spike sent FROM the master INTO
|
// Non-destructive: deleting the temp track on teardown removes every send
|
||||||
// a temp track, which REAPER refuses to carry (master->track is a feedback loop), so
|
// created into it (REAPER cannot leave a send dangling to a deleted
|
||||||
// the temp recorded silence. A regular track->track send has no feedback — it works.
|
// destination), so no source track retains any routing change.
|
||||||
//
|
|
||||||
// Non-destructive: the temp track is deleted on teardown, which removes every send we
|
|
||||||
// created INTO it (REAPER cannot leave a send dangling to a deleted destination) — so
|
|
||||||
// NO source track retains any routing change. We never mutate any existing track's
|
|
||||||
// persistent state; we only add sends FROM the source tracks that vanish with the
|
|
||||||
// temp track. The selected source tracks are UNCHANGED after capture.
|
|
||||||
//
|
|
||||||
// Item realtime is deferred (UnsupportedMode): item scope would need per-item take
|
|
||||||
// isolation on top of the tap, which is a separate increment.
|
|
||||||
|
|
||||||
#include "shell/capture/capture_realtime_shell.h"
|
#include "shell/capture/capture_realtime_shell.h"
|
||||||
|
|
||||||
@@ -125,10 +96,9 @@ std::string readRppPath() {
|
|||||||
return std::string(buf.data());
|
return std::string(buf.data());
|
||||||
}
|
}
|
||||||
|
|
||||||
// The recorded file's current size in bytes, or -1 if it cannot be resolved yet (no
|
// -1 if unresolved yet. Used by the flush wait to detect stability (size
|
||||||
// item/take/source, or the file does not exist on disk this tick). Used by the flush
|
// unchanged across a tick) before moving the file — a take REAPER is still
|
||||||
// wait to detect stability (size unchanged across a tick) BEFORE moving the file — a
|
// flushing grows tick over tick.
|
||||||
// take REAPER is still flushing on the audio thread grows tick over tick.
|
|
||||||
std::int64_t recordedFileSize(MediaTrack* temp) {
|
std::int64_t recordedFileSize(MediaTrack* temp) {
|
||||||
const std::string path = recordedFilePath(temp);
|
const std::string path = recordedFilePath(temp);
|
||||||
if (path.empty()) return -1;
|
if (path.empty()) return -1;
|
||||||
@@ -140,42 +110,33 @@ std::int64_t recordedFileSize(MediaTrack* temp) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// ============================================================================
|
// Holds everything to restore across the many ticks the record spans (temp
|
||||||
// RealtimeCaptureState — the in-flight snapshot + idempotent restore
|
// track + its sends, other tracks' I_RECARM, transport, edit cursor, time
|
||||||
// ============================================================================
|
// selection), plus the request echo needed to finalize the Sample. restore()
|
||||||
// Holds EVERYTHING to restore across the many ticks the record spans (temp track +
|
// is idempotent (restored_ latch) — the single teardown every terminal path calls.
|
||||||
// its receive-sum sends, other tracks' I_RECARM, transport, edit cursor, time selection),
|
|
||||||
// plus the request echo needed to finalize the Sample. restore() is idempotent
|
|
||||||
// (restored_ latch) and is the single teardown every terminal path calls.
|
|
||||||
class RealtimeCaptureState {
|
class RealtimeCaptureState {
|
||||||
public:
|
public:
|
||||||
// Bound at begin(): the record's OWN project (transport reads use *Ex(proj_) so
|
// Transport reads use *Ex(proj_) so a project switch mid-record can't read
|
||||||
// a project switch mid-record cannot read the wrong transport), the request
|
// the wrong transport.
|
||||||
// echo, and the resolved bank paths + tag for finalize.
|
|
||||||
ReaProject* proj_ = nullptr;
|
ReaProject* proj_ = nullptr;
|
||||||
CaptureRequest request_;
|
CaptureRequest request_;
|
||||||
BankPaths paths_;
|
BankPaths paths_;
|
||||||
std::string uniqueTag_;
|
std::string uniqueTag_;
|
||||||
|
|
||||||
// The RECORDED window end in project seconds (>= request_.endSeconds). For a tail
|
// Project seconds, >= request_.endSeconds. A tail mode runs the transport
|
||||||
// mode the transport runs PAST the range end (Auto: +8 s cap; Manual: +the set
|
// past the range end (Auto: +8s cap; Manual: +set length) — this, not
|
||||||
// length), so this — not request_.endSeconds — is the end the completion state
|
// request_.endSeconds, is what the completion machine waits for.
|
||||||
// machine waits for. Equals request_.endSeconds for TailMode::None (exact bounds).
|
|
||||||
double recordWindowEnd_ = 0.0;
|
double recordWindowEnd_ = 0.0;
|
||||||
|
|
||||||
// The transient sink. The sends we create (from each selected source track INTO
|
// Sends created into temp_ are removed automatically when temp_ is
|
||||||
// temp_) live on those source tracks pointing AT temp_, and are removed automatically
|
// deleted — no separate send handle to track.
|
||||||
// when temp_ is deleted — REAPER cannot leave a send dangling to a deleted
|
|
||||||
// destination. So there is no separate send handle to track here.
|
|
||||||
MediaTrack* temp_ = nullptr;
|
MediaTrack* temp_ = nullptr;
|
||||||
|
|
||||||
// The record phase (pure state machine drives the transition). Starts Recording.
|
|
||||||
RecordPhase phase_ = RecordPhase::Recording;
|
RecordPhase phase_ = RecordPhase::Recording;
|
||||||
|
|
||||||
// Wall-clock anchors for the pure machine's safety ceilings (a steady clock — not
|
// Steady clock (not the play cursor) so a stuck/looping transport is still
|
||||||
// the play cursor — so a stuck/looping transport is still caught, review §3).
|
// caught. begunAt_ set at begin(); finalizingAt_ set on the Recording->
|
||||||
// begunAt_ is set at begin(); finalizingAt_ is set on the Recording->Finalizing
|
// Finalizing edge so the flush wait is bounded from the stop, not begin.
|
||||||
// edge (the transport stop) so the flush wait is bounded from the stop, not begin.
|
|
||||||
std::chrono::steady_clock::time_point begunAt_{};
|
std::chrono::steady_clock::time_point begunAt_{};
|
||||||
std::chrono::steady_clock::time_point finalizingAt_{};
|
std::chrono::steady_clock::time_point finalizingAt_{};
|
||||||
|
|
||||||
@@ -225,37 +186,26 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The single, idempotent teardown. Called on EVERY terminal path (normal
|
// Idempotent teardown called on every terminal path: stop transport if
|
||||||
// completion, user stop, error, project switch, unload). Safe to call more than
|
// still running, delete temp track (drops its sends + recorded item),
|
||||||
// once — the restored_ latch makes every call after the first a no-op. Order:
|
// restore other tracks' arm, restore time selection + edit cursor.
|
||||||
// 1. stop the transport if anything is still running (we own it),
|
// OnStopButtonEx(proj_) is project-scoped, not the global CSurf_OnStop, so
|
||||||
// 2. delete the temp track (drops its receive-sum sends + the recorded item),
|
// a project switch mid-record (proj_ no longer active) still stops OUR
|
||||||
// 3. restore every other track's arm,
|
// project's transport, never the foreign now-active one.
|
||||||
// 4. restore the time selection + edit cursor.
|
|
||||||
// Stop the record's OWN project transport if it is still playing/recording. Uses
|
|
||||||
// the project-scoped OnStopButtonEx(proj_) (not the global CSurf_OnStop) so a
|
|
||||||
// project switch mid-record — where proj_ is no longer the ACTIVE project — stops
|
|
||||||
// OUR project's transport, never the foreign now-active one. &1=playing,
|
|
||||||
// &4=recording. Idempotent to call (the playstate guard makes a repeat a no-op).
|
|
||||||
void stopOwnTransport() {
|
void stopOwnTransport() {
|
||||||
if (GetPlayStateEx(proj_) & (1 | 4)) OnStopButtonEx(proj_);
|
if (GetPlayStateEx(proj_) & (1 | 4)) OnStopButtonEx(proj_);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is the captured project STILL OPEN? (review §1 — CRITICAL). If the captured
|
// If the captured project was closed mid-record, proj_/temp_ point at freed
|
||||||
// project was CLOSED mid-record, proj_/temp_ point at freed memory;
|
// memory; touching them is a use-after-free. ValidatePtr2 with a null
|
||||||
// touching them (stopOwnTransport, DeleteTrack, arm restore) is a use-after-free.
|
// project validates the ReaProject* itself. Every teardown that
|
||||||
// ValidatePtr2 with a null project validates the ReaProject* itself (the header:
|
// dereferences a captured REAPER object must gate on this first.
|
||||||
// "proj is ignored if pointer is itself a project"). Every teardown that
|
|
||||||
// dereferences a captured REAPER object MUST gate on this first.
|
|
||||||
bool captureProjectStillOpen() const {
|
bool captureProjectStillOpen() const {
|
||||||
return proj_ && ValidatePtr2(nullptr, proj_, "ReaProject*");
|
return proj_ && ValidatePtr2(nullptr, proj_, "ReaProject*");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop the handle WITHOUT touching any REAPER state — for the closed-project case
|
// For the closed-project case: a closed project already reclaimed its temp
|
||||||
// (review §1). A closed project already reclaimed its temp track, arms, and
|
// track, arms, and transport, so drop the handle without touching REAPER state.
|
||||||
// transport; there is nothing to restore and the pointers are freed. Latch
|
|
||||||
// restored_ so any later terminal path is a no-op (idempotent), but skip every
|
|
||||||
// REAPER call restore() would make.
|
|
||||||
void dropWithoutRestore() {
|
void dropWithoutRestore() {
|
||||||
restored_ = true;
|
restored_ = true;
|
||||||
temp_ = nullptr;
|
temp_ = nullptr;
|
||||||
@@ -266,21 +216,16 @@ public:
|
|||||||
if (restored_) return;
|
if (restored_) return;
|
||||||
restored_ = true;
|
restored_ = true;
|
||||||
|
|
||||||
// 1. Transport: stop OUR project's if still running (usually already stopped
|
|
||||||
// by the terminal path's explicit stop-before-finalize — a safe no-op then).
|
|
||||||
stopOwnTransport();
|
stopOwnTransport();
|
||||||
|
|
||||||
// 2. Temp track: deleting it drops the source-track sends (REAPER removes every
|
// Deleting the temp track drops the source-track sends (REAPER removes
|
||||||
// send whose destination is deleted — no source track is left mutated) AND the
|
// every send whose destination is deleted) and the recorded item in one move.
|
||||||
// recorded arrange item in one move — nothing stays behind (load-bearing).
|
|
||||||
if (temp_) { DeleteTrack(temp_); temp_ = nullptr; }
|
if (temp_) { DeleteTrack(temp_); temp_ = nullptr; }
|
||||||
|
|
||||||
// 3. Other tracks' record-arm.
|
|
||||||
for (const ArmSnap& s : armSnaps_)
|
for (const ArmSnap& s : armSnaps_)
|
||||||
SetMediaTrackInfo_Value(s.track, "I_RECARM", s.recarm);
|
SetMediaTrackInfo_Value(s.track, "I_RECARM", s.recarm);
|
||||||
armSnaps_.clear();
|
armSnaps_.clear();
|
||||||
|
|
||||||
// 4. Time selection + edit cursor (no view move, no seek).
|
|
||||||
GetSet_LoopTimeRange(true, false, &tsStart_, &tsEnd_, false);
|
GetSet_LoopTimeRange(true, false, &tsStart_, &tsEnd_, false);
|
||||||
SetEditCurPos(curPos_, false, false);
|
SetEditCurPos(curPos_, false, false);
|
||||||
}
|
}
|
||||||
@@ -294,13 +239,6 @@ private:
|
|||||||
bool finalized_ = false;
|
bool finalized_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The FILE-SIDE finalize half (recorded-file discovery, move-into-bank, the
|
|
||||||
// Auto-tail PCM decay-scan trim, and the finished-Sample population) lives in
|
|
||||||
// capture_realtime_finalize.cpp (T4-08). This TU owns only the async lifecycle.
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// begin — start the record, snapshot, return immediately (no UI block)
|
|
||||||
// ============================================================================
|
|
||||||
void RealtimeCaptureStateDeleter::operator()(RealtimeCaptureState* p) const noexcept {
|
void RealtimeCaptureStateDeleter::operator()(RealtimeCaptureState* p) const noexcept {
|
||||||
delete p; // full type is visible here — keeps capture.h REAPER-free
|
delete p; // full type is visible here — keeps capture.h REAPER-free
|
||||||
}
|
}
|
||||||
@@ -309,8 +247,8 @@ RealtimeCaptureHandle
|
|||||||
RealtimeRecordBackend::begin(const CaptureRequest& request,
|
RealtimeRecordBackend::begin(const CaptureRequest& request,
|
||||||
const std::vector<MediaTrack*>& sourceTracks,
|
const std::vector<MediaTrack*>& sourceTracks,
|
||||||
CaptureResult& outFailure) {
|
CaptureResult& outFailure) {
|
||||||
// Only the track scope is implemented this increment (see §TAP). Item realtime
|
// Item realtime is deferred — needs per-item take isolation on top of the
|
||||||
// is deferred — it needs per-item take isolation on top of the track-output tap.
|
// track-output tap.
|
||||||
if (request.sourceMode != SourceMode::SelectedTracks) {
|
if (request.sourceMode != SourceMode::SelectedTracks) {
|
||||||
outFailure.status = CaptureStatus::UnsupportedMode;
|
outFailure.status = CaptureStatus::UnsupportedMode;
|
||||||
outFailure.message = "RealtimeRecordBackend implements TRACK scope only this "
|
outFailure.message = "RealtimeRecordBackend implements TRACK scope only this "
|
||||||
@@ -354,7 +292,7 @@ RealtimeRecordBackend::begin(const CaptureRequest& request,
|
|||||||
// .rpp parent. Prompt Save-As once when unsaved; refuse if still unsaved.
|
// .rpp parent. Prompt Save-As once when unsaved; refuse if still unsaved.
|
||||||
std::string rppPath = readRppPath();
|
std::string rppPath = readRppPath();
|
||||||
if (rppPath.empty()) {
|
if (rppPath.empty()) {
|
||||||
Main_SaveProject(proj, true); // DAW-only: opens Save-As, blocks (verify)
|
Main_SaveProject(proj, true);
|
||||||
rppPath = readRppPath();
|
rppPath = readRppPath();
|
||||||
}
|
}
|
||||||
if (rppPath.empty()) {
|
if (rppPath.empty()) {
|
||||||
@@ -365,68 +303,51 @@ RealtimeRecordBackend::begin(const CaptureRequest& request,
|
|||||||
const std::string projectDir =
|
const std::string projectDir =
|
||||||
normSlashes(std::filesystem::path(rppPath).parent_path().string());
|
normSlashes(std::filesystem::path(rppPath).parent_path().string());
|
||||||
|
|
||||||
// --- Build the in-flight state (owns the snapshot + teardown) ---------------
|
|
||||||
RealtimeCaptureHandle st(new RealtimeCaptureState());
|
RealtimeCaptureHandle st(new RealtimeCaptureState());
|
||||||
st->proj_ = proj;
|
st->proj_ = proj;
|
||||||
st->request_ = request;
|
st->request_ = request;
|
||||||
st->uniqueTag_ = makeUniqueTag("rt-"); // shared mint (T1-11 monotonic counter)
|
st->uniqueTag_ = makeUniqueTag("rt-");
|
||||||
st->paths_ = deriveBankPaths(projectDir, request.baseName, st->uniqueTag_);
|
st->paths_ = deriveBankPaths(projectDir, request.baseName, st->uniqueTag_);
|
||||||
|
|
||||||
// The recorded window end: extended past the range end for a tail mode (Auto/Manual),
|
// Extended past the range end for a tail mode (Auto/Manual), exact for
|
||||||
// exact for None. This — not request.endSeconds — is what the completion machine
|
// None; the extra window is trimmed later (Auto) or kept (Manual).
|
||||||
// waits for; the extra window past the range end is trimmed later (Auto) or kept
|
|
||||||
// (Manual). Pure mapping (render_settings), shared caps with the offline tail.
|
|
||||||
st->recordWindowEnd_ = realtimeRecordWindowEnd(request.tailMode,
|
st->recordWindowEnd_ = realtimeRecordWindowEnd(request.tailMode,
|
||||||
request.endSeconds,
|
request.endSeconds,
|
||||||
request.tailMs);
|
request.tailMs);
|
||||||
|
|
||||||
// DELIBERATE: the transient temp-track / arm / send / transport mutations are NOT
|
// Deliberately NOT wrapped in an undo block — this backend fully restores
|
||||||
// wrapped in an Undo_BeginBlock/Undo_EndBlock — divergence from the insert/view
|
// its own state across every terminal path, so an undo point would surface
|
||||||
// shells is intentional. This backend fully restores its own state across every
|
// an internal, fully-reversed scaffold for no user-meaningful action.
|
||||||
// terminal path (the restore() latch); an undo point would surface an internal,
|
// Disarm every other track BEFORE the temp track exists so it's never in
|
||||||
// fully-reversed scaffold in the user's undo history for no user-meaningful action.
|
// the arm snapshot.
|
||||||
// Snapshot cursor + time selection, and disarm every OTHER track BEFORE the temp
|
|
||||||
// track exists (so it is never in the arm snapshot and keeps the arm we set).
|
|
||||||
st->snapshotAndDisarmOthers();
|
st->snapshotAndDisarmOthers();
|
||||||
|
|
||||||
// Hidden temp track at the end: no default FX/envelopes (clean sink), hidden from
|
// Hidden temp track: no default FX/envelopes, hidden from both panels,
|
||||||
// both panels, B_MAINSEND=0 so it does NOT sum back into the master (monitoring
|
// B_MAINSEND=0 so it doesn't sum back into the master (would otherwise
|
||||||
// invariant — it would otherwise double the tapped tracks in the user's monitoring).
|
// double the tapped tracks in the user's monitoring).
|
||||||
const int idx = CountTracks(proj);
|
const int idx = CountTracks(proj);
|
||||||
InsertTrackAtIndex(idx, false);
|
InsertTrackAtIndex(idx, false);
|
||||||
st->temp_ = GetTrack(proj, idx);
|
st->temp_ = GetTrack(proj, idx);
|
||||||
if (!st->temp_) {
|
if (!st->temp_) {
|
||||||
outFailure.status = CaptureStatus::RenderFailed;
|
outFailure.status = CaptureStatus::RenderFailed;
|
||||||
outFailure.message = "Could not create the hidden temp record track.";
|
outFailure.message = "Could not create the hidden temp record track.";
|
||||||
st->restore(); // undo the disarm + cursor/time-sel snapshot
|
st->restore();
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
SetMediaTrackInfo_Value(st->temp_, "B_SHOWINTCP", 0.0);
|
SetMediaTrackInfo_Value(st->temp_, "B_SHOWINTCP", 0.0);
|
||||||
SetMediaTrackInfo_Value(st->temp_, "B_SHOWINMIXER", 0.0);
|
SetMediaTrackInfo_Value(st->temp_, "B_SHOWINMIXER", 0.0);
|
||||||
SetMediaTrackInfo_Value(st->temp_, "B_MAINSEND", 0.0);
|
SetMediaTrackInfo_Value(st->temp_, "B_MAINSEND", 0.0);
|
||||||
|
|
||||||
// Route the TRACK-OUTPUT tap: a send FROM each selected source track INTO the temp
|
// A send FROM each selected source track INTO the temp track; the temp
|
||||||
// track (CreateTrackSend(source, temp)). The temp records its OWN output, so the
|
// records its own output, so sends sum in it — matching offline track
|
||||||
// sends' outputs SUM in it — multiple selected tracks are captured together (same as
|
// scope's multi-track handling. Sends default to post-fader/full-stereo,
|
||||||
// offline track scope). See §TAP for why this faithfully captures each track's own
|
// left at defaults deliberately — that IS the track-scope tap point.
|
||||||
// output and needs no FxBypassGuard.
|
|
||||||
//
|
|
||||||
// Sends default to post-fader (I_SENDMODE 0) and full-stereo (I_SRCCHAN default,
|
|
||||||
// (srcchan>>10)==0 — SDK ~3302/3304): post-fader = after the source track's FX and
|
|
||||||
// fader/pan = the track's OWN output, tapped BEFORE the parent sums it. Left at
|
|
||||||
// defaults deliberately — that IS the track-scope tap point.
|
|
||||||
//
|
|
||||||
// DAW-ONLY ASSUMPTION (flag): that a post-fader track->temp send + output-record
|
|
||||||
// reproduces the track's own output sample-for-sample (latency comp, pan law,
|
|
||||||
// mono/stereo folding) is the crux to verify live.
|
|
||||||
int sendsMade = 0;
|
int sendsMade = 0;
|
||||||
for (MediaTrack* src : sourceTracks) {
|
for (MediaTrack* src : sourceTracks) {
|
||||||
if (!src || src == st->temp_) continue;
|
if (!src || src == st->temp_) continue;
|
||||||
if (CreateTrackSend(src, st->temp_) >= 0) ++sendsMade;
|
if (CreateTrackSend(src, st->temp_) >= 0) ++sendsMade;
|
||||||
}
|
}
|
||||||
if (sendsMade == 0) {
|
if (sendsMade == 0) {
|
||||||
// Every send failed (should not happen for valid selected tracks). Refuse
|
|
||||||
// rather than record a guaranteed-silent file.
|
|
||||||
outFailure.status = CaptureStatus::RenderFailed;
|
outFailure.status = CaptureStatus::RenderFailed;
|
||||||
outFailure.message = "Could not route any selected track into the record tap — "
|
outFailure.message = "Could not route any selected track into the record tap — "
|
||||||
"nothing to capture.";
|
"nothing to capture.";
|
||||||
@@ -434,10 +355,8 @@ RealtimeRecordBackend::begin(const CaptureRequest& request,
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record-mode values from the pure planner. The temp track records its OWN output;
|
// The temp track has no FX and unity fader, so its post-fader output
|
||||||
// it has no FX and unity fader, so its post-fader output equals the summed sends.
|
// equals the summed sends; track scope is fully wet -> PostFader.
|
||||||
// Track scope is fully wet -> PostFader. (The actual track-scope tap point is the
|
|
||||||
// source sends' default post-fader mode; the temp's recmode only records the sum.)
|
|
||||||
const OutputTap tap = outputTapForWetDry(request.wetDry);
|
const OutputTap tap = outputTapForWetDry(request.wetDry);
|
||||||
const RecordModePlan rec = recordModePlanFor(request.channelCount, tap);
|
const RecordModePlan rec = recordModePlanFor(request.channelCount, tap);
|
||||||
SetMediaTrackInfo_Value(st->temp_, "I_RECMODE", static_cast<double>(rec.recMode));
|
SetMediaTrackInfo_Value(st->temp_, "I_RECMODE", static_cast<double>(rec.recMode));
|
||||||
@@ -446,54 +365,41 @@ RealtimeRecordBackend::begin(const CaptureRequest& request,
|
|||||||
SetMediaTrackInfo_Value(st->temp_, "I_RECARM", 1.0); // arm ONLY the sink
|
SetMediaTrackInfo_Value(st->temp_, "I_RECARM", 1.0); // arm ONLY the sink
|
||||||
SetMediaTrackInfo_Value(st->temp_, "I_RECMON", 0.0); // no input monitoring
|
SetMediaTrackInfo_Value(st->temp_, "I_RECMON", 0.0); // no input monitoring
|
||||||
|
|
||||||
// Record range: time selection over [start, recordWindowEnd], play cursor at start.
|
// recordWindowEnd extends past the range end for a tail mode so the
|
||||||
// recordWindowEnd extends past the request's range end for a tail mode so the
|
// transport captures the decay; cursor + time selection are restored by restore().
|
||||||
// transport captures the decaying tail; it equals the range end for None (exact
|
|
||||||
// bounds). Both cursor + time selection were snapshotted and are restored by
|
|
||||||
// restore().
|
|
||||||
double rs = request.startSeconds, re = st->recordWindowEnd_;
|
double rs = request.startSeconds, re = st->recordWindowEnd_;
|
||||||
GetSet_LoopTimeRange(true, false, &rs, &re, false);
|
GetSet_LoopTimeRange(true, false, &rs, &re, false);
|
||||||
SetEditCurPos(request.startSeconds, false, false);
|
SetEditCurPos(request.startSeconds, false, false);
|
||||||
|
|
||||||
// Start the transport and RETURN. tick() drives the rest across timer ticks.
|
// tick() detects completion via the play cursor reaching the range end
|
||||||
//
|
// (the pure state machine), independent of REAPER's auto-punch settings.
|
||||||
// DAW-ONLY ASSUMPTION (flag): CSurf_OnRecord starts recording and the exact
|
|
||||||
// range/auto-punch/stop behavior depends on the user's transport settings — not
|
|
||||||
// header-guaranteed. tick() detects completion via the play cursor reaching the
|
|
||||||
// range end (the pure state machine), independent of REAPER's auto-punch.
|
|
||||||
CSurf_OnRecord();
|
CSurf_OnRecord();
|
||||||
|
|
||||||
// Anchor the wall-clock safety ceiling from here (steady clock — independent of the
|
// Steady clock, independent of the play cursor, so a transport that starts
|
||||||
// play cursor, so a transport that starts but never advances is still bounded).
|
// but never advances is still bounded.
|
||||||
st->markElapsedStart();
|
st->markElapsedStart();
|
||||||
|
|
||||||
return st;
|
return st;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// tick — advance the in-flight record; on terminal, finalize/abort + restore
|
|
||||||
// ============================================================================
|
|
||||||
RealtimeTickResult RealtimeRecordBackend::tick(RealtimeCaptureState& state) {
|
RealtimeTickResult RealtimeRecordBackend::tick(RealtimeCaptureState& state) {
|
||||||
RealtimeTickResult out;
|
RealtimeTickResult out;
|
||||||
|
|
||||||
// If a prior terminal path already tore this down (e.g. abort() then a stray
|
// A prior terminal path (e.g. abort()) already tore this down — spent.
|
||||||
// tick), do nothing — the state is spent.
|
|
||||||
if (state.restored()) { out.status = RealtimeTickStatus::Failed; return out; }
|
if (state.restored()) { out.status = RealtimeTickStatus::Failed; return out; }
|
||||||
|
|
||||||
const RecordPhase prevPhase = state.phase_;
|
const RecordPhase prevPhase = state.phase_;
|
||||||
|
|
||||||
// Read the transport bound to the record's OWN project (a project switch cannot
|
// *Ex(state.proj_) so a project switch can't point these reads at the
|
||||||
// point these reads at the wrong transport). &4 = recording. Gather everything the
|
// wrong transport.
|
||||||
// pure machine needs (transport + wall-clock ceilings + file-flush readiness).
|
|
||||||
RecordTickInputs inputs;
|
RecordTickInputs inputs;
|
||||||
inputs.transport.recording = (GetPlayStateEx(state.proj_) & 4) != 0;
|
inputs.transport.recording = (GetPlayStateEx(state.proj_) & 4) != 0;
|
||||||
inputs.transport.playPosition = GetPlayPositionEx(state.proj_);
|
inputs.transport.playPosition = GetPlayPositionEx(state.proj_);
|
||||||
inputs.elapsedSeconds = state.elapsedSeconds();
|
inputs.elapsedSeconds = state.elapsedSeconds();
|
||||||
|
|
||||||
// Deferred-finalize flush check (review §2), only meaningful once stopped. The
|
// File is ready when its size is positive and unchanged from the previous
|
||||||
// recorded file is READY when its size is a valid positive value AND unchanged
|
// tick — REAPER finished flushing the take. Comparing across a tick avoids
|
||||||
// from the previous tick — REAPER finished flushing/closing the take on the audio
|
// moving a file mid-write.
|
||||||
// thread. Comparing across a tick avoids moving a file mid-write (truncated take).
|
|
||||||
if (prevPhase == RecordPhase::Finalizing) {
|
if (prevPhase == RecordPhase::Finalizing) {
|
||||||
state.markFinalizingStartOnce();
|
state.markFinalizingStartOnce();
|
||||||
inputs.finalizingSeconds = state.finalizingSeconds();
|
inputs.finalizingSeconds = state.finalizingSeconds();
|
||||||
@@ -502,34 +408,29 @@ RealtimeTickResult RealtimeRecordBackend::tick(RealtimeCaptureState& state) {
|
|||||||
state.lastFileSize_ = sz;
|
state.lastFileSize_ = sz;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the transport to reach the RECORDED window end (extended past the
|
// Waits for the transport to reach the recorded window end (extended for a
|
||||||
// range end for a tail mode), not the request's range end — the extra tail window
|
// tail mode), not the request's range end — the extra tail window is part
|
||||||
// is part of the record. The record safety ceiling scales with it (window - start
|
// of the record.
|
||||||
// + margin) inside the pure machine.
|
|
||||||
state.phase_ = advanceRecordPhase(state.phase_, inputs,
|
state.phase_ = advanceRecordPhase(state.phase_, inputs,
|
||||||
state.request_.startSeconds,
|
state.request_.startSeconds,
|
||||||
state.recordWindowEnd_);
|
state.recordWindowEnd_);
|
||||||
|
|
||||||
// On the Recording -> Finalizing edge, stop OUR project's transport ONCE so REAPER
|
// On Recording -> Finalizing, stop OUR project's transport once so REAPER
|
||||||
// begins closing/flushing the recorded take. Project-scoped (OnStopButtonEx(proj_))
|
// begins flushing the take; project-scoped so a project switch can't stop
|
||||||
// — never the global CSurf_OnStop, which would stop whatever project is ACTIVE (a
|
// the wrong (foreign active) project.
|
||||||
// foreign one during a project switch), not the record's own. The flush wait then
|
|
||||||
// proceeds across subsequent ticks before the file is moved.
|
|
||||||
if (prevPhase == RecordPhase::Recording &&
|
if (prevPhase == RecordPhase::Recording &&
|
||||||
isStopRequested(state.phase_)) {
|
isStopRequested(state.phase_)) {
|
||||||
state.stopOwnTransport();
|
state.stopOwnTransport();
|
||||||
state.markFinalizingStartOnce(); // anchor the flush ceiling from the stop
|
state.markFinalizingStartOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isTerminalPhase(state.phase_)) {
|
if (!isTerminalPhase(state.phase_)) {
|
||||||
out.status = RealtimeTickStatus::InProgress;
|
out.status = RealtimeTickStatus::InProgress;
|
||||||
return out; // keep the OnTimer tick fast — recording or flushing
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Terminal (Done: file flushed + stable; Failed: flush ceiling tripped). On Done,
|
// Done: file flushed + stable, finalize moves it into the bank. Failed:
|
||||||
// finalize moves the now-stable file into the bank + builds the Sample. On Failed
|
// flush ceiling tripped, nothing usable.
|
||||||
// (the flush timeout) there is nothing usable — report RenderFailed. Then restore
|
|
||||||
// ALL snapshotted state — the non-destructive gate, idempotent + unconditional.
|
|
||||||
CaptureResult res;
|
CaptureResult res;
|
||||||
if (state.phase_ == RecordPhase::Done) {
|
if (state.phase_ == RecordPhase::Done) {
|
||||||
res = finalizeRecording(state.proj_, state.temp_, state.request_,
|
res = finalizeRecording(state.proj_, state.temp_, state.request_,
|
||||||
@@ -550,23 +451,15 @@ RealtimeTickResult RealtimeRecordBackend::tick(RealtimeCaptureState& state) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// abort — force-terminate now (shutdown / project switch) + restore
|
|
||||||
// ============================================================================
|
|
||||||
RealtimeTickResult RealtimeRecordBackend::abort(RealtimeCaptureState& state) {
|
RealtimeTickResult RealtimeRecordBackend::abort(RealtimeCaptureState& state) {
|
||||||
RealtimeTickResult out;
|
RealtimeTickResult out;
|
||||||
|
|
||||||
// Already torn down (idempotent): report Failed and leave it.
|
|
||||||
if (state.restored()) { out.status = RealtimeTickStatus::Failed; return out; }
|
if (state.restored()) { out.status = RealtimeTickStatus::Failed; return out; }
|
||||||
|
|
||||||
// CRITICAL (review §1): if the captured project was CLOSED mid-record, proj_ /
|
// If the captured project was closed mid-record, proj_/temp_ point at
|
||||||
// temp_ point at freed memory. The closed project already reclaimed its
|
// freed memory — drop the handle without touching REAPER state. This is
|
||||||
// temp track, arms, and transport — so DROP the handle WITHOUT touching any REAPER
|
// the one terminal path that can run against a possibly-closed project
|
||||||
// state (no stop, no finalize, no DeleteTrack, no arm restore). Touching those
|
// (tick() only runs while proj_ is still the active project).
|
||||||
// freed pointers is the use-after-free bug this guard exists to prevent. This is
|
|
||||||
// the ONE terminal path that can run against a possibly-closed project (tick() only
|
|
||||||
// runs while proj_ is the active — hence still-open — project); guarding here covers
|
|
||||||
// both the project-switch and unload callers.
|
|
||||||
if (!state.captureProjectStillOpen()) {
|
if (!state.captureProjectStillOpen()) {
|
||||||
state.dropWithoutRestore();
|
state.dropWithoutRestore();
|
||||||
out.result.status = CaptureStatus::RenderFailed;
|
out.result.status = CaptureStatus::RenderFailed;
|
||||||
@@ -576,25 +469,18 @@ RealtimeTickResult RealtimeRecordBackend::abort(RealtimeCaptureState& state) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The project is still open (a tab-switch, or a clean unload with the project
|
// Project still open: stop the transport, then try to finalize whatever
|
||||||
// present): stop the transport, then TRY to finalize whatever was captured so a
|
// was captured so a near-complete record keeps its audio; if nothing
|
||||||
// near-complete record still keeps the audio; if nothing was recorded (or the file
|
// usable was recorded, finalize returns RenderFailed and we abort clean.
|
||||||
// has not flushed yet), finalize returns RenderFailed and we abort clean.
|
// abort() is the force-terminate path — unlike tick() it can't span ticks
|
||||||
// Project-scoped stop (OnStopButtonEx(proj_)) — on a project switch proj_ is no
|
// to wait for the flush, so it still races REAPER's audio-thread take close.
|
||||||
// longer active, so the global CSurf_OnStop would stop the wrong (foreign) project.
|
|
||||||
//
|
|
||||||
// NOTE (residual timing — DAW-verify): abort is the force-terminate path (unload /
|
|
||||||
// switch); it cannot span ticks to wait for the flush the way tick() does, so its
|
|
||||||
// finalize still races REAPER's audio-thread take close. That is inherent to a
|
|
||||||
// best-effort terminal grab and is acceptable — the normal completion path (tick)
|
|
||||||
// is the one that must be flush-safe.
|
|
||||||
state.stopOwnTransport();
|
state.stopOwnTransport();
|
||||||
|
|
||||||
CaptureResult res = finalizeRecording(state.proj_, state.temp_, state.request_,
|
CaptureResult res = finalizeRecording(state.proj_, state.temp_, state.request_,
|
||||||
state.paths_, state.uniqueTag_,
|
state.paths_, state.uniqueTag_,
|
||||||
state.recordWindowEnd_);
|
state.recordWindowEnd_);
|
||||||
state.markFinalized();
|
state.markFinalized();
|
||||||
state.restore(); // the non-destructive gate — always runs
|
state.restore();
|
||||||
|
|
||||||
out.result = res;
|
out.result = res;
|
||||||
out.status = (res.status == CaptureStatus::Ok)
|
out.status = (res.status == CaptureStatus::Ok)
|
||||||
|
|||||||
@@ -1,31 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// capture_realtime_shell — the ASYNC realtime-record seam (Q-W6 split of the former
|
// The ASYNC realtime-record seam: begin/tick/abort. capture.h keeps the shared
|
||||||
// fat capture.h: this header owns the realtime backend's begin/tick/abort surface;
|
// CaptureRequest/CaptureResult types, the offline backend, and shared helpers.
|
||||||
// capture.h keeps the shared CaptureRequest/CaptureResult types, the offline
|
// Implemented by capture_realtime_shell.cpp; driven by exactly one caller
|
||||||
// backend, and the shared backend helpers). Implemented by
|
// (realtime_lifecycle).
|
||||||
// capture_realtime_shell.cpp; driven by exactly one caller (realtime_lifecycle).
|
|
||||||
//
|
//
|
||||||
// A realtime record is inherently asynchronous: CSurf_OnRecord starts the transport
|
// CSurf_OnRecord starts the transport on REAPER's audio thread and returns
|
||||||
// on REAPER's audio thread and returns immediately — it does NOT block until the
|
// immediately — it does not block until the range completes. Blocking the main
|
||||||
// range completes, which takes (end - start) wall-clock seconds. Blocking the main
|
// thread would freeze REAPER's UI, so the backend is driven across timer ticks
|
||||||
// thread for that duration freezes REAPER's UI, so the realtime backend is DRIVEN
|
// instead: begin() starts and returns at once; tick() (called from the same
|
||||||
// ACROSS TIMER TICKS instead: begin() starts and returns at once; tick() (called
|
// OnTimer that runs session.poll()) advances the in-flight record.
|
||||||
// from the same OnTimer that runs session.poll()) advances the in-flight record and
|
|
||||||
// reports when it is done.
|
|
||||||
//
|
//
|
||||||
// SEAM CHOICE (surfaced): the two backends deliberately share NO interface. The
|
// The two backends deliberately share NO interface — do not reintroduce one.
|
||||||
// lifecycles are genuinely different (offline is headless + immediate — one
|
// Offline is headless + immediate (one synchronous capture() call); realtime is
|
||||||
// synchronous capture() call returns a finished Sample; realtime is
|
// transport-driven + async. A shared interface would make offline fake a
|
||||||
// transport-driven + async — begin/tick/abort across timer ticks), so a shared
|
// lifecycle it doesn't have (tick() always Done on first call).
|
||||||
// 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.
|
|
||||||
// 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.)
|
|
||||||
//
|
//
|
||||||
// REAPER-free like capture.h: MediaTrack is forward-declared there and never
|
// REAPER-free like capture.h: MediaTrack is forward-declared there, never
|
||||||
// dereferenced here; the REAPER-facing TU is capture_realtime_shell.cpp.
|
// dereferenced here; the REAPER-facing TU is capture_realtime_shell.cpp.
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -47,74 +37,64 @@ struct RealtimeTickResult {
|
|||||||
CaptureResult result; // meaningful only when status == Done or Failed
|
CaptureResult result; // meaningful only when status == Done or Failed
|
||||||
};
|
};
|
||||||
|
|
||||||
// The opaque in-flight capture state. Owns the snapshot of everything to restore
|
// The opaque in-flight capture state: the snapshot of everything to restore
|
||||||
// (temp track + its receive sends from the source tracks, other tracks' I_RECARM,
|
// (temp track + its sends from the source tracks, other tracks' I_RECARM,
|
||||||
// transport, edit cursor, time selection) and the record's own project handle.
|
// transport, edit cursor, time selection) and the record's own project handle.
|
||||||
// Defined in capture_realtime_shell.cpp; the header stays REAPER-free (nothing is
|
// Defined in capture_realtime_shell.cpp; forward-declared here to stay REAPER-free.
|
||||||
// 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
|
// 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.
|
// RAII guard, because the record spans ticks — no single stack frame outlives it.
|
||||||
// Every terminal path (normal completion, user stop, error, project switch, unload)
|
// Every terminal path (completion, user stop, error, project switch, unload)
|
||||||
// funnels through the same single restore, safe to call once from whichever fires.
|
// funnels through the same restore, safe to call once from whichever fires.
|
||||||
class RealtimeCaptureState;
|
class RealtimeCaptureState;
|
||||||
|
|
||||||
// Out-of-line deleter so callers (realtime_lifecycle) can own a unique_ptr to the
|
// Out-of-line deleter so callers can own a unique_ptr to the opaque
|
||||||
// opaque RealtimeCaptureState WITHOUT its full (REAPER-typed) definition — the
|
// RealtimeCaptureState without its full (REAPER-typed) definition.
|
||||||
// delete is compiled in capture_realtime_shell.cpp where the type is complete,
|
|
||||||
// keeping this header REAPER-free (load-bearing split).
|
|
||||||
struct RealtimeCaptureStateDeleter {
|
struct RealtimeCaptureStateDeleter {
|
||||||
void operator()(RealtimeCaptureState* p) const noexcept;
|
void operator()(RealtimeCaptureState* p) const noexcept;
|
||||||
};
|
};
|
||||||
using RealtimeCaptureHandle =
|
using RealtimeCaptureHandle =
|
||||||
std::unique_ptr<RealtimeCaptureState, RealtimeCaptureStateDeleter>;
|
std::unique_ptr<RealtimeCaptureState, RealtimeCaptureStateDeleter>;
|
||||||
|
|
||||||
// Realtime-record backend — captures by RECORDING in realtime (transport-driven)
|
// Captures by recording in realtime into a hidden temp track, then moves the
|
||||||
// into a hidden temp track, then moves the recorded file into the bank as a Sample.
|
// recorded file into the bank as a Sample. For sources offline render can't do
|
||||||
// For sources offline render cannot do (hardware, performed FX) and as the true
|
// (hardware, performed FX) and as the true pre-FX-dry path (I_RECMODE_FLAGS
|
||||||
// pre-FX-dry path (I_RECMODE_FLAGS &3==1 — the only pre-FX tap in the SDK; offline
|
// &3==1 — the only pre-FX tap in the SDK). Dialog-free: never invokes the
|
||||||
// render has none). Dialog-free: never invokes the offline-render progress window.
|
// offline-render progress window.
|
||||||
//
|
//
|
||||||
// Non-bit-identical by nature (it is realtime); offline stays the deterministic
|
// Non-bit-identical by nature; offline stays the deterministic default.
|
||||||
// default. Non-destructive across EVERY terminal path — the review gate — which is
|
// Non-destructive across every terminal path is harder here than offline
|
||||||
// harder here than offline because the record spans ticks: the snapshot + restore
|
// because the record spans ticks: snapshot + restore live on
|
||||||
// live on RealtimeCaptureState, not a function-scope RAII destructor.
|
// RealtimeCaptureState, not a function-scope RAII destructor.
|
||||||
//
|
//
|
||||||
// SCOPE (this increment): TRACK scope only — records the selected track's OWN
|
// TRACK scope only (this increment): records the selected track's own output
|
||||||
// output (item + that track's own FX + its own fader/pan, PRE-parent), matching
|
// (item + track's own FX/fader/pan, pre-parent), matching offline's track
|
||||||
// offline's track scope. This needs NO FxBypassGuard: a send tapping a track's
|
// scope. Needs no FxBypassGuard — a send tapping a track's output is naturally
|
||||||
// output is naturally PRE-parent (the parent has not summed it yet), so the tap is
|
// pre-parent, so the tap is chain-independent by construction. Item realtime
|
||||||
// chain-independent by construction. Item realtime is deferred (UnsupportedMode).
|
// is deferred (UnsupportedMode).
|
||||||
class RealtimeRecordBackend {
|
class RealtimeRecordBackend {
|
||||||
public:
|
public:
|
||||||
// Starts a realtime record: validates the request (track scope, non-empty range,
|
// Validates the request (track scope, non-empty range, >=1 source track,
|
||||||
// at least one source track, active + saved project, transport idle), snapshots
|
// active+saved project, transport idle), snapshots state, creates the hidden
|
||||||
// all state to restore, creates the hidden temp track, routes a send FROM each
|
// temp track, routes a send from each source track into it, arms, and
|
||||||
// source track INTO the temp track, arms, and CSurf_OnRecord — then returns
|
// CSurf_OnRecord — then returns immediately. `sourceTracks` are resolved by
|
||||||
// IMMEDIATELY (no wait, no UI block). `sourceTracks` are the selected tracks to
|
// the caller; CaptureRequest itself stays REAPER-free. On success the
|
||||||
// tap (resolved by the action layer — the CaptureRequest itself stays REAPER-free,
|
// returned unique_ptr owns the in-flight state; on failure returns nullptr
|
||||||
// carrying only the provenance GUIDs). On success the returned unique_ptr owns the
|
// with `outFailure` filled (nothing left mutated).
|
||||||
// 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,
|
RealtimeCaptureHandle begin(const CaptureRequest& request,
|
||||||
const std::vector<MediaTrack*>& sourceTracks,
|
const std::vector<MediaTrack*>& sourceTracks,
|
||||||
CaptureResult& outFailure);
|
CaptureResult& outFailure);
|
||||||
|
|
||||||
// Advances the in-flight record one tick. Reads the transport (bound to the
|
// Reads the transport (bound to the record's OWN project handle so a project
|
||||||
// record's OWN project handle so a project switch cannot confuse it), and on a
|
// switch can't confuse it); on a terminal verdict stops the transport,
|
||||||
// terminal verdict stops the transport, finalizes the recorded file into the
|
// finalizes the recorded file (Done) or reports the failure (Failed), then
|
||||||
// bank Sample (Done) or reports the failure (Failed), then restores ALL
|
// restores all snapshotted state. After Done/Failed the state is spent.
|
||||||
// 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);
|
RealtimeTickResult tick(RealtimeCaptureState& state);
|
||||||
|
|
||||||
// Force-terminate an in-flight record NOW without waiting for the range end:
|
// Force-terminate now without waiting for the range end: stops transport,
|
||||||
// stops the transport, finalizes whatever was captured (best effort) or abandons
|
// finalizes best-effort or abandons, restores all snapshotted state. For
|
||||||
// it, and restores ALL snapshotted state. For the shutdown / project-switch
|
// shutdown/project-switch paths where the record must not leak a temp
|
||||||
// paths (extension unload, a new project became active) where the record must
|
// track/armed track/altered transport. Idempotent.
|
||||||
// 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);
|
RealtimeTickResult abort(RealtimeCaptureState& state);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,27 @@
|
|||||||
// insert.cpp — REAPER-facing placement shell (M6). See insert.h.
|
// insert.cpp — REAPER-facing placement shell. See insert.h.
|
||||||
//
|
//
|
||||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
|
||||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
|
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
|
||||||
// extern (CLAUDE.md §contract).
|
// extern.
|
||||||
//
|
//
|
||||||
// THIS IS THE INTENDED PLACEMENT PATH. Unlike capture / bank_panel (which never
|
// Unlike capture / bank_panel (which never touch the arrange), insert deliberately
|
||||||
// touch the arrange), insert deliberately adds items to the arrange — that is its
|
// adds items to the arrange — that is its whole job. Runs only from its own action.
|
||||||
// whole job (CONTEXT.md §load-bearing principle). It runs ONLY from its own action.
|
|
||||||
//
|
//
|
||||||
// FLAGGED RUNTIME ASSUMPTIONS (semantics the header does not fully specify — must
|
// Runtime assumptions the SDK header doesn't fully spell out (flagged, not yet
|
||||||
// be DAW-verified by Daniel post-merge; see the handoff):
|
// DAW-verified):
|
||||||
// A. InsertMedia base mode 0 ("add to current track") targets the track that is
|
// A. InsertMedia mode 0 ("add to current track") is assumed to target the sole
|
||||||
// currently the ONLY selected track. The header names the base target but does
|
// selected track — the header doesn't spell out how "current" resolves, so we
|
||||||
// not spell out how "current track" resolves at runtime. We force exactly one
|
// force exactly one selection via SetOnlyTrackSelected before each call. If
|
||||||
// selected track via SetOnlyTrackSelected before each InsertMedia call, which
|
// REAPER means last-focused rather than last-selected, this needs revisiting.
|
||||||
// is the most defensible interpretation; if REAPER uses a different notion of
|
// B. Mode 0 is assumed to insert at the edit cursor (REAPER's documented
|
||||||
// "current" (e.g. last-focused, not last-selected), DAW-verify and adjust.
|
// convention for base modes 0/1; the header has no explicit "at cursor" bit).
|
||||||
// B. InsertMedia mode 0 inserts AT THE EDIT CURSOR. Placement at the edit cursor
|
// C. InsertMedia may advance the cursor to the end of the inserted media; we
|
||||||
// is REAPER's documented convention for base modes 0/1 (the header does not
|
// reset to the snapshot position before each track's insert, so this doesn't
|
||||||
// spell out an explicit "at edit cursor" bit). Flagged for DAW-verification.
|
// matter either way.
|
||||||
// C. InsertMedia ADVANCES the edit cursor to the end of the inserted media. We
|
// D. SetEditCurPos(time, false, false) — moveview=false, seekplay=false — is
|
||||||
// reset the cursor to the snapshot position before EACH track's insert, so
|
// assumed to move the cursor without scrolling the view or the transport.
|
||||||
// assumption C's truth or falsity is irrelevant: we own the cursor reset.
|
// E. SetOnlyTrackSelected deselects all tracks and selects exactly one (per its
|
||||||
// D. SetEditCurPos(time, false, false) moves the cursor without scrolling the view
|
// header doc-comment — the strongest confirmation we have here).
|
||||||
// and without seeking the transport. The header lists the args as
|
|
||||||
// (time, moveview, seekplay) — moveview=false and seekplay=false are the
|
|
||||||
// non-disruptive choice; flagged in case the DAW shows otherwise.
|
|
||||||
// E. SetOnlyTrackSelected deselects all tracks and selects exactly one. The header
|
|
||||||
// doc-comment says "Set exactly one track selected, deselect all others" —
|
|
||||||
// this is the strongest confirmation we have; flagged for DAW-verification.
|
|
||||||
|
|
||||||
#include "shell/capture/insert.h"
|
#include "shell/capture/insert.h"
|
||||||
|
|
||||||
@@ -57,7 +50,6 @@
|
|||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
|
|
||||||
using capture::computeInsertMode;
|
using capture::computeInsertMode;
|
||||||
using capture::normalizeSlashes;
|
using capture::normalizeSlashes;
|
||||||
using capture::resolveBankFile;
|
using capture::resolveBankFile;
|
||||||
@@ -67,11 +59,10 @@ namespace {
|
|||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
// The current project's directory (mirrors bank_panel/capture/persist). The bank
|
// Mirrors bank_panel/capture/persist's own derivation; the bank index stores
|
||||||
// index stores relative paths; resolving a bank file needs the current .rpp dir.
|
// relative paths, so resolving a file needs the current .rpp dir. A shared "current
|
||||||
// FOLLOW-UP (already noted in panel_bank_ops.cpp): a shared "current project dir"
|
// project dir" helper would be a clean small refactor now that a fourth consumer
|
||||||
// REAPER helper is a clean small refactor now that a fourth consumer exists — out
|
// exists (also noted in panel_bank_ops.cpp) — out of scope here.
|
||||||
// of scope for M6.
|
|
||||||
std::string currentProjectDir() {
|
std::string currentProjectDir() {
|
||||||
std::vector<char> buf(4096, '\0');
|
std::vector<char> buf(4096, '\0');
|
||||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||||
@@ -80,9 +71,8 @@ std::string currentProjectDir() {
|
|||||||
return normalizeSlashes(fs::path(rpp).parent_path().string());
|
return normalizeSlashes(fs::path(rpp).parent_path().string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot the user's currently-selected track set (ignores master, matches
|
// Snapshot of the currently-selected track set (master is skipped, matching
|
||||||
// CountSelectedTracks / GetSelectedTrack which both skip master). Returns the
|
// CountSelectedTracks/GetSelectedTrack), in selection order, for restore later.
|
||||||
// tracks in selection order so we can restore the original state afterward.
|
|
||||||
std::vector<MediaTrack*> snapshotSelectedTracks() {
|
std::vector<MediaTrack*> snapshotSelectedTracks() {
|
||||||
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
||||||
std::vector<MediaTrack*> tracks;
|
std::vector<MediaTrack*> tracks;
|
||||||
@@ -92,12 +82,10 @@ std::vector<MediaTrack*> snapshotSelectedTracks() {
|
|||||||
return tracks;
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore a previously-snapshotted track selection: deselect all (by setting the
|
// Restores a snapshotted selection: deselect all via the first track, then
|
||||||
// first track alone) then re-select the full set. If the snapshot is empty we
|
// re-select the rest. Empty snapshot -> no-op (guards an empty project).
|
||||||
// leave all tracks deselected; no-op guard handles a completely empty project.
|
|
||||||
void restoreSelectedTracks(const std::vector<MediaTrack*>& tracks) {
|
void restoreSelectedTracks(const std::vector<MediaTrack*>& tracks) {
|
||||||
if (tracks.empty()) return;
|
if (tracks.empty()) return;
|
||||||
// Deselect all via the first track, then re-add the rest.
|
|
||||||
SetOnlyTrackSelected(tracks[0]);
|
SetOnlyTrackSelected(tracks[0]);
|
||||||
for (size_t i = 1; i < tracks.size(); ++i)
|
for (size_t i = 1; i < tracks.size(); ++i)
|
||||||
SetTrackSelected(tracks[i], true);
|
SetTrackSelected(tracks[i], true);
|
||||||
@@ -109,9 +97,8 @@ InsertResult runInsert(ReaSamplerSession* session, const InsertRequest& request)
|
|||||||
InsertResult result;
|
InsertResult result;
|
||||||
if (!session) { result.status = InsertStatus::NoSelection; return result; }
|
if (!session) { result.status = InsertStatus::NoSelection; return result; }
|
||||||
|
|
||||||
// WHO to target: the user's currently-selected track set. No-op (with a clear
|
// WHO: the user's selected track set. No-op when nothing is selected — inserting
|
||||||
// console message) when nothing is selected — inserting without a target track
|
// without a target track would create an unintended track or behave unpredictably.
|
||||||
// would create an unintended new track or behave unpredictably.
|
|
||||||
const std::vector<MediaTrack*> selectedTracks = snapshotSelectedTracks();
|
const std::vector<MediaTrack*> selectedTracks = snapshotSelectedTracks();
|
||||||
if (selectedTracks.empty()) {
|
if (selectedTracks.empty()) {
|
||||||
ShowConsoleMsg("ReaSampler insert: select a track first.\n");
|
ShowConsoleMsg("ReaSampler insert: select a track first.\n");
|
||||||
@@ -119,22 +106,20 @@ InsertResult runInsert(ReaSamplerSession* session, const InsertRequest& request)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// WHAT to place: the single focused sample from the panel. Multi-select is
|
// WHAT: the single focused sample from the panel; multi-select is deprioritized,
|
||||||
// deprioritized; take the first (or only) selected id. An empty panel selection
|
// so take the first id. Empty selection -> no-op.
|
||||||
// is a no-op — nothing to place.
|
|
||||||
const std::vector<std::string> ids = bankPanelSelectedSampleIds();
|
const std::vector<std::string> ids = bankPanelSelectedSampleIds();
|
||||||
if (ids.empty()) { result.status = InsertStatus::NoSelection; return result; }
|
if (ids.empty()) { result.status = InsertStatus::NoSelection; return result; }
|
||||||
const std::string& id = ids.front(); // focused / first selected — single sample
|
const std::string& id = ids.front(); // focused / first selected — single sample
|
||||||
|
|
||||||
// WHERE the bank lives on disk. An unsaved project has no resolvable bank dir;
|
// WHERE: an unsaved project has no resolvable bank dir; no-op rather than
|
||||||
// insert is a no-op rather than resolving against CWD (CLAUDE.md invariant).
|
// resolving against CWD.
|
||||||
const std::string projectDir = currentProjectDir();
|
const std::string projectDir = currentProjectDir();
|
||||||
if (projectDir.empty()) { result.status = InsertStatus::NoProject; return result; }
|
if (projectDir.empty()) { result.status = InsertStatus::NoProject; return result; }
|
||||||
|
|
||||||
// Resolve the id against the bank the SELECTION came from — under B4's vertical
|
// Resolve against the bank the selection came from — it may be the pool or a
|
||||||
// split the selection may live in the pool or a shown named bank, which is NOT
|
// shown named bank, not necessarily the active/capture-target bank. Fall back to
|
||||||
// necessarily the active/capture-target bank. Fall back to the active bank when
|
// the active bank when the source id names no bank (defensive).
|
||||||
// the source id names no bank (defensive).
|
|
||||||
const std::string srcBankId = bankPanelSelectedSourceBankId();
|
const std::string srcBankId = bankPanelSelectedSourceBankId();
|
||||||
const BankModel* srcIndex = session->book().index(srcBankId);
|
const BankModel* srcIndex = session->book().index(srcBankId);
|
||||||
const BankModel& bank = srcIndex ? *srcIndex : session->bank();
|
const BankModel& bank = srcIndex ? *srcIndex : session->bank();
|
||||||
@@ -153,16 +138,14 @@ InsertResult runInsert(ReaSamplerSession* session, const InsertRequest& request)
|
|||||||
// position for each track insert (and after the whole operation).
|
// position for each track insert (and after the whole operation).
|
||||||
const double cursorPos = GetCursorPosition();
|
const double cursorPos = GetCursorPosition();
|
||||||
|
|
||||||
// Wrap the whole placement (all tracks + selection/cursor save-restore) in ONE
|
// One undo block around the whole placement (all tracks + selection/cursor
|
||||||
// undo block so a single undo removes every item and restores the state before
|
// restore) so a single undo removes every item and restores prior state. Always
|
||||||
// the action. Opened before the first InsertMedia, closed after the restore,
|
// balanced — opened before the first insert, closed after the restore.
|
||||||
// unconditionally — the block is always balanced.
|
|
||||||
Undo_BeginBlock2(nullptr);
|
Undo_BeginBlock2(nullptr);
|
||||||
|
|
||||||
// Insert onto EACH selected track at the SAME edit-cursor position (assumption B).
|
// Insert onto each selected track at the same cursor position (assumption B): per
|
||||||
// For each track: isolate it as the only selection so InsertMedia mode 0 targets
|
// track, isolate it as the only selection (A + E), reset the cursor (C is
|
||||||
// it unambiguously (assumption A + E), reset the cursor to the snapshot position
|
// irrelevant since we own the reset), then insert.
|
||||||
// (assumption C cursor advance is irrelevant — we own the reset), then insert.
|
|
||||||
for (MediaTrack* track : selectedTracks) {
|
for (MediaTrack* track : selectedTracks) {
|
||||||
SetOnlyTrackSelected(track); // assumption A + E
|
SetOnlyTrackSelected(track); // assumption A + E
|
||||||
SetEditCurPos(cursorPos, false, false); // assumption D
|
SetEditCurPos(cursorPos, false, false); // assumption D
|
||||||
@@ -170,14 +153,12 @@ InsertResult runInsert(ReaSamplerSession* session, const InsertRequest& request)
|
|||||||
++result.inserted;
|
++result.inserted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the user's original track selection and cursor position so the action
|
// Restore the original selection + cursor — non-destructive to the user's DAW state.
|
||||||
// is non-destructive to their DAW state (non-negotiable per the brief).
|
|
||||||
restoreSelectedTracks(selectedTracks);
|
restoreSelectedTracks(selectedTracks);
|
||||||
SetEditCurPos(cursorPos, false, false);
|
SetEditCurPos(cursorPos, false, false);
|
||||||
|
|
||||||
// Label reflects the count and the conform choice so the undo history reads
|
// Label reflects count + conform choice for a clear undo history. extraflags -1 =
|
||||||
// clearly ("ReaSampler: insert on 2 tracks" etc.). extraflags -1 = UNDO_STATE_ALL
|
// UNDO_STATE_ALL (tracks, items, envelope points, project state).
|
||||||
// (superset: tracks, items, envelope points, project state).
|
|
||||||
const std::string label =
|
const std::string label =
|
||||||
"ReaSampler: insert on " + std::to_string(result.inserted) +
|
"ReaSampler: insert on " + std::to_string(result.inserted) +
|
||||||
(result.inserted == 1 ? " track" : " tracks") +
|
(result.inserted == 1 ? " track" : " tracks") +
|
||||||
|
|||||||
+14
-18
@@ -1,21 +1,17 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// insert — placement of bank samples into the arrange (M6). REAPER-facing shell:
|
// Placement of bank samples into the arrange. REAPER-facing shell: reads the
|
||||||
// it reads the bank_panel's current selection, resolves each selected sample's
|
// bank_panel's current selection, resolves each selected sample's file, and drops
|
||||||
// file, and drops it into the arrange at the edit cursor via InsertMedia, wrapped
|
// it into the arrange at the edit cursor via InsertMedia, wrapped in an undo block.
|
||||||
// in an undo block.
|
|
||||||
//
|
//
|
||||||
// THE INTENDED PLACEMENT PATH (CONTEXT.md §load-bearing principle): capture NEVER
|
// The deliberate, user-invoked placement act (root CLAUDE.md §load-bearing
|
||||||
// auto-inserts; `insert` is the deliberate, user-invoked placement act, so it IS
|
// principle: capture never auto-inserts) — must only ever run from its own action,
|
||||||
// allowed and expected to add items to the arrange. It must only ever run from its
|
// never from a capture path.
|
||||||
// own action — never from a capture path.
|
|
||||||
//
|
//
|
||||||
// Non-destructive to the bank: insert references the bank file (adds an arrange
|
// Non-destructive to the bank: references the bank file, never modifies it or ext
|
||||||
// item pointing at it); it never modifies the bank, the bank files, or ext state.
|
// state. No silent time-stretch: conform-to-tempo is an explicit opt-in, defaulting
|
||||||
// No SILENT time-stretch: conform-to-tempo is an explicit opt-in on the request,
|
// off (native length) — see insert_plan for the mode-bit computation.
|
||||||
// defaulting OFF (native length). See insert_plan for the mode-bit computation.
|
|
||||||
//
|
//
|
||||||
// The header is SDK-free: all REAPER API use lives in insert.cpp. The pure
|
// SDK-free header; all REAPER API use lives in insert.cpp.
|
||||||
// mode-bit arithmetic lives in insert_plan (unit-tested outside the DAW).
|
|
||||||
|
|
||||||
#include "core/capture/insert_plan.h"
|
#include "core/capture/insert_plan.h"
|
||||||
|
|
||||||
@@ -32,16 +28,16 @@ struct InsertRequest {
|
|||||||
|
|
||||||
// The outcome of an insert action, for the caller to log to the console.
|
// The outcome of an insert action, for the caller to log to the console.
|
||||||
enum class InsertStatus {
|
enum class InsertStatus {
|
||||||
Ok, // one or more samples inserted
|
Ok,
|
||||||
NoSelection, // the panel had no selection — a no-op (not an error)
|
NoSelection, // the panel had no selection — a no-op, not an error
|
||||||
NoProject, // no saved project, so no resolvable bank dir — no-op
|
NoProject, // no saved project, so no resolvable bank dir — no-op
|
||||||
NothingResolved, // a selection existed but no sample resolved to a file
|
NothingResolved, // a selection existed but no sample resolved to a file
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InsertResult {
|
struct InsertResult {
|
||||||
InsertStatus status = InsertStatus::NoSelection;
|
InsertStatus status = InsertStatus::NoSelection;
|
||||||
int inserted = 0; // how many samples were actually placed
|
int inserted = 0;
|
||||||
int skipped = 0; // selected-but-unresolvable/unreadable samples skipped
|
int skipped = 0; // selected-but-unresolvable/unreadable samples
|
||||||
};
|
};
|
||||||
|
|
||||||
// Runs the insert: reads the bank panel's single focused sample and the user's
|
// Runs the insert: reads the bank panel's single focused sample and the user's
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// item_read.cpp — the single MediaItem* read seam (GUID + fixed-lane name). See
|
// item_read.cpp — the single MediaItem* read seam (GUID + fixed-lane name). See
|
||||||
// item_read.h. Compiled into the reaper_reasampler MODULE; includes
|
// item_read.h. Compiled into the reaper_reasampler module; includes
|
||||||
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT (main.cpp is the one TU that
|
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT (main.cpp is the one TU that
|
||||||
// defines the API pointers — CLAUDE.md §contract).
|
// defines the API pointers).
|
||||||
|
|
||||||
#include "shell/capture/item_read.h"
|
#include "shell/capture/item_read.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// item_read — the ONE place a MediaItem* is read for its canonical GUID string and for
|
// The one place a MediaItem* is read for its canonical GUID string and for the
|
||||||
// the durable P_LANENAME of the fixed lane it sits on. Before this seam, view.cpp and
|
// durable P_LANENAME of the fixed lane it sits on — the item-read analog of
|
||||||
// bank_panel.cpp each carried a near-identical private itemGuid / itemLaneName pair
|
// track_guid's single MediaTrack* -> GUID-key formatter. Extracted from
|
||||||
// (both files' comments acknowledged the deliberate copy); the D2 Wave-3-B item actions
|
// near-identical private itemGuid/itemLaneName pairs previously duplicated in
|
||||||
// need the same two reads, so the duplication is extracted here — the item-read analog
|
// view.cpp and bank_panel.cpp.
|
||||||
// of track_guid's single MediaTrack* -> GUID-key formatter.
|
|
||||||
//
|
//
|
||||||
// REAPER-facing shell: the .cpp includes reaper_plugin_functions.h WITHOUT
|
// The .cpp includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT
|
||||||
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers; here they are extern —
|
// (main.cpp owns the API pointers). MediaItem/MediaTrack are forward-declared so
|
||||||
// CLAUDE.md §contract). MediaItem / MediaTrack are forward-declared so this header
|
// this header stays SDK-lite. These are shell reads; the managed/manual decision
|
||||||
// stays SDK-lite. These are shell reads (REAPER string/value getters); the managed/
|
// that consumes the lane name stays pure in lane_keys (isOnManualLane).
|
||||||
// manual DECISION that consumes the lane name stays pure in lane_keys (isOnManualLane).
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
// provenance_shell.cpp — the REAPER reads behind Milestone 10. See provenance_shell.h.
|
// provenance_shell.cpp — the REAPER reads behind provenance. See provenance_shell.h.
|
||||||
|
// Every REAPER symbol used here is verified against
|
||||||
|
// vendor/reaper-sdk/sdk/reaper_plugin_functions.h.
|
||||||
//
|
//
|
||||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
|
||||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API pointers
|
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API pointers.
|
||||||
// (CLAUDE.md §contract). Every REAPER symbol used here is verified against
|
|
||||||
// vendor/reaper-sdk/sdk/reaper_plugin_functions.h:
|
|
||||||
// * TrackFX_GetCount(MediaTrack*) (~7283)
|
|
||||||
// * TrackFX_GetFXName(MediaTrack*, int, char*, int) -> bool (~7356)
|
|
||||||
// * TrackFX_GetFXGUID(MediaTrack*, int) -> GUID* (~7348)
|
|
||||||
// * TrackFX_GetEnabled(MediaTrack*, int) -> bool (~7291)
|
|
||||||
// * TakeFX_GetCount(MediaItem_Take*) (~6710)
|
|
||||||
// * TakeFX_GetFXName(MediaItem_Take*, int, char*, int) -> bool (~6758)
|
|
||||||
// * TakeFX_GetFXGUID(MediaItem_Take*, int) -> GUID* (~6750)
|
|
||||||
// * TakeFX_GetEnabled(MediaItem_Take*, int) -> bool (~6718)
|
|
||||||
// * CountSelectedMediaItems / GetSelectedMediaItem (selection reads)
|
|
||||||
// * GetActiveTake(MediaItem*) -> MediaItem_Take* (active take)
|
|
||||||
// * GetMediaItemTake_Source(MediaItem_Take*) -> PCM_source* (~2053)
|
|
||||||
// * GetMediaSourceFileName(PCM_source*, char*, int) (~2141)
|
|
||||||
// * CountTracks / GetTrack (track scan)
|
|
||||||
// * guidToString (via track_guid)
|
|
||||||
|
|
||||||
#include "shell/capture/provenance_shell.h"
|
#include "shell/capture/provenance_shell.h"
|
||||||
|
|
||||||
@@ -51,7 +37,6 @@
|
|||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
|
|
||||||
using capture::normalizeSlashes;
|
using capture::normalizeSlashes;
|
||||||
using capture::resolveBankFile;
|
using capture::resolveBankFile;
|
||||||
|
|
||||||
@@ -80,9 +65,8 @@ std::string fxChainIdentityForTrack(MediaTrack* tr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string fxChainIdentityForItems(const std::vector<MediaItem*>& items) {
|
std::string fxChainIdentityForItems(const std::vector<MediaItem*>& items) {
|
||||||
// For Item scope the in-scope chain is each item's active take's FX chain, NOT
|
// The owning track's chain is out of scope for an item capture (bypassed
|
||||||
// the owning track's FX chain (the track chain is out-of-scope and is bypassed
|
// during render) — TakeFX_* on the active take is the correct family here.
|
||||||
// during render). TakeFX_* is the correct family here.
|
|
||||||
std::vector<std::string> perItem;
|
std::vector<std::string> perItem;
|
||||||
perItem.reserve(items.size());
|
perItem.reserve(items.size());
|
||||||
for (MediaItem* it : items) {
|
for (MediaItem* it : items) {
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// provenance_shell — the REAPER-facing reads Milestone 10 needs, in one place.
|
// The REAPER-facing reads provenance needs, in one place. The pure provenance
|
||||||
//
|
// module (provenance.h) owns the fingerprint encoding, the recipe model, the
|
||||||
// The PURE provenance module (provenance.h) owns the fingerprint encoding, the
|
// FX-identity fold, and the parent-detection decision, all over plain
|
||||||
// recipe model, the FX-identity fold, and the parent-detection DECISION — all over
|
// strings/values; this shell gathers those strings/values from REAPER:
|
||||||
// plain strings/values. This shell gathers those strings/values FROM REAPER:
|
|
||||||
// * the in-scope FX-chain identity of a source track (name/GUID/enabled rows),
|
// * the in-scope FX-chain identity of a source track (name/GUID/enabled rows),
|
||||||
// * the media-file paths of a resolved capture's source items,
|
// * the media-file paths of a resolved capture's source items,
|
||||||
// * the active book's bank samples resolved to absolute file paths,
|
// * the active book's bank samples resolved to absolute file paths,
|
||||||
// * a canonical track-GUID string back to a live MediaTrack*.
|
// * a canonical track-GUID string back to a live MediaTrack*.
|
||||||
//
|
//
|
||||||
// REAPER-facing: the .cpp includes reaper_plugin_functions.h WITHOUT
|
// The .cpp includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT
|
||||||
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers; here they are extern —
|
// (main.cpp owns the API pointers). MediaTrack is forward-declared so this header
|
||||||
// CLAUDE.md §contract). MediaTrack is forward-declared so this header stays
|
// stays SDK-lite.
|
||||||
// SDK-lite. It depends on the pure provenance module (FxIdentityEntry / recipe /
|
|
||||||
// BankFileRef) and bank_book (to enumerate the active book's samples).
|
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -28,53 +25,43 @@ namespace reasampler {
|
|||||||
|
|
||||||
class BankBook;
|
class BankBook;
|
||||||
|
|
||||||
// Real-namespace-home using-declaration (Q-W6: the namespaces.h shim is retired).
|
|
||||||
using model::BankFileRef;
|
using model::BankFileRef;
|
||||||
|
|
||||||
// The in-scope FX-chain identity of a source track (Track scope), folded to the
|
// The in-scope FX-chain identity of a source track (Track scope), folded to the
|
||||||
// pure provenance string. Reads the track's own FX chain via TrackFX_GetCount /
|
// pure provenance string via the track's own FX chain (TrackFX_*) in chain order.
|
||||||
// TrackFX_GetFXName / TrackFX_GetFXGUID / TrackFX_GetEnabled in chain order.
|
|
||||||
std::string fxChainIdentityForTrack(MediaTrack* tr);
|
std::string fxChainIdentityForTrack(MediaTrack* tr);
|
||||||
|
|
||||||
// The in-scope FX-chain identity for Item scope: enumerates each item's active
|
// The in-scope FX-chain identity for Item scope: enumerates each item's active
|
||||||
// take FX chain via TakeFX_GetCount / TakeFX_GetFXName / TakeFX_GetFXGUID /
|
// take FX chain (TakeFX_*), in item order then FX order, combined with
|
||||||
// TakeFX_GetEnabled, in item order then FX order, combined with
|
// combineChainIdentities so distinct per-item partitions never collide. `items` is
|
||||||
// combineChainIdentities so distinct per-item partitions never collide. Returns
|
// the same source-item set the shell collected for the item-scope capture.
|
||||||
// the combined identity string (empty combined identity for a no-FX or no-item
|
|
||||||
// set). The items vector is the same source-item set the shell collected for the
|
|
||||||
// item-scope capture (selected items whose owning tracks were also collected).
|
|
||||||
std::string fxChainIdentityForItems(const std::vector<MediaItem*>& items);
|
std::string fxChainIdentityForItems(const std::vector<MediaItem*>& items);
|
||||||
|
|
||||||
// Reads the media-file path of every SELECTED media item's active take source
|
// The media-file path of every selected media item's active take source,
|
||||||
// (GetMediaItemTake_Source -> GetMediaSourceFileName), normalized to forward-slash.
|
// normalized to forward-slash. Unresolvable items (no take/source/name) are
|
||||||
// Unresolvable items (no take / no source / empty name) are omitted — never an
|
// omitted — never an empty string in the result, so detectParent's "not in bank"
|
||||||
// empty string in the result, so detectParent's "not in bank" branch is honest.
|
// branch is honest. This is the item-scope source set.
|
||||||
// The active-project selection is read directly (mirrors main.cpp's collectors).
|
|
||||||
// This is the ITEM-scope source set (the user selected the items being resampled).
|
|
||||||
std::vector<std::string> selectedItemSourceFiles();
|
std::vector<std::string> selectedItemSourceFiles();
|
||||||
|
|
||||||
// The TRACK-scope source set: the media-file paths of the items ON `tracks` that
|
// The track-scope source set: media-file paths of the items on `tracks` that
|
||||||
// OVERLAP the capture range [startSeconds, endSeconds). For a track capture the user
|
// overlap the capture range [startSeconds, endSeconds). A track capture selects
|
||||||
// selects the track, not the item, so the "what audio is being captured" set is the
|
// the track, not the item, so this is what "the source audio" means for it. Same
|
||||||
// range-overlapping items on the source tracks. Same normalize + omit-unresolvable
|
// normalize + omit-unresolvable contract as selectedItemSourceFiles; an item
|
||||||
// contract as selectedItemSourceFiles. An item overlaps iff its [pos, pos+len)
|
// overlaps iff its [pos, pos+len) intersects the range with positive overlap (a
|
||||||
// intersects the range with positive overlap (a zero-length touch does not count).
|
// zero-length touch does not count).
|
||||||
std::vector<std::string> trackItemSourceFiles(const std::vector<MediaTrack*>& tracks,
|
std::vector<std::string> trackItemSourceFiles(const std::vector<MediaTrack*>& tracks,
|
||||||
double startSeconds, double endSeconds);
|
double startSeconds, double endSeconds);
|
||||||
|
|
||||||
// Enumerates the ACTIVE book's samples across every bank (pool + named) as pure
|
// Enumerates the active book's samples across every bank as pure BankFileRefs,
|
||||||
// BankFileRefs — each sample id paired with its file resolved to a normalized
|
// each id paired with its file resolved to an absolute path against `projectDir`.
|
||||||
// ABSOLUTE path against `projectDir` (resolveBankFile + normalizeSlashes). A sample
|
// An unresolvable path (empty projectDir/relativePath) gets an empty absolutePath,
|
||||||
// whose path cannot be resolved (empty projectDir / empty relativePath) is emitted
|
// which detectParent never matches.
|
||||||
// with an empty absolutePath, which detectParent never matches. `projectDir` is the
|
|
||||||
// current .rpp parent (the shell resolves it; empty -> all refs unresolved).
|
|
||||||
std::vector<BankFileRef> bankFileRefs(const BankBook& book, const std::string& projectDir);
|
std::vector<BankFileRef> bankFileRefs(const BankBook& book, const std::string& projectDir);
|
||||||
|
|
||||||
// Resolves a canonical track-GUID string (guidString form) to a live MediaTrack*
|
// Resolves a canonical track-GUID string to a live MediaTrack* in the active
|
||||||
// in the active project by scanning tracks and comparing guidString(tr). Returns
|
// project. Returns nullptr when no live track carries that GUID (the source track
|
||||||
// nullptr when no live track carries that GUID (the source track was deleted since
|
// was deleted since capture — a re-capture failure mode the caller reports). The
|
||||||
// capture — a re-capture failure mode the caller reports). The master track is not
|
// master track is not scanned (no membership GUID, never a capture source).
|
||||||
// scanned (it has no membership GUID and is never a capture source).
|
|
||||||
MediaTrack* trackByGuid(const std::string& guid);
|
MediaTrack* trackByGuid(const std::string& guid);
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
// realtime_lifecycle.cpp — the in-flight realtime-capture state machine + globals
|
// realtime_lifecycle.cpp — the in-flight realtime-capture state machine + globals.
|
||||||
// (Q-W3 hoist out of main.cpp; the code moved verbatim, the session threaded as a
|
// See the header.
|
||||||
// parameter). See the header.
|
|
||||||
//
|
//
|
||||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
|
||||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
||||||
// pointers; here they are extern (CLAUDE.md §contract).
|
// pointers; here they are extern.
|
||||||
|
|
||||||
#include "shell/capture/realtime_lifecycle.h"
|
#include "shell/capture/realtime_lifecycle.h"
|
||||||
|
|
||||||
@@ -17,15 +16,13 @@
|
|||||||
|
|
||||||
namespace reasampler::capture {
|
namespace reasampler::capture {
|
||||||
|
|
||||||
// --- M8 in-flight realtime capture (async, timer-driven) --------------------
|
|
||||||
RealtimeRecordBackend g_rtBackend;
|
RealtimeRecordBackend g_rtBackend;
|
||||||
RealtimeCaptureHandle g_rtCapture;
|
RealtimeCaptureHandle g_rtCapture;
|
||||||
ReaProject* g_rtCaptureProject = nullptr;
|
ReaProject* g_rtCaptureProject = nullptr;
|
||||||
|
|
||||||
// Commit a finished realtime capture (a Done tick/abort with an Ok result): add the
|
// Commits a finished realtime capture (a Done tick/abort with an Ok result): adds
|
||||||
// Sample to the ACTIVE bank (session.bank() resolves to book.activeIndex() — B2),
|
// the Sample to the active bank, persists + MarkProjectDirty. Shared by the
|
||||||
// persist + MarkProjectDirty. Shared by the tick-completion path and the abort
|
// tick-completion and abort paths. On a non-Ok result, logs the failure only.
|
||||||
// paths. On a non-Ok result, logs the failure only.
|
|
||||||
void CommitRealtimeResult(ReaSamplerSession& session, const CaptureResult& res)
|
void CommitRealtimeResult(ReaSamplerSession& session, const CaptureResult& res)
|
||||||
{
|
{
|
||||||
if (res.status != CaptureStatus::Ok)
|
if (res.status != CaptureStatus::Ok)
|
||||||
@@ -34,40 +31,36 @@ void CommitRealtimeResult(ReaSamplerSession& session, const CaptureResult& res)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
session.bank().add(res.sample);
|
session.bank().add(res.sample);
|
||||||
// B-cap: record the file the capture created in the owned-file manifest, at the same
|
// Record the file in the owned manifest regardless of the index AddResult — even
|
||||||
// point the Sample is added and before the same persist. Recorded regardless of the
|
// a hash-collapse still wrote a file the tool owns; the manifest dedups a repeat
|
||||||
// index AddResult — even a hash-collapse still WROTE a file the tool owns, and the
|
// path itself (prune reconciles manifest vs index).
|
||||||
// manifest dedups a repeat path itself (Phase R prune reconciles manifest vs index).
|
|
||||||
session.owned().add(res.sample.relativePath);
|
session.owned().add(res.sample.relativePath);
|
||||||
// S9: a capture add changes what a live instance could play (a new sample landed in the
|
// A capture add changes what a live instance could play, so bump the generation
|
||||||
// active bank) -> bump before the persist so the stamped generation refreshes instances.
|
// before persisting to refresh instances.
|
||||||
session.bumpBankGeneration();
|
session.bumpBankGeneration();
|
||||||
session.saveToActiveProject(); // persist book + manifest + generation + MarkProjectDirty (travels with .rpp)
|
session.saveToActiveProject(); // persist book + manifest + generation + MarkProjectDirty
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance any in-flight realtime capture one tick. Cheap when none is running (a
|
// Advances any in-flight realtime capture one tick. Detects a project switch
|
||||||
// null check) and fast even mid-record (tick() only reads the transport until the
|
// mid-capture and aborts+restores so the capture never leaks across projects.
|
||||||
// terminal tick). Detects a project switch mid-capture and aborts+restores so the
|
// Called from OnTimer before session.poll() so poll's own project-switch handling
|
||||||
// capture never leaks across projects. Called from OnTimer BEFORE session.poll() so
|
// sees an already-cleaned-up project.
|
||||||
// poll's project-switch handling sees a cleaned-up project.
|
|
||||||
void DriveRealtimeCapture(ReaSamplerSession& session)
|
void DriveRealtimeCapture(ReaSamplerSession& session)
|
||||||
{
|
{
|
||||||
if (!g_rtCapture) return;
|
if (!g_rtCapture) return;
|
||||||
|
|
||||||
// Project switch guard: if the active project is no longer the one the capture
|
// If the active project is no longer the one the capture belongs to, a project
|
||||||
// belongs to, a new/other project became active mid-record — abort + restore
|
// switch happened mid-record: abort + restore into the original project the
|
||||||
// (into the ORIGINAL project the state is bound to) and drop it. Do NOT finalize
|
// state is bound to, and drop it — never finalize into the new project.
|
||||||
// into the new project.
|
|
||||||
ReaProject* active = EnumProjects(-1, nullptr, 0);
|
ReaProject* active = EnumProjects(-1, nullptr, 0);
|
||||||
if (active != g_rtCaptureProject)
|
if (active != g_rtCaptureProject)
|
||||||
{
|
{
|
||||||
RealtimeTickResult r = g_rtBackend.abort(*g_rtCapture);
|
RealtimeTickResult r = g_rtBackend.abort(*g_rtCapture);
|
||||||
// Only commit if the ORIGINAL project is still open and active would be it —
|
// Log without persisting — we restored into the original project but must
|
||||||
// on a switch we restored into the original but must not persist into the
|
// not persist into the now-active foreign one. A Failed abort surfaces
|
||||||
// now-active foreign project. Log the outcome without persisting. On a Failed
|
// abort()'s own message, distinguishing a clean tab-switch abort from the
|
||||||
// abort surface abort()'s own message — it distinguishes a clean tab-switch
|
// closed-project case (nothing restored because the pointers were already
|
||||||
// abort from the closed-project DROP (the captured project was closed mid-record,
|
// freed).
|
||||||
// review §1: nothing restored because the pointers were already freed).
|
|
||||||
if (r.status == RealtimeTickStatus::Done)
|
if (r.status == RealtimeTickStatus::Done)
|
||||||
ShowConsoleMsg("ReaSampler realtime capture: project switched mid-record -- "
|
ShowConsoleMsg("ReaSampler realtime capture: project switched mid-record -- "
|
||||||
"captured audio restored into the original project; not "
|
"captured audio restored into the original project; not "
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// realtime_lifecycle — the in-flight realtime-capture state machine + globals
|
// The in-flight realtime-capture state machine + globals. A realtime record spans
|
||||||
// (Q-W3 hoist out of main.cpp). A realtime record spans many timer ticks (it takes
|
// many timer ticks (it takes end-start wall-clock seconds and must not block
|
||||||
// end-start wall-clock seconds and must NOT block REAPER's UI): the action STARTS
|
// REAPER's UI): the action starts it (RunCaptureRealtimeTrack -> g_rtBackend.begin),
|
||||||
// it (capture_orchestrator::RunCaptureRealtimeTrack -> g_rtBackend.begin), OnTimer
|
// OnTimer drives it here (DriveRealtimeCapture -> g_rtBackend.tick) each tick until a
|
||||||
// drives it here (DriveRealtimeCapture -> g_rtBackend.tick) each tick until a
|
|
||||||
// terminal verdict, then the handle is cleared.
|
// terminal verdict, then the handle is cleared.
|
||||||
//
|
//
|
||||||
// The three globals are EXPOSED (extern) rather than wrapped: the action bodies in
|
// The three globals are extern rather than wrapped so the timer's idle fast path
|
||||||
// capture_orchestrator manipulate them exactly as main.cpp did (zero-behavior-change
|
// stays a single pointer test at the call site — load-bearing:
|
||||||
// move), and — load-bearing (CONTEXT.md §Phase Q hot-path guardrail) — the timer's
|
|
||||||
// IDLE FAST-PATH stays a SINGLE POINTER TEST at the call site:
|
|
||||||
// if (capture::g_rtCapture) capture::DriveRealtimeCapture(g_session);
|
// if (capture::g_rtCapture) capture::DriveRealtimeCapture(g_session);
|
||||||
// No per-tick cross-TU call, no accessor indirection, when nothing is recording.
|
// No per-tick cross-TU call, no accessor indirection, when nothing is recording.
|
||||||
//
|
//
|
||||||
// REAPER-facing: the .cpp includes reaper_plugin_functions.h WITHOUT
|
// The .cpp includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT
|
||||||
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers — CLAUDE.md §contract).
|
// (main.cpp owns the API pointers).
|
||||||
|
|
||||||
#include "shell/capture/capture_realtime_shell.h" // RealtimeRecordBackend / RealtimeCaptureHandle
|
#include "shell/capture/capture_realtime_shell.h" // RealtimeRecordBackend / RealtimeCaptureHandle
|
||||||
|
|
||||||
@@ -24,33 +21,30 @@ class ReaSamplerSession;
|
|||||||
|
|
||||||
namespace reasampler::capture {
|
namespace reasampler::capture {
|
||||||
|
|
||||||
// The realtime backend + the in-flight capture handle. Non-null handle == a
|
// Non-null g_rtCapture == a capture is in progress: used to reject a second one,
|
||||||
// capture is in progress (used to reject a second one, to drive the per-tick
|
// drive the per-tick advance, and abort on project switch / unload.
|
||||||
// advance, and to abort on project switch / unload).
|
|
||||||
extern RealtimeRecordBackend g_rtBackend;
|
extern RealtimeRecordBackend g_rtBackend;
|
||||||
extern RealtimeCaptureHandle g_rtCapture;
|
extern RealtimeCaptureHandle g_rtCapture;
|
||||||
|
|
||||||
// The ReaProject* the in-flight capture belongs to (opaque, compare-only) — lets
|
// The project the in-flight capture belongs to (opaque, compare-only) — lets
|
||||||
// OnTimer detect a project switch mid-capture and abort+restore rather than leak the
|
// OnTimer detect a project switch mid-capture and abort+restore rather than leak the
|
||||||
// temp track/arm/transport into or across projects. Only meaningful when
|
// temp track/arm/transport across projects. Meaningful only when g_rtCapture != nullptr.
|
||||||
// g_rtCapture != nullptr.
|
|
||||||
extern ReaProject* g_rtCaptureProject;
|
extern ReaProject* g_rtCaptureProject;
|
||||||
|
|
||||||
// Commit a finished realtime capture (a Done tick/abort with an Ok result): add the
|
// Commits a finished realtime capture (a Done tick/abort with an Ok result): adds
|
||||||
// Sample to the ACTIVE bank, record the owned file, bump the generation, persist +
|
// the Sample to the active bank, records the owned file, bumps the generation,
|
||||||
// MarkProjectDirty. On a non-Ok result, logs the failure only.
|
// persists + MarkProjectDirty. On a non-Ok result, logs the failure only.
|
||||||
void CommitRealtimeResult(ReaSamplerSession& session, const CaptureResult& res);
|
void CommitRealtimeResult(ReaSamplerSession& session, const CaptureResult& res);
|
||||||
|
|
||||||
// Advance any in-flight realtime capture one tick. Cheap when none is running (a
|
// Advances any in-flight realtime capture one tick. Detects a project switch
|
||||||
// null check — though the caller already guards, see the header note) and fast even
|
// mid-capture and aborts+restores so the capture never leaks across projects.
|
||||||
// mid-record. Detects a project switch mid-capture and aborts+restores so the
|
// Called from OnTimer before session.poll().
|
||||||
// capture never leaks across projects. Called from OnTimer BEFORE session.poll().
|
|
||||||
void DriveRealtimeCapture(ReaSamplerSession& session);
|
void DriveRealtimeCapture(ReaSamplerSession& session);
|
||||||
|
|
||||||
// Unload teardown: abort any in-flight capture while the API pointers are still
|
// Unload teardown: abort any in-flight capture while the API pointers are still
|
||||||
// live — finalize-or-abort + restore so we never leave a temp track, an armed
|
// live — finalize-or-abort + restore so we never leave a temp track, an armed
|
||||||
// track, or an altered transport/cursor in the user's project on unload. Commits
|
// track, or an altered transport/cursor behind. Commits whatever was captured
|
||||||
// whatever was captured (best effort) before tearing down. No-op when idle.
|
// (best effort) before tearing down. No-op when idle.
|
||||||
void AbortRealtimeCaptureForUnload(ReaSamplerSession& session);
|
void AbortRealtimeCaptureForUnload(ReaSamplerSession& session);
|
||||||
|
|
||||||
} // namespace reasampler::capture
|
} // namespace reasampler::capture
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
// scope_resolve.cpp — scope/source resolution for the capture action family
|
// scope_resolve.cpp — scope/source resolution for the capture action family. See
|
||||||
// (Q-W3 hoist out of main.cpp; the code moved verbatim, session state threaded as
|
// the header.
|
||||||
// parameters). See the header.
|
|
||||||
//
|
//
|
||||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
|
||||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
||||||
// pointers; here they are extern (CLAUDE.md §contract).
|
// pointers; here they are extern.
|
||||||
|
|
||||||
#include "shell/capture/scope_resolve.h"
|
#include "shell/capture/scope_resolve.h"
|
||||||
|
|
||||||
@@ -50,8 +49,7 @@ model::ProvenanceScope provenanceScopeFor(CaptureScope scope)
|
|||||||
|
|
||||||
// Collects the tracks that own the selected items (Item scope) into
|
// Collects the tracks that own the selected items (Item scope) into
|
||||||
// out.sourceTracks (deduped) — these are the tracks whose FX must be bypassed so an
|
// out.sourceTracks (deduped) — these are the tracks whose FX must be bypassed so an
|
||||||
// item capture hears take/item FX only. GetMediaItem_Track(item) gives the owning
|
// item capture hears take/item FX only. GUIDs recorded for provenance.
|
||||||
// track (SDK header, verify). GUIDs recorded for provenance.
|
|
||||||
bool collectSelectedItemTracks(ResolvedSource& out)
|
bool collectSelectedItemTracks(ResolvedSource& out)
|
||||||
{
|
{
|
||||||
const int n = CountSelectedMediaItems(nullptr);
|
const int n = CountSelectedMediaItems(nullptr);
|
||||||
@@ -76,9 +74,8 @@ bool collectSelectedItemTracks(ResolvedSource& out)
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// Reads every track's P_RAZOREDITS (SDK header ~2899: space-separated triples of
|
// Reads every track's P_RAZOREDITS (SDK header ~2899: space-separated triples of
|
||||||
// start, end, envGuidString), parses the track-audio areas (pure parseRazorEdits),
|
// start, end, envGuidString) and returns the union of parsed track-audio areas.
|
||||||
// and returns the union bound. Reads only — never clears the razor selection.
|
// Reads only — never clears the razor selection.
|
||||||
// Returns false when no track-audio razor area exists on any track.
|
|
||||||
bool resolveRazorRange(double& start, double& end)
|
bool resolveRazorRange(double& start, double& end)
|
||||||
{
|
{
|
||||||
std::vector<RazorRange> allRanges;
|
std::vector<RazorRange> allRanges;
|
||||||
@@ -100,9 +97,6 @@ bool resolveRazorRange(double& start, double& end)
|
|||||||
return end > start;
|
return end > start;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infers the render RANGE for any scope: razor union when a razor area is present,
|
|
||||||
// else the time selection (pure inferRangeSource decides which). Orthogonal to
|
|
||||||
// scope. Returns false (with a reason) when neither yields a non-empty range.
|
|
||||||
bool resolveRange(double& start, double& end, std::string& why)
|
bool resolveRange(double& start, double& end, std::string& why)
|
||||||
{
|
{
|
||||||
double rzStart = 0.0, rzEnd = 0.0;
|
double rzStart = 0.0, rzEnd = 0.0;
|
||||||
@@ -117,7 +111,6 @@ bool resolveRange(double& start, double& end, std::string& why)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collects the selected tracks (Track scope) into out.sourceTracks + GUIDs.
|
|
||||||
bool collectSelectedTracks(ResolvedSource& out)
|
bool collectSelectedTracks(ResolvedSource& out)
|
||||||
{
|
{
|
||||||
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
||||||
@@ -133,8 +126,6 @@ bool collectSelectedTracks(ResolvedSource& out)
|
|||||||
return !out.sourceTracks.empty();
|
return !out.sourceTracks.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolves the source for a scope: the selection tracks (item/track), plus the
|
|
||||||
// inferred range. Returns false with a reason on nothing to do.
|
|
||||||
bool ResolveScopeSource(CaptureScope scope, ResolvedSource& out, std::string& why)
|
bool ResolveScopeSource(CaptureScope scope, ResolvedSource& out, std::string& why)
|
||||||
{
|
{
|
||||||
switch (scope)
|
switch (scope)
|
||||||
@@ -153,11 +144,8 @@ bool ResolveScopeSource(CaptureScope scope, ResolvedSource& out, std::string& wh
|
|||||||
return resolveRange(out.startSeconds, out.endSeconds, why);
|
return resolveRange(out.startSeconds, out.endSeconds, why);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current project's directory (parent of its .rpp), forward-slashed, no trailing
|
// Empty for an unsaved project (EnumProjects writes an empty .rpp path), which
|
||||||
// slash — the same derivation capture.cpp does internally, needed here so M10 can
|
// makes every bank file resolve empty -> no false parentage.
|
||||||
// resolve the bank's relative paths to absolute for parent detection. Empty for an
|
|
||||||
// unsaved project (EnumProjects writes an empty .rpp path), which makes every bank
|
|
||||||
// file resolve empty -> no false parentage. Read-only; mutates nothing.
|
|
||||||
std::string currentProjectDir()
|
std::string currentProjectDir()
|
||||||
{
|
{
|
||||||
std::vector<char> buf(4096, '\0');
|
std::vector<char> buf(4096, '\0');
|
||||||
@@ -171,16 +159,8 @@ std::string currentProjectDir()
|
|||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds the M10 provenance for a capture IF it genuinely resamples from a bank
|
// Detection rule: the capture's source item media file(s) must all resolve, by
|
||||||
// sample, else returns nullopt (the common, non-resample case). Detection rule
|
// exact normalized absolute path, to one bank sample's file.
|
||||||
// (stated honestly): the capture's source item media file(s) must all resolve, by
|
|
||||||
// exact normalized absolute path, to ONE bank sample's file (detectParent). On a
|
|
||||||
// match, records that sample's id as the parent plus a THIN capture-recipe
|
|
||||||
// fingerprint (P1=a) — scope + source mode + exact range + tail + rate + channels +
|
|
||||||
// source track GUIDs + the in-scope source FX-chain identity — so "re-capture from
|
|
||||||
// source" can replay the request and report drift. NEVER a serialized chain to
|
|
||||||
// restore. Item scope reads the active take's TakeFX chain (via TakeFX_*) per
|
|
||||||
// selected item, combined in item order; Track scope reads the track FX chain.
|
|
||||||
std::optional<model::Provenance> buildCaptureProvenance(
|
std::optional<model::Provenance> buildCaptureProvenance(
|
||||||
const BankBook& book, const CaptureRequest& req,
|
const BankBook& book, const CaptureRequest& req,
|
||||||
CaptureScope scope, const ResolvedSource& src)
|
CaptureScope scope, const ResolvedSource& src)
|
||||||
@@ -188,9 +168,9 @@ std::optional<model::Provenance> buildCaptureProvenance(
|
|||||||
const std::string projectDir = currentProjectDir();
|
const std::string projectDir = currentProjectDir();
|
||||||
const std::vector<model::BankFileRef> bankFiles = bankFileRefs(book, projectDir);
|
const std::vector<model::BankFileRef> bankFiles = bankFileRefs(book, projectDir);
|
||||||
|
|
||||||
// The "what audio is being captured" source set depends on scope: item scope uses
|
// What "the source audio" means depends on scope: item scope uses the selected
|
||||||
// the SELECTED items (the user picked them); track scope uses the range-overlapping
|
// items (the user picked them); track scope uses the range-overlapping items on
|
||||||
// items ON the source tracks (the user picked the track, not the item).
|
// the source tracks (the user picked the track, not the item).
|
||||||
const std::vector<std::string> sourceFiles =
|
const std::vector<std::string> sourceFiles =
|
||||||
scope == CaptureScope::Item
|
scope == CaptureScope::Item
|
||||||
? selectedItemSourceFiles()
|
? selectedItemSourceFiles()
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// scope_resolve — scope/source resolution for the capture action family (Q-W3
|
// Scope/source resolution shared by every capture entry point. Three concerns:
|
||||||
// hoist out of main.cpp). The three concerns every capture entry point shares:
|
// * range inference — razor union else time selection, orthogonal to scope;
|
||||||
// * RANGE inference — razor union else time selection (razor-else-time),
|
// * source-track collection — selected tracks (Track scope) or selected items'
|
||||||
// orthogonal to scope;
|
// owning tracks (Item scope), deduped, with canonical GUIDs;
|
||||||
// * SOURCE-TRACK collection — the selected tracks (Track scope) or the selected
|
// * provenance-assembly inputs — resample-from-sample detection + the thin
|
||||||
// items' owning tracks (Item scope), deduped, with canonical GUIDs;
|
// capture-recipe fingerprint built from the live (un-bypassed) chain.
|
||||||
// * PROVENANCE ASSEMBLY inputs — the M10 resample-from-sample detection + the
|
|
||||||
// thin capture-recipe fingerprint built from the LIVE (un-bypassed) chain.
|
|
||||||
//
|
//
|
||||||
// All reads are non-destructive: selection, razor, and time selection are read,
|
// All reads are non-destructive: selection, razor, and time selection are read,
|
||||||
// never mutated. REAPER-facing: the .cpp includes reaper_plugin_functions.h
|
// never mutated. The .cpp includes reaper_plugin_functions.h WITHOUT
|
||||||
// WITHOUT REAPERAPI_IMPLEMENT (main.cpp owns the API pointers — CLAUDE.md
|
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers). MediaTrack is forward-
|
||||||
// §contract). MediaTrack is forward-declared (via capture.h) so this header stays
|
// declared (via capture.h) so this header stays SDK-lite.
|
||||||
// SDK-lite.
|
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -35,18 +32,16 @@ struct ResolvedSource
|
|||||||
{
|
{
|
||||||
double startSeconds = 0.0;
|
double startSeconds = 0.0;
|
||||||
double endSeconds = 0.0;
|
double endSeconds = 0.0;
|
||||||
std::vector<MediaTrack*> sourceTracks; // item-owning tracks / selected tracks
|
std::vector<MediaTrack*> sourceTracks;
|
||||||
std::vector<std::string> trackGuids; // canonical GUIDs of sourceTracks
|
std::vector<std::string> trackGuids;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reads every track's P_RAZOREDITS, parses the track-audio areas (pure
|
// Reads every track's P_RAZOREDITS and returns the union of parsed track-audio
|
||||||
// parseRazorEdits), and returns the union bound. Reads only — never clears the
|
// areas. Reads only — never clears the razor selection.
|
||||||
// razor selection. Returns false when no track-audio razor area exists on any track.
|
|
||||||
bool resolveRazorRange(double& start, double& end);
|
bool resolveRazorRange(double& start, double& end);
|
||||||
|
|
||||||
// Infers the render RANGE for any scope: razor union when a razor area is present,
|
// Infers the render range for any scope: razor union when present, else the time
|
||||||
// else the time selection (pure inferRangeSource decides which). Orthogonal to
|
// selection. Returns false with a reason when neither yields a non-empty range.
|
||||||
// scope. Returns false (with a reason) when neither yields a non-empty range.
|
|
||||||
bool resolveRange(double& start, double& end, std::string& why);
|
bool resolveRange(double& start, double& end, std::string& why);
|
||||||
|
|
||||||
// Collects the selected tracks (Track scope) into out.sourceTracks + GUIDs.
|
// Collects the selected tracks (Track scope) into out.sourceTracks + GUIDs.
|
||||||
@@ -60,10 +55,10 @@ bool ResolveScopeSource(CaptureScope scope, ResolvedSource& out, std::string& wh
|
|||||||
// slash. Empty for an unsaved project (no false parentage). Read-only.
|
// slash. Empty for an unsaved project (no false parentage). Read-only.
|
||||||
std::string currentProjectDir();
|
std::string currentProjectDir();
|
||||||
|
|
||||||
// Builds the M10 provenance for a capture IF it genuinely resamples from a bank
|
// Builds the provenance for a capture if it genuinely resamples from a bank sample
|
||||||
// sample (detectParent over `book`'s resolved file refs), else returns nullopt (the
|
// (detectParent over `book`'s resolved file refs), else returns nullopt (the common,
|
||||||
// common, non-resample case). Must run BEFORE the FxBypassGuard neutralizes the
|
// non-resample case). Must run BEFORE the FxBypassGuard neutralizes the in-scope
|
||||||
// in-scope chain — the source FX-chain identity is read from the LIVE chain.
|
// chain — the source FX-chain identity is read from the live chain.
|
||||||
std::optional<model::Provenance> buildCaptureProvenance(
|
std::optional<model::Provenance> buildCaptureProvenance(
|
||||||
const BankBook& book, const CaptureRequest& req,
|
const BankBook& book, const CaptureRequest& req,
|
||||||
CaptureScope scope, const ResolvedSource& src);
|
CaptureScope scope, const ResolvedSource& src);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// track_guid.cpp — the single MediaTrack* -> canonical GUID key formatter. See
|
// track_guid.cpp — the single MediaTrack* -> canonical GUID key formatter. See
|
||||||
// track_guid.h. Compiled into the reaper_reasampler MODULE; includes
|
// track_guid.h. Compiled into the reaper_reasampler module; includes
|
||||||
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT (main.cpp is the one TU
|
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT (main.cpp is the one TU
|
||||||
// that defines the API pointers — CLAUDE.md §contract).
|
// that defines the API pointers).
|
||||||
|
|
||||||
#include "shell/capture/track_guid.h"
|
#include "shell/capture/track_guid.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// track_guid — the ONE place a MediaTrack* is formatted into the canonical GUID
|
// The one place a MediaTrack* is formatted into the canonical GUID string used as
|
||||||
// string used as a membership-index key. Both the Design View shell (view.cpp) and
|
// a membership-index key. Both the Design View shell (view.cpp) and the actions
|
||||||
// the actions layer (design_view_actions.cpp) key membership on this exact string, so the key
|
// layer (design_view_actions.cpp) key membership on this exact string, so the
|
||||||
// contract lives in a single helper rather than being re-derived (and drifting) at
|
// contract lives in a single helper rather than being re-derived at two call sites.
|
||||||
// two call sites (the cross-module key contract flagged in D2 review).
|
|
||||||
//
|
//
|
||||||
// REAPER-facing: the .cpp includes reaper_plugin_functions.h WITHOUT
|
// The .cpp includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT
|
||||||
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers; here they are extern —
|
// (main.cpp owns the API pointers). MediaTrack is forward-declared so this header
|
||||||
// CLAUDE.md §contract). MediaTrack is forward-declared so this header stays SDK-lite.
|
// stays SDK-lite.
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user