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:
@@ -0,0 +1,93 @@
|
||||
#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"
|
||||
|
||||
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 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
|
||||
// (identity changed) and a Save-As (same project, path changed) and react:
|
||||
//
|
||||
// * project load -> load the index from ext state, resolve bank paths
|
||||
// * Save-As (new dir) -> relocate the bank folder under the new .rpp
|
||||
//
|
||||
// Identity is CONTENT-BASED, not pointer-based: persist keys Load/Save-As off a
|
||||
// GUID it mints and stores in each project's ext state, not the ReaProject*
|
||||
// pointer (REAPER recycles pointer addresses across close/open, which let a
|
||||
// project switch masquerade as a Save-As and clobber a bank — the M4 defect).
|
||||
//
|
||||
// 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_; }
|
||||
|
||||
// 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 project identity last observed by poll(), used to detect load/Save-As.
|
||||
// Identity is the GUID we mint per project (kProjExtGuidKey), NOT the raw
|
||||
// ReaProject* pointer — see the class comment for why. The .rpp path is
|
||||
// tracked alongside so a same-GUID path change (Save-As) is distinguishable
|
||||
// from a same-GUID same-path idle tick (Save in place / no change).
|
||||
std::string lastGuid_; // "" until the first saved project is seen
|
||||
std::string lastRppPath_; // .rpp path last seen for lastGuid_
|
||||
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
|
||||
Reference in New Issue
Block a user