7d0fcc49ac
Item scope now reads the active take's FX chain via TakeFX_* (new fxChainIdentityForItems helper), not the owning track's chain. normalizeSlashes lowercases on _WIN32 for detectParent. Stale comments corrected; case-fold tests added.
150 lines
7.9 KiB
C++
150 lines
7.9 KiB
C++
#pragma once
|
|
// provenance — the REAPER-free core behind Milestone 10 (re-capture from source).
|
|
//
|
|
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
|
// vendor/ includes. Standard library only. The shell (main.cpp / actions.cpp)
|
|
// gathers the raw inputs from REAPER — the source item media-file names, the
|
|
// source track FX-chain identity (names / GUIDs / enabled flags), the exact
|
|
// capture range, scope, tail — and hands plain strings/values here. This module
|
|
// owns:
|
|
//
|
|
// * CaptureRecipe — the recorded capture request PLUS the source FX-chain
|
|
// identity at capture time. Everything "re-capture from
|
|
// source" needs to re-run the SAME request against the
|
|
// source's CURRENT state, and to tell whether the source
|
|
// drifted since capture.
|
|
// * the ENCODING of a recipe into the single `Provenance.fxChainSnapshot`
|
|
// string (M1's field already JSON-round-trips one string,
|
|
// so the whole thin fingerprint rides in it — no schema
|
|
// change to Sample).
|
|
// * fxChainIdentity — folds the shell-gathered FX-chain rows into one identity
|
|
// string (the drift-detection component of the fingerprint).
|
|
// * detectParent — the pure parent-detection decision: given the resolved
|
|
// absolute media-file path(s) of the capture's source item(s)
|
|
// and the bank's path->sampleId map, decide whether this
|
|
// capture genuinely derives from a bank sample (P1: identity
|
|
// by resolved file path only — no fuzzy match, no false
|
|
// parentage).
|
|
//
|
|
// Fork picks (docs/product/provenance.md, settled 2026-07-23): P1 = a THIN
|
|
// reproducibility fingerprint (drift-detect + re-run the same request), NOT a
|
|
// serialized FX chunk to restore. P2 = bank-only re-capture. So nothing here
|
|
// stores a restorable chain, and nothing here reaches into view_mode_model.
|
|
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace reasampler {
|
|
|
|
// Capture scope, mirrored from render_settings' CaptureScope but kept independent
|
|
// here so the pure provenance module does not pull the whole render_settings graph
|
|
// in. The shell maps its CaptureScope onto this two-value enum. Item = item/take FX
|
|
// only; Track = item FX + the track's own FX (CLAUDE.md §Capture FX scope).
|
|
enum class ProvenanceScope {
|
|
Item,
|
|
Track,
|
|
};
|
|
|
|
// One FX-chain entry as the shell reads it from REAPER. For Track scope the shell
|
|
// uses TrackFX_GetFXName/GetFXGUID/GetEnabled; for Item scope it uses the TakeFX_*
|
|
// equivalents over the active take's FX chain. Plain data — the shell fills it, the
|
|
// pure fold turns the vector into the identity string.
|
|
struct FxIdentityEntry {
|
|
std::string name; // TrackFX_GetFXName / TakeFX_GetFXName
|
|
std::string guid; // TrackFX_GetFXGUID / TakeFX_GetFXGUID -> guidToString
|
|
bool enabled; // TrackFX_GetEnabled / TakeFX_GetEnabled
|
|
};
|
|
|
|
// The recorded capture recipe + source FX-chain identity — the thin fingerprint.
|
|
// Re-capture replays the request fields verbatim against the source's CURRENT
|
|
// state; fxChainIdentity is compared post-hoc to report drift. Ordinary equality
|
|
// (via ==) is a full recipe match; fxChainIdentity difference alone is "the source
|
|
// drifted but the recipe is the same" (the re-run still succeeds, the user is told).
|
|
struct CaptureRecipe {
|
|
ProvenanceScope scope = ProvenanceScope::Track;
|
|
int sourceMode = 0; // reasampler::SourceMode as int (bank_model)
|
|
|
|
double startSeconds = 0.0; // exact bounds — no rounding (invariant)
|
|
double endSeconds = 0.0;
|
|
|
|
int tailMode = 0; // reasampler::TailMode as int (render_settings)
|
|
double tailMs = 0.0;
|
|
|
|
int sampleRate = 0; // 0 = follow project rate
|
|
int channelCount = 2;
|
|
|
|
// Canonical GUID strings of the source track(s) the capture came from
|
|
// (guidString form). Re-capture resolves these back to live tracks.
|
|
std::vector<std::string> trackGuids;
|
|
|
|
// The source FX-chain identity at capture time — the drift component. A folded
|
|
// string (fxChainIdentity) of the in-scope FX rows. Not a restorable chunk.
|
|
std::string fxChainIdentity;
|
|
|
|
bool operator==(const CaptureRecipe& o) const;
|
|
bool operator!=(const CaptureRecipe& o) const { return !(*this == o); }
|
|
};
|
|
|
|
// Folds the shell-gathered FX rows into ONE identity string. Order-sensitive
|
|
// (chain order is part of identity), delimited so a name containing the delimiter
|
|
// cannot forge a different chain (the fields are length-prefixed). Empty vector ->
|
|
// empty string (a no-FX source has an empty, stable identity). Pure + deterministic.
|
|
std::string fxChainIdentity(const std::vector<FxIdentityEntry>& entries);
|
|
|
|
// Combines several per-track FX-chain identity strings (one per source track, in
|
|
// track order) into ONE identity, length-prefixing each so two different per-track
|
|
// partitions can never collide by concatenation (e.g. {"X",""} != {"","X"}). Used
|
|
// for a multi-track Track-scope capture. A single-track capture combines to a
|
|
// stable, unambiguous wrapping of its one identity. Pure + deterministic.
|
|
std::string combineChainIdentities(const std::vector<std::string>& perTrack);
|
|
|
|
// Encodes a CaptureRecipe into the single string stored in
|
|
// Provenance.fxChainSnapshot. Self-describing, versioned, and escape-safe so it
|
|
// round-trips losslessly through the Sample JSON (which treats the whole thing as
|
|
// one opaque string value). buildFingerprint(x) then parseFingerprint(...) == x.
|
|
std::string buildFingerprint(const CaptureRecipe& recipe);
|
|
|
|
// Parses a fingerprint produced by buildFingerprint. Returns nullopt on any
|
|
// malformed / unrecognized-version input (never throws, never UB) so a legacy or
|
|
// corrupt provenance string degrades to "no recipe" gracefully rather than
|
|
// mis-driving a re-capture.
|
|
std::optional<CaptureRecipe> parseFingerprint(const std::string& fingerprint);
|
|
|
|
// --- Parent detection (P1: identity by resolved file path) -------------------
|
|
|
|
// One bank sample as the detector sees it: its stable id and the ABSOLUTE,
|
|
// normalized path its file resolves to (the shell resolves relativePath against
|
|
// the current project dir via resolveBankFile + normalizeSlashes before handing
|
|
// it here). Plain data so the decision is pure and testable.
|
|
struct BankFileRef {
|
|
std::string sampleId;
|
|
std::string absolutePath; // normalized (forward-slash, no trailing slash)
|
|
};
|
|
|
|
// Decides whether a capture derives from a bank sample.
|
|
//
|
|
// RULE (stated for the handoff, honest — no false parentage): a capture derives
|
|
// from a bank sample iff EVERY source item whose media file could be resolved
|
|
// points at the SAME bank sample's file (by exact normalized absolute path). If
|
|
// the source items resolve to files not in the bank, or to MORE THAN ONE distinct
|
|
// bank sample (ambiguous parentage), no parent is recorded. An empty source-file
|
|
// set (nothing resolvable) yields no parent.
|
|
//
|
|
// sourceItemFiles : normalized absolute paths of the capture's source items'
|
|
// take media files (the shell gathers + normalizes them). A
|
|
// file that could not be resolved is simply omitted by the
|
|
// shell — it never becomes an empty string here.
|
|
// bankFiles : the active book's samples as BankFileRefs (path -> id).
|
|
//
|
|
// Returns the parent sample id, or nullopt when the capture is not a genuine
|
|
// resample-from-sample. Comparison is exact path identity; the caller normalizes
|
|
// both sides identically via normalizeSlashes (which lowercases on Windows) so a
|
|
// slash/case difference never spuriously matches or misses. On Windows both sides
|
|
// are lowercased before they reach here; on macOS/Linux they are case-exact.
|
|
std::optional<std::string> detectParent(
|
|
const std::vector<std::string>& sourceItemFiles,
|
|
const std::vector<BankFileRef>& bankFiles);
|
|
|
|
} // namespace reasampler
|