Prove the render bounds at the boundary they cross, and name a short render whose count is exactly a millisecond-floored window

No truncation exists on our side of that boundary, so the read-back is the
only evidence available for whether REAPER kept the window — and it fires on
every tail mode, where only None was ever judged.
This commit is contained in:
2026-08-02 14:28:03 -04:00
parent e589addc54
commit 292d14d14c
6 changed files with 246 additions and 6 deletions
+19
View File
@@ -34,6 +34,7 @@
#include "core/capture/wav_codec.h" // hashWavContent / collapseToMono — the one WAV/RIFF owner
#include "core/util/file_bytes.h"
#include "core/capture/render_settings.h"
#include "core/capture/render_window.h" // describeBoundsDrift — the read-back's verdict
#include "shell/capture/render_bounds_gate.h" // the exact-bounds verdict on the landed render
#define REAPERAPI_MINIMAL
@@ -424,6 +425,12 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
GetSetProjectInfo(proj, "RENDER_STARTPOS", request.startSeconds, true);
GetSetProjectInfo(proj, "RENDER_ENDPOS", request.endSeconds, true);
// The requested window crosses out of this process HERE and nowhere else, so the
// read-back is the only evidence available on this side of that boundary for
// whether REAPER kept it. Reported below, once the project rate is known.
const double storedStart = GetSetProjectInfo(proj, "RENDER_STARTPOS", 0.0, false);
const double storedEnd = GetSetProjectInfo(proj, "RENDER_ENDPOS", 0.0, false);
// TAILFLAG/TAILMS/NORMALIZE/TRIMEND from the pure mapping: None -> exact
// bounds + disable-all normalize; Auto -> 8s tail + surgical trim-end
// normalize + -72 dB TRIMEND; Manual -> clamped fixed tail + disable-all, no
@@ -453,6 +460,18 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
GetSetProjectInfo(proj, "RENDER_SRATE",
static_cast<double>(effectiveSampleRate), true);
}
// Silent unless a bound came back changed. Fires on EVERY tail mode on purpose:
// only None is judged against its window after the render, so this is the sole
// signal an Auto/Manual capture was shortened before it ever started.
{
const std::string drift =
describeBoundsDrift(request.startSeconds, request.endSeconds,
storedStart, storedEnd, effectiveSampleRate);
if (!drift.empty())
ShowConsoleMsg(("ReaSampler capture: " + drift + "\n").c_str());
}
GetSetProjectInfo(proj, "RENDER_CHANNELS",
static_cast<double>(request.channelCount), true);
+12 -1
View File
@@ -99,6 +99,17 @@ BoundsVerdict checkRenderedBounds(const std::string& renderedPath,
const long long actualFrames = static_cast<long long>(layout.frameCount());
if (renderHonoredBounds(expectedFrames, actualFrames)) return v;
// Says whether this shortfall has the one shape two live short renders already
// matched to the frame, so every refusal from here on adds to (or breaks) that
// evidence instead of needing the arithmetic done by hand. A count coincidence
// only — it does not establish how the render resolved anything.
const std::string msNote =
actualFrames == msFlooredEndFrameCount(request.startSeconds,
request.endSeconds, rate)
? " Those are exactly the frames this window holds with its end floored to"
" the millisecond -- a match on the count, not a measured cause."
: std::string();
v.refused = true;
v.message = "Render produced " + std::to_string(actualFrames) +
" frames but the requested range is " + std::to_string(expectedFrames) +
@@ -108,7 +119,7 @@ BoundsVerdict checkRenderedBounds(const std::string& renderedPath,
std::to_string(request.endSeconds) + "s) -> frame indices [" +
std::to_string(std::llround(request.startSeconds * rate)) + ", " +
std::to_string(std::llround(request.endSeconds * rate)) + ")." +
retainRefusedRender(renderedPath, projectDir);
msNote + retainRefusedRender(renderedPath, projectDir);
return v;
}