feat(persist): M4 bank persistence + Save-As relocation

Serialize BankIndex to project ext state ('reasampler'), reload on project
load, resolve paths project-relative. Save-As copies the bank to the new .rpp;
identity keyed off a minted GUID (not the recycled ReaProject*) so project
switches don't clobber banks. Pure classifyProjectTransition tested.
This commit is contained in:
2026-07-22 19:55:09 -04:00
parent a9ba532f07
commit 53da916917
7 changed files with 683 additions and 6 deletions
+60
View File
@@ -73,4 +73,64 @@ BankPaths deriveBankPaths(const std::string& projectDir,
return p;
}
std::string resolveBankFile(const std::string& projectDir,
const std::string& relativePath) {
// No default-location fallback (CLAUDE.md invariant): an empty project dir or
// relative path yields empty, not a bare relative path resolved against CWD.
if (projectDir.empty() || relativePath.empty()) {
return {};
}
const std::string dir = normalizeSlashes(projectDir);
const std::string rel = normalizeSlashes(relativePath);
if (dir.empty() || rel.empty()) {
return {};
}
return dir + "/" + rel;
}
BankRelocation deriveRelocationPlan(const std::string& oldProjectDir,
const std::string& newProjectDir) {
BankRelocation r;
if (oldProjectDir.empty() || newProjectDir.empty()) {
return r; // needed=false, empty dirs — nothing to relocate
}
const std::string oldDir = normalizeSlashes(oldProjectDir);
const std::string newDir = normalizeSlashes(newProjectDir);
r.oldBankDir = oldDir + "/" + kBankSubfolder;
r.newBankDir = newDir + "/" + kBankSubfolder;
// A Save (in place) leaves the project dir unchanged — nothing to relocate.
// Only a Save-As to a different directory needs the bank moved.
r.needed = (oldDir != newDir);
return r;
}
ProjectTransition classifyProjectTransition(const std::string& lastGuid,
const std::string& lastPath,
const std::string& currentGuid,
const std::string& currentPath) {
// A changed GUID is the unambiguous signal of a different project — load it.
// This is the whole point of the content-based identity: it survives pointer
// recycling that the old pointer-equality check could not distinguish from a
// Save-As.
if (currentGuid != lastGuid) {
return ProjectTransition::Load;
}
// Same GUID from here on. If it is empty, we have NO proof the two ticks saw
// the same project (an unsaved project can't carry a stored GUID). A path
// change under an empty GUID is therefore a first-save or a switch between
// unsaved projects — load, never relocate (relocating would copy the wrong
// bank over a real one, the defect being fixed).
if (currentGuid.empty()) {
return (currentPath == lastPath) ? ProjectTransition::NoOp
: ProjectTransition::Load;
}
// Same NON-EMPTY GUID: proven same project. A path change is a genuine
// Save-As to a new location; an unchanged path is Save-in-place / idle.
return (currentPath == lastPath) ? ProjectTransition::NoOp
: ProjectTransition::SaveAsRelocate;
}
} // namespace reasampler