// render_bounds_gate.cpp — see the header. #include "shell/capture/render_bounds_gate.h" #include #include #include #include #include #include "core/capture/render_settings.h" #include "core/capture/render_window.h" #include "core/capture/wav_codec.h" #include "core/util/file_bytes.h" namespace reasampler::capture { namespace { // Deliberately a sibling of the bank folder, never inside it: prune enumerates the bank // directory, and a refused render must be reachable by the person debugging it and by // nothing else. constexpr const char* kRefusedSubfolder = "reasampler_refused"; // Moves a refused render out of the bank and returns the sentence naming where it went. // A failed move leaves the file where the renderer wrote it and says so — never a path // that does not exist. std::string retainRefusedRender(const std::string& renderedPath, const std::string& projectDir) { namespace fs = std::filesystem; std::error_code ec; const std::string dir = projectDir + "/" + kRefusedSubfolder; fs::create_directories(dir, ec); if (!ec) { const std::string dest = dir + "/" + fs::path(renderedPath).filename().string(); fs::rename(renderedPath, dest, ec); if (!ec) return " Nothing was added to the bank. The refused render was KEPT for " "diagnosis at " + dest + " -- outside the bank, indexed by nothing; " "delete it when done."; } return " Nothing was added to the bank. The refused render was kept for diagnosis " "but could not be moved out of the bank folder; it is still at " + renderedPath + ", indexed by nothing. Delete it when done."; } } // namespace std::string refusedRenderFolder(const std::string& projectDir) { return projectDir + "/" + kRefusedSubfolder; } BoundsVerdict checkRenderedFileNotEmpty(const std::string& renderedPath, const std::string& projectDir) { BoundsVerdict v; std::error_code ec; const std::uintmax_t size = std::filesystem::file_size(renderedPath, ec); if (ec || size != 0) return v; // stat failure isn't this check's job — leave it be v.refused = true; v.message = "Render at " + renderedPath + " is 0 bytes -- REAPER produced an empty " "file, so there is nothing to check the requested range against." + retainRefusedRender(renderedPath, projectDir); return v; } BoundsVerdict checkRenderedBounds(const std::string& renderedPath, const std::string& projectDir, const CaptureRequest& request) { BoundsVerdict v; if (request.tailMode != TailMode::None) return v; // Whole-file read, not a bounded/header-only one: parseWavLayout marks the data // chunk valid only when the buffer holds its FULL declared body, so a truncated // read would read as invalid on every real capture — and invalid refuses here. const WavLayout layout = parseWavLayout(util::readFileBytes(renderedPath)); // Why the refusal names the render source at all: render_settings.h's renderSourceLabel. const std::string source = std::string(" Render source: ") + renderSourceLabel(request.sourceMode) + "."; // An unmeasurable render used to SKIP this gate and land in the bank with an // unknown channel count. It is a refusal now: exact bounds cannot be asserted over // a file whose frames were never counted. if (!layout.valid || layout.sampleRate == 0) { v.refused = true; v.message = "Render at " + renderedPath + " could not be measured -- it could " "not be read (locked, missing, or a permissions error), its WAV " "header did not parse, or it declared no sample rate -- so the " "frames it holds were never checked against the requested range." + source + retainRefusedRender(renderedPath, projectDir); return v; } const int rate = static_cast(layout.sampleRate); const long long expectedFrames = frameCountFor(request.startSeconds, request.endSeconds, rate); const long long actualFrames = static_cast(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: the END alone floored to the millisecond. Checked against // the END only -- a refusal whose START is also off-grid and independently floored // would not match this shape, and this note's silence on that refusal is this // check not covering it, not the coincidence breaking. Excludes 0, which every // sub-millisecond window (a legitimate day-one capture) also floors to, and which // would otherwise match a render that produced nothing. A count coincidence only — // it does not establish how the render resolved anything. const long long msFlooredEnd = msFlooredEndFrameCount(request.startSeconds, request.endSeconds, rate); const std::string msNote = (msFlooredEnd > 0 && actualFrames == msFlooredEnd) ? " 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) + " at " + std::to_string(rate) + " Hz -- the render did not honor the requested bounds." + source + " Requested [" + std::to_string(request.startSeconds) + "s, " + std::to_string(request.endSeconds) + "s) -> frame indices [" + std::to_string(std::llround(request.startSeconds * rate)) + ", " + std::to_string(std::llround(request.endSeconds * rate)) + ")." + msNote + retainRefusedRender(renderedPath, projectDir); return v; } } // namespace reasampler::capture