Cut core/capture and core/version comment bloat ~45% (comments only, zero code change)
This commit is contained in:
@@ -1,16 +1,8 @@
|
||||
#pragma once
|
||||
// capture_paths — the REAPER-free path arithmetic behind offline capture.
|
||||
//
|
||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
||||
// vendor/ includes. Standard library only. The capture shell resolves the
|
||||
// current project directory via REAPER APIs, then hands the raw strings here so
|
||||
// the fiddly, easy-to-get-wrong path arithmetic (bank subfolder, unique file
|
||||
// name, absolute render dir, project-relative index path) is unit-tested outside
|
||||
// the DAW.
|
||||
//
|
||||
// Path convention: this module works in forward-slash form and does NOT touch
|
||||
// the filesystem. The bank subfolder name is a fixed constant so the same
|
||||
// project always resolves the same bank location (determinism).
|
||||
// capture_paths — the REAPER-free path arithmetic behind offline capture. The
|
||||
// capture shell resolves the current project directory via REAPER APIs, then
|
||||
// hands the raw strings here. Forward-slash form throughout, no filesystem
|
||||
// access; the bank subfolder name is a fixed constant.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -20,7 +12,7 @@
|
||||
namespace reasampler::capture {
|
||||
|
||||
// The project-relative bank subfolder. All captured wavs live here so the bank
|
||||
// travels with the .rpp (CONTEXT.md §Settled decisions: per-project bank).
|
||||
// travels with the .rpp.
|
||||
inline constexpr const char* kBankSubfolder = "reasampler_bank";
|
||||
|
||||
// A resolved pair of paths for one capture: where REAPER must be told to write
|
||||
@@ -34,150 +26,87 @@ struct BankPaths {
|
||||
std::string fileStem; // <stem> (RENDER_PATTERN — REAPER appends the extension)
|
||||
};
|
||||
|
||||
// NOTE (Q-W3, audit §4e): the content-identity hashes (hashBytes / hashWavContent)
|
||||
// moved to core/capture/wav_codec.{h,cpp} — the ONE pure owner of the WAV/RIFF byte
|
||||
// format — so this module holds path arithmetic only, with no RIFF chunk knowledge.
|
||||
|
||||
// Normalizes a path to forward slashes and strips any trailing slash. Empty in
|
||||
// -> empty out. Pure string transform (does not consult the filesystem).
|
||||
// Platform case rule: on Windows (_WIN32) the result is also lowercased so that
|
||||
// paths differing only in drive-letter or component casing compare equal (Windows
|
||||
// paths are case-insensitive). On macOS/Linux the case is preserved exactly (those
|
||||
// filesystems are case-sensitive).
|
||||
// Normalizes a path to forward slashes and strips any trailing slash (does not
|
||||
// consult the filesystem). On Windows (_WIN32) also lowercases the result so
|
||||
// paths differing only in casing compare equal; macOS/Linux preserve case.
|
||||
std::string normalizeSlashes(const std::string& path);
|
||||
|
||||
// Sanitizes a caller-supplied base name into a filesystem-safe stem: keeps
|
||||
// [A-Za-z0-9._-], replaces every other byte (spaces, slashes, quotes, control)
|
||||
// with '_', and collapses to "capture" if nothing usable remains. Deterministic:
|
||||
// the same input always yields the same stem (feeds bit-identical file naming).
|
||||
// [A-Za-z0-9._-], replaces every other byte with '_', and collapses to
|
||||
// "capture" if nothing usable remains. Deterministic.
|
||||
std::string sanitizeStem(const std::string& baseName);
|
||||
|
||||
// Derives the bank paths for one capture.
|
||||
// projectDir : absolute directory of the current .rpp (any slash style)
|
||||
// baseName : human base for the file stem (sanitized)
|
||||
// uniqueTag : caller-supplied disambiguator appended to the stem (e.g. a
|
||||
// timestamp or counter) so repeated captures do not collide.
|
||||
// Also sanitized. May be empty.
|
||||
// Produces "<stem>[_<tag>].wav". The relativePath is always project-relative and
|
||||
// forward-slashed so it satisfies BankModel::add's relative-only invariant.
|
||||
// Derives the bank paths for one capture: baseName is the sanitized file-stem
|
||||
// source, uniqueTag an optional sanitized disambiguator (timestamp/counter) so
|
||||
// repeated captures don't collide. Produces "<stem>[_<tag>].wav".
|
||||
BankPaths deriveBankPaths(const std::string& projectDir,
|
||||
const std::string& baseName,
|
||||
const std::string& uniqueTag);
|
||||
|
||||
// The project-relative index spelling for a bank file KNOWN ONLY by its file name —
|
||||
// the forward derivation the Phase R prune shell uses to spell an ENUMERATED folder
|
||||
// entry the SAME way deriveBankPaths spelled it at capture time. By construction it
|
||||
// is the identical expression deriveBankPaths().relativePath uses (kBankSubfolder +
|
||||
// "/" + fileName), so a file the capture path created and a directory listing of that
|
||||
// same file resolve to the byte-identical relative string — the safety-critical
|
||||
// spelling-consistency the prune core's exact-string match depends on (a divergence
|
||||
// here could make a referenced file look like an orphan). fileName is a bare entry
|
||||
// name (no directory component); the caller supplies forward-slash-free names from the
|
||||
// folder enumeration. Empty in -> empty out.
|
||||
// The project-relative index spelling for a bank file known only by its file
|
||||
// name (bare entry, no directory) — the prune shell uses this to spell an
|
||||
// enumerated folder entry the SAME way deriveBankPaths spelled it at capture
|
||||
// time; a divergence here could make a referenced file look like an orphan.
|
||||
std::string bankRelativeForName(const std::string& fileName);
|
||||
|
||||
// --- Persist-side path arithmetic (M4) --------------------------------------
|
||||
// --- Persist-side path arithmetic -------------------------------------------
|
||||
//
|
||||
// The index stores relative paths only; on project load the persist shell must
|
||||
// turn each entry's relativePath back into an absolute path against the CURRENT
|
||||
// project directory (so a project opened from a new location still resolves its
|
||||
// bank). This is the inverse of the relativePath the capture path produced.
|
||||
//
|
||||
// projectDir : absolute directory of the current .rpp (any slash style)
|
||||
// relativePath : a project-relative index entry (e.g. "reasampler_bank/x.wav")
|
||||
//
|
||||
// Returns "<projectDir>/<relativePath>" forward-slashed. Returns empty when
|
||||
// either input is empty (no default-location fallback — CLAUDE.md invariant) so
|
||||
// a caller that ignores an unsaved/unset project fails loudly rather than
|
||||
// resolving against CWD.
|
||||
// The index stores relative paths only; on project load the persist shell
|
||||
// turns each relativePath back into an absolute path against the current
|
||||
// project directory — the inverse of deriveBankPaths.
|
||||
|
||||
// Returns "<projectDir>/<relativePath>" forward-slashed, or empty if either
|
||||
// input is empty (no default-location fallback — an unsaved/unset project
|
||||
// fails loudly rather than resolving against CWD).
|
||||
std::string resolveBankFile(const std::string& projectDir,
|
||||
const std::string& relativePath);
|
||||
|
||||
// The project directory that holds a .rpp: its parent directory, forward-slashed,
|
||||
// trailing slash stripped. Empty in -> empty out (an unsaved project has an empty
|
||||
// .rpp path, which must stay empty so resolveBankFile refuses to resolve — the
|
||||
// no-default-location invariant). This is the M4 convention persist uses to place
|
||||
// the bank alongside the .rpp; extracted here (pure) so the VST3 instrument resolves
|
||||
// audio paths the SAME way persist does rather than re-implementing the derivation.
|
||||
// The project directory that holds a .rpp: parent directory, forward-slashed,
|
||||
// trailing slash stripped. Empty in -> empty out (an unsaved project reports
|
||||
// an empty .rpp path). Pure so the VST3 instrument resolves audio paths the
|
||||
// same way persist does.
|
||||
std::string projectDirOfRpp(const std::string& rppPath);
|
||||
|
||||
// A relocation plan for the physical bank folder on Save-As to a new project
|
||||
// location. The index's relative paths do NOT change (they are relative to the
|
||||
// project dir, which is what moved with the .rpp), so relocation is purely a
|
||||
// folder move: copy/move the whole bank subfolder from the old project dir to
|
||||
// the new one. Both dirs are absolute, forward-slashed, trailing-slash-stripped.
|
||||
// project dir, which moved with the .rpp), so relocation is purely a folder
|
||||
// move. Both dirs are absolute, forward-slashed, trailing-slash-stripped.
|
||||
struct BankRelocation {
|
||||
std::string oldBankDir; // <oldProjectDir>/reasampler_bank
|
||||
std::string newBankDir; // <newProjectDir>/reasampler_bank
|
||||
bool needed = false; // false when old==new (Save in place, not Save-As)
|
||||
};
|
||||
|
||||
// Derives the relocation plan from the old and new project directories.
|
||||
// oldProjectDir : project dir the bank currently sits under (any slash style)
|
||||
// newProjectDir : project dir the .rpp was just saved to (any slash style)
|
||||
// `needed` is true iff the normalized dirs differ (a genuine Save-As-to-new-dir).
|
||||
// Returns a plan with empty dirs and needed=false when either input is empty.
|
||||
// Derives the relocation plan: `needed` is true iff the normalized old/new
|
||||
// project dirs differ (a genuine Save-As-to-new-dir); empty dirs/needed=false
|
||||
// when either input is empty.
|
||||
BankRelocation deriveRelocationPlan(const std::string& oldProjectDir,
|
||||
const std::string& newProjectDir);
|
||||
|
||||
// --- Project-identity transition (W12 combined identity fix) -----------------
|
||||
// --- Project-identity transition ---------------------------------------------
|
||||
//
|
||||
// What the persist timer must do on each tick. Identity rests on TWO facts,
|
||||
// layered GUID-PRIMARY:
|
||||
// 1. the minted GUID — content-based identity of record, stored in ext state.
|
||||
// It is IMMUNE to REAPER recycling a closed project's ReaProject* address,
|
||||
// so it is checked FIRST.
|
||||
// 2. sameProjectObject — did the same live ReaProject* stay active across the
|
||||
// two ticks (computed in poll() as `proj == lastProject_`)? Used ONLY to
|
||||
// disambiguate the same-GUID case: a forked sibling (Save-As copied our GUID
|
||||
// onto a distinct object) vs a genuine Save-As (one object, new path).
|
||||
//
|
||||
// This fix layers both prior designs, GUID-primary. M4 (GUID-only) broke Save-As
|
||||
// forks: Save-As copies the whole .rpp incl. our stored GUID, so a fork and its
|
||||
// parent share a GUID on disk. W10 (pointer-primary, GUID voided) broke pointer
|
||||
// RECYCLING: REAPER reuses a closed project's address, so a reopened/new project
|
||||
// can present the previous project's pointer with a different stored GUID —
|
||||
// pointer-primary read that as NoOp/SaveAsRelocate and the bank never reloaded.
|
||||
// Checking the GUID first catches recycling; the pointer then separates a fork
|
||||
// (same GUID, different object -> Load) from a Save-As (same GUID, same object,
|
||||
// new path -> relocate).
|
||||
//
|
||||
// The load-bearing rule: a DIFFERENT record identity (GUID) is always a Load; a
|
||||
// DIFFERENT project object with the same GUID is a fork Load, never a relocate.
|
||||
// What the persist timer must do on each tick. GUID is checked FIRST because
|
||||
// two prior pointer-primary/GUID-only designs each broke a real case: a
|
||||
// GUID-only check misreads a Save-As fork as the same project (fork and
|
||||
// parent share a GUID on disk); a pointer-primary check misreads REAPER
|
||||
// recycling a closed project's ReaProject* address onto an unrelated project
|
||||
// (a different project, same recycled pointer, read as NoOp/SaveAsRelocate —
|
||||
// the bank never reloads). Checking GUID first catches recycling; the pointer
|
||||
// (sameProjectObject) then separates a forked sibling (Load) from a genuine
|
||||
// Save-As (SaveAsRelocate).
|
||||
enum class ProjectTransition {
|
||||
NoOp, // same object, same GUID, same location — nothing to do
|
||||
Load, // a different project is active — load ITS index from ext state
|
||||
SaveAsRelocate, // SAME object + SAME GUID, new .rpp location — relocate the bank
|
||||
};
|
||||
|
||||
// Classifies what a poll tick observed.
|
||||
// sameProjectObject : true iff the SAME ReaProject* stayed active across the two
|
||||
// ticks (poll() computes `proj == lastProject_`). The pure
|
||||
// classifier takes the bool, not the raw pointer, to stay
|
||||
// REAPER-free and testable.
|
||||
// lastGuid : the GUID of the project persist last acted on ("" if none/unsaved)
|
||||
// lastPath : that project's .rpp path when last seen ("" if unsaved)
|
||||
// currentGuid : the GUID stored in the now-active project's ext state ("" if
|
||||
// unsaved or never written)
|
||||
// currentPath : the now-active project's .rpp path ("" if unsaved)
|
||||
//
|
||||
// Rules (evaluated in EXACTLY this order):
|
||||
// 1. currentGuid != lastGuid -> Load (different record identity:
|
||||
// recycled pointer w/ different GUID,
|
||||
// new/unsaved<->saved, or two distinct
|
||||
// saved projects)
|
||||
// 2. !sameProjectObject -> Load (same GUID, different object:
|
||||
// forked sibling, or two unsaved projects)
|
||||
// 3. currentPath != lastPath -> SaveAsRelocate (same object + same GUID,
|
||||
// new path: genuine Save-As, or first save
|
||||
// of an unsaved project — relocate no-ops
|
||||
// on the empty old dir, poll() mints a GUID)
|
||||
// 4. otherwise -> NoOp (same object, same GUID, same path)
|
||||
//
|
||||
// The GUID (identity of record) leads; the pointer only disambiguates the same-GUID
|
||||
// case (fork-Load in step 2 vs Save-As in step 3). The empty-GUID safety (unsaved
|
||||
// projects never physically relocate) is preserved because an empty old project dir
|
||||
// makes deriveRelocationPlan's `needed` false.
|
||||
// Classifies what a poll tick observed. sameProjectObject is passed as a bool
|
||||
// (not the raw pointer) to keep the classifier REAPER-free and testable;
|
||||
// lastGuid/lastPath is the project persist last acted on, currentGuid/
|
||||
// currentPath the now-active project (both "" if unsaved/unwritten).
|
||||
// Evaluated in order: currentGuid!=lastGuid -> Load; !sameProjectObject ->
|
||||
// Load (forked sibling); currentPath!=lastPath -> SaveAsRelocate (also covers
|
||||
// first save of an unsaved project); else NoOp.
|
||||
ProjectTransition classifyProjectTransition(bool sameProjectObject,
|
||||
const std::string& lastGuid,
|
||||
const std::string& lastPath,
|
||||
|
||||
Reference in New Issue
Block a user