Q-W3: main.cpp → pointers+entry+dispatch via 4 capture hoists; one pure wav_codec RIFF owner; ICaptureBackend deleted; capture_realtime rename + finalize split; shared stampCaptureSample; makeUniqueTag gains monotonic counter (fixes same-second batch collisions). 60/60 green.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#include "core/namespaces.h"
|
||||
// capture.cpp — REAPER-facing offline-render backend (OfflineRenderBackend).
|
||||
// capture.cpp — REAPER-facing offline-render backend (OfflineRenderBackend) plus
|
||||
// the shared backend helpers (makeUniqueTag / stampCaptureSample — Q-W3 riders).
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes
|
||||
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
#include "shell/capture/capture.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
@@ -44,6 +45,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "core/capture/capture_paths.h"
|
||||
#include "core/capture/wav_codec.h" // hashWavContent — the one WAV/RIFF owner
|
||||
#include "core/util/file_bytes.h"
|
||||
#include "core/capture/render_settings.h"
|
||||
|
||||
@@ -58,7 +60,7 @@
|
||||
#define REAPERAPI_WANT_TimeMap_GetTimeSigAtTime
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
namespace reasampler::capture {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -219,21 +221,84 @@ struct ScopedRenderSettings {
|
||||
ScopedRenderSettings& operator=(const ScopedRenderSettings&) = delete;
|
||||
};
|
||||
|
||||
// A monotonic, filesystem-safe timestamp tag so repeated captures in one session
|
||||
// do not collide on the file name. NOTE: the tag varies the file NAME, not the
|
||||
// audio bytes — bit-identical-repeat is about identical *content* for identical
|
||||
// requests; two deliberate captures naturally live in two files.
|
||||
std::string makeUniqueTag() {
|
||||
std::time_t now = std::time(nullptr);
|
||||
return std::to_string(static_cast<long long>(now));
|
||||
}
|
||||
|
||||
// Whole-file reads go through the shared core/util readFileBytes (Q-W1, T2-03):
|
||||
// empty on any I/O failure (the caller then leaves contentHash empty — the safe,
|
||||
// confirm-eliciting direction for an unreadable file).
|
||||
|
||||
} // namespace
|
||||
|
||||
// --- Shared backend helpers (Q-W3 riders — see capture.h) --------------------
|
||||
|
||||
std::string makeUniqueTag(const std::string& prefix) {
|
||||
// Timestamp + PER-SESSION MONOTONIC counter (T1-11 fix). The timestamp alone
|
||||
// had one-second resolution: two captures of the same baseName within the same
|
||||
// wall-clock second derived the same file stem, so the second render silently
|
||||
// overwrote the first file and minted two Samples with colliding ids —
|
||||
// reachable in practice via batch capture. The counter (shared across both
|
||||
// backends — this is the one definition both call) makes every tag of a
|
||||
// session distinct regardless of timing. NOTE: the tag varies the file NAME,
|
||||
// not the audio bytes — bit-identical-repeat is about identical *content* for
|
||||
// identical requests; two deliberate captures naturally live in two files.
|
||||
static std::atomic<unsigned long long> counter{0};
|
||||
const std::time_t now = std::time(nullptr);
|
||||
return prefix + std::to_string(static_cast<long long>(now)) + "-" +
|
||||
std::to_string(++counter);
|
||||
}
|
||||
|
||||
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).
|
||||
s.trackGuids = req.trackGuids;
|
||||
s.channelCount = req.channelCount;
|
||||
|
||||
// Resolved sample rate: the request's pinned rate, else PROJECT_SRATE read
|
||||
// from the caller's project handle. PROJECT_SRATE can read 0 on a project that
|
||||
// never explicitly pinned a rate — the value stays 0 (the Sample zero-value)
|
||||
// rather than a bogus literal (the honest "unknown" both backends shared).
|
||||
s.sampleRate = (req.sampleRate > 0)
|
||||
? req.sampleRate
|
||||
: static_cast<int>(GetSetProjectInfo(rateProj, "PROJECT_SRATE", 0.0, false));
|
||||
|
||||
s.captureTempo = Master_GetTempo(); // BPM at capture time (verified ~4651)
|
||||
|
||||
// Time signature at the capture's START time (L7 F1 stamp). TimeMap_GetTimeSigAtTime
|
||||
// (verified reaper_plugin_functions.h:7130 — void(ReaProject*, double time,
|
||||
// int* numOut, int* denomOut, double* tempoOut)) reads the meter effective at
|
||||
// that project time, so a sample captured under 3/4 keeps a 3/4 read-out even
|
||||
// if the project later switches to 4/4. `timeSigProj` is the CALLER's project
|
||||
// pin — offline passes nullptr (the active project); realtime pins the record's
|
||||
// own project (the T2-09 divergence, kept caller-visible as this argument).
|
||||
// tempoOut is ignored — captureTempo already carries the master tempo. Leaves
|
||||
// 0/0 (unstamped) if the API is somehow unavailable; the formatter renders a
|
||||
// blank musical read-out.
|
||||
{
|
||||
int tsNum = 0, tsDenom = 0;
|
||||
double tsTempo = 0.0;
|
||||
TimeMap_GetTimeSigAtTime(timeSigProj, req.startSeconds, &tsNum, &tsDenom, &tsTempo);
|
||||
s.captureTimeSigNum = tsNum;
|
||||
s.captureTimeSigDenom = tsDenom;
|
||||
}
|
||||
|
||||
// Content hash: WAV-aware FNV-1a over the finished file's fmt+data chunks so
|
||||
// hashReferencedElsewhere can identify copies in other banks and suppress the
|
||||
// last-reference confirm when another bank still holds the same file. Using
|
||||
// hashWavContent (not the raw hashBytes) skips render-varying metadata chunks
|
||||
// (bext origination timestamp, iXML, LIST/INFO, etc.) so two renders/records of
|
||||
// identical audio collapse to the same hash. Best-effort: an unreadable file
|
||||
// leaves contentHash empty — the safe, confirm-eliciting direction (bank_model
|
||||
// treats "" as non-participating in dedup).
|
||||
{
|
||||
const std::vector<std::uint8_t> fileBytes = util::readFileBytes(absolutePath);
|
||||
if (!fileBytes.empty()) {
|
||||
s.contentHash = hashWavContent(fileBytes);
|
||||
}
|
||||
}
|
||||
|
||||
s.createdTimestamp = static_cast<std::int64_t>(std::time(nullptr));
|
||||
}
|
||||
|
||||
CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
CaptureResult result;
|
||||
|
||||
@@ -340,9 +405,9 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
}();
|
||||
|
||||
// Compute the unique tag ONCE so the file stem and Sample.id carry the same
|
||||
// timestamp. Calling makeUniqueTag() twice could yield different values if a
|
||||
// second boundary crosses between the two calls (bug: id and filename diverge).
|
||||
const std::string uniqueTag = makeUniqueTag();
|
||||
// tag. Calling makeUniqueTag() twice would yield different values (the counter
|
||||
// advances per call — bug: id and filename diverge).
|
||||
const std::string uniqueTag = makeUniqueTag("");
|
||||
const BankPaths paths =
|
||||
deriveBankPaths(projectDir, request.baseName, uniqueTag);
|
||||
|
||||
@@ -477,50 +542,16 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
// Seconds are the authoritative source for the render. Do NOT add DAW-
|
||||
// unverifiable PPQ resolution here — it requires a live REAPER to validate.
|
||||
s.wetDry = request.wetDry;
|
||||
// Track GUIDs for track-scoped captures (empty for master/items/razor). The
|
||||
// caller resolved the selection to canonical GUID strings; we record them so a
|
||||
// "re-capture from source" (M10) knows which tracks the sample came from.
|
||||
s.trackGuids = request.trackGuids;
|
||||
s.channelCount = request.channelCount;
|
||||
// Store the resolved sample rate only when it is known (> 0). If the project
|
||||
// never pinned a rate (PROJECT_SRATE read 0), we did not force RENDER_SRATE
|
||||
// either, so the render ran at REAPER's project default — an unknown value from
|
||||
// this code's perspective. Leave sampleRate at 0 (the Sample zero-value) rather
|
||||
// than store a bogus literal; M6/M7 can fill it in by probing the rendered file.
|
||||
s.sampleRate = effectiveSampleRate; // 0 when project rate was unknown
|
||||
s.lengthSeconds = request.endSeconds - request.startSeconds;
|
||||
s.captureTempo = Master_GetTempo(); // BPM at capture time (verified ~4651)
|
||||
// Time signature at the capture's START time (L7 F1 stamp). TimeMap_GetTimeSigAtTime
|
||||
// (verified reaper_plugin_functions.h:7130 — void(ReaProject*, double time,
|
||||
// int* numOut, int* denomOut, double* tempoOut)) reads the meter effective at that
|
||||
// project time, so a sample captured under 3/4 keeps a 3/4 read-out even if the
|
||||
// project later switches to 4/4. proj=nullptr => the active project (matches the
|
||||
// Master_GetTempo() call above, which is also active-project). The tempoOut is
|
||||
// ignored — captureTempo already carries the master tempo. Leaves 0/0 (unstamped)
|
||||
// if the API is somehow unavailable; the formatter renders a blank musical read-out.
|
||||
{
|
||||
int tsNum = 0, tsDenom = 0;
|
||||
double tsTempo = 0.0;
|
||||
TimeMap_GetTimeSigAtTime(nullptr, request.startSeconds, &tsNum, &tsDenom, &tsTempo);
|
||||
s.captureTimeSigNum = tsNum;
|
||||
s.captureTimeSigDenom = tsDenom;
|
||||
}
|
||||
s.tier = Tier::Scratch; // captures land in scratch by default
|
||||
// Content hash: WAV-aware FNV-1a over the rendered file's fmt+data chunks so
|
||||
// hashReferencedElsewhere can identify copies in other banks and suppress the
|
||||
// last-reference confirm when another bank still holds the same file. Using
|
||||
// hashWavContent (not the raw hashBytes) skips render-varying metadata chunks
|
||||
// (bext origination timestamp, iXML, LIST/INFO, etc.) so two renders of identical
|
||||
// audio collapse to the same hash. Best-effort: an unreadable file leaves
|
||||
// contentHash empty — the safe, confirm-eliciting direction (bank_model treats
|
||||
// "" as non-participating in dedup, which is the existing fallback semantics).
|
||||
{
|
||||
const std::vector<std::uint8_t> fileBytes = readFileBytes(expectedPath);
|
||||
if (!fileBytes.empty()) {
|
||||
s.contentHash = hashWavContent(fileBytes);
|
||||
}
|
||||
}
|
||||
s.createdTimestamp = static_cast<std::int64_t>(std::time(nullptr));
|
||||
s.tier = model::Tier::Scratch; // captures land in scratch by default
|
||||
// The SHARED finished-capture stamp (T2-09 dedupe): trackGuids + channelCount
|
||||
// (request echo), resolved sampleRate (request rate else PROJECT_SRATE(proj) —
|
||||
// 0 stays 0 when the project never pinned a rate; we did not force RENDER_SRATE
|
||||
// either, so the render ran at REAPER's default), captureTempo, the capture-
|
||||
// start time signature (timeSigProj = nullptr => the active project — matching
|
||||
// the Master_GetTempo read, which is also active-project), the WAV-aware
|
||||
// contentHash of the rendered file, and createdTimestamp.
|
||||
stampCaptureSample(s, request, proj, /*timeSigProj=*/nullptr, expectedPath);
|
||||
// Phase S seam fields (rootNote / loop) left empty (D-B). An offline render of a
|
||||
// master mix / track / time-selection is not a single played note, so no root
|
||||
// note is derivable here — we do NOT guess one. Loop points are set later by an
|
||||
@@ -537,4 +568,4 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
// guard's dtor restores every RENDER_* setting here.
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
} // namespace reasampler::capture
|
||||
|
||||
Reference in New Issue
Block a user