Files
reasampler/src/persist.h
T
daniel c33cb50632 Merge dev into phase-b-multibank (integrate parallel M7/8 + Phase D work before dev promotion)
# Conflicts:
#	CLAUDE.md
#	CMakeLists.txt
#	src/actions.cpp
#	src/bank_panel.cpp
#	src/persist.h
2026-07-25 23:30:44 -04:00

183 lines
10 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_book.h"
#include "bank_model.h"
#include "tail_control.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 RETIRED legacy ext-state key: pre-multi-bank projects stored the whole
// serialized BankIndex here (single bank). Phase B2 no longer WRITES it — on save
// the key is cleared (SetProjExtState with "" deletes it) and the book is written
// under kProjExtBanksKey instead. It is still READ once, on load of a legacy
// project, to migrate its single index into the pool (BankBook's parse-time
// promotion). FOREVER-STABLE as a read key for that migration path.
inline constexpr const char* kProjExtIndexKey = "bank_index";
// The multi-bank ext-state key (Phase B): one key holds the whole serialized
// BankBook — the pool folded in as bank-zero plus every named bank, each with its
// own BankIndex, ordinals, and the active-bank id. AUTHORITATIVE going forward;
// supersedes kProjExtIndexKey. FOREVER-STABLE once shipped: changing it orphans
// every already-saved project's banks.
inline constexpr const char* kProjExtBanksKey = "banks";
// 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 the docked panel's TailSetting JSON (mode + manualMs) is stored
// under, so the tail choice travels inside the .rpp and loads per project. Distinct
// from the index/view keys — one namespace, three keys. FOREVER-STABLE: changing it
// orphans every already-saved project's tail setting (which then falls back to the
// default — graceful, but the user's saved choice would be lost).
inline constexpr const char* kProjExtTailKey = "tail_setting";
// 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 BankBook (Phase B: pool + named banks) 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 is layered GUID-PRIMARY: the minted GUID (content-based identity of
// record, immune to REAPER recycling a closed project's ReaProject* address) is
// checked FIRST, and the live pointer disambiguates only the same-GUID case — a
// forked sibling (same GUID, different object -> Load) vs a genuine Save-As (same
// GUID, same object, new path -> relocate). GUID-first catches pointer recycling
// (a reopened/new project reusing the previous address with a different GUID — the
// W12 defect that stopped the bank reloading); the pointer catches forks (Save-As
// copies our GUID onto a distinct object — the W10 defect that clobbered a bank).
//
// The book 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 multi-bank book (Phase B): the pool + named banks, each wrapping a
// BankIndex, plus the active-bank id. The action layer (B3) creates / renames /
// reorders / deletes banks and moves samples here; the panel (B4) reads it;
// persist serializes it under the `banks` key on save and replaces it on load.
BankBook& book() { return book_; }
const BankBook& book() const { return book_; }
// The capture add-target: the ACTIVE bank's BankIndex (defaults to the pool).
// The capture path adds a captured Sample through this seam, so a capture lands
// in whichever bank is active — the single behavioural change B2 wires in over
// M7/M8 (the capture backends are untouched; only the target index moved). The
// panel/insert readers that displayed the single index continue to read it here
// unchanged; today it resolves to the pool (default active), matching prior
// single-bank behaviour, until B3/B4 let the user switch the active bank.
BankIndex& bank() { return book_.activeIndex(); }
const BankIndex& bank() const { return book_.activeIndex(); }
// 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_; }
// The docked panel's tail setting (mode + manualMs), authoritative here — NOT in
// panel state — so it travels inside the .rpp: persist serializes it on save and
// replaces it on project load exactly as it treats the bank and view model. The
// panel reads/writes it through this seam (bank_panel holds the session), and the
// capture actions read it via bankPanelTailSetting. Default None / 2 s manual for
// an unsaved or pre-feature project (no stored key -> this default survives load).
TailSetting& tail() { return tail_; }
const TailSetting& tail() const { return tail_; }
// Serialize the current book (under the `banks` key), view model, and tail setting
// to the active project's ext state (namespace "reasampler"), and clear the retired
// legacy `bank_index` key. Non-destructive beyond writing our own ext-state keys.
// 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();
// Load signal for the D4 reapply-on-open glue. poll() raises this whenever it
// (re)loads the view model from a project — prime, a project switch/open, or a
// forked-sibling load. consumeLoadSignal() returns true ONCE per load and clears
// it, so the integration layer (main.cpp) can react by reapplying the saved
// active mode's visibility exactly once, then goes quiet on idle ticks.
//
// Signal-based seam by design: persist stays MODEL-ONLY (it never calls the view
// shell), so there is no persist -> view dependency. main.cpp owns the glue —
// it drives both persist.poll() and view::applyMode, so the reapply wiring lives
// where those two already meet. D3 deliberately deferred exactly this to D4.
bool consumeLoadSignal();
private:
BankBook book_;
// 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 tail setting. Default None / kDefaultManualTailMs; loadFromProject resets it
// to this default when a project has no stored tail_setting key (older / never-
// adjusted project), so an absent key is graceful. Peer to bank_/view_.
TailSetting tail_;
// The project identity last observed by poll(), used to detect load/Save-As.
// The GUID is the PRIMARY signal (a different stored GUID = a different project
// of record = Load, immune to pointer recycling). The pointer disambiguates the
// same-GUID case (different object = forked sibling -> Load; same object + new
// path -> Save-As) 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
bool loadPending_ = false; // raised by loadFromProject; drained by consumeLoadSignal
// Load the book from the given project's ext state (the `banks` key, else the
// legacy `bank_index` key migrated into the pool) and resolve bank paths against
// projectDir at read time. Replaces the in-memory book. projectDir empty -> the
// book is reset to empty (unsaved project has no resolvable banks).
void loadFromProject(void* proj, const std::string& projectDir);
};
} // namespace reasampler