#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 #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 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 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 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 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 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_; } // 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 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(); // 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: 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 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 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