Cut core/wire and shell/persist comment bloat ~46% (comments only, zero code change)
This commit is contained in:
@@ -1,26 +1,22 @@
|
||||
// ext_state_io.cpp — the ext-state ↔ JSON serialization half of the persist seam
|
||||
// (Q-W5 split of the former persist.cpp; see session.h for the TU map and
|
||||
// ext_state_io.h for the key contract): the session's save/load/assignment-request
|
||||
// bridge, plus the shared persist_detail helpers (active-project read, growing
|
||||
// ext-state read, GUID minting, bank-folder relocation) the sibling TUs call.
|
||||
// ext_state_io.cpp — the ext-state <-> JSON serialization half of the persist
|
||||
// seam (see session.h for the TU map, ext_state_io.h for the key contract):
|
||||
// the session's save/load/assignment-request bridge, plus the shared
|
||||
// persist_detail helpers the sibling TUs call.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
||||
// pointers; here they are extern (CLAUDE.md §contract).
|
||||
//
|
||||
// Storage: SetProjExtState / GetProjExtState, namespace "reasampler". Phase B: the
|
||||
// whole BankBook (pool as bank-zero + named banks) is written under key "banks"
|
||||
// (authoritative); the legacy single-bank key "bank_index" is RETIRED — cleared on
|
||||
// save (SetProjExtState with "" deletes it) and read only once, to migrate a pre-
|
||||
// multi-bank project's index into the pool. Ext state is stored INSIDE the .rpp, so
|
||||
// the banks travel with the project automatically (CONTEXT.md §Persistence & paths).
|
||||
// The only thing that does NOT travel for free is the physical bank folder; on
|
||||
// Save-As to a new directory we relocate it so the indices' relative paths still
|
||||
// resolve (poll(), session.cpp, executes the relocation this TU implements).
|
||||
// Storage: SetProjExtState/GetProjExtState, namespace "reasampler". The whole
|
||||
// BankBook (pool as bank-zero + named banks) is written under key "banks"
|
||||
// (authoritative); the legacy single-bank key "bank_index" is retired —
|
||||
// cleared on save, read only once to migrate a pre-multi-bank project into
|
||||
// the pool. Ext state is stored inside the .rpp, so the banks travel with the
|
||||
// project automatically; the physical bank folder does not, so a Save-As to a
|
||||
// new directory relocates it (poll(), session.cpp).
|
||||
//
|
||||
// NON-DESTRUCTIVE: this module writes ONLY our own ext-state key and moves ONLY
|
||||
// our own reasampler_bank/ folder. It never touches the user's media, items, or
|
||||
// other ext-state namespaces.
|
||||
// Non-destructive: this module writes only our own ext-state keys and moves
|
||||
// only our own reasampler_bank/ folder.
|
||||
|
||||
#include "shell/persist/ext_state_io.h"
|
||||
|
||||
@@ -53,11 +49,9 @@ namespace reasampler::persist_detail {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Read the active project pointer and its .rpp path in one shot. idx=-1 is the
|
||||
// current project tab (SDK header line ~1262). The out-buffer receives the full
|
||||
// .rpp path, EMPTY for a never-saved project (the reliable unsaved sentinel —
|
||||
// same fact capture.cpp relies on). Returns nullptr proj only when there is no
|
||||
// active project at all.
|
||||
// idx=-1 is the current project tab. rppPathOut is empty for a never-saved
|
||||
// project (the reliable unsaved sentinel); returns nullptr only with no active
|
||||
// project at all.
|
||||
void* readActiveProject(std::string& rppPathOut) {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
ReaProject* proj = EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
@@ -65,24 +59,13 @@ void* readActiveProject(std::string& rppPathOut) {
|
||||
return proj;
|
||||
}
|
||||
|
||||
// Parent directory of the .rpp, forward-slashed, no trailing slash. Empty in ->
|
||||
// empty out. Mirrors capture.cpp's derivation so the bank sits alongside the
|
||||
// .rpp (NOT GetProjectPathEx, which returns the recording path — see capture.cpp
|
||||
// for the full rationale). The derivation itself is projectDirOfRpp in capture_paths
|
||||
// (pure) — the SAME convention the VST3 instrument resolves audio paths by, so both
|
||||
// artifacts share one implementation rather than duplicating the parent-of-.rpp step.
|
||||
// NOT GetProjectPathEx, which returns the recording path, not the .rpp's own
|
||||
// directory. Delegates to the pure projectDirOfRpp so both artifacts share one
|
||||
// implementation.
|
||||
std::string projectDirOf(const std::string& rppPath) {
|
||||
return capture::projectDirOfRpp(rppPath);
|
||||
}
|
||||
|
||||
// GetProjExtState needs a caller-supplied buffer; the index JSON can be large
|
||||
// (many samples). The grow-until-strict-fit retry policy is the SHARED pure
|
||||
// wire::readProjExtStateGrowing (T2-04 — the same policy the usage_scan and
|
||||
// VST-bridge reads run); this wrapper binds the REAPER call and
|
||||
// folds the terminal cases persist's callers expect: "" for an absent key (a valid
|
||||
// empty bank, not an error) and a console warning + "" for a value exceeding the
|
||||
// 16 MB ceiling, so an over-large value reads as "too large to load", not silent
|
||||
// data loss (mirrors the malformed-JSON warning in loadFromProject).
|
||||
std::string getProjExtStateString(void* proj, const char* ns, const char* key) {
|
||||
using wire::GrowingExtStateRead;
|
||||
const GrowingExtStateRead read = wire::readProjExtStateGrowing(
|
||||
@@ -103,8 +86,7 @@ std::string getProjExtStateString(void* proj, const char* ns, const char* key) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// The GUID we mint per project, formatted "{XXXXXXXX-....}" by guidToString.
|
||||
// guidToString wants a >=64-char destination (SDK header line ~3846).
|
||||
// guidToString wants a >=64-char destination.
|
||||
std::string genProjectGuidString() {
|
||||
GUID g{};
|
||||
genGuid(&g);
|
||||
@@ -113,13 +95,8 @@ std::string genProjectGuidString() {
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// Ensure a SAVED project carries a stored GUID, minting and writing one if it
|
||||
// has none yet (a project saved before this feature shipped, or a brand-new
|
||||
// first save). Returns the effective GUID: the existing one, the freshly minted
|
||||
// one, or "" for an unsaved project (no .rpp to store ext state into — the same
|
||||
// gate SetProjExtState/saveToActiveProject already respect on empty path).
|
||||
// Called from BOTH prime and the Load branch so identity is established the same
|
||||
// way on every entry to a project (peer-symmetry: no path skips the mint).
|
||||
// Returns the existing GUID, a freshly minted one, or "" for an unsaved
|
||||
// project. Called from both prime and the Load branch so no path skips the mint.
|
||||
std::string ensureProjectGuid(void* proj, const std::string& rppPath,
|
||||
const std::string& currentGuid) {
|
||||
if (!proj || rppPath.empty()) return {}; // unsaved -> cannot store a GUID
|
||||
@@ -130,11 +107,9 @@ std::string ensureProjectGuid(void* proj, const std::string& rppPath,
|
||||
return minted;
|
||||
}
|
||||
|
||||
// Copy the bank folder from oldDir to newDir, non-destructively (copy, do not
|
||||
// move — see the handoff for the copy-vs-move rationale). Overwrites existing
|
||||
// files at the destination so a re-save is idempotent. Best-effort: filesystem
|
||||
// errors are swallowed and reported to the console rather than thrown across the
|
||||
// REAPER boundary. Returns true if the copy ran (source existed).
|
||||
// Copy, not move (non-destructive); overwrites existing files at the
|
||||
// destination so a re-save is idempotent. Best-effort: filesystem errors are
|
||||
// swallowed and reported to the console. Returns true if the copy ran.
|
||||
bool relocateBankFolder(const std::string& oldBankDir,
|
||||
const std::string& newBankDir) {
|
||||
std::error_code ec;
|
||||
@@ -168,58 +143,37 @@ bool ReaSamplerSession::saveToActiveProject() {
|
||||
if (!proj) return false; // no active project — nothing to persist
|
||||
if (rppPath.empty()) return false; // unsaved project — no .rpp to store into
|
||||
|
||||
// Phase B: the whole book (pool as bank-zero + named banks) is authoritative and
|
||||
// rides in the `banks` key.
|
||||
const std::string banksJson = book_.serialize();
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtBanksKey, banksJson.c_str());
|
||||
|
||||
// Retire the legacy single-bank `bank_index` key: SetProjExtState with an empty
|
||||
// value DELETES the key (SDK header ~6288: val NULL or "" deletes the data). This
|
||||
// realizes retirement concretely — after any save, a formerly-legacy project
|
||||
// carries `banks` and NO `bank_index`, and going forward the legacy key is never
|
||||
// written. Cheap and idempotent when the key is already absent.
|
||||
// Retire the legacy single-bank key: SetProjExtState with an empty value
|
||||
// deletes it. Idempotent when already absent.
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtIndexKey, "");
|
||||
|
||||
// Additive: the Design-View model rides alongside the banks in its own key.
|
||||
// Independent write — does not disturb the `banks` blob above.
|
||||
// Each of the following rides in its own key, independent of `banks`.
|
||||
const std::string viewJson = view_.serialize();
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtViewKey, viewJson.c_str());
|
||||
|
||||
// Additive: the docked panel's tail setting rides alongside in its own key, so the
|
||||
// tail choice travels inside the .rpp. Independent write — does not disturb the
|
||||
// bank_index or view_state above.
|
||||
const std::string tailJson = capture::serializeTailSetting(tail_);
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtTailKey, tailJson.c_str());
|
||||
|
||||
// Additive: the owned-file manifest (Phase B B-cap) rides alongside in its own
|
||||
// `owned_files` key. Independent write — does not disturb the blobs above. Written
|
||||
// on EVERY save so a capture's manifest record survives Save / Save-As / reopen,
|
||||
// and so the manifest and the bank stay in lockstep on disk (both persisted by the
|
||||
// same saveToActiveProject the capture add-path calls). Uses the channel-derived
|
||||
// namespace (projExtNamespace) like its sibling keys — V4 isolation applies here too.
|
||||
// Written on every save so the manifest and the bank stay in lockstep on disk.
|
||||
const std::string ownedJson = owned_.serialize();
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtOwnedKey, ownedJson.c_str());
|
||||
|
||||
// Phase V (V1/V4): stamp the WRITING version — the build producing this save — under
|
||||
// the version key, on the SAME seam as the keys above so the stamp and MarkProjectDirty
|
||||
// stay paired (no drifting ad-hoc SetProjExtState). stampVersion() (NOT appVersion()) is
|
||||
// the NUMERIC TRIPLE ONLY on both channels — no "-beta" suffix — so the stamp parses as
|
||||
// Stamped on read-back and stays byte-identical to stable regardless of channel; the
|
||||
// channel is already carried by the isolated namespace (projExtNamespace) this writes to.
|
||||
// stampVersion() (not appVersion()) is the numeric triple only, no "-beta"
|
||||
// suffix, so the stamp is byte-identical to stable regardless of channel
|
||||
// — the channel is already carried by the isolated namespace.
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtVersionKey, version::stampVersion().c_str());
|
||||
|
||||
// S9: stamp the current bank-generation counter under its own wire-shared key, on the SAME
|
||||
// seam so the counter and MarkProjectDirty stay paired. The value is whatever
|
||||
// bumpBankGeneration() advanced it to since the last save (0 if never bumped / pre-S9), so
|
||||
// every content mutation's own save carries the fresh generation the instrument reads. The
|
||||
// format is the SHARED pure encoder (instrument::map::formatBankGeneration) so writer and reader agree
|
||||
// byte-for-byte — a decimal integer. Additive: does not disturb the blobs above.
|
||||
// Whatever bumpBankGeneration() advanced the counter to since the last
|
||||
// save (0 if never bumped). Shared encoder so writer/reader agree byte-for-byte.
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtBankGenKey,
|
||||
instrument::map::formatBankGeneration(bankGeneration_).c_str());
|
||||
@@ -234,11 +188,8 @@ bool ReaSamplerSession::writeAssignmentRequest(const std::string& wire) {
|
||||
if (!proj) return false; // no active project — nothing to signal
|
||||
if (rppPath.empty()) return false; // unsaved project — no .rpp to store into
|
||||
|
||||
// One-shot write of the ingest assignment request under its own key (S8). Independent
|
||||
// of the book/view/tail blobs — this is a transient signal to the instrument, not
|
||||
// session state that must ride every save. Uses the channel-derived namespace
|
||||
// (projExtNamespace) like every sibling key — V4 isolation applies here too, so a beta
|
||||
// instrument reads only a beta extension's assignment requests.
|
||||
// One-shot write under its own key: a transient signal to the instrument,
|
||||
// not session state that rides every save.
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtAssignKey, wire.c_str());
|
||||
MarkProjectDirty(static_cast<ReaProject*>(proj));
|
||||
@@ -247,12 +198,8 @@ bool ReaSamplerSession::writeAssignmentRequest(const std::string& wire) {
|
||||
|
||||
namespace {
|
||||
|
||||
// Load the Design-View model from a project's view_state key, or return a fresh
|
||||
// default. An absent/empty key (older project with no view state) yields a
|
||||
// default-constructed model (Arrange + Design seeded, active = Arrange) — graceful,
|
||||
// never a crash. Malformed JSON is warned and also falls back to default, mirroring
|
||||
// the bank's malformed-index handling. The whole model round-trips: modes,
|
||||
// membership, show-both, snapshots, and active mode all ride inside the one blob.
|
||||
// Absent/empty key -> default-constructed model, graceful, never a crash.
|
||||
// Malformed JSON is warned and also falls back to default.
|
||||
ViewModeModel loadViewModel(ReaProject* proj) {
|
||||
if (!proj) return ViewModeModel{};
|
||||
const std::string viewJson =
|
||||
@@ -266,10 +213,8 @@ ViewModeModel loadViewModel(ReaProject* proj) {
|
||||
return std::move(*loaded);
|
||||
}
|
||||
|
||||
// Load the tail setting from a project's tail_setting key, or return the default. An
|
||||
// absent/empty key (older / never-adjusted project) yields the default setting (None /
|
||||
// 2 s manual) — graceful, never a crash. Malformed JSON is warned and also falls back
|
||||
// to default, mirroring the bank's and view's malformed handling.
|
||||
// Absent/empty key -> default (None / 2 s manual). Malformed JSON warns and
|
||||
// falls back to default.
|
||||
capture::TailSetting loadTailSetting(ReaProject* proj) {
|
||||
if (!proj) return capture::TailSetting{};
|
||||
const std::string tailJson =
|
||||
@@ -284,12 +229,9 @@ capture::TailSetting loadTailSetting(ReaProject* proj) {
|
||||
return *loaded;
|
||||
}
|
||||
|
||||
// Load the owned-file manifest from a project's owned_files key, or return an empty
|
||||
// manifest. An absent/empty key (older / never-captured project) yields an empty
|
||||
// manifest — graceful, never a crash. Malformed JSON is warned and also falls back to
|
||||
// empty, mirroring the bank's / view's / tail's malformed handling. Phase R prune then
|
||||
// sees an empty ownership record and (safely) attributes nothing until the next capture
|
||||
// rebuilds it — losing the record degrades safety, never correctness.
|
||||
// Absent/empty key -> empty manifest. Malformed JSON warns and falls back to
|
||||
// empty; prune then attributes nothing until the next capture rebuilds it —
|
||||
// degrades safety, never correctness.
|
||||
model::OwnedFileManifest loadOwnedManifest(ReaProject* proj) {
|
||||
if (!proj) return model::OwnedFileManifest{};
|
||||
const std::string ownedJson =
|
||||
@@ -307,45 +249,28 @@ model::OwnedFileManifest loadOwnedManifest(ReaProject* proj) {
|
||||
} // namespace
|
||||
|
||||
void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDir) {
|
||||
// Raise the load signal for the D4 reapply-on-open glue. loadFromProject is the
|
||||
// single choke point for every load path (prime, project switch/open, forked-
|
||||
// sibling load), so setting it here — and NOT on the Save-As branch, which keeps
|
||||
// the in-memory model as-is — makes the signal fire exactly when a fresh view
|
||||
// model has been installed and its active mode's visibility needs reapplying.
|
||||
// main.cpp drains it via consumeLoadSignal() on the same tick.
|
||||
// loadFromProject is the single choke point for every load path (prime,
|
||||
// project switch/open, forked-sibling load) — NOT the Save-As branch,
|
||||
// which keeps the in-memory model as-is. main.cpp drains this via
|
||||
// consumeLoadSignal() on the same tick.
|
||||
loadPending_ = true;
|
||||
|
||||
// The view model is restored on EVERY load path (peer-symmetry with the bank
|
||||
// reset below): switching to a project with no view state must clear stale
|
||||
// in-memory state, not inherit the previous project's. D3 restores MODEL STATE
|
||||
// only — no visibility/processing is applied here (that is D4).
|
||||
// view_/tail_/owned_ are all restored on EVERY load path: switching to a
|
||||
// project with no stored state must reset to default, never inherit the
|
||||
// previous project's. An undo/redo reload must re-read the restored
|
||||
// values so they match the rolled-back state.
|
||||
view_ = loadViewModel(static_cast<ReaProject*>(proj));
|
||||
|
||||
// The tail setting is restored on EVERY load path too (peer-symmetry): switching
|
||||
// to a project with no stored setting must fall back to the default, not inherit
|
||||
// the previous project's choice (this REPLACES the old session-carry behavior).
|
||||
tail_ = loadTailSetting(static_cast<ReaProject*>(proj));
|
||||
|
||||
// The owned-file manifest is restored on EVERY load path too (peer-symmetry with the
|
||||
// bank/view/tail resets): switching to a project with no stored manifest must reset
|
||||
// to empty, not inherit the previous project's ownership record; an undo/redo reload
|
||||
// (R-B) must re-read the restored manifest so it matches the rolled-back bank state.
|
||||
owned_ = loadOwnedManifest(static_cast<ReaProject*>(proj));
|
||||
|
||||
// Phase V (V1): recover the writing-version stamp on EVERY load path (peer-symmetry
|
||||
// with tail_/view_ above). An absent stamp classifies as PreVersioning, a malformed
|
||||
// one as Unknown — both silent, no console warning (a pre-versioning project is not
|
||||
// an error). getProjExtStateString returns "" for an absent key, which is exactly the
|
||||
// PreVersioning input classifyWritingVersion expects. proj == nullptr -> "" -> default.
|
||||
// An absent stamp classifies as PreVersioning, a malformed one as Unknown
|
||||
// — both silent. proj == nullptr -> "" -> default.
|
||||
writingVersion_ = version::classifyWritingVersion(
|
||||
proj ? getProjExtStateString(proj, projExtNamespace(), kProjExtVersionKey)
|
||||
: std::string{});
|
||||
|
||||
// S9: recover the bank-generation counter on EVERY load path (peer-symmetry with
|
||||
// writingVersion_/tail_/view_ above), so it continues monotonic from the stored value
|
||||
// rather than resetting to 0 on reopen — a next bump then reads > the stored value. A
|
||||
// project switch reads THAT project's counter, not the previous one's; an absent/malformed
|
||||
// stamp (pre-S9 or corrupt) parses to 0 via the SHARED decoder. proj == nullptr -> 0.
|
||||
// Continues monotonic from the stored value rather than resetting to 0 on
|
||||
// reopen; absent/malformed parses to 0 via the shared decoder.
|
||||
bankGeneration_ = instrument::map::parseBankGeneration(
|
||||
proj ? getProjExtStateString(proj, projExtNamespace(), kProjExtBankGenKey)
|
||||
: std::string{});
|
||||
@@ -355,14 +280,9 @@ void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDi
|
||||
return;
|
||||
}
|
||||
|
||||
// Read both possible sources: the authoritative `banks` blob and the retired-but-
|
||||
// possibly-still-present legacy `bank_index`. The precedence + migration decision
|
||||
// (`banks` wins; else the legacy index migrates into the pool; else an empty book)
|
||||
// is pure logic; it is inlined here rather than via BankBook::loadFromPersisted only
|
||||
// so a malformed `banks` blob can be warned on the console (single parse) — a corrupt
|
||||
// blob must read as "ignored", not silent loss, mirroring the prior malformed-index
|
||||
// warning. A malformed `banks` degrades to an empty book and does NOT fall back to
|
||||
// the stale legacy key (which would resurrect superseded single-bank state).
|
||||
// `banks` is authoritative when present; a malformed blob degrades to an
|
||||
// empty book rather than falling back to the stale legacy key (which
|
||||
// would resurrect superseded single-bank state).
|
||||
const std::string banksJson =
|
||||
getProjExtStateString(proj, projExtNamespace(), kProjExtBanksKey);
|
||||
if (!banksJson.empty()) {
|
||||
@@ -374,29 +294,18 @@ void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDi
|
||||
book_ = std::move(*loaded);
|
||||
}
|
||||
} else {
|
||||
// No `banks` yet — fall back to the legacy `bank_index`, migrated into the pool
|
||||
// by BankBook's parse-time promotion. loadFromPersisted covers the legacy-or-
|
||||
// empty tail; passing "" for banksJson takes exactly that branch.
|
||||
// No `banks` yet — migrate the legacy `bank_index` into the pool.
|
||||
const std::string legacyJson =
|
||||
getProjExtStateString(proj, projExtNamespace(), kProjExtIndexKey);
|
||||
book_ = BankBook::loadFromPersisted(std::string{}, legacyJson);
|
||||
}
|
||||
|
||||
// L7 slot migration: seed every bank's display-position SlotMap from its index
|
||||
// insertion order when the loaded blob carried none (a pre-L7 project -> dense,
|
||||
// gap-free, visually identical on first post-L7 load), and reconcile a partial map
|
||||
// (drop stale markers, append unmapped samples) for a blob written by an earlier L7
|
||||
// build. One-way: once the book is re-saved the reconciled slot data is authoritative.
|
||||
// Idempotent, so a fresh empty book is a cheap no-op.
|
||||
// Seed each bank's display-position SlotMap from insertion order when the
|
||||
// loaded blob carried none, and reconcile a partial map. Idempotent.
|
||||
book_.reconcileSlots();
|
||||
|
||||
// Project-relative resolution is a READ-time concern: every BankModel in the book
|
||||
// stores only relative paths (invariant, enforced per-bank at add()), and consumers
|
||||
// (M5 panel, M6 insert) resolve each entry against the CURRENT project dir via
|
||||
// resolveBankFile(projectDir, relativePath). We do NOT rewrite stored paths to
|
||||
// absolute here — that would break the relative-only invariant and travel-with-.rpp.
|
||||
// projectDir is threaded through for those consumers; nothing to do at load time
|
||||
// beyond replacing the in-memory book.
|
||||
// Paths stay relative (read-time resolution is the consumers' job);
|
||||
// nothing to do here beyond replacing the in-memory book.
|
||||
(void)projectDir;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user