Files
reasampler/src/persist.h
T
daniel 36270e2064 fix(persist): use project-object identity to stop forked-bank cross-contamination
M4's GUID-only classifier read a tab-switch between two Save-As forks (shared
copied GUID, different paths) as a Save-As and clobbered a bank. Thread
sameProjectObject into classifyProjectTransition: a different object always
Loads, never relocates; a forked sibling gets re-GUID'd to diverge.
2026-07-22 21:28:54 -04:00

119 lines
6.3 KiB
C++

#pragma once
// persist — the REAPER-facing bridge between the in-memory BankIndex and project
// ext state (CLAUDE.md §load-bearing split; CONTEXT.md §Persistence & paths).
//
// Save: serialize the BankIndex JSON -> SetProjExtState under namespace
// "reasampler" (ext state lives inside the .rpp, so the index travels with the
// project for free).
// Load: on project load, GetProjExtState -> bank_model::deserialize -> in-memory
// BankIndex, then resolve each entry's bank file against the CURRENT project
// dir (project-relative resolution — a project opened from a new location still
// finds its bank).
// Save-As: when the project path changes, relocate the physical bank folder so
// the wavs end up under the new .rpp (the index's relative paths stay valid).
//
// The header is REAPER-free (no SDK types leak here): callers interact through a
// ReaSamplerSession that owns the bank and the persist lifecycle. All REAPER API
// calls live in persist.cpp. It depends on bank_model (pure) for JSON round-trip
// and capture_paths (pure) for the path arithmetic it drives.
#include <string>
#include "bank_model.h"
#include "view_mode_model.h"
namespace reasampler {
// The ext-state namespace the index JSON is stored under. FOREVER-STABLE once
// shipped: changing it orphans every already-saved project's index.
inline constexpr const char* kProjExtNamespace = "reasampler";
// The ext-state key the index JSON is stored under (one key holds the whole
// serialized BankIndex). FOREVER-STABLE for the same reason.
inline constexpr const char* kProjExtIndexKey = "bank_index";
// The ext-state key the Design-View ViewModeModel JSON is stored under (one key
// holds the whole serialized model: modes + membership + show-both + snapshots +
// active mode). Distinct from kProjExtIndexKey — one namespace, two keys.
// FOREVER-STABLE: changing it orphans every already-saved project's view state.
inline constexpr const char* kProjExtViewKey = "view_state";
// The ext-state key holding a GUID we mint per project to establish CONTENT-BASED
// project identity (REAPER exposes no stable per-project GUID). poll() uses it to
// tell a genuine Save-As (same GUID, new .rpp path) apart from a project switch
// onto a recycled ReaProject* pointer (different GUID). FOREVER-STABLE: changing
// it strands the identity of every already-saved project. See persist.cpp.
inline constexpr const char* kProjExtGuidKey = "project_guid";
// Owns the session's BankIndex and drives persistence against the active REAPER
// project. One instance lives for the extension's lifetime (main.cpp). It tracks
// the project identity it last saw so the timer tick can detect a project load
// (a different project became active) and a Save-As (SAME project, path changed):
//
// * project load -> load the index from ext state, resolve bank paths
// * Save-As (new dir) -> relocate the bank folder under the new .rpp
//
// Identity rests on the live ReaProject* pointer FIRST (a genuine Save-As is one
// object saved to a new path — same pointer; a tab-switch/open is a different
// object), with a minted GUID as a SECONDARY signal to survive pointer *reuse*
// (REAPER recycles a closed project's address). The pointer is what distinguishes
// a Save-As from a switch between two FORKED siblings that share a copied GUID on
// disk (the W10 data-integrity defect: dropping the pointer let a fork tab-switch
// masquerade as a Save-As and clobber a bank).
//
// The bank itself is exposed for the capture/action layer to mutate; persist
// only reads it on save and replaces it on load.
class ReaSamplerSession {
public:
ReaSamplerSession() = default;
// The in-memory bank. The action/capture layer adds captures here; persist
// serializes it on save and replaces it on project load.
BankIndex& bank() { return bank_; }
const BankIndex& bank() const { return bank_; }
// The in-memory Design-View model. The view/action layer mutates it (tag,
// toggle, snapshot); persist serializes it on save and replaces it on project
// load — exactly as it treats the bank. D3 persists MODEL STATE only; applying
// visibility/processing (reapply-on-open) is D4's job, not this member's.
ViewModeModel& view() { return view_; }
const ViewModeModel& view() const { return view_; }
// Serialize the current bank to the active project's ext state (namespace
// "reasampler"). Non-destructive beyond writing our own ext-state key. Safe
// to call when there is no active/saved project (it no-ops).
void saveToActiveProject();
// Poll the active project. Detects a project load (active project changed)
// and a Save-As (active project's .rpp path changed) and reacts accordingly.
// Intended to be driven by REAPER's "timer" register. Idempotent per tick.
void poll();
private:
BankIndex bank_;
// The Design-View model. Default-constructed = Arrange + Design seeded, active
// = Arrange; loadFromProject leaves this default when a project has no stored
// view_state (older project), so an absent key is graceful, not a crash.
ViewModeModel view_;
// The project identity last observed by poll(), used to detect load/Save-As.
// The pointer is the PRIMARY signal (same object across ticks = a candidate
// Save-As; different object = a switch/open, never a relocate). The GUID and
// path travel alongside: the GUID distinguishes pointer *reuse* and drives
// forked-sibling re-divergence; the path tells a Save-As from an idle tick.
// Held as void* so the header stays REAPER-free; it is a compared-only opaque
// handle (never dereferenced), so a stale/recycled address is harmless.
void* lastProject_ = nullptr; // last active ReaProject* (opaque; compare only)
std::string lastGuid_; // "" until the first saved project is seen
std::string lastRppPath_; // .rpp path last seen for lastProject_
bool primed_ = false; // false until the first poll() observes state
// Load the index from the given project's ext state and resolve bank paths
// against projectDir. Replaces the in-memory bank. projectDir empty -> clears
// the bank (unsaved project has no resolvable bank).
void loadFromProject(void* proj, const std::string& projectDir);
};
} // namespace reasampler