Merge fix: populate contentHash at capture — last-reference confirm now accurate; dedup-by-hash live
This commit is contained in:
+27
-3
@@ -38,6 +38,7 @@
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -224,6 +225,21 @@ std::string makeUniqueTag() {
|
||||
return std::to_string(static_cast<long long>(now));
|
||||
}
|
||||
|
||||
// Reads the whole file into a byte buffer. Returns an empty vector on any I/O
|
||||
// failure (the caller then leaves contentHash empty — the safe, confirm-eliciting
|
||||
// direction for an unreadable file).
|
||||
std::vector<std::uint8_t> readFileBytes(const std::string& path) {
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f) return {};
|
||||
const std::streamoff size = f.tellg();
|
||||
if (size <= 0) return {};
|
||||
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(size));
|
||||
f.seekg(0);
|
||||
f.read(reinterpret_cast<char*>(bytes.data()), size);
|
||||
if (!f) return {};
|
||||
return bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
@@ -483,9 +499,17 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
s.lengthSeconds = request.endSeconds - request.startSeconds;
|
||||
s.captureTempo = Master_GetTempo(); // BPM at capture time (verified ~4651)
|
||||
s.tier = Tier::Scratch; // captures land in scratch by default
|
||||
// contentHash left empty for M3: hashing the rendered file is a peaks/M2-
|
||||
// adjacent concern wired in a later milestone. Empty hashes do NOT dedup, so
|
||||
// this is safe (bank_model treats "" as non-participating).
|
||||
// Content hash: FNV-1a over the rendered file bytes so hashReferencedElsewhere
|
||||
// can identify copies in other banks and suppress the last-reference confirm when
|
||||
// another bank still holds the same file. 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 = hashBytes(fileBytes.data(), fileBytes.size());
|
||||
}
|
||||
}
|
||||
s.createdTimestamp = static_cast<std::int64_t>(std::time(nullptr));
|
||||
|
||||
result.status = CaptureStatus::Ok;
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
#include "capture_paths.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
std::string hashBytes(const std::uint8_t* data, std::size_t len) {
|
||||
// FNV-1a 64-bit: deterministic, no dependencies, adequate for dedup identity.
|
||||
// Constants from the FNV spec (http://www.isthe.com/chongo/tech/comp/fnv/).
|
||||
constexpr std::uint64_t kOffsetBasis = 14695981039346656037ULL;
|
||||
constexpr std::uint64_t kPrime = 1099511628211ULL;
|
||||
std::uint64_t h = kOffsetBasis;
|
||||
for (std::size_t i = 0; i < len; ++i) {
|
||||
h ^= static_cast<std::uint64_t>(data[i]);
|
||||
h *= kPrime;
|
||||
}
|
||||
// Format as 16-digit lowercase hex (zero-padded) for a fixed-length string.
|
||||
char buf[17];
|
||||
std::snprintf(buf, sizeof(buf), "%016llx",
|
||||
static_cast<unsigned long long>(h));
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string normalizeSlashes(const std::string& path) {
|
||||
std::string out = path;
|
||||
for (char& c : out) {
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// the filesystem. The bank subfolder name is a fixed constant so the same
|
||||
// project always resolves the same bank location (determinism).
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace reasampler {
|
||||
@@ -31,6 +33,15 @@ struct BankPaths {
|
||||
std::string fileStem; // <stem> (RENDER_PATTERN — REAPER appends the extension)
|
||||
};
|
||||
|
||||
// Computes a deterministic FNV-1a 64-bit content hash over `len` bytes at `data`
|
||||
// and returns it as a 16-character lowercase hex string. Designed to fill
|
||||
// Sample::contentHash so the confirm-on-last-reference guardrail
|
||||
// (BankBook::hashReferencedElsewhere) can distinguish "no other bank holds this
|
||||
// file" from "another bank holds the same file." An empty buffer returns the bare
|
||||
// FNV-1a 64-bit offset basis in hex (a stable, non-empty sentinel that two empty
|
||||
// files would share, but real WAV files are never empty).
|
||||
std::string hashBytes(const std::uint8_t* data, std::size_t len);
|
||||
|
||||
// Normalizes a path to forward slashes and strips any trailing slash. Empty in
|
||||
// -> empty out. Pure string transform (does not consult the filesystem).
|
||||
std::string normalizeSlashes(const std::string& path);
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "capture_paths.h"
|
||||
#include "capture_paths.h" // hashBytes, deriveBankPaths
|
||||
#include "peaks.h" // lastFrameAboveThreshold, AudioSample
|
||||
#include "realtime_record.h"
|
||||
#include "render_settings.h" // autoTrimEndRatio, realtimeRecordWindowEnd
|
||||
@@ -512,6 +512,19 @@ CaptureResult finalizeRecording(RealtimeCaptureState& st) {
|
||||
result.status = CaptureStatus::Ok;
|
||||
result.sample = sampleFromRecordedCapture(cap);
|
||||
|
||||
// Content hash: FNV-1a over the (possibly trimmed) bank file bytes so
|
||||
// hashReferencedElsewhere can identify copies in other banks and suppress the
|
||||
// last-reference confirm when another bank still holds the same file.
|
||||
// Best-effort: an unreadable file leaves contentHash empty — the safe,
|
||||
// confirm-eliciting direction (bank_model treats "" as non-participating).
|
||||
{
|
||||
const std::vector<std::uint8_t> fileBytes = readAllBytes(destPath);
|
||||
if (!fileBytes.empty()) {
|
||||
result.sample.contentHash =
|
||||
hashBytes(fileBytes.data(), fileBytes.size());
|
||||
}
|
||||
}
|
||||
|
||||
// The recorded file's true length differs from the request range when a tail was
|
||||
// recorded, so the Sample length must reflect the FILE, not the range:
|
||||
// Auto with a trim applied -> the trimmed length trimAutoTailInPlace returned.
|
||||
|
||||
@@ -52,7 +52,10 @@ Sample sampleFromRecordedCapture(const RecordedCapture& cap) {
|
||||
s.lengthSeconds = cap.endSeconds - cap.startSeconds;
|
||||
s.captureTempo = cap.captureTempo;
|
||||
s.tier = Tier::Scratch; // captures land in scratch by default
|
||||
// contentHash left empty: empty hashes do not participate in dedup (bank_model).
|
||||
// contentHash set by the caller (capture_realtime.cpp) after the file is
|
||||
// finalized and on disk — the hash is over the finished file bytes. Left empty
|
||||
// here because sampleFromRecordedCapture runs before the file exists (the
|
||||
// mapping is pure / DAW-free); the shell patches it in after the move+trim.
|
||||
s.createdTimestamp = cap.createdTimestamp;
|
||||
return s;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user