Refuse every multi-track selected-tracks render, both scopes; make a failed mono collapse observable

This commit is contained in:
2026-08-01 22:40:54 -04:00
parent 6e6f4a6a15
commit f69c4bf6bf
14 changed files with 366 additions and 114 deletions
+41 -15
View File
@@ -45,6 +45,7 @@
#define REAPERAPI_WANT_Main_OnCommand
#define REAPERAPI_WANT_Main_SaveProject
#define REAPERAPI_WANT_Master_GetTempo
#define REAPERAPI_WANT_ShowConsoleMsg
#define REAPERAPI_WANT_TimeMap_GetTimeSigAtTime
#include "reaper_plugin_functions.h"
@@ -228,12 +229,30 @@ CaptureName captureNameFor(const std::vector<std::string>& sourceNames,
return composeCaptureName(in);
}
bool collapseCapturedFileToMono(const std::string& absolutePath) {
namespace {
// One console line per genuine collapse failure. The capture itself is intact and was
// measured after this step, so the failure costs only the size win — but silence here is
// what made a failed rewrite read exactly like a legitimately stereo capture.
void reportCollapseFailure(const std::string& absolutePath, const char* what) {
ShowConsoleMsg(("ReaSampler capture: the lossless mono collapse " + std::string(what) +
" -- " + absolutePath +
" landed intact, as captured.\n").c_str());
}
} // namespace
MonoCollapseOutcome collapseCapturedFileToMono(const std::string& absolutePath) {
const std::vector<std::uint8_t> bytes = util::readFileBytes(absolutePath);
if (bytes.empty()) return false;
if (bytes.empty()) {
// Failed, not Declined: the read that would have decided never happened, so
// "the channels differ" is a claim this path cannot make.
reportCollapseFailure(absolutePath, "could not read the captured file");
return MonoCollapseOutcome::Failed;
}
const MonoCollapse collapse = collapseToMono(bytes);
if (!collapse.collapsed) return false;
if (!collapse.collapsed) return MonoCollapseOutcome::Declined;
// Sibling temp + rename, NOT an in-place truncating write: this runs unconditionally
// on the deterministic offline path (which never reopened its render for write before
@@ -245,7 +264,10 @@ bool collapseCapturedFileToMono(const std::string& absolutePath) {
const std::string tempPath = absolutePath + ".moncollapse.tmp";
{
std::ofstream out(tempPath, std::ios::binary | std::ios::trunc);
if (!out) return false;
if (!out) {
reportCollapseFailure(absolutePath, "could not open its temporary file");
return MonoCollapseOutcome::Failed;
}
out.write(reinterpret_cast<const char*>(collapse.bytes.data()),
static_cast<std::streamsize>(collapse.bytes.size()));
const bool wroteOk = static_cast<bool>(out);
@@ -253,16 +275,18 @@ bool collapseCapturedFileToMono(const std::string& absolutePath) {
if (!wroteOk) {
std::error_code ec;
std::filesystem::remove(tempPath, ec);
return false;
reportCollapseFailure(absolutePath, "could not write the rebuilt file");
return MonoCollapseOutcome::Failed;
}
}
std::error_code ec;
std::filesystem::rename(tempPath, absolutePath, ec);
if (ec) {
std::filesystem::remove(tempPath, ec); // don't leave litter on a failed rename
return false;
reportCollapseFailure(absolutePath, "could not replace the captured file");
return MonoCollapseOutcome::Failed;
}
return true;
return MonoCollapseOutcome::Collapsed;
}
void stampCaptureSample(Sample& s, const CaptureRequest& req,
@@ -473,11 +497,13 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
// (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.) 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.
// (The landed file is read three times on this path — this gate, the mono collapse,
// and stampCaptureSample — plus one rewrite when the collapse fires; a
// once-per-capture cost on an already-warm file, judged acceptable.) 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));
@@ -525,7 +551,8 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
// renderer's file rather than one this step had already rewritten. The collapse
// preserves the frame count, so the two are order-independent in outcome — only
// in what each is measuring.
const bool collapsedToMono = collapseCapturedFileToMono(expectedPath);
const MonoCollapseOutcome collapseOutcome =
collapseCapturedFileToMono(expectedPath);
// Record the request's own bounds (exact) rather than re-measuring the file.
Sample s;
@@ -553,8 +580,7 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
result.message = "Captured [" +
std::to_string(request.startSeconds) + "s, " +
std::to_string(request.endSeconds) + "s] -> " +
paths.relativePath +
(collapsedToMono ? " (collapsed to mono)" : "");
paths.relativePath + monoCollapseSuffix(collapseOutcome);
return result;
}