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:
2026-07-22 19:55:09 -04:00
parent a9ba532f07
commit 53da916917
7 changed files with 683 additions and 6 deletions
+28 -6
View File
@@ -22,6 +22,7 @@
#include "bank_model.h"
#include "capture.h"
#include "persist.h"
// Persistent action-id prefix for the ReaSampler action family.
// Every bindable action (capture / insert / slot / verify) mints its command id
@@ -43,10 +44,20 @@ reaper_plugin_info_t* g_rec = nullptr; // REAPER's dispatch struct
// (user keybindings key off it) — see the prefix note above.
static int g_cmdCaptureMasterSpike = 0;
// In-memory bank for the spike. M4 replaces this with project ext-state persist;
// for M3 the index lives only for the session, proving the capture->Sample->add
// path end to end.
static reasampler::BankIndex g_bank;
// The persistence session (M4): owns the in-memory BankIndex and bridges it to
// project ext state. A timer tick drives g_session.poll() to detect project
// load / Save-As; capture adds Samples to g_session.bank(); after a capture we
// serialize the bank back into the active project's ext state so it travels with
// the .rpp. Replaces the M3 session-only g_bank.
static reasampler::ReaSamplerSession g_session;
// The timer callback REAPER runs periodically (registered via "timer"). It only
// forwards to the session poll — cheap per tick (reads the active project id and
// its .rpp path, acts only on a change).
static void OnTimer()
{
g_session.poll();
}
// Runs the M3 spike: render the time-selection master mix, add the Sample, log.
static void RunCaptureMasterSpike()
@@ -76,9 +87,14 @@ static void RunCaptureMasterSpike()
return;
}
reasampler::AddResult added = g_bank.add(res.sample);
reasampler::AddResult added = g_session.bank().add(res.sample);
// Persist the updated bank into the active project's ext state so the capture
// survives Save / close+reopen (M4). Non-destructive: writes only our own
// ext-state key. No-ops on an unsaved project (nothing to store into yet).
g_session.saveToActiveProject();
std::string log = "ReaSampler: " + res.message + "\n";
log += " bank size now " + std::to_string(g_bank.size()) +
log += " bank size now " + std::to_string(g_session.bank().size()) +
(added == reasampler::AddResult::Added ? " (added)\n"
: added == reasampler::AddResult::Collapsed ? " (collapsed on hash)\n"
: " (rejected)\n");
@@ -106,6 +122,7 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
// callback with the same strings prefixed '-' (per the contract).
if (g_rec)
{
g_rec->Register("-timer", (void*)&OnTimer);
g_rec->Register("-hookcommand", (void*)&OnHookCommand);
g_rec->Register("-gaccel", (void*)&g_accelCaptureMaster);
g_rec->Register("-command_id",
@@ -139,6 +156,11 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
rec->Register("hookcommand", (void*)&OnHookCommand);
}
// Drive project-load / Save-As detection (M4 persist). The timer polls the
// active project each tick; on a project load it reloads the bank from ext
// state, on a Save-As it relocates the bank folder under the new .rpp.
rec->Register("timer", (void*)&OnTimer);
ShowConsoleMsg("ReaSampler loaded.\n");
return 1; // success — REAPER keeps us loaded