Merge Ψ-W2-T2: a bit-identical capture collapses to one lossless mono channel
# Conflicts: # src/shell/capture/CLAUDE.md # src/shell/capture/capture.cpp # src/shell/capture/capture.h
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// REAPER-facing offline-render backend (OfflineRenderBackend) plus the shared
|
||||
// backend helpers (makeUniqueTag / stampCaptureSample).
|
||||
// backend helpers (makeUniqueTag / captureNameFor / collapseCapturedFileToMono /
|
||||
// stampCaptureSample).
|
||||
//
|
||||
// Includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is
|
||||
// the one TU that defines the API pointers; here they are extern.
|
||||
@@ -31,7 +32,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "core/capture/capture_paths.h"
|
||||
#include "core/capture/wav_codec.h" // hashWavContent — the one WAV/RIFF owner
|
||||
#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" // frameCountFor — the exact-bounds number
|
||||
@@ -227,13 +228,54 @@ CaptureName captureNameFor(const std::vector<std::string>& sourceNames,
|
||||
return composeCaptureName(in);
|
||||
}
|
||||
|
||||
bool collapseCapturedFileToMono(const std::string& absolutePath) {
|
||||
const std::vector<std::uint8_t> bytes = util::readFileBytes(absolutePath);
|
||||
if (bytes.empty()) return false;
|
||||
|
||||
const MonoCollapse collapse = collapseToMono(bytes);
|
||||
if (!collapse.collapsed) return false;
|
||||
|
||||
// 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
|
||||
// this step existed), so a mid-write failure here must not land a truncated file that
|
||||
// stampCaptureSample then hashes as a false CaptureStatus::Ok. rename() replaces the
|
||||
// destination in one step, so the original bytes are never destroyed until the
|
||||
// replacement is known-complete; a failed write or rename leaves the original file
|
||||
// untouched and self-cleans the temp rather than littering it.
|
||||
const std::string tempPath = absolutePath + ".moncollapse.tmp";
|
||||
{
|
||||
std::ofstream out(tempPath, std::ios::binary | std::ios::trunc);
|
||||
if (!out) return false;
|
||||
out.write(reinterpret_cast<const char*>(collapse.bytes.data()),
|
||||
static_cast<std::streamsize>(collapse.bytes.size()));
|
||||
const bool wroteOk = static_cast<bool>(out);
|
||||
out.close();
|
||||
if (!wroteOk) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(tempPath, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void stampCaptureSample(Sample& s, const CaptureRequest& req,
|
||||
ReaProject* rateProj, ReaProject* timeSigProj,
|
||||
const std::string& absolutePath) {
|
||||
// Track GUIDs + channel count: echoed from the request (the caller resolved
|
||||
// the selection; the backends stay source-agnostic).
|
||||
// Track GUIDs echoed from the request (the caller resolved the selection; the
|
||||
// backends stay source-agnostic). channelCount starts at 0 (unknown, the same
|
||||
// sentinel bank_model already uses for a pre-field entry) rather than the
|
||||
// request's value — the request always asks for 2, so echoing it would claim a
|
||||
// measurement that never happened for the unparseable-file case below. The
|
||||
// produced FILE overrides it below whenever it parses.
|
||||
s.trackGuids = req.trackGuids;
|
||||
s.channelCount = req.channelCount;
|
||||
s.channelCount = 0;
|
||||
|
||||
// PROJECT_SRATE can read 0 on a project that never pinned a rate — stays 0
|
||||
// (honest "unknown") rather than a bogus literal.
|
||||
@@ -264,6 +306,10 @@ void stampCaptureSample(Sample& s, const CaptureRequest& req,
|
||||
const std::vector<std::uint8_t> fileBytes = util::readFileBytes(absolutePath);
|
||||
if (!fileBytes.empty()) {
|
||||
s.contentHash = hashWavContent(fileBytes);
|
||||
// The one authority for the entry's channel count is the file's own `fmt`
|
||||
// — never the render request, which asks for 2 on every capture path.
|
||||
const WavLayout layout = parseWavLayout(fileBytes);
|
||||
if (layout.valid) s.channelCount = static_cast<int>(layout.channelCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,6 +519,14 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Lossless mono collapse, deliberately AFTER the bounds gate: the gate measures
|
||||
// REAPER's own render against the requested window, so nothing of ours may sit
|
||||
// between the render and that measurement, and a refusal must delete the
|
||||
// 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);
|
||||
|
||||
// Record the request's own bounds (exact) rather than re-measuring the file.
|
||||
Sample s;
|
||||
// Same uniqueTag that named the file — calling makeUniqueTag() again could
|
||||
@@ -499,7 +553,8 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
result.message = "Captured [" +
|
||||
std::to_string(request.startSeconds) + "s, " +
|
||||
std::to_string(request.endSeconds) + "s] -> " +
|
||||
paths.relativePath;
|
||||
paths.relativePath +
|
||||
(collapsedToMono ? " (collapsed to mono)" : "");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user