Ψ-W1-T1 second-round remediation: ±1-frame bounds tolerance, self-cleanup a refused render, hedge two unverified render-source inferences

Loosens the exact-bounds gate against REAPER's edge rounding; deletes the bytes a BoundsMismatch refusal writes, per prune_fs's self-cleanup carve-out.
This commit is contained in:
2026-08-01 21:18:03 -04:00
parent 7dc80e7a4f
commit a0220e8c57
7 changed files with 81 additions and 18 deletions
+32 -9
View File
@@ -21,11 +21,13 @@
#include "shell/capture/capture.h"
#include <atomic>
#include <cmath>
#include <cstdint>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <string>
#include <system_error>
#include <vector>
#include "core/capture/capture_paths.h"
@@ -396,13 +398,14 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
}
// Exact bounds, made structural: with no tail requested the file must contain
// exactly the requested window's frames, so a source mode that silently widened
// the render fails loudly here instead of landing as a successful capture. Auto
// and Manual add frames by design and are skipped. UNVERIFIED: that REAPER
// resolves the window's two edges to frame indices the same way frameCountFor
// does — a DAW pass decides whether this equality is exact or off by a frame.
// (within a tolerance, see below) the requested window's frames, so a source
// mode that silently widened the render fails loudly here instead of landing as
// a successful capture. Auto and Manual add frames by design and are skipped.
// (The file is read again by stampCaptureSample below; the duplicate read is a
// once-per-capture cost on an already-warm file.)
// once-per-capture cost on an already-warm file.) A bounded/header-only read is
// not a clean substitute: parseWavLayout only marks the data chunk valid when
// the buffer holds the chunk's FULL declared body (bodyInBounds), so a truncated
// read would read as invalid here on every real capture, not just malformed ones.
if (request.tailMode == TailMode::None) {
const WavLayout layout =
parseWavLayout(util::readFileBytes(expectedPath));
@@ -411,15 +414,35 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
static_cast<int>(layout.sampleRate))
: 0;
const long long actualFrames = static_cast<long long>(layout.frameCount());
if (expectedFrames > 0 && actualFrames != expectedFrames) {
// frameCountFor is a difference of frame indices, not a rounded duration
// (see render_window.h) — REAPER's own edge-rounding can legitimately land
// one frame off that, so the gate tolerates +/-1 rather than exact equality.
// The defect this refuses is a whole-item widening (seconds of extra audio,
// thousands of frames), which a 1-frame tolerance still catches with
// certainty. Tightening to exact equality needs a DAW pass confirming REAPER
// resolves the window's two edges to frame indices the same way this does.
const long long frameDelta = actualFrames > expectedFrames
? actualFrames - expectedFrames
: expectedFrames - actualFrames;
if (expectedFrames > 0 && frameDelta > 1) {
result.status = CaptureStatus::BoundsMismatch;
result.message = "Render produced " + std::to_string(actualFrames) +
" frames but the requested range is " +
std::to_string(expectedFrames) + " at " +
std::to_string(layout.sampleRate) +
" Hz -- the render did not honor the requested bounds. "
"Nothing was added to the bank; the file is at: " +
expectedPath;
"Requested [" + std::to_string(request.startSeconds) +
"s, " + std::to_string(request.endSeconds) +
"s) -> frame indices [" +
std::to_string(std::llround(request.startSeconds *
layout.sampleRate)) +
", " +
std::to_string(std::llround(request.endSeconds *
layout.sampleRate)) +
"). Nothing was added to the bank; the render at " +
expectedPath + " was never indexed and has been cleaned up.";
std::error_code ec;
std::filesystem::remove(expectedPath, ec);
return result;
}
}
+3 -3
View File
@@ -192,9 +192,9 @@ CaptureResult renderOffline(CaptureScope scope,
refused.status = CaptureStatus::MultiTrackRange;
refused.message =
"This range is narrower than the selected items, so it renders through "
"their tracks -- and those items span more than one track, which renders "
"one file per track. Capture one track's items at a time, or make the "
"range match the items' extent.";
"their tracks -- and those items span more than one track, which this "
"shape cannot land as a single file. Capture one track's items at a "
"time, or make the range match the items' extent.";
return refused;
}
+1 -1
View File
@@ -2,7 +2,7 @@
// The transient silencing a ranged ITEM render needs: REAPER's selected-tracks
// source prints everything upstream of the track — its folder children and its
// receives — which an item capture must not hear. Stack RAII, restored on every
// path. The .cpp includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT.
// path.
#include <vector>
+3 -2
View File
@@ -54,8 +54,9 @@ model::ProvenanceScope provenanceScopeFor(CaptureScope scope)
// out.sourceTracks (deduped) — these are the tracks whose FX must be bypassed so an
// item capture hears take/item FX only. GUIDs recorded for provenance.
// extentStart/extentEnd come back as the union of the selected items' own extents:
// the window REAPER's selected-items render source would print (SDK ~1990:
// D_POSITION/D_LENGTH in seconds).
// INFERRED to be the window REAPER's selected-items render source would print
// (SDK ~1990: D_POSITION/D_LENGTH in seconds) — unverified; see
// src/core/capture/CLAUDE.md §Gotchas for what that inference rests on.
bool collectSelectedItemTracks(ResolvedSource& out,
double& extentStart, double& extentEnd)
{