Q-W6: registration table (OCP) in main.cpp; bank verbs -> shell/bank_ops(Session&); persist.h + wav_trim + namespaces.h shims deleted; 61/61

capture.h realtime seam split to capture_realtime_shell.h; GetProjExtState grow-loop rehomed to core/wire/ext_state_read; stale persist.cpp/bank_panel.cpp comment refs fixed; CLAUDE.md persist/bank_book/actions bullets updated. Command-id suffixes, display phrases, and undo labels byte-identical.
This commit is contained in:
2026-07-29 13:40:09 -04:00
parent 4831e0e172
commit f3be4d8cce
81 changed files with 970 additions and 1085 deletions
+135 -386
View File
@@ -14,24 +14,25 @@
// storage for those global pointers. Every other .cpp includes
// reaper_plugin_functions.h WITHOUT the define and gets `extern` declarations.
//
// Since Q-W3 this TU is ONLY pointers + entry + dispatch: the capture
// orchestration it used to carry lives in shell/capture/ (capture_orchestrator /
// capture_batch / scope_resolve / realtime_lifecycle). The registration blocks
// below are slated for Q-W6's registration table.
// Since Q-W3 this TU is ONLY pointers + entry + dispatch; since Q-W6 its own
// action family registers through the DATA-DRIVEN TABLE below (kMainActionRows +
// action_registry's registerActionTable/actionTableHandleCommand/
// unregisterActionTable) — adding a bindable action here means adding ONE row and
// its handler function, nothing else (OCP). The design_view / bank / ingest
// families keep their own register/handle/unregister triples, called from entry.
#define REAPERAPI_IMPLEMENT
#include "reaper_plugin.h"
#include "reaper_plugin_functions.h"
#include <cstddef>
#include <deque>
#include <string>
#include <vector>
#include "core/capture/render_settings.h" // captureActionTable
#include "core/version/app_version.h" // channelCommandId / channelActionName / appVersion
#include "core/version/app_version.h" // channelCommandId / appVersion
#include "ingest.h"
#include "persist.h"
#include "shell/actions/action_registry.h" // the Q-W6 registration table
#include "shell/actions/bank_actions.h" // multi-bank action family (B3; Q-W4 home)
#include "shell/actions/design_view_actions.h" // Design View action family (D4; Q-W4 home)
#include "shell/capture/capture_batch.h" // batch + recapture action bodies
@@ -39,75 +40,26 @@
#include "shell/capture/realtime_lifecycle.h" // in-flight realtime state + tick driver
#include "shell/panel/panel_input.h" // bankPanelRefresh / bankPanelNotifyProjectLoaded
#include "shell/panel/panel_window.h" // panel lifecycle (init/toggle/open-query/shutdown)
#include "shell/persist/session.h" // ReaSamplerSession
#include "shell/view/view.h" // reconcileManagedLanes / applyMode
namespace capture = reasampler::capture;
using reasampler::version::channelActionName;
using reasampler::version::channelCommandId;
// Persistent action-id family (Phase V, V4 — channel-qualified). Every bindable action
// mints its command id from commandIdPrefix() + a per-action SUFFIX, and its Actions-list
// name from actionDisplayPrefix() + a phrase, both derived from the ONE channel bit in the
// pure app_version module (channelCommandId / channelActionName). Stable rebuilds the exact
// shipped id ("CEREBELLUM_REASAMPLER_CAPTURE_TRACK"); beta yields the isolated forever-
// family id ("CEREBELLUM_REASAMPLER_BETA_CAPTURE_TRACK"). FOREVER-STABLE per channel: a
// shipped suffix is as permanent as the prefix; user keybindings key off the composed id.
//
// The composed id strings are held here for the module's lifetime (idStore) so both the
// register call and the mirroring '-command_id' unregister pass the SAME stable pointer.
// A std::deque (NOT vector) is used deliberately: it never invalidates references to
// existing elements on push_back, so a c_str() handed out early stays valid after later
// interning — the unload path re-presents these same pointers.
static std::deque<std::string> g_idStore;
// Interns a composed command-id string for the module lifetime and returns its C string.
// Appended-to only during startup registration and read on unload; never cleared until
// process exit, and deque guarantees the returned pointer stays valid.
static const char* internCmdId(const std::string& suffix) {
g_idStore.push_back(channelCommandId(suffix));
return g_idStore.back().c_str();
}
// Globals other files reference via `extern`.
REAPER_PLUGIN_HINSTANCE g_hInst = nullptr; // this module's instance handle
reaper_plugin_info_t* g_rec = nullptr; // REAPER's dispatch struct
// ---- Capture action family (two FX scopes) ---------------------------------
// Two bindable SCOPE actions from captureActionTable() (render_settings, pure):
// capture item / track. Each infers its range (razor-else-time) and enforces the
// FX-scope invariant via FX-bypass-around-render (FxBypassGuard, now in
// capture_orchestrator):
// Item -> take/item FX only (bypass the item's track + ancestors + master).
// Track -> item FX + track's own FX (bypass ancestors + master).
// There is NO master scope — to capture the master you render a track. (The master
// track's FX/gain/pan are STILL neutralized for both scopes as the out-of-scope
// chain — master is a bypass target, not a capture scope.) The retired M7
// CAPTURE_TRACKS_WET / CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids AND the removed
// CAPTURE_MASTER / CAPTURE_MASTER_REALTIME ids are mirror-unregistered on unload so
// old keybindings clear cleanly.
//
// The minted command ids parallel the table rows 1:1 (same index). gaccel storage
// must outlive registration (REAPER holds each pointer), so both vectors are file-
// scope and sized to the table. FOREVER-STABLE id strings live in the table.
static std::vector<int> g_captureCmdIds;
static std::vector<gaccel_register_t> g_captureAccels;
// Channel-qualified capture-action labels, one per table row. REAPER holds each gaccel's
// `desc` pointer, so the composed strings live here for the module lifetime (parallel to
// g_captureAccels; never resized after the registration loop sets it).
static std::vector<std::string> g_captureDescs;
// Retired capture-action command-id SUFFIXES. Kept ONLY to mirror-unregister them on
// unload so a user's stale keybindings are cleaned up. Never re-register these. Composed
// through the channel prefix at unload (channelCommandId) so a beta unload clears beta-
// qualified retired ids and a stable unload clears stable's — each channel cleans up only
// its own family.
// Retired command-id SUFFIXES. Kept ONLY to mirror-unregister them on unload so a
// user's stale keybindings are cleaned up. Never re-register these. Composed through
// the channel prefix at unload (channelIdFor) so a beta unload clears beta-qualified
// retired ids and a stable unload clears stable's — each channel cleans up only its
// own family.
// * The M7 four-mode ids (tracks/items/razor WET).
// * CAPTURE_MASTER and CAPTURE_MASTER_REALTIME — the master offline scope and the
// master realtime action are REMOVED (capture is now item + track only; realtime
// taps the selected track). Their shipped ids are retired so old keybindings clear.
// * CAPTURE_ITEM_TAIL and CAPTURE_TRACK_TAIL — the former per-action tail variants
// are REMOVED; tail is now a panel-setting toggle, not a paired action. Retired so
// old keybindings clear.
// are REMOVED; tail is now a panel-setting toggle, not a paired action.
static const char* const kRetiredCaptureCmdSuffixes[] = {
"CAPTURE_TRACKS_WET",
"CAPTURE_ITEMS_WET",
@@ -118,60 +70,6 @@ static const char* const kRetiredCaptureCmdSuffixes[] = {
"CAPTURE_TRACK_TAIL",
};
// Command id for "ReaSampler: toggle bank panel" (M5). FOREVER-STABLE string.
// The docked grid window is display-only this wave (Wave A) — the action just
// shows/hides it; it never captures, inserts, or mutates the bank.
static int g_cmdToggleBankPanel = 0;
// Command ids for the M6 insert actions. FOREVER-STABLE strings. Two variants that
// differ ONLY in the InsertOptions they build: the default inserts at native length
// (no stretch, no conform); the "conform" variant is the EXPLICIT opt-in to REAPER's
// try-to-match-project-tempo path (CONTEXT.md §insert: conform is opt-in, never
// silent). Both read the bank panel's current selection and place at the edit cursor.
static int g_cmdInsertSelected = 0;
static int g_cmdInsertSelectedConform = 0;
// Command ids for the M11 batch-capture actions. NEW FOREVER-STABLE strings. One action
// fires N captures: CAPTURE_BATCH_ITEMS -> one bank sample per selected item (item scope);
// CAPTURE_BATCH_RAZOR -> one bank sample per razor area (track scope, each area's range).
// Each unit honors every precision invariant; the original selection is restored on every
// exit path. Bank-only, never places on the timeline (load-bearing principle).
static int g_cmdCaptureBatchItems = 0;
static int g_cmdCaptureBatchRazor = 0;
// Command id for the "capture selected track (realtime)" action. NEW FOREVER-STABLE
// string. Records the selected track's OWN output in realtime (transport-driven) into
// a hidden temp track via RealtimeRecordBackend, then moves the recorded file into the
// bank. The realtime SIBLING of the offline CAPTURE_TRACK scope action: same range
// logic (razor-else-time), same track selection, same bank/persist path, different
// backend. Dialog-free. (Replaces the removed CAPTURE_MASTER_REALTIME action.)
static int g_cmdCaptureTrackRealtime = 0;
// Command id for the M10 "re-capture from source" action. NEW FOREVER-STABLE string
// (suffix RECAPTURE_FROM_SOURCE). Regenerates the bank panel's selected PROVENANCED
// sample from its recorded source's current state and updates the Sample in place —
// BANK-ONLY, never places on the timeline (load-bearing principle).
static int g_cmdRecaptureFromSource = 0;
// Command id for the S8 "capture selected item / time-selection into bank + assign"
// action. NEW FOREVER-STABLE string (suffix CAPTURE_ITEM_ASSIGN). Reuses the offline
// Item-scope capture path (RunCapture) verbatim, then writes an S8 assignment request.
// Lives in the capture family (not the ingest family) because it leans on the capture
// render machinery (capture_orchestrator).
static int g_cmdCaptureItemAssign = 0;
// Command id for the M8 "cancel realtime capture" action. FOREVER-STABLE string.
// Aborts the in-flight realtime capture (stop + restore, non-destructive). No-op
// (with a note) when nothing is in flight.
static int g_cmdCancelRealtime = 0;
// Command id for the Phase V "show version" action. FOREVER-STABLE string. On demand
// ONLY — prints the CMake-sourced version string to the console when fired. This is the
// SOLE new console output the versioning wave adds; there is no unconditional startup
// version print (routine console chatter was deliberately removed — it pops the console
// window). The user copies this line into a bug report.
static int g_cmdShowVersion = 0;
// The persistence session (M4): owns the in-memory BankModel 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() — which (B2) resolves to
@@ -180,6 +78,102 @@ static int g_cmdShowVersion = 0;
// with the .rpp. Replaces the M3 session-only g_bank.
static reasampler::ReaSamplerSession g_session;
// Command id of the TOGGLE_BANK_PANEL row, resolved from the table once at load so
// OnToggleAction's checked-state poll is a single int compare (no per-poll lookup).
static int g_cmdToggleBankPanel = 0;
// --- Action handlers (the table's function pointers) --------------------------
//
// Each is a thin stateless routing shim: (session, per-row arg) -> the action body
// hoisted in Q-W3/Q-W4 (shell/capture/, shell/panel/). The bodies own all behavior;
// these exist only so the table rows can be plain data with flat function pointers.
// Capture scope family: `arg` is the captureActionTable() row index — the table rows
// below are built by iterating that pure taxonomy, so the routing stays 1:1 by
// construction (never a hand-kept parallel list).
static void RunCaptureScopeRow(int arg) {
capture::RunCapture(g_session,
capture::captureActionTable()[static_cast<std::size_t>(arg)]);
}
static void RunToggleBankPanel(int) { reasampler::bankPanelToggle(); }
static void RunCaptureItemAssign(int) { capture::RunCaptureItemAssign(g_session); }
// Insert: `arg` != 0 is the EXPLICIT conform-to-project-tempo opt-in (CONTEXT.md
// §insert: conform is opt-in, never silent); 0 inserts at native length.
static void RunInsertSelected(int arg) {
capture::RunInsertSelected(g_session, arg != 0);
}
static void RunBatchCaptureItems(int) { capture::RunBatchCaptureItems(g_session); }
static void RunBatchCaptureRazor(int) { capture::RunBatchCaptureRazor(g_session); }
static void RunCaptureRealtime(int) { capture::RunCaptureRealtimeTrack(g_session); }
static void RunCancelRealtime(int) { capture::RunCancelRealtime(g_session); }
static void RunRecaptureFromSource(int) { capture::RunRecaptureFromSource(g_session); }
static void RunShowVersion(int) {
// On-demand version readout — the ONLY version output on any path (Phase V: no
// unconditional startup print; routine console chatter pops the console window).
ShowConsoleMsg(("ReaSampler " + reasampler::version::appVersion() + "\n").c_str());
}
// --- The registration table (Q-W6) --------------------------------------------
//
// ONE row per bindable action this TU owns: FOREVER-STABLE id suffix (channel prefix
// composed at register — stable rebuilds the exact shipped id, e.g.
// "CEREBELLUM_REASAMPLER_CAPTURE_TRACK"; beta its isolated forever-family), the
// Actions-list phrase (after the "ReaSampler[ beta]: " lead), the handler, and its
// per-row arg. Registration, hookcommand dispatch, and the unload mirror-unregister
// all iterate this data — adding an action = adding a row + a handler above.
//
// The capture scope rows (CAPTURE_ITEM / CAPTURE_TRACK) come first, sourced from the
// pure captureActionTable() taxonomy (render_settings) — suffix/phrase live in that
// one testable list, and `arg` carries the row index back to RunCapture. The
// remaining rows are this TU's singles, in the pre-table registration order.
static std::vector<reasampler::ActionTableRow> buildMainActionTable() {
using reasampler::ActionTableRow;
std::vector<ActionTableRow> rows;
const auto& cap = capture::captureActionTable();
for (std::size_t i = 0; i < cap.size(); ++i)
rows.push_back(ActionTableRow{cap[i].commandSuffix, cap[i].descriptionPhrase,
&RunCaptureScopeRow, static_cast<int>(i)});
// M5: show/hide the docked bank panel (display-only; never captures/inserts).
rows.push_back({"TOGGLE_BANK_PANEL", "toggle bank panel", &RunToggleBankPanel});
// S8: Item-scope capture + assignment-request write (capture family because it
// leans on the capture render machinery; the other ingest surfaces live in the
// ingest family and the panel drop callback).
rows.push_back({"CAPTURE_ITEM_ASSIGN",
"capture selected item into bank + assign to active instance",
&RunCaptureItemAssign});
// M6: place the panel's selected sample at the edit cursor. Two variants that
// differ ONLY in InsertOptions — native length vs the explicit conform opt-in.
rows.push_back({"INSERT_SELECTED", "insert selected sample at edit cursor",
&RunInsertSelected, 0});
rows.push_back({"INSERT_SELECTED_CONFORM",
"insert selected sample at edit cursor (conform to tempo)",
&RunInsertSelected, 1});
// M11: one action fires N captures (per selected item / per razor area); the
// original selection is restored on every exit path. Bank-only, never places.
rows.push_back({"CAPTURE_BATCH_ITEMS",
"batch capture selected items (one per item)",
&RunBatchCaptureItems});
rows.push_back({"CAPTURE_BATCH_RAZOR", "batch capture razor areas (one per area)",
&RunBatchCaptureRazor});
// M8: realtime sibling of the offline CAPTURE_TRACK scope — records the selected
// track's own output into a hidden temp track, dialog-free — plus its
// cancel-in-flight companion (stop + restore, non-destructive).
rows.push_back({"CAPTURE_TRACK_REALTIME", "capture selected track (realtime)",
&RunCaptureRealtime});
rows.push_back({"CANCEL_REALTIME_CAPTURE", "cancel realtime capture",
&RunCancelRealtime});
// M10: regenerate the selected PROVENANCED sample from its recorded source's
// current state, in place. Bank-only, never places on the timeline.
rows.push_back({"RECAPTURE_FROM_SOURCE", "re-capture from source",
&RunRecaptureFromSource});
// Phase V: on-demand version readout for bug reports.
rows.push_back({"SHOW_VERSION", "show version", &RunShowVersion});
return rows;
}
// 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).
@@ -275,33 +269,12 @@ static project_config_extension_t g_projectConfig{
};
// REAPER calls this for EVERY action fired anywhere; claim only our own id,
// return false otherwise so REAPER keeps looking.
// return false otherwise so REAPER keeps looking. This TU's own family dispatches
// through the registration table; the Q-W4 families claim their own ids after it.
static bool OnHookCommand(int command, int /*flag*/)
{
if (command == 0) return false;
// Three-scope capture family: command ids parallel captureActionTable() 1:1 by index.
// Claim the fired id if it is one of ours and route to its table row.
for (std::size_t i = 0; i < g_captureCmdIds.size(); ++i)
if (command == g_captureCmdIds[i])
{
capture::RunCapture(g_session, capture::captureActionTable()[i]);
return true;
}
if (command == g_cmdToggleBankPanel) { reasampler::bankPanelToggle(); return true; }
if (command == g_cmdCaptureItemAssign) { capture::RunCaptureItemAssign(g_session); return true; }
if (command == g_cmdInsertSelected) { capture::RunInsertSelected(g_session, false); return true; }
if (command == g_cmdInsertSelectedConform) { capture::RunInsertSelected(g_session, true); return true; }
if (command == g_cmdCaptureBatchItems) { capture::RunBatchCaptureItems(g_session); return true; }
if (command == g_cmdCaptureBatchRazor) { capture::RunBatchCaptureRazor(g_session); return true; }
if (command == g_cmdCaptureTrackRealtime) { capture::RunCaptureRealtimeTrack(g_session); return true; }
if (command == g_cmdCancelRealtime) { capture::RunCancelRealtime(g_session); return true; }
if (command == g_cmdRecaptureFromSource) { capture::RunRecaptureFromSource(g_session); return true; }
if (command == g_cmdShowVersion)
{
// On-demand version readout — the ONLY version output on any path.
ShowConsoleMsg(("ReaSampler " + reasampler::version::appVersion() + "\n").c_str());
return true;
}
if (reasampler::actionTableHandleCommand(command)) return true;
// Design View action family (D4). Claims only its own ids; returns false for the
// rest so this hook keeps looking (per the contract).
if (reasampler::designViewHandleCommand(command)) return true;
@@ -316,51 +289,11 @@ static bool OnHookCommand(int command, int /*flag*/)
// Return 1 (on) / 0 (off) for ids we own, -1 for everything else (per the contract).
static int OnToggleAction(int command)
{
if (command == g_cmdToggleBankPanel)
if (command != 0 && command == g_cmdToggleBankPanel)
return reasampler::bankPanelIsOpen() ? 1 : 0;
return -1; // not ours / non-toggling
}
// gaccel storage must outlive registration — REAPER holds the pointer.
// (The capture family's accels live in g_captureAccels, sized to the table.)
static gaccel_register_t g_accelToggleBankPanel{};
static gaccel_register_t g_accelCaptureItemAssign{};
static gaccel_register_t g_accelInsertSelected{};
static gaccel_register_t g_accelInsertSelectedConform{};
static gaccel_register_t g_accelCaptureBatchItems{};
static gaccel_register_t g_accelCaptureBatchRazor{};
static gaccel_register_t g_accelCaptureTrackRealtime{};
static gaccel_register_t g_accelCancelRealtime{};
static gaccel_register_t g_accelRecaptureFromSource{};
static gaccel_register_t g_accelShowVersion{};
// gaccel desc storage. The Actions-list label is channel-qualified at runtime
// (channelActionName) so it cannot be a string literal; REAPER holds the gaccel's `desc`
// pointer, so each label lives here for the module lifetime. Composed once at registration.
static std::string g_descToggleBankPanel;
static std::string g_descCaptureItemAssign;
static std::string g_descInsertSelected;
static std::string g_descInsertSelectedConform;
static std::string g_descCaptureBatchItems;
static std::string g_descCaptureBatchRazor;
static std::string g_descCaptureTrackRealtime;
static std::string g_descCancelRealtime;
static std::string g_descRecaptureFromSource;
static std::string g_descShowVersion;
// Composed command-id strings (channel-qualified), interned so register and the mirroring
// '-command_id' unregister pass the SAME pointer. Set during registration; read on unload.
static const char* g_idToggleBankPanel = nullptr;
static const char* g_idCaptureItemAssign = nullptr;
static const char* g_idInsertSelected = nullptr;
static const char* g_idInsertSelectedConform = nullptr;
static const char* g_idCaptureBatchItems = nullptr;
static const char* g_idCaptureBatchRazor = nullptr;
static const char* g_idCaptureTrackRealtime = nullptr;
static const char* g_idCancelRealtime = nullptr;
static const char* g_idRecaptureFromSource = nullptr;
static const char* g_idShowVersion = nullptr;
extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t* rec)
{
@@ -387,49 +320,15 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
reasampler::bankUnregisterActions(g_rec);
// Tear down the S8 ingest action family — same mirror-unregister.
reasampler::ingestUnregisterActions(g_rec);
// Each '-command_id' re-presents the SAME interned, channel-qualified pointer
// used at register (g_id*), so the mirror-unregister matches exactly.
g_rec->Register("-gaccel", (void*)&g_accelShowVersion);
g_rec->Register("-command_id", (void*)g_idShowVersion);
g_rec->Register("-gaccel", (void*)&g_accelRecaptureFromSource);
g_rec->Register("-command_id", (void*)g_idRecaptureFromSource);
g_rec->Register("-gaccel", (void*)&g_accelCancelRealtime);
g_rec->Register("-command_id", (void*)g_idCancelRealtime);
g_rec->Register("-gaccel", (void*)&g_accelCaptureTrackRealtime);
g_rec->Register("-command_id", (void*)g_idCaptureTrackRealtime);
g_rec->Register("-gaccel", (void*)&g_accelCaptureBatchRazor);
g_rec->Register("-command_id", (void*)g_idCaptureBatchRazor);
g_rec->Register("-gaccel", (void*)&g_accelCaptureBatchItems);
g_rec->Register("-command_id", (void*)g_idCaptureBatchItems);
g_rec->Register("-gaccel", (void*)&g_accelInsertSelectedConform);
g_rec->Register("-command_id", (void*)g_idInsertSelectedConform);
g_rec->Register("-gaccel", (void*)&g_accelInsertSelected);
g_rec->Register("-command_id", (void*)g_idInsertSelected);
g_rec->Register("-gaccel", (void*)&g_accelCaptureItemAssign);
g_rec->Register("-command_id", (void*)g_idCaptureItemAssign);
g_rec->Register("-gaccel", (void*)&g_accelToggleBankPanel);
g_rec->Register("-command_id", (void*)g_idToggleBankPanel);
// Mirror-unregister the capture family: gaccel + command_id per row, with
// '-'-prefixed strings (per the contract). The command id is re-composed from
// the same suffix + channel prefix used at register — identical string.
{
const auto& table = capture::captureActionTable();
for (std::size_t i = 0; i < table.size(); ++i)
{
if (i < g_captureAccels.size())
g_rec->Register("-gaccel", (void*)&g_captureAccels[i]);
const std::string id = channelCommandId(table[i].commandSuffix);
g_rec->Register("-command_id", (void*)id.c_str());
}
}
// Retire the removed M7 command ids (command_id only — we never held a gaccel
// Tear down this TU's own family from the registration table (reverse
// table order; each '-command_id' re-presents the SAME interned,
// channel-qualified pointer used at register).
reasampler::unregisterActionTable(g_rec);
// Retire the REMOVED command ids (command_id only — we never held a gaccel
// for them this session). Clears stale user keybindings on unload. Composed
// per channel so a beta clears beta-qualified retired ids, stable clears its own.
// per channel so a beta clears beta-qualified retired ids, stable its own.
for (const char* suffix : kRetiredCaptureCmdSuffixes)
{
const std::string id = channelCommandId(suffix);
g_rec->Register("-command_id", (void*)id.c_str());
}
g_rec->Register("-command_id", (void*)reasampler::channelIdFor(suffix));
}
// Destroy the docked window and release cached thumbnails before we drop
// the API pointers (DockWindowRemove/DestroyWindow need them live).
@@ -450,173 +349,23 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
g_hInst = hInstance;
g_rec = rec;
// Register the three-scope capture action family (command_id -> gaccel per table row).
// The single hookcommand below routes every fired id back to its row by index.
// g_captureAccels must be sized BEFORE the loop and never reallocated after —
// REAPER holds a pointer to each element until we mirror-unregister it.
{
const auto& table = capture::captureActionTable();
g_captureCmdIds.assign(table.size(), 0);
g_captureAccels.assign(table.size(), gaccel_register_t{});
g_captureDescs.assign(table.size(), std::string{});
for (std::size_t i = 0; i < table.size(); ++i)
{
// Compose the channel-qualified id (prefix + suffix) and label
// ("ReaSampler[ beta]: " + phrase). The id is interned so unregister re-presents
// the same pointer; the label lives in g_captureDescs for the gaccel's lifetime.
const int cmd =
rec->Register("command_id", (void*)internCmdId(table[i].commandSuffix));
g_captureCmdIds[i] = cmd;
if (cmd)
{
g_captureDescs[i] = channelActionName(table[i].descriptionPhrase);
g_captureAccels[i].accel.cmd = cmd;
g_captureAccels[i].desc = g_captureDescs[i].c_str();
rec->Register("gaccel", (void*)&g_captureAccels[i]);
}
}
}
// Point the bank panel at the live session BEFORE registering its action, so
// a toggle firing immediately has a session to read (M5). Does not open the
// window — only stores the session pointer.
reasampler::bankPanelInit(&g_session);
// Register the M5 "toggle bank panel" action (command_id -> gaccel ->
// hookcommand + toggleaction for the checked state). Id + label are channel-qualified.
g_idToggleBankPanel = internCmdId("TOGGLE_BANK_PANEL");
g_cmdToggleBankPanel = rec->Register("command_id", (void*)g_idToggleBankPanel);
// Register this TU's whole action family from the table: command_id -> gaccel
// per row, all channel-qualified, all FOREVER-STABLE per channel.
{
const std::vector<reasampler::ActionTableRow> rows = buildMainActionTable();
reasampler::registerActionTable(rec, rows.data(), rows.size());
}
// The panel toggle renders a checked state — resolve its minted id once and
// register the toggleaction hook that reports it.
g_cmdToggleBankPanel = reasampler::actionTableCommandId("TOGGLE_BANK_PANEL");
if (g_cmdToggleBankPanel)
{
g_descToggleBankPanel = channelActionName("toggle bank panel");
g_accelToggleBankPanel.accel.cmd = g_cmdToggleBankPanel;
g_accelToggleBankPanel.desc = g_descToggleBankPanel.c_str();
rec->Register("gaccel", (void*)&g_accelToggleBankPanel);
rec->Register("toggleaction", (void*)&OnToggleAction);
}
// Register the S8 "capture selected item / time-selection into bank + assign" action
// (command_id -> gaccel -> hookcommand). Reuses the Item-scope offline capture path and
// writes an assignment request so the active instance plays the new sample. Channel-
// qualified FOREVER-STABLE id (suffix CAPTURE_ITEM_ASSIGN). MIDI-bindable like every
// capture action. Registered in the capture family (main.cpp) because it leans on the
// capture render machinery; the other two ingest surfaces live in the ingest family
// (Media-Explorer import) and the panel drop callback.
g_idCaptureItemAssign = internCmdId("CAPTURE_ITEM_ASSIGN");
g_cmdCaptureItemAssign = rec->Register("command_id", (void*)g_idCaptureItemAssign);
if (g_cmdCaptureItemAssign)
{
g_descCaptureItemAssign = channelActionName(
"capture selected item into bank + assign to active instance");
g_accelCaptureItemAssign.accel.cmd = g_cmdCaptureItemAssign;
g_accelCaptureItemAssign.desc = g_descCaptureItemAssign.c_str();
rec->Register("gaccel", (void*)&g_accelCaptureItemAssign);
}
// Register the M6 insert actions (command_id -> gaccel -> hookcommand). Two
// variants: native-length (default, no stretch) and the EXPLICIT conform-to-
// tempo opt-in. Both read the bank panel selection and place at the edit cursor.
g_idInsertSelected = internCmdId("INSERT_SELECTED");
g_cmdInsertSelected = rec->Register("command_id", (void*)g_idInsertSelected);
if (g_cmdInsertSelected)
{
g_descInsertSelected = channelActionName("insert selected sample at edit cursor");
g_accelInsertSelected.accel.cmd = g_cmdInsertSelected;
g_accelInsertSelected.desc = g_descInsertSelected.c_str();
rec->Register("gaccel", (void*)&g_accelInsertSelected);
}
g_idInsertSelectedConform = internCmdId("INSERT_SELECTED_CONFORM");
g_cmdInsertSelectedConform = rec->Register("command_id", (void*)g_idInsertSelectedConform);
if (g_cmdInsertSelectedConform)
{
g_descInsertSelectedConform = channelActionName(
"insert selected sample at edit cursor (conform to tempo)");
g_accelInsertSelectedConform.accel.cmd = g_cmdInsertSelectedConform;
g_accelInsertSelectedConform.desc = g_descInsertSelectedConform.c_str();
rec->Register("gaccel", (void*)&g_accelInsertSelectedConform);
}
// Register the M11 batch-capture actions (command_id -> gaccel -> hookcommand). Each
// fires N captures (one bank sample per selected item / per razor area), honoring every
// precision invariant per unit and restoring the original selection on every path.
// Channel-qualified FOREVER-STABLE ids.
g_idCaptureBatchItems = internCmdId("CAPTURE_BATCH_ITEMS");
g_cmdCaptureBatchItems = rec->Register("command_id", (void*)g_idCaptureBatchItems);
if (g_cmdCaptureBatchItems)
{
g_descCaptureBatchItems =
channelActionName("batch capture selected items (one per item)");
g_accelCaptureBatchItems.accel.cmd = g_cmdCaptureBatchItems;
g_accelCaptureBatchItems.desc = g_descCaptureBatchItems.c_str();
rec->Register("gaccel", (void*)&g_accelCaptureBatchItems);
}
g_idCaptureBatchRazor = internCmdId("CAPTURE_BATCH_RAZOR");
g_cmdCaptureBatchRazor = rec->Register("command_id", (void*)g_idCaptureBatchRazor);
if (g_cmdCaptureBatchRazor)
{
g_descCaptureBatchRazor =
channelActionName("batch capture razor areas (one per area)");
g_accelCaptureBatchRazor.accel.cmd = g_cmdCaptureBatchRazor;
g_accelCaptureBatchRazor.desc = g_descCaptureBatchRazor.c_str();
rec->Register("gaccel", (void*)&g_accelCaptureBatchRazor);
}
// Register the "capture selected track (realtime)" action (command_id -> gaccel ->
// hookcommand). Realtime sibling of the offline CAPTURE_TRACK scope: records the
// selected track's own output in realtime into a hidden temp track, moves it into
// the bank. Dialog-free. Channel-qualified FOREVER-STABLE id.
g_idCaptureTrackRealtime = internCmdId("CAPTURE_TRACK_REALTIME");
g_cmdCaptureTrackRealtime = rec->Register("command_id", (void*)g_idCaptureTrackRealtime);
if (g_cmdCaptureTrackRealtime)
{
g_descCaptureTrackRealtime =
channelActionName("capture selected track (realtime)");
g_accelCaptureTrackRealtime.accel.cmd = g_cmdCaptureTrackRealtime;
g_accelCaptureTrackRealtime.desc = g_descCaptureTrackRealtime.c_str();
rec->Register("gaccel", (void*)&g_accelCaptureTrackRealtime);
}
// Cancel-in-flight sibling: aborts a running realtime capture (stop + restore).
// Channel-qualified FOREVER-STABLE id.
g_idCancelRealtime = internCmdId("CANCEL_REALTIME_CAPTURE");
g_cmdCancelRealtime = rec->Register("command_id", (void*)g_idCancelRealtime);
if (g_cmdCancelRealtime)
{
g_descCancelRealtime = channelActionName("cancel realtime capture");
g_accelCancelRealtime.accel.cmd = g_cmdCancelRealtime;
g_accelCancelRealtime.desc = g_descCancelRealtime.c_str();
rec->Register("gaccel", (void*)&g_accelCancelRealtime);
}
// Register the M10 "re-capture from source" action (command_id -> gaccel ->
// hookcommand). Regenerates the selected provenanced sample from its recorded
// source's current state; bank-only, never places on the timeline. Channel-
// qualified FOREVER-STABLE id (suffix RECAPTURE_FROM_SOURCE).
g_idRecaptureFromSource = internCmdId("RECAPTURE_FROM_SOURCE");
g_cmdRecaptureFromSource = rec->Register("command_id", (void*)g_idRecaptureFromSource);
if (g_cmdRecaptureFromSource)
{
g_descRecaptureFromSource = channelActionName("re-capture from source");
g_accelRecaptureFromSource.accel.cmd = g_cmdRecaptureFromSource;
g_accelRecaptureFromSource.desc = g_descRecaptureFromSource.c_str();
rec->Register("gaccel", (void*)&g_accelRecaptureFromSource);
}
// Register the Phase V "show version" action (command_id -> gaccel -> hookcommand).
// On-demand only — prints the CMake-sourced version to the console when fired; no
// startup print. Channel-qualified FOREVER-STABLE id; label carries the channel prefix
// so a beta's "show version" is distinguishable from stable's in the Actions list.
g_idShowVersion = internCmdId("SHOW_VERSION");
g_cmdShowVersion = rec->Register("command_id", (void*)g_idShowVersion);
if (g_cmdShowVersion)
{
g_descShowVersion = channelActionName("show version");
g_accelShowVersion.accel.cmd = g_cmdShowVersion;
g_accelShowVersion.desc = g_descShowVersion.c_str();
rec->Register("gaccel", (void*)&g_accelShowVersion);
}
// Register the Design View action family (D4): toggle/activate mode, tag/untag/
// show-both selected tracks. Each mints its own command_id + gaccel; the single
@@ -632,11 +381,11 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
// Register the S8 ingest action family: the Media-Explorer import-into-bank+assign
// action. Shares g_session with the other families; routed by the same hookcommand via
// ingestHandleCommand. (The arrange capture+assign action is registered in the capture
// family above; the drop path is a bank_panel callback, not a bindable action.)
// ingestHandleCommand. (The arrange capture+assign action is a table row above; the
// drop path is a bank_panel callback, not a bindable action.)
reasampler::ingestRegisterActions(rec, &g_session);
// One hookcommand routes every ReaSampler action (spike + toggle + Design View).
// One hookcommand routes every ReaSampler action (table + the three families).
// Registered once, after all command ids are minted.
rec->Register("hookcommand", (void*)&OnHookCommand);
+1 -1
View File
@@ -110,7 +110,7 @@ std::string resolveBankFile(const std::string& projectDir,
std::string projectDirOfRpp(const std::string& rppPath) {
// An unsaved project reports an empty .rpp path; keep it empty so downstream
// resolution refuses (no default-location fallback). Mirrors persist.cpp's prior
// resolution refuses (no default-location fallback). Mirrors the former persist shell's
// projectDirOf exactly: parent_path of the .rpp, then normalizeSlashes.
if (rppPath.empty()) return {};
std::string dir = std::filesystem::path(rppPath).parent_path().string();
+2 -2
View File
@@ -1,6 +1,6 @@
#pragma once
// tail_control — the REAPER-free logic behind the docked bank_panel's tail-mode
// toggle. The panel shell (bank_panel.cpp) owns the SWELL window, LICE drawing, and
// toggle. The panel shell (shell/panel/) owns the SWELL window, LICE drawing, and
// click hit-testing; what is NOT DAW-bound — the cycle order, the manual-length
// clamp, and the toggle's label text — lives here so it is unit-tested outside the
// DAW (CLAUDE.md §load-bearing split). Mirror of bank_grid / mode_switch.
@@ -27,7 +27,7 @@ inline constexpr double kDefaultManualTailMs = 2000.0;
inline constexpr double kManualStepMs = 250.0;
// The panel's current tail setting: the mode plus the length used ONLY when the
// mode is Manual. Held as in-memory panel/session state (bank_panel.cpp), default
// mode is Manual. Held as in-memory panel/session state (shell/panel), default
// None so a capture with no explicit choice stays exact-bounds / byte-identical to
// today. `manualMs` is a stored default a future fine-adjust UI can tune; it is
// clamped to the 8 s cap (kMaxTailMs) before it ever reaches a CaptureRequest.
-15
View File
@@ -1,15 +0,0 @@
#pragma once
// wav_trim — TRANSITIONAL forwarding header (Q-W3, audit §4e WAV/RIFF consolidation).
//
// The one pure owner of the WAV/RIFF byte format is now core/capture/wav_codec.{h,cpp}
// (chunk walker + layout parse + float32 build + size-field patch + content hash).
// Everything this header used to declare (WavLayout / parseWavLayout /
// extractFloatFrames / WavTruncatePlan / planWavTruncate) lives there, same
// namespace (reasampler::capture), same signatures — this include is a pure alias.
//
// Kept ONLY so the TUs a parallel wave owns (sample_map.h and the VST editor/
// processor god-TUs, Q-W2v) compile untouched — editing them here would collide
// with that wave's in-flight split. Retire this header (and point its includers at
// wav_codec.h) once Q-W2v lands.
#include "core/capture/wav_codec.h"
+2 -2
View File
@@ -25,7 +25,7 @@
// #include <windows.h>` unconditionally, which CANNOT enter the pure sampler_core module
// (CLAUDE.md load-bearing split: NO vendor/host/SDK types; sampler_core_tests links neither
// SDK and compiles outside the DAW). So the Preserve DSP lands as route (b): a house-native
// pure module alongside peaks / wav_trim, CTest-testable, RT-disciplined. Same
// pure module alongside peaks / wav_codec, CTest-testable, RT-disciplined. Same
// PitchEngine::Preserve contract behind the seam — if WDL is ever preferred it swaps in at
// the SHELL, never in the pure core.
//
@@ -53,7 +53,7 @@
//
// PURE MODULE: NO VST3, NO REAPER, NO SWELL, NO vendor/ includes. Standard library only.
// Shares the `AudioSample` float alias from peaks (the one house precedent — sampler_core /
// wav_trim do the same).
// wav_codec does the same).
//
// RT DISCIPLINE (S16 hard constraint). `configure()` sizes the ring ONCE (off the audio
// thread, at voice allocation). `prime()` / `warm()` only copy into the pre-sized ring
+1 -1
View File
@@ -13,7 +13,7 @@
// structurally: sampler_core_tests links neither SDK (see CMakeLists §2i).
//
// It shares the `AudioSample` float alias from peaks — the one house precedent for a
// pure module leaning on peaks for the audio-domain type (wav_trim does the same). The
// pure module leaning on peaks for the audio-domain type (wav_codec does the same). The
// S2 seam fields (root note, loop points) enter as plain int / frame-index inputs; the
// core does no file I/O — it is handed decoded sample frames and produces audio frames.
+4 -59
View File
@@ -4,7 +4,7 @@
// The bridge shell (reaper_bridge.cpp) resolves REAPER API functions by name over the
// host callback and invokes them; the one fiddly-and-easy-to-get-wrong part around
// GetProjExtState — interpreting its int return against the buffer it filled — is pure
// and unit-tested here. Mirror of capture_paths / wav_trim splitting the arithmetic out
// and unit-tested here. Mirror of capture_paths / wav_codec splitting the arithmetic out
// of a REAPER-facing shell.
//
// The S1 spike ALSO carried a string-scan JSON reader (extractJsonStringField) as a
@@ -36,63 +36,8 @@ namespace reasampler::instrument::map {
std::optional<std::string> decodeGetProjExtState(int apiReturn,
const std::string& buffer);
// ---------------------------------------------------------------------------
// The GetProjExtState GROW-LOOP retry policy (Q-W5 rider, T2-04).
// ---------------------------------------------------------------------------
// GetProjExtState writes into a caller-supplied buffer with no documented
// query-the-size call, so a large value (bank blob, usage record) must be read by
// growing a buffer until the value fits strictly inside it. Three shells carried
// hand-rolled copies of that loop (persist's ext-state reads, usage_scan's
// prune-safety-adjacent record read, reaper_bridge's VST-side bank read); the ONE
// policy now lives here so the retry/termination rules cannot drift. The fiddly
// part is the termination taxonomy, which each caller folds differently:
//
// * Absent — the API returned <= 0 on some attempt: the key holds no value.
// (persist -> "" empty bank; usage_scan / bridge -> nullopt)
// * Complete — the written C string fits STRICTLY inside the buffer (size+1 <
// cap), so it cannot have been clipped: `value` is the whole value.
// * Overflow — the value never fit under the 16 MB ceiling: it is unreadable
// WHOLE, which is NOT the same as absent. (persist warns on the
// console; usage_scan folds it to the prune fail-safe abort)
//
// `read` is one GetProjExtState-shaped attempt: int read(char* buf, int cap),
// returning the API's int. A template, statically dispatched per call site — no
// virtual calls, no std::function (the §3 performance guardrail); the caller binds
// the project/namespace/key (or a resolved function pointer, VST side) in a lambda.
struct GrowingExtStateRead {
enum class Status { Absent, Complete, Overflow };
Status status = Status::Absent;
int apiReturn = 0; // the FINAL attempt's return (<= 0 iff Absent); feeds
// decodeGetProjExtState on the bridge path unchanged
std::string value; // the whole value; meaningful only when Complete
};
template <class ReadFn>
GrowingExtStateRead readProjExtStateGrowing(ReadFn&& read) {
// Start generous; grow ×4 if REAPER reports the value may have been clipped
// (the return is the value length; equal-to-capacity-minus-NUL is ambiguous,
// so only a strict fit terminates). Ceiling 16 MB — give up rather than loop
// forever on a pathological value.
GrowingExtStateRead result;
for (int cap = 1 << 16; cap <= (1 << 24); cap <<= 2) {
std::vector<char> buf(static_cast<std::size_t>(cap), '\0');
const int rv = read(buf.data(), cap);
result.apiReturn = rv;
if (rv <= 0) {
result.status = GrowingExtStateRead::Status::Absent;
return result;
}
buf[static_cast<std::size_t>(cap) - 1] = '\0'; // defensive: guard against a read() that fills the buffer without honoring NUL-termination within cap
std::string s(buf.data());
if (static_cast<int>(s.size()) + 1 < cap) {
result.status = GrowingExtStateRead::Status::Complete;
result.value = std::move(s);
return result;
}
// else: possibly truncated -> grow and retry.
}
result.status = GrowingExtStateRead::Status::Overflow;
return result;
}
// The GetProjExtState GROW-LOOP retry policy (T2-04) lived here through Q-W5; it
// was rehomed to core/wire/ext_state_read.h in Q-W6 (its consumers are 2:1
// extension-side, so it belongs on the neutral wire seam, not the instrument map).
} // namespace reasampler::instrument::map
+1 -1
View File
@@ -1,6 +1,6 @@
// sample_map — pure implementation (the RESOLUTION half; the ComponentState codec
// lives in component_state_io.cpp since Q-W2v). See sample_map.h. NO VST3 / REAPER /
// SWELL / vendor includes; standard library + the pure bank_book / wav_trim / sampler_core.
// SWELL / vendor includes; standard library + the pure bank_book / wav_codec / sampler_core.
#include "core/instrument/map/sample_map.h"
+4 -4
View File
@@ -3,7 +3,7 @@
// "reasampler" bank ext-state + a decoded WAV into the plain data the sampler core
// plays, and (de)serialize the instance's selected-sample choice for VST3 component
// state. NO VST3, NO REAPER, NO SWELL, NO vendor/ includes at the boundary — the
// mirror of capture_paths / wav_trim / bridge_marshal splitting the fiddly, testable
// mirror of capture_paths / wav_codec / bridge_marshal splitting the fiddly, testable
// arithmetic out of a host-facing shell.
//
// WHY IT EXISTS (S4 seams). The instrument reads the bank over the live-state seam
@@ -14,7 +14,7 @@
// downmix its decoded PCM to the core's mono contract, and build the Tier-0 chromatic
// Keymap — is pure and unit-tested here.
//
// It links bank_book (the shared BankBook::deserialize) and wav_trim (the shared
// It links bank_book (the shared BankBook::deserialize) and wav_codec (the shared
// 32-bit-float WAV parse — no third WAV reader) and sampler_core (the Keymap /
// SampleData it produces). All three are pure; this stays pure.
@@ -25,7 +25,7 @@
#include "core/model/bank_book.h" // BankBook::deserialize (shared bank JSON parse)
#include "core/instrument/engine/sampler_core.h" // Keymap, SampleData, SampleLoop
#include "core/capture/wav_trim.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
#include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
namespace reasampler::instrument::map {
@@ -167,7 +167,7 @@ struct BankChoice {
};
std::vector<BankChoice> listBanks(const std::string& banksJson);
// Downmix interleaved float frames (the shape wav_trim::extractFloatFrames yields:
// Downmix interleaved float frames (the shape wav_codec's extractFloatFrames yields:
// [f0c0,f0c1,...,f1c0,...]) to the core's MONO contract by AVERAGING channels per
// frame. `channelCount` is the interleave stride (>= 1). CHANNEL POLICY (Tier 0,
// documented + surfaced): the S3 core is mono-per-sample by design; bank WAVs preserve
+1 -1
View File
@@ -46,7 +46,7 @@
// right edge and wins the tie, so the fade can be dragged open from zero).
//
// DELIBERATELY ENGINE-FREE (house pattern — param_slider does the same). It does NOT depend on
// sample_map / sampler_core (which would drag bank_book / wav_trim in). The shell reads the
// sample_map / sampler_core (which would drag bank_book / wav_codec in). The shell reads the
// zone's AdsrSeconds / TriggerParams and packs them into the small AmpEnvelope view struct here.
// AHDSR times are wall-clock SECONDS (rate-free, matching the stored domain — Daniel's no-
// hardcoded-rate ruling); Trigger fades are FRACTIONS of the play span. The one rate-bound input
+1 -1
View File
@@ -17,7 +17,7 @@
//
// It reuses the same Rect + contains() as editor_geometry (one shared geometry idiom), so
// this header depends on editor_geometry.h rather than redefining a rectangle type. Audio
// is the peaks AudioSample float alias (the one house precedent — sampler_core / wav_trim do
// is the peaks AudioSample float alias (the one house precedent — sampler_core / wav_codec do
// the same), so the zero-crossing helper takes the same mono PCM the shell already decoded.
#pragma once
+1 -1
View File
@@ -3,7 +3,7 @@
//
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
// vendor/ includes. Standard library only. Unit-tested outside the DAW — the same
// "small pure type + JSON round-trip" pattern as wav_trim / tab_strip.
// "small pure type + JSON round-trip" pattern as wav_codec / tab_strip.
//
// -- What it is --------------------------------------------------------------
//
-48
View File
@@ -1,48 +0,0 @@
#pragma once
// core/namespaces.h — Q-W1 INTERIM flat-namespace shim for the not-yet-split
// god/shell TUs (bank_panel / actions / persist / ingest / main / view / capture
// shells / the VST editor+processor). The Q-W1 sub-namespaces move every clean pure
// module's symbols out of the flat `reasampler` namespace; the god modules keep their
// pre-split internals, which reference those symbols unqualified (or qualified as
// `reasampler::X`). Nominating every sub-namespace inside `reasampler` restores both
// forms ([namespace.qual]p2 routes qualified lookup through using-directives), so the
// god internals stay untouched until their own split waves.
//
// SCOPE CONTRACT: included ONLY by god/shell TUs pending their split wave
// (Q-W2/Q-W2v/Q-W3/Q-W4/Q-W5). Clean core modules must NOT include this — they
// reference cross-subsystem symbols by their real namespace homes. Each split wave
// drops this include from the TUs it rewrites; when the last split lands, delete
// this header.
namespace reasampler {
namespace model {}
namespace view {}
namespace capture {}
namespace audio {}
namespace ui {}
namespace reclaim {}
namespace version {}
namespace json {}
namespace util {}
namespace wire {}
namespace instrument {
namespace engine {}
namespace map {}
namespace ui {}
} // namespace instrument
using namespace model;
using namespace view;
using namespace capture;
using namespace audio;
using namespace ui;
using namespace reclaim;
using namespace version;
using namespace util;
using namespace wire;
using namespace instrument::engine;
using namespace instrument::map;
using namespace instrument::ui;
} // namespace reasampler
+2 -2
View File
@@ -1,7 +1,7 @@
#pragma once
#include "core/ui/rect.h"
// bank_grid — the REAPER-free layout math and cache-key logic behind the docked
// bank_panel (M5, Wave A). The panel shell (bank_panel.cpp) owns the SWELL window,
// bank_panel (M5, Wave A). The panel shell (shell/panel/) owns the SWELL window,
// LICE drawing, and PCM reads; ALL of that is REAPER-bound and DAW-verified. What
// is NOT DAW-bound — how N sample cells tile a panel of a given pixel size, and
// the key that identifies a cached thumbnail — lives here so it is unit-tested
@@ -80,7 +80,7 @@ std::string thumbnailKeyString(const ThumbnailKey& key);
// --- Interaction (M5 Wave B): hit-test, selection, keyboard nav --------------
//
// All REAPER-free so the panel's interaction LOGIC is unit-tested outside the DAW,
// exactly as the layout math is. The panel shell (bank_panel.cpp) reads live mouse
// exactly as the layout math is. The panel shell (shell/panel/) reads live mouse
// coordinates / key codes / modifier state via SWELL and calls into these; it owns
// no selection arithmetic of its own.
+1 -1
View File
@@ -2,7 +2,7 @@
// card_drag — the REAPER-free decision logic behind the L7 in-grid reorder drag. Three
// pure concerns live here so they are unit-tested outside the DAW (CLAUDE.md §load-bearing
// split); the SWELL wiring, SetCursor call, cursor resources, and drop-target draw stay in
// the shell (bank_panel.cpp). Mirror of drag_out::decideGesture.
// the shell (shell/panel/panel_drag.cpp). Mirror of drag_out::decideGesture.
//
// 1. GESTURE PRECEDENCE (F3 settled). A live drag resolves to exactly one gesture, in a
// strict precedence the shell evaluates on every mouse-move / at drop:
+1 -1
View File
@@ -3,7 +3,7 @@
// drag_out — the REAPER-free / OS-free decision logic behind the bank_panel's native OS
// drag-out (Milestone 11, the final polish point). Two pure concerns live here so they are
// unit-tested outside the DAW (CLAUDE.md §load-bearing split); the OLE / SWELL initiation
// and the bank_panel gesture hook stay in the shell (drag_out_win.* + bank_panel.cpp).
// and the bank_panel gesture hook stay in the shell (drag_out_win.* + shell/panel/panel_drag.cpp).
//
// 1. GESTURE BOUNDARY (invariant #4 — do not regress the internal drag). The panel
// already runs an INTERNAL drag: press a selected cell, cross a threshold, drop onto
+1 -1
View File
@@ -3,7 +3,7 @@
// footer_bar — the REAPER-free, LICE-free layout + hit-test math for the bank_panel's L4
// footer LEFT group: the narrowed [Arrange|Design] mode toggle, its compact per-mode count
// label, and the Tail button, laid out left-to-right at the footer's left. The panel shell
// (bank_panel.cpp) owns the SWELL window, LICE drawing, and the click dispatch (cycle tail /
// (shell/panel/) owns the SWELL window, LICE drawing, and the click dispatch (cycle tail /
// activate a mode); what is NOT DAW-bound — WHERE the toggle box, the count label, and the
// Tail button sit, and which one a click lands on — lives here so it is unit-tested outside
// the DAW (CLAUDE.md §load-bearing split). Mirror of action_bar / mode_switch / prune_button.
+2 -2
View File
@@ -3,7 +3,7 @@
// prune_button — the REAPER-free layout math behind the bank_panel's Prune button
// (Phase R, Wave 3 — R3, fork R-E). A single labelled button drawn in the panel's
// tail-footer strip that fires the "Prune bank folder" command. The panel shell
// (bank_panel.cpp) owns the SWELL window, LICE drawing, and the Main_OnCommand
// (shell/panel/) owns the SWELL window, LICE drawing, and the Main_OnCommand
// dispatch of the registered command id — all REAPER-bound, DAW-verified. What is
// NOT DAW-bound — WHERE the button sits in the footer and whether a click lands on
// it — lives here so it is unit-tested outside the DAW (CLAUDE.md §load-bearing
@@ -42,7 +42,7 @@ using ButtonRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h)
// * buttonWidth — the button's fixed width.
// * rightInset — gap from the footer's right edge to the button's right edge (the
// button sits left of this inset, clearing the right-aligned version
// readout). COUPLED TO drawFooter (bank_panel.cpp): the version readout
// readout). COUPLED TO drawFooter (panel_render.cpp): the version readout
// uses an 8 px right margin. The button's right edge lands at
// footer.right - 84, i.e. 76 px left of the readout's right margin —
// enough clearance for the ~10-char label. ALSO COUPLED to
+1 -1
View File
@@ -8,7 +8,7 @@
// tabs). What is NOT DAW-bound — how N fixed-width tabs tile a strip of a given
// pixel width, where the overflow chevrons sit, which tab/chevron a click lands in,
// and how far the strip may scroll — lives here so it is unit-tested outside the
// DAW (CLAUDE.md §load-bearing split). The panel shell (bank_panel.cpp) owns the
// DAW (CLAUDE.md §load-bearing split). The panel shell (shell/panel/) owns the
// SWELL window, LICE drawing, and the live BankBook read; it calls into this seam
// for every rect and every hit. Mirror of mode_switch / bank_grid.
//
+1 -1
View File
@@ -3,7 +3,7 @@
// mode_switch — the REAPER-free layout math behind the bank_panel's Design-View
// mode switch (Phase D, Wave 4 — D5). A segmented control `[ Arrange | Design ]`
// (N-mode general, one segment per registered mode) drawn in a fixed-height header
// strip at the top of the docked panel. The panel shell (bank_panel.cpp) owns the
// strip at the top of the docked panel. The panel shell (shell/panel/) owns the
// SWELL window, LICE drawing, and the live ViewModeModel read + mode activation —
// all REAPER-bound, DAW-verified. What is NOT DAW-bound — how N segments tile a
// header rectangle, and which segment a click lands in — lives here so it is
+70
View File
@@ -0,0 +1,70 @@
#pragma once
// ext_state_read — the GetProjExtState GROW-LOOP retry policy (T2-04; rehomed to
// core/wire in Q-W6 — its consumers are the extension's persist/usage-scan shells
// AND the instrument's bridge, so it lives on the neutral wire seam rather than in
// the instrument-side bridge_marshal decode helper it started in).
//
// GetProjExtState writes into a caller-supplied buffer with no documented
// query-the-size call, so a large value (bank blob, usage record) must be read by
// growing a buffer until the value fits strictly inside it. Three shells carried
// hand-rolled copies of that loop (persist's ext-state reads, usage_scan's
// prune-safety-adjacent record read, reaper_bridge's VST-side bank read); the ONE
// policy lives here so the retry/termination rules cannot drift. The fiddly part
// is the termination taxonomy, which each caller folds differently:
//
// * Absent — the API returned <= 0 on some attempt: the key holds no value.
// (persist -> "" empty bank; usage_scan / bridge -> nullopt)
// * Complete — the written C string fits STRICTLY inside the buffer (size+1 <
// cap), so it cannot have been clipped: `value` is the whole value.
// * Overflow — the value never fit under the 16 MB ceiling: it is unreadable
// WHOLE, which is NOT the same as absent. (persist warns on the
// console; usage_scan folds it to the prune fail-safe abort)
//
// `read` is one GetProjExtState-shaped attempt: int read(char* buf, int cap),
// returning the API's int. A template, statically dispatched per call site — no
// virtual calls, no std::function (the §3 performance guardrail); the caller binds
// the project/namespace/key (or a resolved function pointer, VST side) in a lambda.
#include <cstddef>
#include <string>
#include <vector>
namespace reasampler::wire {
struct GrowingExtStateRead {
enum class Status { Absent, Complete, Overflow };
Status status = Status::Absent;
int apiReturn = 0; // the FINAL attempt's return (<= 0 iff Absent); feeds
// decodeGetProjExtState on the bridge path unchanged
std::string value; // the whole value; meaningful only when Complete
};
template <class ReadFn>
GrowingExtStateRead readProjExtStateGrowing(ReadFn&& read) {
// Start generous; grow ×4 if REAPER reports the value may have been clipped
// (the return is the value length; equal-to-capacity-minus-NUL is ambiguous,
// so only a strict fit terminates). Ceiling 16 MB — give up rather than loop
// forever on a pathological value.
GrowingExtStateRead result;
for (int cap = 1 << 16; cap <= (1 << 24); cap <<= 2) {
std::vector<char> buf(static_cast<std::size_t>(cap), '\0');
const int rv = read(buf.data(), cap);
result.apiReturn = rv;
if (rv <= 0) {
result.status = GrowingExtStateRead::Status::Absent;
return result;
}
buf[static_cast<std::size_t>(cap) - 1] = '\0'; // defensive: guard against a read() that fills the buffer without honoring NUL-termination within cap
std::string s(buf.data());
if (static_cast<int>(s.size()) + 1 < cap) {
result.status = GrowingExtStateRead::Status::Complete;
result.value = std::move(s);
return result;
}
// else: possibly truncated -> grow and retry.
}
result.status = GrowingExtStateRead::Status::Overflow;
return result;
}
} // namespace reasampler::wire
+3 -3
View File
@@ -1,6 +1,6 @@
#pragma once
// ext_keys — the SINGLE SOURCE OF TRUTH for the "reasampler" project ext-state
// namespace + key names, shared by the extension (writer, via persist.h) and the
// namespace + key names, shared by the extension (writer, via shell/persist) and the
// VST3 instrument (reader, via the bridge). Both sides include this header so the
// wire contract cannot drift between the two artifacts (the S4 reviewer flagged the
// spike's duplicated constants as a drift risk).
@@ -12,7 +12,7 @@
// either SDK.
//
// FOREVER-STABLE once shipped: these strings key every already-saved project's
// stored state. Changing any of them orphans that state. See persist.h for the
// stored state. Changing any of them orphans that state. See shell/persist/ext_state_io.h for the
// per-key retirement / migration semantics — this header only owns the spellings.
#include "core/version/app_version.h"
@@ -29,7 +29,7 @@ namespace reasampler {
inline const char* kProjExtNamespace() { return version::extStateNamespace().c_str(); }
// The multi-bank key: the whole serialized BankBook (pool + named banks). This is
// the key the VST3 instrument reads to see the live bank (read-only, S4). persist.h
// the key the VST3 instrument reads to see the live bank (read-only, S4). ext_state_io
// documents its authority + the legacy-key migration around it.
inline constexpr const char* kProjExtBanksKey = "banks";
+18 -3
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// ingest.cpp — the S8 "ingest through the bank" shell (extension side). See ingest.h.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h WITHOUT
@@ -25,7 +24,7 @@
#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03)
#include "core/wire/instrument_drop.h" // pure buildInstrumentDropPreset (.vstpreset image for a sampleId)
#include "shell/actions/instrument_drop_win.h" // shell loadInstrumentOntoTrack (FX add+apply, no own undo block)
#include "persist.h" // ReaSamplerSession
#include "shell/persist/session.h" // ReaSamplerSession
#include "core/capture/wav_codec.h" // parseWavLayout (32f fast-path validator), buildFloat32Wav, hashWavContent
@@ -47,6 +46,22 @@
namespace reasampler {
// Real-namespace-home using-declarations (Q-W6: the interim core/namespaces.h shim
// is retired; each symbol names its Q-W1 home explicitly).
using capture::BankPaths;
using capture::buildFloat32Wav;
using capture::deriveBankPaths;
using capture::hashWavContent;
using capture::parseWavLayout;
using capture::projectDirOfRpp;
using capture::WavLayout;
using util::readFileBytes;
using version::channelActionName;
using version::channelCommandId;
using wire::AssignmentRequest;
using wire::buildInstrumentDropPreset;
using wire::encodeAssignmentRequest;
namespace {
// The live session the ingest paths mutate. Set once by ingestRegisterActions and read by
@@ -153,7 +168,7 @@ struct ImportResult {
// Imports one OS-native source file into the ACTIVE bank: convert-if-needed to 32-bit-
// float WAV, write to the project-relative bank folder, index-add, hash-dedup applied.
//
// BANK CONTRACT: the instrument (wav_trim) expects every bank file to be a canonical
// BANK CONTRACT: the instrument (wav_codec parse) expects every bank file to be a canonical
// 32-bit-float WAV (WAVE_FORMAT_IEEE_FLOAT, 32 bits). A verbatim copy of a non-WAV (or
// an integer-PCM or double-float WAV) would be unplayable. This function therefore:
// 1. Checks whether the source IS already a valid 32f WAV (parseWavLayout fast path).
-1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// ingest — the S8 "ingest through the bank" shell (EXTENSION side).
//
// Compiled into the reaper_reasampler MODULE. REAPER-facing (PCM_Source metadata reads,
-27
View File
@@ -1,27 +0,0 @@
#pragma once
#include "core/namespaces.h"
// persist.h — COMPATIBILITY UMBRELLA (Q-W5). The former persist god-TU split into
// three TUs under shell/persist/ by responsibility:
//
// * shell/persist/session.h + session.cpp — the ReaSamplerSession class (lifecycle,
// poll identity-transition detection, the projectconfig undo/redo reload drain).
// * shell/persist/ext_state_io.h + ext_state_io.cpp — the ext-state ↔ JSON
// serialization bridge, key contract, GUID minting, bank-folder relocation.
// * shell/persist/prune_fs.cpp — the prune scan + THE SINGLE FILE-DELETION
// AUTHORITY over USER files in the bank folder (deleteOrphanFile, file-local);
// a shell's self-cleanup of its own transient scratch files (.vstpreset temp,
// realtime finalize temp) is excluded from this authority.
//
// This header re-exports the split APIs so every existing caller (actions.cpp,
// main.cpp, ingest.cpp, the panel TUs, capture shells) keeps compiling untouched —
// Q-W4 is rewriting actions.cpp in parallel, so touching callers this wave is a
// guaranteed conflict. Retiring this umbrella (callers include the split headers
// directly) is Q-W6 cleanup.
//
// core/namespaces.h stays HERE, not in the split headers/TUs: the unsplit callers
// still reference flat-namespace symbols (TailSetting, PruneReport, BankModel, ...)
// through this include, while the split persist TUs themselves reference real
// namespace homes and are shim-free.
#include "shell/persist/ext_state_io.h"
#include "shell/persist/session.h"
+1 -1
View File
@@ -6,5 +6,5 @@
// numeric ids stable and unique across the extension.
// The docked bank panel (M5). A bare owner-drawn child dialog: it carries no
// controls — bank_panel.cpp paints the whole client area with LICE.
// controls — the panel shell (shell/panel/panel_render.cpp) paints the whole client area with LICE.
#define IDD_BANK_PANEL 1000
+51 -3
View File
@@ -1,9 +1,10 @@
// action_registry.cpp — shared registration plumbing (Q-W4 split of actions.cpp).
// See action_registry.h. Needs no REAPER API pointers: rec->Register is a member
// call on the dispatch struct REAPER hands the entry point.
// action_registry.cpp — shared registration plumbing (Q-W4) + the registration
// table (Q-W6). See action_registry.h. Needs no REAPER API pointers: rec->Register
// is a member call on the dispatch struct REAPER hands the entry point.
#include "shell/actions/action_registry.h"
#include <cstring>
#include <deque>
#include <string>
@@ -23,6 +24,18 @@ using version::channelCommandId;
// pointer for a given action.
std::deque<std::string> g_strStore;
// One registered table row: the row data plus the registry-owned registration
// artifacts (interned id, minted cmd, gaccel storage REAPER holds a pointer to).
// A std::deque so element addresses never move after push_back — REAPER keeps each
// &accel until the mirror-unregister.
struct TableEntry {
ActionTableRow row;
const char* id = nullptr; // interned channel-qualified command id
int cmd = 0; // minted command id (0 = mint failed / inert row)
gaccel_register_t accel{}; // Actions-list entry; address handed to REAPER
};
std::deque<TableEntry> g_table;
} // namespace
const char* channelIdFor(const char* suffix) {
@@ -46,4 +59,39 @@ int registerAction(reaper_plugin_info_t* rec, const char* suffix,
return cmd;
}
void registerActionTable(reaper_plugin_info_t* rec, const ActionTableRow* rows,
std::size_t count) {
for (std::size_t i = 0; i < count; ++i) {
g_table.push_back(TableEntry{rows[i]});
TableEntry& e = g_table.back();
e.id = channelIdFor(e.row.suffix);
e.cmd = registerAction(rec, e.row.suffix, e.accel, e.row.phrase);
}
}
bool actionTableHandleCommand(int command) {
if (command == 0) return false;
for (const TableEntry& e : g_table)
if (e.cmd != 0 && command == e.cmd) {
e.row.run(e.row.arg);
return true;
}
return false;
}
int actionTableCommandId(const char* suffix) {
for (const TableEntry& e : g_table)
if (std::strcmp(e.row.suffix, suffix) == 0) return e.cmd;
return 0;
}
void unregisterActionTable(reaper_plugin_info_t* rec) {
// Reverse table order, mirroring the register loop. Each '-command_id'
// re-presents the SAME interned pointer channelIdFor handed out at register.
for (auto it = g_table.rbegin(); it != g_table.rend(); ++it) {
rec->Register("-gaccel", (void*)&it->accel);
rec->Register("-command_id", (void*)it->id);
}
}
} // namespace reasampler
+61 -7
View File
@@ -1,13 +1,30 @@
#pragma once
// action_registry — shared registration plumbing for the bindable action families
// (Q-W4 split of actions.cpp). Owns the durable interned-string store both the
// Design View and multi-bank families register through, so a composed command id
// keeps ONE stable pointer from register to the mirror-unregister, and the
// register-a-command_id-then-gaccel sequence has one implementation.
// action_registry — shared registration plumbing + the Q-W6 registration TABLE.
//
// Two layers, one TU:
//
// * The Q-W4 plumbing (channelIdFor / registerAction): the durable interned-string
// store the action families register through, so a composed command id keeps ONE
// stable pointer from register to the mirror-unregister, and the
// register-a-command_id-then-gaccel sequence has one implementation. The
// design_view / bank / ingest families still register row-by-row through this.
//
// * The Q-W6 registration TABLE (ActionTableRow + registerActionTable /
// actionTableHandleCommand / actionTableCommandId / unregisterActionTable): the
// data-driven home of main.cpp's own action family (capture scopes, panel toggle,
// insert, batch, realtime, recapture, version). One row = one action (FOREVER-
// STABLE id suffix, display phrase, flat function-pointer handler); registration
// iterates the rows, hookcommand dispatch walks the same rows, and unload
// mirror-unregisters from them — adding an action touches the table only (OCP).
// Handlers are plain function pointers (a static dispatch walk, no std::function,
// no virtual — the §3 performance guardrail); gaccel + interned-id storage is
// owned here for the module lifetime, so REAPER's held pointers stay valid and
// the '-command_id' unregister re-presents the IDENTICAL pointer registered.
//
// Includes reaper_plugin.h (gaccel_register_t / reaper_plugin_info_t full defs);
// only the action-family TUs include this header. Q-W6's registration table
// subsumes this helper when the hand-written blocks become data.
// only the action-family TUs and main.cpp include this header.
#include <cstddef>
#include "reaper_plugin.h" // reaper_plugin_info_t, gaccel_register_t
@@ -26,4 +43,41 @@ const char* channelIdFor(const char* suffix);
int registerAction(reaper_plugin_info_t* rec, const char* suffix,
gaccel_register_t& accel, const char* phrase);
// --- The registration table (Q-W6) -------------------------------------------
// One bindable action. `suffix` and `phrase` are the channel-AGNOSTIC pieces (the
// registry composes the full id/label via channelCommandId / channelActionName);
// both must have static storage duration (string literals, or a pure static table
// like captureActionTable()). `run` fires when the minted command does; `arg` is an
// opaque per-row value passed through to it (e.g. a captureActionTable row index, or
// a bool-like flag), so sibling actions can share one handler without captures.
struct ActionTableRow {
const char* suffix; // FOREVER-STABLE command-id suffix — never change shipped
const char* phrase; // Actions-list display phrase (after the channel prefix)
void (*run)(int arg); // handler — a flat function pointer, no state
int arg = 0; // opaque per-row handler argument
};
// Registers every row (command_id -> gaccel, via the same interning plumbing as
// registerAction) in table order. Rows are COPIED into registry-owned storage whose
// element addresses never move (REAPER holds each gaccel pointer until unload).
// Call once at load; a failed command_id mint (cmd 0) leaves that row inert but
// still mirror-unregistered on unload (harmless, matches the pre-table behavior).
void registerActionTable(reaper_plugin_info_t* rec, const ActionTableRow* rows,
std::size_t count);
// Dispatches one fired command: fires the matching row's handler and returns true;
// false when the command belongs to no table row (caller's hookcommand keeps
// looking, per the claim-only contract). A flat walk over the registered rows.
bool actionTableHandleCommand(int command);
// The minted command id for `suffix` (0 when unregistered / mint failed). For the
// callers that need a raw command id outside dispatch — e.g. the toggleaction
// checked-state hook resolving TOGGLE_BANK_PANEL once at load.
int actionTableCommandId(const char* suffix);
// Mirror-unregisters every table row (reverse table order): '-gaccel' with the same
// held storage, '-command_id' with the SAME interned pointer used at register.
void unregisterActionTable(reaper_plugin_info_t* rec);
} // namespace reasampler
+20 -18
View File
@@ -1,11 +1,12 @@
// bank_actions.cpp — the multi-bank bindable action family (Phase B3; Q-W4 split of
// actions.cpp). See bank_actions.h.
//
// Q-W4 dedupe: each mutating handler is a THIN UX SKIN — text prompts (promptBankName),
// name resolution, and console feedback — over the promptless bankOp* inner verbs
// homed in panel_bank_ops (model op + persistBankOp, one bank op = one Ctrl-Z). The
// book's rules (pool privileges, collapse-by-hash, active-fallback-to-pool) all live
// in bank_book; these handlers only drive the verbs and react to the boolean.
// Q-W4 dedupe / Q-W6 seam: each mutating handler is a THIN UX SKIN — text prompts
// (promptBankName), name resolution, and console feedback — over the promptless
// bankOp* inner verbs homed in shell/bank_ops (model op + persistBankOp, one bank op
// = one Ctrl-Z), driven against this family's registered session. The book's rules
// (pool privileges, collapse-by-hash, active-fallback-to-pool) all live in
// bank_book; these handlers only drive the verbs and react to the boolean.
//
// REFERENCE-INVALIDATION GUARDRAIL (B2 review): book().activeIndex() / bank()->index
// return a reference INTO the book's internal vector, which a create/delete can
@@ -27,9 +28,10 @@
#include "shell/actions/prune_action.h" // doBankPruneFolder — the guarded prune body
#include "core/model/bank_book.h" // BankBook, nextBankId, kPoolBankId (B1)
#include "persist.h" // ReaSamplerSession (owns book())
#include "shell/panel/panel_bank_ops.h" // bankOp* inner verbs + promptBankName + selection seam
#include "shell/bank_ops/bank_ops.h" // bankOp* promptless verbs (Q-W6 non-UI seam)
#include "shell/panel/panel_bank_ops.h" // promptBankName + the panel selection seam
#include "shell/panel/panel_layout.h" // full-height toggles (B3)
#include "shell/persist/session.h" // ReaSamplerSession (owns book())
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_ShowConsoleMsg
@@ -61,9 +63,9 @@ constexpr const char* kIdBankBanksFull = "BANK_BANKS_FULLHEIGHT";
// and R3 extends the confirm-and-delete step behind this SAME id — never a throwaway id.
constexpr const char* kIdBankPruneFolder = "BANK_PRUNE_FOLDER";
// The live session the actions read (name resolution, member counts, prune). The
// mutations themselves run through the bankOp* verbs, which resolve the same session
// via the panel seam. Set once by bankRegisterActions; not owned here.
// The live session the actions read (name resolution, member counts, prune) and
// pass to the bankOp* verbs by reference (bankHandleCommand guards it non-null
// before any handler runs). Set once by bankRegisterActions; not owned here.
ReaSamplerSession* g_session = nullptr;
int g_cmdBankCreate = 0;
@@ -116,7 +118,7 @@ std::string bankIdByDisplayName(const std::string& name) {
void doBankCreate() {
std::string name;
if (!promptBankName("ReaSampler: create bank", "Bank name:", "", name)) return;
if (bankOpCreate(name).empty()) {
if (bankOpCreate(*g_session, name).empty()) {
ShowConsoleMsg(
("ReaSampler: could not create bank \"" + name +
"\" (a bank with that name already exists).\n")
@@ -139,7 +141,7 @@ void doBankRename() {
}
std::string newName;
if (!promptBankName("ReaSampler: rename bank", "New name:", which, newName)) return;
if (!bankOpRename(id, newName)) {
if (!bankOpRename(*g_session, id, newName)) {
// The verb rejects the pool (un-renamable) or a name already used by another
// bank (unique display names, trimmed + case-insensitive).
ShowConsoleMsg("ReaSampler: cannot rename that bank (the pool is un-renamable, "
@@ -184,7 +186,7 @@ void doBankDelete() {
}
// S9: bump only when the deleted bank held samples — dropping them changes what a live
// instance referencing one could play. Deleting an EMPTY bank is purely organizational.
if (!bankOpDelete(id, /*bumpGeneration=*/members > 0)) {
if (!bankOpDelete(*g_session, id, /*bumpGeneration=*/members > 0)) {
ShowConsoleMsg("ReaSampler: cannot delete that bank (the pool is un-deletable).\n");
}
}
@@ -202,7 +204,7 @@ void doBankEvacuate() {
ShowConsoleMsg(("ReaSampler: no bank named \"" + which + "\".\n").c_str());
return;
}
if (!bankOpEvacuate(id)) {
if (!bankOpEvacuate(*g_session, id)) {
ShowConsoleMsg("ReaSampler: cannot evacuate that bank (the pool is the "
"destination, not a source).\n");
}
@@ -218,13 +220,13 @@ void doBankActivateNext() {
for (const Bank& b : g_session->book().banks()) ids.push_back(b.id);
const std::string target = nextBankId(ids, g_session->book().activeBankId());
if (target.empty()) return; // degenerate (no banks) — cannot happen (pool seeded)
bankOpActivate(target);
bankOpActivate(*g_session, target);
}
// Activate the pool directly (the common "back to the default target" jump). Bindable
// direct-by-id form; a general activate-bank-by-name/menu is a panel affordance.
void doBankActivatePool() {
bankOpActivate(kPoolBankId);
bankOpActivate(*g_session, kPoolBankId);
}
// Move or copy the panel's selected samples into a named destination bank (prompted
@@ -257,7 +259,7 @@ void doBankTransferSelected(bool copy) {
ShowConsoleMsg("ReaSampler: source and destination are the same bank.\n");
return;
}
bankOpTransfer(selected, srcId, destId, copy);
bankOpTransfer(*g_session, selected, srcId, destId, copy);
}
// Remove the panel's selected samples from the SOURCE bank (the focused region's
@@ -275,7 +277,7 @@ void doBankRemoveSelected() {
ShowConsoleMsg("ReaSampler: the selection's bank no longer exists.\n");
return;
}
bankOpRemove(selected, srcId);
bankOpRemove(*g_session, selected, srcId);
}
} // namespace
+1 -1
View File
@@ -29,7 +29,7 @@
#include "core/view/lane_keys.h" // view::isOnManualLane — the single managed/manual predicate
#include "core/view/view_mode_model.h"
#include "persist.h" // ReaSamplerSession (owns view() model)
#include "shell/persist/session.h" // ReaSamplerSession (owns view() model)
#include "shell/capture/item_read.h" // shared MediaItem* -> GUID + fixed-lane-name reads (D2 W3-B)
#include "shell/capture/track_guid.h" // shared MediaTrack* -> canonical GUID key
#include "shell/panel/panel_window.h" // bankPanelInvalidate — footer toggle repaint
-1
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// drag_out_win — OS/COM initiation of native OS drag-out (M11). See drag_out_win.h.
//
// Windows path (primary): a hand-rolled minimal IDataObject exposing exactly one format,
-1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// drag_out_win — the OS/COM initiation half of native OS drag-out (Milestone 11). The pure
// gesture-boundary decision and path-list assembly live in drag_out.*; THIS is the platform
// shell that hands a resolved, existing-file path list to the operating system's drag-drop
+5 -2
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// instrument_drop_win — the REAPER shell for S17 drop-and-load. See instrument_drop_win.h.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h WITHOUT
@@ -30,6 +29,10 @@
namespace reasampler {
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
using version::vstPluginName;
using wire::infoNamesFxHotspot;
namespace {
// Write `bytes` to a fresh uniquely-named .vstpreset in the OS temp dir and return its path;
@@ -43,7 +46,7 @@ namespace {
//
// Non-throwing: every std::filesystem call uses the error_code overload. The whole body is
// wrapped in try/catch to guarantee no exception crosses the REAPER C callback boundary
// (the same discipline persist.cpp uses — see its non-throwing scanPruneOrphans comment).
// (the same discipline the prune shell uses — see prune_fs.cpp's non-throwing scan comment).
//
// Returns the path object (not a narrow string) so the caller can:
// (a) pass path.u8string() to TrackFX_SetPreset — UTF-8 on MSVC, not ACP-converted,
-1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// instrument_drop_win — the REAPER-facing shell half of S17 drop-and-load. The pure gesture
// decision lives in drag_out (DragGesture::InstrumentDrop) and the pure payload construction
// in instrument_drop; THIS is the platform shell that (a) resolves a screen point to a track
+3 -3
View File
@@ -9,7 +9,7 @@
#include <string>
#include <vector>
#include "persist.h" // ReaSamplerSession — pruneDryRun / pruneOrphanSet / pruneReclaim
#include "shell/persist/session.h" // ReaSamplerSession — pruneDryRun / pruneOrphanSet / pruneReclaim
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_ShowConsoleMsg
@@ -31,7 +31,7 @@ namespace reasampler {
// is opened (file deletion is not REAPER-undoable and pruneReclaim mutates no project
// state) — a Ctrl-Z after a prune correctly cannot claim to restore deleted files.
void doBankPruneFolder(ReaSamplerSession& session) {
const PruneReport report = session.pruneDryRun();
const reclaim::PruneReport report = session.pruneDryRun();
// pS-usage FAIL-SAFE: a present instance-usage record could not be read — the
// protected set is unknowable, so the prune HALTS outright (deletes nothing) rather
@@ -85,7 +85,7 @@ void doBankPruneFolder(ReaSamplerSession& session) {
}
// Confirmed -> delete exactly the confirmed set (recomputed fresh, stale entries skipped).
const PruneDeletionResult del = session.pruneReclaim(orphanSet);
const reclaim::PruneDeletionResult del = session.pruneReclaim(orphanSet);
std::string done = "ReaSampler prune: reclaimed " +
std::to_string(del.reclaimedCount) + " file(s), " +
+183
View File
@@ -0,0 +1,183 @@
// bank_ops.cpp — the promptless bank-verb seam (Q-W6 lift; see bank_ops.h for the
// contract). The ONE implementation home of the bank verbs (create / rename /
// delete / evacuate / activate / move / copy / remove): each mutates the given
// session's book() then persists via persistBankOp() (one bank op = one Ctrl-Z; a
// true index no-op opens NO undo point). It DOES mutate the bank BOOK — but only
// the index/model + ext-state, never the arrange, never a sample file on disk
// (bank ops are index-only; files stay put — CONTEXT.md §Multi-bank).
// REFERENCE-INVALIDATION GUARDRAIL: after a STRUCTURAL mutation any
// Bank*/BankModel& is invalid — verbs take ids and resolve fresh per model call.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
// extern (CLAUDE.md §contract). DAW-verified, not unit tested.
#include "shell/bank_ops/bank_ops.h"
#include <string>
#include <vector>
#include "core/model/bank_book.h" // BankBook / TransferResult / RemoveScope
#include "shell/persist/session.h" // ReaSamplerSession — the session the verbs mutate
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_genGuid
#define REAPERAPI_WANT_guidToString
#define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2
#include "reaper_plugin_functions.h"
namespace reasampler {
namespace {
// Mints a fresh, genuine REAPER GUID string as a stable bank id (the B2/model
// design: ids are caller-supplied and stable; the model stays pure and mints none).
// Distinct from a track GUID by origin only — both are canonical guidToString output.
std::string mintBankId() {
GUID g{};
genGuid(&g);
char buf[64] = {0}; // guidToString needs >=64 chars (SDK contract)
guidToString(&g, buf);
return std::string(buf);
}
} // namespace
// Persists a completed bank-index verb as a SINGLE batched REAPER undo point (R-B) —
// one bank op = one Ctrl-Z.
//
// WHY THIS WRAPS AND saveToActiveProject() DOES NOT: a bank verb mutates ONLY our
// project ext-state (SetProjExtState under "reasampler"), which REAPER's undo system
// captures iff UNDO_STATE_MISCCFG is set in the Undo_EndBlock2 flags — the SDK
// documents MISCCFG as covering "extensions!" project ext-state (reaper_plugin.h
// ~1544, ~1199). We pass exactly UNDO_STATE_MISCCFG (not -1 / UNDO_STATE_ALL as the
// item-move family does): a bank verb touches no tracks, FX, items, or envelopes, so
// snapshotting them would be both heavier and semantically wrong. The persist runs
// INSIDE the block so the post-mutation ext-state is the block's "after" image.
//
// UNSAVED-PROJECT GUARDRAIL: on an unsaved / no-active project saveToActiveProject()
// no-ops (nothing is written to ext state). We must still CLOSE the block we opened,
// but with an EMPTY label and a zero flag so REAPER DISCARDS the point instead of
// recording a no-effect undo entry — mirroring view.cpp's empty-plan close. The
// in-session model change stands and persists on the user's next save; it just earns
// no undo point until there is a project to persist into (undo of an unsaved bank op
// has nothing to roll back to anyway). The Begin/End must still be balanced, hence
// the close-either-way. (Quiet persist by design — mirrors the CAPTURE path, NOT the
// Design-View path; deliberately NO Save-As prompt.)
void persistBankOp(ReaSamplerSession& session, const char* label,
bool bumpGeneration) {
Undo_BeginBlock2(nullptr);
// S9: bump the bank-generation counter INSIDE the block, before the persist, so the
// fresh generation rides the same ext-state write (saveToActiveProject() stamps
// bankGeneration()). Bumped only for content-changing verbs (the caller decides); a
// pure-organizational verb passes false and leaves the counter be, so a
// rename/activate does not needlessly refresh live instances.
if (bumpGeneration) session.bumpBankGeneration();
const bool persisted = session.saveToActiveProject();
if (persisted)
Undo_EndBlock2(nullptr, label, UNDO_STATE_MISCCFG);
else
Undo_EndBlock2(nullptr, "", 0); // no ext-state write -> discard the empty point
}
// --- Promptless inner bank verbs (one home) ------------------------------------
// Model op + persistBankOp only; NO UX. Callers own prompts/confirms/nudges and the
// session-liveness question. Each verb persists ONLY after the model accepted — a
// rejected op opens no undo point.
std::string bankOpCreate(ReaSamplerSession& session, const std::string& name) {
const std::string id = mintBankId();
if (!session.book().createBank(id, name)) return {}; // duplicate display name (model rule)
persistBankOp(session, "ReaSampler: create bank");
return id;
}
bool bankOpRename(ReaSamplerSession& session, const std::string& bankId,
const std::string& newName) {
if (!session.book().renameBank(bankId, newName)) return false; // pool / name in use
persistBankOp(session, "ReaSampler: rename bank");
return true;
}
bool bankOpDelete(ReaSamplerSession& session, const std::string& bankId,
bool bumpGeneration) {
if (!session.book().deleteBank(bankId)) return false; // pool un-deletable (model rule)
persistBankOp(session, "ReaSampler: delete bank", bumpGeneration);
return true;
}
bool bankOpEvacuate(ReaSamplerSession& session, const std::string& bankId) {
if (!session.book().evacuate(bankId)) return false; // pool is a destination, not a source
// S9: evacuate moves members between banks (bank membership changes) -> bump.
persistBankOp(session, "ReaSampler: evacuate bank", /*bumpGeneration=*/true);
return true;
}
bool bankOpActivate(ReaSamplerSession& session, const std::string& bankId) {
if (!session.book().setActiveBank(bankId)) return false; // rejects an unknown id
persistBankOp(session, "ReaSampler: activate bank");
return true;
}
// NO-OP GUARDRAIL — VERB-AWARE (a collapse means different things per verb):
// * MOVE collapse: the source entry WAS removed (bank_book moveSample removes
// unconditionally before the dest add collapses on hash), so the index DID
// mutate — it counts toward opening an undo point.
// * COPY collapse: the source is left intact AND the dest already held the hash,
// so NOTHING changed — a true index no-op. It must NOT open an undo point.
// Hence: copy counts only real gains; move counts gains OR collapses. Ids pass
// straight to the model op — no BankModel& cached across the loop's mutations.
bool bankOpTransfer(ReaSamplerSession& session,
const std::vector<std::string>& sampleIds,
const std::string& srcBankId, const std::string& destBankId,
bool copy) {
BankBook& b = session.book();
if (sampleIds.empty() || srcBankId == destBankId) return false;
if (!b.bank(srcBankId) || !b.bank(destBankId)) return false;
int ok = 0, collapsed = 0;
for (const std::string& sid : sampleIds) {
const TransferResult r =
copy ? b.copySample(sid, srcBankId, destBankId)
: b.moveSample(sid, srcBankId, destBankId);
switch (r) {
case TransferResult::Moved:
case TransferResult::Copied: ++ok; break;
case TransferResult::Collapsed: ++collapsed; break;
case TransferResult::RejectedUnknownBank:
case TransferResult::RejectedSampleAbsent:
case TransferResult::RejectedSameBank: break;
}
}
const bool mutated = copy ? (ok > 0) : (ok > 0 || collapsed > 0);
if (!mutated) return false; // nothing changed — no persist, no undo point
// S9: a move/copy changes bank membership (a sample arrives in / leaves a bank an
// instance may reference) -> bump so assigned instances refresh hands-free.
persistBankOp(session,
copy ? "ReaSampler: copy sample(s)" : "ReaSampler: move sample(s)",
/*bumpGeneration=*/true);
return true;
}
// Index-only, this-bank scope (fork R-A: the sole surfaced verb; RemoveScope::AllBanks
// stays latent in the model). Non-destructive to the file: a last-reference remove
// leaves the file on disk, orphaned until Phase R prune — remove NEVER deletes bytes
// (the manifest is untouched). Silent: recoverability is the batched undo (R-B).
bool bankOpRemove(ReaSamplerSession& session,
const std::vector<std::string>& sampleIds,
const std::string& srcBankId) {
BankBook& b = session.book();
if (sampleIds.empty() || !b.bank(srcBankId)) return false;
int removed = 0;
for (const std::string& sid : sampleIds)
if (b.removeSample(sid, srcBankId, RemoveScope::ThisBank) ==
RemoveResult::Removed)
++removed;
if (removed == 0) return false; // every id already absent — no undo point
// S9: a remove drops a sample from a bank (an instance referencing it must refresh —
// it will resolve to silence, per the stale-id policy) -> bump.
persistBankOp(session, "ReaSampler: remove sample(s)", /*bumpGeneration=*/true);
return true;
}
} // namespace reasampler
+83
View File
@@ -0,0 +1,83 @@
#pragma once
// bank_ops — the promptless bank-verb seam (Q-W6 lift of the Q-W4 single-owner
// verbs out of shell/panel/panel_bank_ops into a NON-UI home). Each verb is a model
// op on the given session's BankBook + persistBankOp (undo-batched ext-state
// persist) — NO prompts, NO message boxes, NO panel-state nudges, NO panel-global
// reads. The two UX surfaces consume these as thin skins:
//
// * shell/panel/panel_bank_ops — the panel's menu handlers (prompts / confirms /
// repaints), passing the panel's live session.
// * shell/actions/bank_actions — the bindable family (text prompts / console
// feedback), passing its registered session.
//
// The session arrives BY REFERENCE: there is exactly one session pointer question
// per call site (the caller's), so a missing session can never be half-reported as
// a model rejection from in here (the Q-W4 review's fail-safe-collapse concern).
// Every verb returns whether the model accepted the mutation — a rejected op
// persists nothing and opens no undo point.
//
// REAPER-facing (persist + undo blocks + GUID minting) but SDK-free in this header.
#include <string>
#include <vector>
namespace reasampler {
class ReaSamplerSession;
// Mints a stable GUID bank id, creates `name` in the book. Returns the new bank id,
// or "" when the model rejects the name (duplicate, trimmed + case-insensitive).
// Create is purely organizational — no generation bump.
std::string bankOpCreate(ReaSamplerSession& session, const std::string& name);
// Renames `bankId`. False when the model rejects (pool un-renamable / name in use).
bool bankOpRename(ReaSamplerSession& session, const std::string& bankId,
const std::string& newName);
// Deletes `bankId`. False when the model rejects (pool un-deletable). The caller
// passes `bumpGeneration` from the member count it read BEFORE any evacuate/delete
// (an evacuate-then-delete flow must still bump on the ORIGINAL membership).
bool bankOpDelete(ReaSamplerSession& session, const std::string& bankId,
bool bumpGeneration);
// Evacuates `bankId`'s members to the pool. False when the model rejects (the pool
// itself). Bumps the generation (membership changed).
bool bankOpEvacuate(ReaSamplerSession& session, const std::string& bankId);
// Activates `bankId` as the capture target. False on an unknown id. No bump.
bool bankOpActivate(ReaSamplerSession& session, const std::string& bankId);
// Moves (copy=false) or copies (copy=true) `sampleIds` from `srcBankId` to
// `destBankId` (index-only; files never relocate). Returns whether the index
// actually mutated — the verb-aware no-op guardrail: a COPY collapse changes
// nothing (no undo point); a MOVE collapse did remove the source entry (counts).
// Persists ONE undo point ("move/copy sample(s)") only when mutated.
bool bankOpTransfer(ReaSamplerSession& session,
const std::vector<std::string>& sampleIds,
const std::string& srcBankId, const std::string& destBankId,
bool copy);
// Removes `sampleIds` from `srcBankId` (index-only, this-bank scope; never deletes
// bytes). Returns whether anything was removed; persists one undo point when so.
bool bankOpRemove(ReaSamplerSession& session,
const std::vector<std::string>& sampleIds,
const std::string& srcBankId);
// Persists a completed bank-index verb as a single REAPER undo point (R-B).
// Wraps the session persist (SetProjExtState) in a Begin/End block with
// UNDO_STATE_MISCCFG so the bank op is one Ctrl-Z. On an unsaved / no-active project
// the persist no-ops and the block is closed with an empty label + zero flag (REAPER
// discards it). Callers must invoke this ONLY after a successful/effective mutation —
// rejected ops (duplicate name, un-deletable pool, etc.) must return before reaching
// here so no empty undo point is ever opened for a no-op.
//
// S9 bank-generation bump: pass `bumpGeneration = true` for a verb that changes what a
// live instance would PLAY — move / copy / remove / evacuate / delete-with-members. Leave
// it false (the default) for a PURELY ORGANIZATIONAL verb — create / rename / activate /
// reorder. The bump (when requested) happens INSIDE the block, BEFORE the persist, so
// the stamped counter rides the same ext-state write and undo captures the pre/post
// generation with the rest of the blob.
void persistBankOp(ReaSamplerSession& session, const char* label,
bool bumpGeneration = false);
} // namespace reasampler
+5 -111
View File
@@ -1,16 +1,16 @@
#pragma once
// capture — the REAPER-facing capture shell (CLAUDE.md §load-bearing split).
//
// This header declares the capture *seam* the later milestones fill:
// * CaptureRequest — everything a capture needs, source-mode-agnostic.
// This header declares the SHARED capture seam (Q-W6 split of the former fat
// header — the realtime backend's async begin/tick/abort surface now lives in
// capture_realtime_shell.h):
// * CaptureRequest / CaptureResult — everything a capture needs and yields,
// source-mode-agnostic; the types BOTH backends speak.
// * OfflineRenderBackend — the deterministic default; a plain CONCRETE class
// (the former ICaptureBackend interface was deleted in
// Q-W3, T4-26 — it had one deriver and zero polymorphic
// call sites; every construction site instantiates the
// concrete type).
// * RealtimeRecordBackend — the ASYNC realtime seam (begin/tick/abort), driven
// across timer ticks; a genuinely different lifecycle
// (see the SEAM CHOICE note at its declaration).
// * makeUniqueTag / stampCaptureSample — the shared file-tag mint and the shared
// finished-capture metadata stamp both backends call
// (Q-W3 riders T1-11 / T2-09).
@@ -20,7 +20,6 @@
// REAPER-free lets callers (the capture orchestration TUs) depend on the seam
// without dragging the SDK into every include site.
#include <memory>
#include <string>
#include <vector>
@@ -156,109 +155,4 @@ void stampCaptureSample(Sample& s, const CaptureRequest& req,
ReaProject* rateProj, ReaProject* timeSigProj,
const std::string& absolutePath);
// --- Realtime-record backend: the ASYNC seam ---------------------------------
//
// A realtime record is inherently asynchronous: CSurf_OnRecord starts the transport
// on REAPER's audio thread and returns immediately — it does NOT block until the
// range completes, which takes (end - start) wall-clock seconds. Blocking the main
// thread for that duration freezes REAPER's UI, so the realtime backend is DRIVEN
// ACROSS TIMER TICKS instead: begin() starts and returns at once; tick() (called
// from the same OnTimer that runs session.poll()) advances the in-flight record and
// reports when it is done.
//
// SEAM CHOICE (surfaced): the two backends deliberately share NO interface. The
// lifecycles are genuinely different (offline is headless + immediate — one
// synchronous capture() call returns a finished Sample; realtime is
// transport-driven + async — begin/tick/abort across timer ticks), so a shared
// interface would make offline fake a lifecycle it does not have (its tick()
// would always be Done on the first call — dead code / an LSP smell). Offline
// stays synchronous; the realtime backend owns this small bespoke async seam,
// driven by exactly one caller (the timer-driven realtime_lifecycle). This is the
// split-sync/async fork, chosen over a unified async interface for that reason.
// (The old synchronous ICaptureBackend interface over OfflineRenderBackend was
// deleted in Q-W3 — T4-26: one deriver, zero polymorphic call sites.)
// One tick's verdict from the in-flight record.
enum class RealtimeTickStatus {
InProgress, // still recording — call tick() again next timer tick
Done, // finished (range end reached, or the user stopped) — `result` is set
Failed, // an error tore the capture down — `result.message` explains
};
struct RealtimeTickResult {
RealtimeTickStatus status = RealtimeTickStatus::InProgress;
CaptureResult result; // meaningful only when status == Done or Failed
};
// The opaque in-flight capture state. Owns the snapshot of everything to restore
// (temp track + its receive sends from the source tracks, other tracks' I_RECARM,
// transport, edit cursor, time selection) and the record's own project handle.
// Defined in capture_realtime_shell.cpp; the header stays REAPER-free (nothing is
// dereferenced here) by holding it behind a forward-declared type + unique_ptr.
//
// restore()/teardown is idempotent and lives ON THIS OBJECT (not a function-scope
// RAII guard) because the record spans ticks — no single stack frame outlives it.
// Every terminal path (normal completion, user stop, error, project switch, unload)
// funnels through the same single restore, safe to call once from whichever fires.
class RealtimeCaptureState;
// Out-of-line deleter so callers (realtime_lifecycle) can own a unique_ptr to the
// opaque RealtimeCaptureState WITHOUT its full (REAPER-typed) definition — the
// delete is compiled in capture_realtime_shell.cpp where the type is complete,
// keeping this header REAPER-free (load-bearing split).
struct RealtimeCaptureStateDeleter {
void operator()(RealtimeCaptureState* p) const noexcept;
};
using RealtimeCaptureHandle =
std::unique_ptr<RealtimeCaptureState, RealtimeCaptureStateDeleter>;
// Realtime-record backend — captures by RECORDING in realtime (transport-driven)
// into a hidden temp track, then moves the recorded file into the bank as a Sample.
// For sources offline render cannot do (hardware, performed FX) and as the true
// pre-FX-dry path (I_RECMODE_FLAGS &3==1 — the only pre-FX tap in the SDK; offline
// render has none). Dialog-free: never invokes the offline-render progress window.
//
// Non-bit-identical by nature (it is realtime); offline stays the deterministic
// default. Non-destructive across EVERY terminal path — the review gate — which is
// harder here than offline because the record spans ticks: the snapshot + restore
// live on RealtimeCaptureState, not a function-scope RAII destructor.
//
// SCOPE (this increment): TRACK scope only — records the selected track's OWN
// output (item + that track's own FX + its own fader/pan, PRE-parent), matching
// offline's track scope. This needs NO FxBypassGuard: a send tapping a track's
// output is naturally PRE-parent (the parent has not summed it yet), so the tap is
// chain-independent by construction. Item realtime is deferred (UnsupportedMode).
class RealtimeRecordBackend {
public:
// Starts a realtime record: validates the request (track scope, non-empty range,
// at least one source track, active + saved project, transport idle), snapshots
// all state to restore, creates the hidden temp track, routes a send FROM each
// source track INTO the temp track, arms, and CSurf_OnRecord — then returns
// IMMEDIATELY (no wait, no UI block). `sourceTracks` are the selected tracks to
// tap (resolved by the action layer — the CaptureRequest itself stays REAPER-free,
// carrying only the provenance GUIDs). On success the returned unique_ptr owns the
// in-flight state; drive it with tick(). On a validation/setup failure returns
// nullptr and fills `outFailure` with the CaptureStatus + message (nothing was
// left mutated — begin() restores on its own failure paths).
RealtimeCaptureHandle begin(const CaptureRequest& request,
const std::vector<MediaTrack*>& sourceTracks,
CaptureResult& outFailure);
// Advances the in-flight record one tick. Reads the transport (bound to the
// record's OWN project handle so a project switch cannot confuse it), and on a
// terminal verdict stops the transport, finalizes the recorded file into the
// bank Sample (Done) or reports the failure (Failed), then restores ALL
// snapshotted state. Returns InProgress while the record is still running.
// After Done/Failed the state is spent — the caller drops the unique_ptr.
RealtimeTickResult tick(RealtimeCaptureState& state);
// Force-terminate an in-flight record NOW without waiting for the range end:
// stops the transport, finalizes whatever was captured (best effort) or abandons
// it, and restores ALL snapshotted state. For the shutdown / project-switch
// paths (extension unload, a new project became active) where the record must
// not leak a temp track / armed track / altered transport into the user's
// project. Idempotent — safe even if a prior tick already tore the state down.
RealtimeTickResult abort(RealtimeCaptureState& state);
};
} // namespace reasampler::capture
+1 -1
View File
@@ -19,7 +19,7 @@
#include "core/capture/batch_capture.h" // planCaptureUnits / BatchOutcome
#include "core/model/bank_book.h" // BankBook / Bank
#include "core/model/provenance.h" // recipe parse/build, fingerprint
#include "persist.h" // ReaSamplerSession
#include "shell/persist/session.h" // ReaSamplerSession
#include "shell/capture/capture_orchestrator.h" // captureAndIndexOne / renderOffline
#include "shell/capture/provenance_shell.h" // fxChainIdentity* / trackByGuid
#include "shell/capture/scope_resolve.h" // ResolvedSource
+1 -1
View File
@@ -17,7 +17,7 @@
#include "core/capture/tail_control.h" // TailSetting
#include "core/model/provenance.h" // model::Provenance
#include "ingest.h" // ingestAssignActiveInstance
#include "persist.h" // ReaSamplerSession
#include "shell/persist/session.h" // ReaSamplerSession
#include "shell/capture/insert.h" // runInsert / InsertRequest
#include "shell/capture/realtime_lifecycle.h" // the in-flight realtime state
+1 -1
View File
@@ -74,7 +74,7 @@
// Item realtime is deferred (UnsupportedMode): item scope would need per-item take
// isolation on top of the tap, which is a separate increment.
#include "shell/capture/capture.h"
#include "shell/capture/capture_realtime_shell.h"
#include <chrono>
#include <cstdint>
+121
View File
@@ -0,0 +1,121 @@
#pragma once
// capture_realtime_shell — the ASYNC realtime-record seam (Q-W6 split of the former
// fat capture.h: this header owns the realtime backend's begin/tick/abort surface;
// capture.h keeps the shared CaptureRequest/CaptureResult types, the offline
// backend, and the shared backend helpers). Implemented by
// capture_realtime_shell.cpp; driven by exactly one caller (realtime_lifecycle).
//
// A realtime record is inherently asynchronous: CSurf_OnRecord starts the transport
// on REAPER's audio thread and returns immediately — it does NOT block until the
// range completes, which takes (end - start) wall-clock seconds. Blocking the main
// thread for that duration freezes REAPER's UI, so the realtime backend is DRIVEN
// ACROSS TIMER TICKS instead: begin() starts and returns at once; tick() (called
// from the same OnTimer that runs session.poll()) advances the in-flight record and
// reports when it is done.
//
// SEAM CHOICE (surfaced): the two backends deliberately share NO interface. The
// lifecycles are genuinely different (offline is headless + immediate — one
// synchronous capture() call returns a finished Sample; realtime is
// transport-driven + async — begin/tick/abort across timer ticks), so a shared
// interface would make offline fake a lifecycle it does not have (its tick()
// would always be Done on the first call — dead code / an LSP smell). Offline
// stays synchronous; the realtime backend owns this small bespoke async seam.
// This is the split-sync/async fork, chosen over a unified async interface for
// that reason. (The old synchronous ICaptureBackend interface over
// OfflineRenderBackend was deleted in Q-W3 — T4-26: one deriver, zero polymorphic
// call sites.)
//
// REAPER-free like capture.h: MediaTrack is forward-declared there and never
// dereferenced here; the REAPER-facing TU is capture_realtime_shell.cpp.
#include <memory>
#include <vector>
#include "shell/capture/capture.h" // CaptureRequest / CaptureResult / MediaTrack fwd
namespace reasampler::capture {
// One tick's verdict from the in-flight record.
enum class RealtimeTickStatus {
InProgress, // still recording — call tick() again next timer tick
Done, // finished (range end reached, or the user stopped) — `result` is set
Failed, // an error tore the capture down — `result.message` explains
};
struct RealtimeTickResult {
RealtimeTickStatus status = RealtimeTickStatus::InProgress;
CaptureResult result; // meaningful only when status == Done or Failed
};
// The opaque in-flight capture state. Owns the snapshot of everything to restore
// (temp track + its receive sends from the source tracks, other tracks' I_RECARM,
// transport, edit cursor, time selection) and the record's own project handle.
// Defined in capture_realtime_shell.cpp; the header stays REAPER-free (nothing is
// dereferenced here) by holding it behind a forward-declared type + unique_ptr.
//
// restore()/teardown is idempotent and lives ON THIS OBJECT (not a function-scope
// RAII guard) because the record spans ticks — no single stack frame outlives it.
// Every terminal path (normal completion, user stop, error, project switch, unload)
// funnels through the same single restore, safe to call once from whichever fires.
class RealtimeCaptureState;
// Out-of-line deleter so callers (realtime_lifecycle) can own a unique_ptr to the
// opaque RealtimeCaptureState WITHOUT its full (REAPER-typed) definition — the
// delete is compiled in capture_realtime_shell.cpp where the type is complete,
// keeping this header REAPER-free (load-bearing split).
struct RealtimeCaptureStateDeleter {
void operator()(RealtimeCaptureState* p) const noexcept;
};
using RealtimeCaptureHandle =
std::unique_ptr<RealtimeCaptureState, RealtimeCaptureStateDeleter>;
// Realtime-record backend — captures by RECORDING in realtime (transport-driven)
// into a hidden temp track, then moves the recorded file into the bank as a Sample.
// For sources offline render cannot do (hardware, performed FX) and as the true
// pre-FX-dry path (I_RECMODE_FLAGS &3==1 — the only pre-FX tap in the SDK; offline
// render has none). Dialog-free: never invokes the offline-render progress window.
//
// Non-bit-identical by nature (it is realtime); offline stays the deterministic
// default. Non-destructive across EVERY terminal path — the review gate — which is
// harder here than offline because the record spans ticks: the snapshot + restore
// live on RealtimeCaptureState, not a function-scope RAII destructor.
//
// SCOPE (this increment): TRACK scope only — records the selected track's OWN
// output (item + that track's own FX + its own fader/pan, PRE-parent), matching
// offline's track scope. This needs NO FxBypassGuard: a send tapping a track's
// output is naturally PRE-parent (the parent has not summed it yet), so the tap is
// chain-independent by construction. Item realtime is deferred (UnsupportedMode).
class RealtimeRecordBackend {
public:
// Starts a realtime record: validates the request (track scope, non-empty range,
// at least one source track, active + saved project, transport idle), snapshots
// all state to restore, creates the hidden temp track, routes a send FROM each
// source track INTO the temp track, arms, and CSurf_OnRecord — then returns
// IMMEDIATELY (no wait, no UI block). `sourceTracks` are the selected tracks to
// tap (resolved by the action layer — the CaptureRequest itself stays REAPER-free,
// carrying only the provenance GUIDs). On success the returned unique_ptr owns the
// in-flight state; drive it with tick(). On a validation/setup failure returns
// nullptr and fills `outFailure` with the CaptureStatus + message (nothing was
// left mutated — begin() restores on its own failure paths).
RealtimeCaptureHandle begin(const CaptureRequest& request,
const std::vector<MediaTrack*>& sourceTracks,
CaptureResult& outFailure);
// Advances the in-flight record one tick. Reads the transport (bound to the
// record's OWN project handle so a project switch cannot confuse it), and on a
// terminal verdict stops the transport, finalizes the recorded file into the
// bank Sample (Done) or reports the failure (Failed), then restores ALL
// snapshotted state. Returns InProgress while the record is still running.
// After Done/Failed the state is spent — the caller drops the unique_ptr.
RealtimeTickResult tick(RealtimeCaptureState& state);
// Force-terminate an in-flight record NOW without waiting for the range end:
// stops the transport, finalizes whatever was captured (best effort) or abandons
// it, and restores ALL snapshotted state. For the shutdown / project-switch
// paths (extension unload, a new project became active) where the record must
// not leak a temp track / armed track / altered transport into the user's
// project. Idempotent — safe even if a prior tick already tore the state down.
RealtimeTickResult abort(RealtimeCaptureState& state);
};
} // namespace reasampler::capture
+8 -3
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// insert.cpp — REAPER-facing placement shell (M6). See insert.h.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
@@ -40,7 +39,7 @@
#include "core/model/bank_model.h"
#include "shell/panel/panel_bank_ops.h" // bankPanelSelectedSampleIds / SourceBankId
#include "core/capture/capture_paths.h"
#include "persist.h"
#include "shell/persist/session.h"
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_CountSelectedTracks
@@ -58,13 +57,19 @@
namespace reasampler {
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
using capture::computeInsertMode;
using capture::normalizeSlashes;
using capture::resolveBankFile;
using capture::TempoConform;
namespace {
namespace fs = std::filesystem;
// The current project's directory (mirrors bank_panel/capture/persist). The bank
// index stores relative paths; resolving a bank file needs the current .rpp dir.
// FOLLOW-UP (already noted in bank_panel.cpp): a shared "current project dir"
// FOLLOW-UP (already noted in panel_bank_ops.cpp): a shared "current project dir"
// REAPER helper is a clean small refactor now that a fourth consumer exists — out
// of scope for M6.
std::string currentProjectDir() {
+1 -2
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// insert — placement of bank samples into the arrange (M6). REAPER-facing shell:
// it reads the bank_panel's current selection, resolves each selected sample's
// file, and drops it into the arrange at the edit cursor via InsertMedia, wrapped
@@ -28,7 +27,7 @@ class ReaSamplerSession;
// tempo-conform choice) so the two action variants (native-length vs
// conform-to-tempo) differ only by this struct — no divergent code paths.
struct InsertRequest {
InsertOptions options; // defaults: current track, no conform, native length
capture::InsertOptions options; // defaults: current track, no conform, native length
};
// The outcome of an insert action, for the caller to log to the console.
-1
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// item_read.cpp — the single MediaItem* read seam (GUID + fixed-lane name). See
// item_read.h. Compiled into the reaper_reasampler MODULE; includes
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT (main.cpp is the one TU that
-1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// item_read — the ONE place a MediaItem* is read for its canonical GUID string and for
// the durable P_LANENAME of the fixed lane it sits on. Before this seam, view.cpp and
// bank_panel.cpp each carried a near-identical private itemGuid / itemLaneName pair
+4 -1
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// provenance_shell.cpp — the REAPER reads behind Milestone 10. See provenance_shell.h.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
@@ -52,6 +51,10 @@
namespace reasampler {
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
using capture::normalizeSlashes;
using capture::resolveBankFile;
std::string fxChainIdentityForTrack(MediaTrack* tr) {
if (!tr) return fxChainIdentity({});
std::vector<FxIdentityEntry> rows;
+3 -1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// provenance_shell — the REAPER-facing reads Milestone 10 needs, in one place.
//
// The PURE provenance module (provenance.h) owns the fingerprint encoding, the
@@ -29,6 +28,9 @@ namespace reasampler {
class BankBook;
// Real-namespace-home using-declaration (Q-W6: the namespaces.h shim is retired).
using model::BankFileRef;
// The in-scope FX-chain identity of a source track (Track scope), folded to the
// pure provenance string. Reads the track's own FX chain via TrackFX_GetCount /
// TrackFX_GetFXName / TrackFX_GetFXGUID / TrackFX_GetEnabled in chain order.
+1 -1
View File
@@ -8,7 +8,7 @@
#include "shell/capture/realtime_lifecycle.h"
#include "persist.h" // ReaSamplerSession
#include "shell/persist/session.h" // ReaSamplerSession
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_EnumProjects
+1 -1
View File
@@ -16,7 +16,7 @@
// REAPER-facing: the .cpp includes reaper_plugin_functions.h WITHOUT
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers — CLAUDE.md §contract).
#include "shell/capture/capture.h" // RealtimeRecordBackend / RealtimeCaptureHandle
#include "shell/capture/capture_realtime_shell.h" // RealtimeRecordBackend / RealtimeCaptureHandle
namespace reasampler {
class ReaSamplerSession;
-1
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// track_guid.cpp — the single MediaTrack* -> canonical GUID key formatter. See
// track_guid.h. Compiled into the reaper_reasampler MODULE; includes
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT (main.cpp is the one TU
-1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// track_guid — the ONE place a MediaTrack* is formatted into the canonical GUID
// string used as a membership-index key. Both the Design View shell (view.cpp) and
// the actions layer (design_view_actions.cpp) key membership on this exact string, so the key
+1
View File
@@ -22,6 +22,7 @@
namespace reasampler::vst {
using namespace reasampler::instrument::map; // ZonePlaySeconds vocabulary + trigger_seam converters
using instrument::ui::EnvMode; // envelope_overlay's mode enum (Q-W6: shim retired)
using instrument::engine::formatMasterGainLabel;
using instrument::engine::masterGainLinearFromNorm;
using instrument::engine::masterGainNormFromLinear;
+1 -1
View File
@@ -209,7 +209,7 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
// Capture stolen mid-drag (modal dialog, alt-tab, etc.) — restore map_ to its
// pre-grab snapshot so the in-flight live-drag mutation is rolled back, then reset
// the drag state machine so stale capture-less WM_MOUSEMOVEs don't keep editing.
// Mirror of bank_panel.cpp's WM_CAPTURECHANGED handler.
// Mirror of the panel shell's WM_CAPTURECHANGED handler (panel_window.cpp).
if (self) {
// A held preview note must be released here too (peer of WM_LBUTTONUP) — capture
// loss otherwise leaves the momentary-key voice hung with no note-off.
+1 -1
View File
@@ -15,7 +15,7 @@
#include "core/audio/peaks.h" // computeEnvelope (the cached peak thumbnail)
#include "core/capture/capture_paths.h" // resolveBankFile (shared M4 path resolution)
#include "core/capture/wav_trim.h" // parseWavLayout, extractFloatFrames
#include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames
#include "core/ui/bank_grid.h" // ThumbnailKey / thumbnailKeyString (T2-10: the pure key)
#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03)
#include "ext_keys.h"
+7 -1
View File
@@ -19,7 +19,7 @@
#include <vector>
#include "core/capture/capture_paths.h" // resolveBankFile (shared M4 path resolution)
#include "core/capture/wav_trim.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
#include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
#include "core/instrument/map/bank_sync.h" // S9/S8 pure decisions: parseBankGeneration, consumeDecision
#include "core/instrument/map/sample_map.h" // refs resolve, buildZonedKeymap (pS self-contained)
#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03)
@@ -31,6 +31,12 @@ namespace reasampler::vst {
using namespace instrument::map; // resolution + bank-sync vocabulary this TU drives
using namespace reasampler::wire; // assignment_request + sample_usage wire records
// Q-W6 (shim retired): the shared WAV parse + file loader by their real homes.
using capture::extractFloatFrames;
using capture::parseWavLayout;
using capture::resolveBankFile;
using capture::WavLayout;
using util::readFileBytes;
namespace {
+1
View File
@@ -24,6 +24,7 @@ using namespace Steinberg::Vst;
namespace reasampler::vst {
using namespace instrument::map; // the codec + resolution vocabulary this TU marshals
using instrument::engine::masterGainMaxLinear; // FB1 taper ceiling (Q-W6: shim retired)
tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
if (!state) return kResultFalse;
+12 -7
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// reaper_bridge.cpp — see reaper_bridge.h. The DAW-facing edge; keep it thin.
#include "shell/instrument/reaper_bridge.h"
@@ -6,6 +5,7 @@
#include <vector>
#include "core/instrument/map/bridge_marshal.h"
#include "core/wire/ext_state_read.h" // readProjExtStateGrowing (T2-04 grow-loop policy)
#include "core/capture/capture_paths.h" // projectDirOfRpp (shared M4 project-dir derivation)
#include "ext_keys.h" // kProjExtNamespace (shared wire contract)
@@ -39,6 +39,10 @@ DEF_CLASS_IID(Steinberg::IReaperHostApplication)
namespace reasampler::vst {
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
using capture::projectDirOfRpp;
using instrument::map::decodeGetProjExtState;
bool ReaperBridge::connect(Steinberg::FUnknown* context) {
getProjExtState_ = nullptr;
enumProjExtState_ = nullptr;
@@ -63,7 +67,8 @@ bool ReaperBridge::connect(Steinberg::FUnknown* context) {
enumProjExtState_ = reinterpret_cast<EnumProjExtStateFn>(
reaper->getReaperApi("EnumProjExtState"));
// EnumProjects(-1, ...) yields the active project AND its .rpp path — the same call
// persist.cpp uses, so the instrument derives the project directory identically.
// the persist shell (ext_state_io.cpp) uses, so the instrument derives the project
// directory identically.
enumProjects_ = reinterpret_cast<EnumProjectsFn>(
reaper->getReaperApi("EnumProjects"));
// pS-usage: the (prefix-guarded) usage publish write + the track-identity pair the
@@ -93,17 +98,17 @@ std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::strin
// GetProjExtState writes into a caller buffer; the bank blob can be large (many
// samples), so grow the buffer until the value fits rather than risk a silent
// truncation. The retry policy is the SHARED pure
// instrument::map::readProjExtStateGrowing (Q-W5 rider T2-04 — one loop for the
// truncation. The retry policy is the SHARED pure wire::readProjExtStateGrowing
// (T2-04 — one loop for the
// extension's persist/usage reads and this bridge read; the rules cannot drift):
// absent (rv <= 0) and the >16 MB ceiling both fold to nullopt here, and a
// complete value still runs through decodeGetProjExtState (the stale/empty-buffer
// guard) exactly as before.
const auto read = instrument::map::readProjExtStateGrowing(
const auto read = wire::readProjExtStateGrowing(
[&](char* buf, int cap) {
return getProjExtState_(proj, kProjExtNamespace(), key.c_str(), buf, cap);
});
if (read.status != instrument::map::GrowingExtStateRead::Status::Complete)
if (read.status != wire::GrowingExtStateRead::Status::Complete)
return std::nullopt; // absent / empty key, or pathologically large (>16 MB)
return decodeGetProjExtState(read.apiReturn, read.value);
}
@@ -147,7 +152,7 @@ std::string ReaperBridge::currentTrackGuid() {
std::string ReaperBridge::activeProjectDir() {
if (!enumProjects_) return {};
// idx=-1 is the current project tab; the out-buffer receives the full .rpp path,
// EMPTY for a never-saved project. Same call + convention as persist.cpp; the pure
// EMPTY for a never-saved project. Same call + convention as the persist shell; the pure
// projectDirOfRpp turns the .rpp path into the project directory (parent, forward-
// slashed) and keeps an unsaved project's empty path empty (no default-location
// fallback — the tool's invariant).
+2 -2
View File
@@ -16,7 +16,6 @@
// reaper_vst3_interfaces.h + reaper_plugin_functions.h at the spike.
#pragma once
#include "core/namespaces.h"
#include <optional>
#include <string>
@@ -87,7 +86,8 @@ private:
int valOut_sz);
// EnumProjects(-1, projfnOut, sz) -> active project + its .rpp path (SDK line
// ~1264). The instrument uses idx=-1 (current tab) so it follows the active project,
// and reads the .rpp path from the out-buffer exactly as persist.cpp does.
// and reads the .rpp path from the out-buffer exactly as the persist shell
// (ext_state_io.cpp) does.
using EnumProjectsFn = void* (*)(int idx, char* projfnOut, int projfnOut_sz);
// SetProjExtState(proj, extname, key, value) -> int (SDK line ~6290). Used ONLY by
// writeUsageExtState (prefix-guarded) — see the read-only-bank note there.
+9 -2
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// reasampler_embed.cpp — see reasampler_embed.h. The IReaperUIEmbedInterface shell.
// Windows-only (D5); guarded so a non-Windows build degrades to a stub that reports
// "not supported" and draws nothing.
@@ -43,6 +42,14 @@ DEF_CLASS_IID(Steinberg::IReaperUIEmbedInterface)
namespace reasampler::vst {
// Real-namespace-home using-directives (Q-W6: the namespaces.h shim is retired):
// the embed strip speaks the map vocabulary (listSamples / parseBankGeneration) and
// the pure UI layout (embed_strip / editor_geometry Rect) wholesale.
using namespace reasampler::instrument::map;
using namespace reasampler::instrument::ui;
using reasampler::ui::spectralColor;
using version::vstPluginName;
namespace {
#ifdef _WIN32
// Kit adapter (Phase L, L3): the embed shell's Rect (editor_geometry) -> the kit's KitBox
@@ -201,7 +208,7 @@ bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) {
// reads as "present, no zones" — the default single-capture face lives in the editor.
LICE_FillRect(bmp, layout.keymap.x, layout.keymap.y, layout.keymap.width,
layout.keymap.height, toLice(roleColor(Role::BgCell)), 0.5f, 0);
const std::string label = reasampler::vstPluginName() + // channel-derived (S18)
const std::string label = version::vstPluginName() + // channel-derived (S18)
(samples_.empty() ? " (bank empty)" : " (no zones)");
const Rect labelR = Rect::ltrb(layout.keymap.x + 4, layout.keymap.y, layout.keymap.right(),
layout.keymap.bottom());
+4 -1
View File
@@ -31,7 +31,6 @@
// REAPER's messages to/from it and draws with the same LICE idiom as reasampler_editor.
#pragma once
#include "core/namespaces.h"
#include <cstdint>
#include <string>
@@ -52,6 +51,10 @@ namespace reasampler::vst {
class ReaSamplerProcessor;
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
using instrument::map::PerformanceMap;
using instrument::map::SampleChoice;
// Implements IReaperUIEmbedInterface. Lifetime is OWNED by the processor (the processor
// holds the sole unique_ptr and hands out AddRef'd references from queryInterface); the
// back-pointer to the processor is therefore always valid while this lives.
-1
View File
@@ -18,7 +18,6 @@
// binary UID identity — the string identity lives in the pure module).
#pragma once
#include "core/namespaces.h"
#include "pluginterfaces/base/funknown.h"
+2 -3
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// vst_entry.cpp — the VST3 module class factory (Phase S1). Enumerates the one class
// this module offers (the ReaSampler instrument) via the SDK's factory macros. The
// Windows module exports — GetPluginFactory (here, via BEGIN_FACTORY) and
@@ -74,10 +73,10 @@ DEF_CLASS2(INLINE_UID(REASAMPLER_ACTIVE_UID_1, REASAMPLER_ACTIVE_UID_2,
REASAMPLER_ACTIVE_UID_3, REASAMPLER_ACTIVE_UID_4),
Steinberg::PClassInfo::kManyInstances, // cardinality
kVstAudioEffectClass, // component category (fixed)
reasampler::vstPluginName().c_str(), // plug-in display name (channel-derived)
reasampler::version::vstPluginName().c_str(), // plug-in display name (channel-derived)
0, // single-component => 0
Steinberg::Vst::PlugType::kInstrumentSynthSampler, // subcategory
reasampler::appVersion().c_str(), // plug-in version (channel: -beta render)
reasampler::version::appVersion().c_str(), // plug-in version (channel: -beta render)
kVstVersionString, // VST3 SDK version (fixed)
reasampler::vst::ReaSamplerProcessor::createInstance)
+10 -2
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// draw_kit — the LICE/SWELL shell half of the drawing kit. See draw_kit.h.
//
// Compiled into the reaper_reasampler MODULE. SHELL layer: it is the only kit file that
@@ -12,7 +11,7 @@
#include "core/ui/bank_grid.h" // compressAmplitudeForDisplay — the shared dB display curve (pure)
// SWELL / LICE. On Windows use native Win32 (windows.h first); on mac/linux SWELL is
// provided by the host. Mirrors bank_panel.cpp's include discipline.
// provided by the host. Mirrors the panel TUs' (shell/panel/) include discipline.
#ifdef _WIN32
#include <windows.h>
#else
@@ -24,6 +23,15 @@
namespace reasampler {
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
using audio::ChannelEnvelope;
using audio::columnMinMax;
using audio::MinMax;
using ui::compressAmplitudeForDisplay;
using ui::roleColor;
using ui::roleColorState;
using ui::spectralColor;
// --- KitColor <-> LICE boundary ----------------------------------------------
// The one place a pure KitColor becomes a LICE_pixel. Verified packing: LICE_RGBA(r,g,b,a)
+13 -1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// draw_kit — the LICE-facing SHELL half of the shared drawing kit (Phase L, L1). This is
// the ONE source of drawing for the whole system: every surface (bank_panel now; the VST
// editor + embed strip at L3) fills, buttons, rows, sliders, waveforms, and — above all —
@@ -41,6 +40,19 @@ class LICE_IBitmap;
namespace reasampler {
// Real-namespace-home using-declarations (Q-W6: the interim core/namespaces.h shim
// is retired; the kit's pure vocabulary names its Q-W1 homes explicitly). These are
// deliberate re-exports: every draw_kit consumer speaks these types at the call
// boundary, so they surface here exactly as panel_state.h surfaces the panel's.
using audio::Envelope;
using ui::InteractionState;
using ui::KitBox;
using ui::KitButtonBox;
using ui::KitColor;
using ui::ListRowBox;
using ui::Role;
using ui::SliderGeometry;
// The kit's four cached fonts (§3.1 type scale). Consumers pass a Font to text() to pick
// the size/weight; the kit maps it to the matching LICE_CachedFont.
enum class Font {
+30 -204
View File
@@ -1,17 +1,10 @@
// panel_bank_ops.cpp — the bank-CRUD + menus seam of the docked bank panel (Q-W2
// split of bank_panel.cpp; Phase B4/B5). Since Q-W4 this TU is the ONE implementation
// home of the bank verbs (create / rename / delete / evacuate / activate / move /
// copy / remove): the promptless bankOp* inner verbs (model op + persistBankOp only)
// serve BOTH thin UX skins — the panel's menu handlers here and the bindable
// bank_actions family — plus the book/bank accessors, the popup menus that drive
// them, and the selection-id / OS-drag path resolvers.
//
// Each verb mutates the session's book() then persists via persistBankOp() (one bank
// op = one Ctrl-Z; a true index no-op opens NO undo point). It DOES mutate the bank
// BOOK — that is the whole point of B4 — but only the index/model + ext-state, never
// the arrange, never a sample file on disk (bank ops are index-only; files stay put —
// CONTEXT.md §Multi-bank). REFERENCE-INVALIDATION GUARDRAIL: after a STRUCTURAL
// mutation any Bank*/BankModel& is invalid — resolve fresh, pass ids.
// panel_bank_ops.cpp — the bank-CRUD-UX + menus seam of the docked bank panel
// (Q-W2 split of the former bank_panel god-module; Phase B4/B5). Since Q-W6 the
// promptless bank verbs live in shell/bank_ops (model op + persistBankOp, taking
// ReaSamplerSession&); this TU is the panel's THIN UX SKIN over them — the menu
// handlers (prompts / confirms / message boxes / panel-state nudges / repaint),
// the book/bank accessors, the popup menus that drive them, and the selection-id /
// OS-drag path resolvers. The bindable bank_actions family is the sibling skin.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
@@ -25,24 +18,21 @@
#include "shell/panel/panel_state.h"
#include "shell/panel/panel_bank_ops.h"
#include "persist.h" // ReaSamplerSession — the live session the ops mutate
#include "shell/bank_ops/bank_ops.h" // bankOp* promptless verbs (the Q-W6 non-UI seam)
#include "shell/persist/session.h" // ReaSamplerSession — the live session the ops mutate
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_EnumProjects
#define REAPERAPI_WANT_GetUserInputs
#define REAPERAPI_WANT_ShowMessageBox
#define REAPERAPI_WANT_Main_OnCommand
#define REAPERAPI_WANT_genGuid
#define REAPERAPI_WANT_guidToString
#define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2
#include "reaper_plugin_functions.h"
namespace reasampler::panel {
namespace fs = std::filesystem;
// --- Current-project directory (mirrors persist.cpp's derivation) -------------
// --- Current-project directory (mirrors the persist shell's derivation, ext_state_io.cpp)
std::string currentProjectDir() {
std::vector<char> buf(4096, '\0');
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
@@ -84,19 +74,21 @@ std::vector<const Bank*> namedBanks() {
// --- Bank management ops (id-keyed; THIN UX SKINS over the bankOp* verbs) ------
//
// Q-W4: each handler here owns only the panel's UX (prompts / confirms / message
// boxes / panel-state nudges / repaint); the model op + persist is the shared
// bankOp* inner verb (defined in the public section below). After a STRUCTURAL
// mutation (create/delete/evacuate) any Bank*/BankModel& is invalid — we resolve
// fresh, pass ids, and let the next refreshFingerprint repaint. On an unsaved
// project the empty-close discard in persistBankOp ensures no stale state
// survives (matches the capture/B3 quiet-persist idiom).
// Q-W4/Q-W6: each handler here owns only the panel's UX (prompts / confirms /
// message boxes / panel-state nudges / repaint); the model op + persist is the
// shared bankOp* inner verb (shell/bank_ops), which takes the live session by
// reference — the book() check answers the one session-liveness question per
// handler. After a STRUCTURAL mutation (create/delete/evacuate) any
// Bank*/BankModel& is invalid — we resolve fresh, pass ids, and let the next
// refreshFingerprint repaint. On an unsaved project the empty-close discard in
// persistBankOp ensures no stale state survives (matches the capture/B3
// quiet-persist idiom).
void doCreateBank() {
if (!book()) return;
std::string name;
if (!promptBankName("ReaSampler: create bank", "Bank name:", "", name)) return;
const std::string id = bankOpCreate(name);
const std::string id = bankOpCreate(*g_panel.session, name);
if (id.empty()) {
ShowMessageBox("A bank with that name already exists.",
"ReaSampler: create bank", 0);
@@ -116,7 +108,7 @@ void doRenameBank(const std::string& bankId) {
const std::string current = bk->displayName; // copy before any mutation
std::string newName;
if (!promptBankName("ReaSampler: rename bank", "New name:", current, newName)) return;
if (!bankOpRename(bankId, newName)) {
if (!bankOpRename(*g_panel.session, bankId, newName)) {
ShowMessageBox("Another bank already uses that name.",
"ReaSampler: rename bank", 0);
return;
@@ -156,7 +148,7 @@ void doDeleteBank(const std::string& bankId) {
// delete path moved/dropped members) — both change what a live instance could play. An
// empty-bank delete is purely organizational, no bump. The ORIGINAL member count decides
// (the No-path evacuated them moments ago, but the membership still changed).
if (!bankOpDelete(bankId, /*bumpGeneration=*/members > 0)) return;
if (!bankOpDelete(*g_panel.session, bankId, /*bumpGeneration=*/members > 0)) return;
// shownBankId is reconciled by the next fingerprint pass. If no named banks remain,
// nudge focus to the pool so the selection has a valid home.
if (namedBanks().empty()) g_panel.focusedRegion = Region::Pool;
@@ -167,12 +159,13 @@ void doEvacuateBank(const std::string& bankId) {
if (!book()) return;
const Bank* bk = book()->bank(bankId);
if (!bk || bk->isPool()) return;
if (!bankOpEvacuate(bankId)) return;
if (!bankOpEvacuate(*g_panel.session, bankId)) return;
invalidatePanel();
}
void doActivateBank(const std::string& bankId) {
if (!bankOpActivate(bankId)) return; // rejects an unknown id
if (!book()) return; // no live session — nothing to activate against
if (!bankOpActivate(*g_panel.session, bankId)) return; // rejects an unknown id
invalidatePanel();
}
@@ -185,7 +178,8 @@ void doActivateBank(const std::string& bankId) {
void transferSamples(const std::vector<std::string>& sampleIds,
const std::string& srcBankId, const std::string& destBankId,
bool copy) {
if (!bankOpTransfer(sampleIds, srcBankId, destBankId, copy))
if (!book()) return; // no live session — nothing to transfer within
if (!bankOpTransfer(*g_panel.session, sampleIds, srcBankId, destBankId, copy))
return; // nothing changed — no persist, no undo point
// The selection indexed into the source; after a move those indices are stale, so
// clear it (the fingerprint pass will also clear, but do it now for immediacy).
@@ -198,7 +192,8 @@ void transferSamples(const std::vector<std::string>& sampleIds,
// one-Ctrl-Z contract. Clears the stale selection and repaints on an actual removal.
void removeSamples(const std::vector<std::string>& sampleIds,
const std::string& srcBankId) {
if (!bankOpRemove(sampleIds, srcBankId))
if (!book()) return; // no live session — nothing to remove from
if (!bankOpRemove(*g_panel.session, sampleIds, srcBankId))
return; // nothing changed — no persist, no undo point
// The selection indexed into the source; after a remove those indices are stale, so
// clear it (the fingerprint pass will also clear, but do it now for immediacy).
@@ -410,35 +405,7 @@ void showSelectionMenu(int screenX, int screenY) {
namespace reasampler {
namespace {
// Persists the book after a bank mutation. Mirrors the CAPTURE path, NOT the
// Design-View path: quiet persist — saveToActiveProject no-ops on an unsaved project
// (the change stays valid for the session and persists on the user's next save).
// Deliberately NO Save-As prompt; do not "align" with persistViewState's prompt
// idiom. Returns whether a persist actually happened, so persistBankOp can discard
// its undo block when nothing was written. Guards a null session pointer (false,
// no-op) — see persistBankOp's guard below for why this is defensive rather than
// dead code.
bool persistBook() {
if (!panel::g_panel.session) return false; // no live session: nothing to persist
return panel::g_panel.session->saveToActiveProject();
}
// Mints a fresh, genuine REAPER GUID string as a stable bank id (the B2/model
// design: ids are caller-supplied and stable; the model stays pure and mints none).
// Distinct from a track GUID by origin only — both are canonical guidToString output.
std::string mintBankId() {
GUID g{};
genGuid(&g);
char buf[64] = {0}; // guidToString needs >=64 chars (SDK contract)
guidToString(&g, buf);
return std::string(buf);
}
} // namespace
// One home (Q-W4) for the former actions.cpp/panel_bank_ops.cpp byte-identical twins.
// One home (Q-W4) for the former actions/panel byte-identical twins.
// COMMA GUARD: GetUserInputs splits returned values on a separator defaulting to ',',
// so the return separator is overridden to \x1f (un-typeable) via the documented
// `separator=X` trailing pseudo-caption (SDK ~3806) — any printable name round-trips.
@@ -457,147 +424,6 @@ bool promptBankName(const char* title, const char* caption, const std::string& i
return true;
}
// Persists a completed bank-index verb as a SINGLE batched REAPER undo point (R-B) —
// one bank op = one Ctrl-Z.
//
// WHY THIS WRAPS AND persistBook() DOES NOT: a bank verb mutates ONLY our project
// ext-state (SetProjExtState under "reasampler"), which REAPER's undo system captures
// iff UNDO_STATE_MISCCFG is set in the Undo_EndBlock2 flags — the SDK documents
// MISCCFG as covering "extensions!" project ext-state (reaper_plugin.h ~1544, ~1199).
// We pass exactly UNDO_STATE_MISCCFG (not -1 / UNDO_STATE_ALL as the item-move family
// does): a bank verb touches no tracks, FX, items, or envelopes, so snapshotting them
// would be both heavier and semantically wrong. persistBook() (= SetProjExtState) runs
// INSIDE the block so the post-mutation ext-state is the block's "after" image.
//
// UNSAVED-PROJECT GUARDRAIL: on an unsaved / no-active project persistBook() no-ops
// (nothing is written to ext state). We must still CLOSE the block we opened, but with
// an EMPTY label and a zero flag so REAPER DISCARDS the point instead of recording a
// no-effect undo entry — mirroring view.cpp's empty-plan close. The in-session model
// change stands and persists on the user's next save; it just earns no undo point until
// there is a project to persist into (undo of an unsaved bank op has nothing to roll
// back to anyway). The Begin/End must still be balanced, hence the close-either-way.
//
// NULL-SESSION GUARD: this is a public API (panel_bank_ops.h) with callers outside
// this TU (e.g. panel_drag.cpp), not all of which are guaranteed to have re-checked
// the session pointer immediately beforehand. Bail out BEFORE Undo_BeginBlock2 — no
// block is opened, so there is nothing to balance and no risk of an unbalanced
// Begin/End pair.
void persistBankOp(const char* label, bool bumpGeneration) {
if (!panel::g_panel.session) return; // no live session: no-op, no undo point opened
Undo_BeginBlock2(nullptr);
// S9: bump the bank-generation counter INSIDE the block, before persistBook(), so the
// fresh generation rides the same ext-state write the persist makes (persistBook() ->
// saveToActiveProject() stamps bankGeneration()). Bumped only for content-changing verbs
// (the caller decides); a pure-organizational verb passes false and leaves the counter be,
// so a rename/activate does not needlessly refresh live instances.
if (bumpGeneration) panel::g_panel.session->bumpBankGeneration();
const bool persisted = persistBook();
if (persisted)
Undo_EndBlock2(nullptr, label, UNDO_STATE_MISCCFG);
else
Undo_EndBlock2(nullptr, "", 0); // no ext-state write -> discard the empty point
}
// --- Promptless inner bank verbs (Q-W4 single home) ----------------------------
// Model op + persistBankOp only; NO UX. Callers own prompts/confirms/nudges. Each
// verb resolves the book fresh (panel::book(), null when no live session) and
// persists ONLY after the model accepted — a rejected op opens no undo point.
std::string bankOpCreate(const std::string& name) {
BankBook* b = panel::book();
if (!b) return {};
const std::string id = mintBankId();
if (!b->createBank(id, name)) return {}; // duplicate display name (model rule)
persistBankOp("ReaSampler: create bank");
return id;
}
bool bankOpRename(const std::string& bankId, const std::string& newName) {
BankBook* b = panel::book();
if (!b || !b->renameBank(bankId, newName)) return false; // pool / name in use
persistBankOp("ReaSampler: rename bank");
return true;
}
bool bankOpDelete(const std::string& bankId, bool bumpGeneration) {
BankBook* b = panel::book();
if (!b || !b->deleteBank(bankId)) return false; // pool un-deletable (model rule)
persistBankOp("ReaSampler: delete bank", bumpGeneration);
return true;
}
bool bankOpEvacuate(const std::string& bankId) {
BankBook* b = panel::book();
if (!b || !b->evacuate(bankId)) return false; // pool is a destination, not a source
// S9: evacuate moves members between banks (bank membership changes) -> bump.
persistBankOp("ReaSampler: evacuate bank", /*bumpGeneration=*/true);
return true;
}
bool bankOpActivate(const std::string& bankId) {
BankBook* b = panel::book();
if (!b || !b->setActiveBank(bankId)) return false; // rejects an unknown id
persistBankOp("ReaSampler: activate bank");
return true;
}
// NO-OP GUARDRAIL — VERB-AWARE (a collapse means different things per verb):
// * MOVE collapse: the source entry WAS removed (bank_book moveSample removes
// unconditionally before the dest add collapses on hash), so the index DID
// mutate — it counts toward opening an undo point.
// * COPY collapse: the source is left intact AND the dest already held the hash,
// so NOTHING changed — a true index no-op. It must NOT open an undo point.
// Hence: copy counts only real gains; move counts gains OR collapses. Ids pass
// straight to the model op — no BankModel& cached across the loop's mutations.
bool bankOpTransfer(const std::vector<std::string>& sampleIds,
const std::string& srcBankId, const std::string& destBankId,
bool copy) {
BankBook* b = panel::book();
if (!b || sampleIds.empty() || srcBankId == destBankId) return false;
if (!b->bank(srcBankId) || !b->bank(destBankId)) return false;
int ok = 0, collapsed = 0;
for (const std::string& sid : sampleIds) {
const TransferResult r =
copy ? b->copySample(sid, srcBankId, destBankId)
: b->moveSample(sid, srcBankId, destBankId);
switch (r) {
case TransferResult::Moved:
case TransferResult::Copied: ++ok; break;
case TransferResult::Collapsed: ++collapsed; break;
case TransferResult::RejectedUnknownBank:
case TransferResult::RejectedSampleAbsent:
case TransferResult::RejectedSameBank: break;
}
}
const bool mutated = copy ? (ok > 0) : (ok > 0 || collapsed > 0);
if (!mutated) return false; // nothing changed — no persist, no undo point
// S9: a move/copy changes bank membership (a sample arrives in / leaves a bank an
// instance may reference) -> bump so assigned instances refresh hands-free.
persistBankOp(copy ? "ReaSampler: copy sample(s)" : "ReaSampler: move sample(s)",
/*bumpGeneration=*/true);
return true;
}
// Index-only, this-bank scope (fork R-A: the sole surfaced verb; RemoveScope::AllBanks
// stays latent in the model). Non-destructive to the file: a last-reference remove
// leaves the file on disk, orphaned until Phase R prune — remove NEVER deletes bytes
// (the manifest is untouched). Silent: recoverability is the batched undo (R-B).
bool bankOpRemove(const std::vector<std::string>& sampleIds,
const std::string& srcBankId) {
BankBook* b = panel::book();
if (!b || sampleIds.empty() || !b->bank(srcBankId)) return false;
int removed = 0;
for (const std::string& sid : sampleIds)
if (b->removeSample(sid, srcBankId, RemoveScope::ThisBank) ==
RemoveResult::Removed)
++removed;
if (removed == 0) return false; // every id already absent — no undo point
// S9: a remove drops a sample from a bank (an instance referencing it must refresh —
// it will resolve to silence, per the stale-id policy) -> bump.
persistBankOp("ReaSampler: remove sample(s)", /*bumpGeneration=*/true);
return true;
}
// --- Selection read seam --------------------------------------------------------
std::vector<std::string> bankPanelSelectedSampleIds() {
+12 -78
View File
@@ -1,97 +1,31 @@
#pragma once
// panel_bank_ops — the bank-CRUD + selection-read seam of the bank panel (Q-W2 split
// of bank_panel.h; Phase B4/B5). The .cpp is the SINGLE implementation home of the
// bank verbs (create / rename / delete / evacuate / activate / move / copy / remove):
// each promptless inner verb below drives the B1 BankBook model on the session and
// persists via persistBankOp (one bank op = one Ctrl-Z). Q-W4 dedupe: the panel's
// menu handlers and the bank_actions bindable family are both thin UX skins
// (prompts / confirms / console vs. message boxes / panel-state nudges) over these
// one-home verbs. This header carries that verb surface, the shared prompt/persist
// helpers, and the panel's public selection-read surface.
// panel_bank_ops — the bank-CRUD-UX + selection-read seam of the bank panel (Q-W2
// split of the former bank_panel god-module; Phase B4/B5). Since Q-W6 the
// promptless bank verbs themselves live in the NON-UI shell/bank_ops seam
// (bankOp* + persistBankOp, taking ReaSamplerSession&); this TU is the panel's
// thin UX skin over them — prompts / confirms / message boxes / panel-state
// nudges / repaints — plus the popup menus that drive them. The bank_actions
// bindable family is the sibling skin over the same verbs. This header carries
// the shared prompt helper and the panel's public selection-read surface.
//
// The selection reads are REAPER-free; the verbs and helpers are REAPER-facing
// (persist + stock dialogs) but SDK-free in this header.
// The selection reads are REAPER-free; the prompt helper is REAPER-facing (stock
// dialogs) but SDK-free in this header.
#include <string>
#include <vector>
namespace reasampler {
// --- Promptless inner bank verbs (Q-W4 single home) --------------------------
// Each verb: model op on the session's BankBook + persistBankOp (undo-batched
// ext-state persist) — NO prompts, NO message boxes, NO panel-state nudges. The
// caller owns all UX. Every verb returns whether the model accepted the mutation
// (a rejected op persists nothing and opens no undo point). Verbs resolve the
// session via the panel's live session pointer (set at load by bankPanelInit,
// before any action can fire) and fail safe (false / "") when it is absent.
// Mints a stable GUID bank id, creates `name` in the book. Returns the new bank id,
// or "" when the model rejects the name (duplicate, trimmed + case-insensitive).
// Create is purely organizational — no generation bump.
std::string bankOpCreate(const std::string& name);
// Renames `bankId`. False when the model rejects (pool un-renamable / name in use).
bool bankOpRename(const std::string& bankId, const std::string& newName);
// Deletes `bankId`. False when the model rejects (pool un-deletable). The caller
// passes `bumpGeneration` from the member count it read BEFORE any evacuate/delete
// (an evacuate-then-delete flow must still bump on the ORIGINAL membership).
bool bankOpDelete(const std::string& bankId, bool bumpGeneration);
// Evacuates `bankId`'s members to the pool. False when the model rejects (the pool
// itself). Bumps the generation (membership changed).
bool bankOpEvacuate(const std::string& bankId);
// Activates `bankId` as the capture target. False on an unknown id. No bump.
bool bankOpActivate(const std::string& bankId);
// Moves (copy=false) or copies (copy=true) `sampleIds` from `srcBankId` to
// `destBankId` (index-only; files never relocate). Returns whether the index
// actually mutated — the verb-aware no-op guardrail: a COPY collapse changes
// nothing (no undo point); a MOVE collapse did remove the source entry (counts).
// Persists ONE undo point ("move/copy sample(s)") only when mutated.
bool bankOpTransfer(const std::vector<std::string>& sampleIds,
const std::string& srcBankId, const std::string& destBankId,
bool copy);
// Removes `sampleIds` from `srcBankId` (index-only, this-bank scope; never deletes
// bytes). Returns whether anything was removed; persists one undo point when so.
bool bankOpRemove(const std::vector<std::string>& sampleIds,
const std::string& srcBankId);
// --- Shared UX/persist helpers ------------------------------------------------
// Prompts the user for a single line of text via REAPER's stock input dialog
// (GetUserInputs). `initial` pre-fills the field. Returns false (leaving `out`
// untouched) on cancel or an empty entry. COMMA GUARD: the return separator is
// overridden to \x1f (un-typeable) via the documented `separator=X` pseudo-caption,
// so any printable name — commas included — round-trips whole (SDK ~3806/3808).
// One home (Q-W4) for the former actions.cpp/panel_bank_ops.cpp twins.
// One home (Q-W4) for the former actions/panel byte-identical twins; shared by the
// panel menus and the bank_actions bindable family.
bool promptBankName(const char* title, const char* caption, const std::string& initial,
std::string& out);
// Persists a completed bank-index verb as a single REAPER undo point (R-B).
// Wraps the session persist (SetProjExtState) in a Begin/End block with
// UNDO_STATE_MISCCFG so the bank op is one Ctrl-Z. On an unsaved / no-active project
// the persist no-ops and the block is closed with an empty label + zero flag (REAPER
// discards it). Callers must invoke this ONLY after a successful/effective mutation —
// rejected ops (duplicate name, un-deletable pool, etc.) must return before reaching
// here so no empty undo point is ever opened for a no-op.
//
// NULL-SESSION GUARD: this is a public API with callers outside panel_bank_ops.cpp
// (e.g. panel_drag.cpp). If the panel's session pointer is absent (no live session),
// this is a no-op — no undo block is opened. Today every real caller only reaches
// here via a prior session-backed check, so the guard is not yet reachable in
// practice; it exists to make the function safe to call standalone.
//
// S9 bank-generation bump: pass `bumpGeneration = true` for a verb that changes what a
// live instance would PLAY — move / copy / remove / evacuate / delete-with-members. Leave
// it false (the default) for a PURELY ORGANIZATIONAL verb — create / rename / activate /
// reorder. The bump (when requested) happens INSIDE the block, BEFORE the persist, so
// the stamped counter rides the same ext-state write and undo captures the pre/post
// generation with the rest of the blob.
void persistBankOp(const char* label, bool bumpGeneration = false);
// The stable ids of the currently-selected samples, in bank (insertion) order.
// Empty when nothing is selected or the panel has never opened. This is the clean
// seam the `insert` action reads to know WHAT to place — it returns ids (not grid
+3 -3
View File
@@ -20,7 +20,7 @@
#include "shell/panel/panel_state.h"
#include "shell/panel/panel_bank_ops.h" // persistBankOp — shared undo-block wrapper (R-B)
#include "shell/bank_ops/bank_ops.h" // persistBankOp — shared undo-block wrapper (R-B)
#include "shell/actions/drag_out_win.h" // OLE / SWELL drag-out initiation seam (M11)
#include "shell/actions/instrument_drop_win.h" // resolveFxDropTarget / performInstrumentDrop (S17)
@@ -374,7 +374,7 @@ namespace {
void doReorderDrop(const std::string& id, const std::string& bankId, int targetSlot) {
if (!book() || id.empty() || bankId.empty() || targetSlot < 0) return;
if (!book()->reorderSample(id, bankId, targetSlot)) return; // rejected/no-op: no undo point
persistBankOp("ReaSampler: reorder sample");
persistBankOp(*g_panel.session, "ReaSampler: reorder sample");
g_panel.selection = Selection{};
invalidatePanel();
}
@@ -387,7 +387,7 @@ void doReplaceDrop(const std::string& newId, const std::string& oldId,
const std::string& bankId) {
if (!book() || newId.empty() || oldId.empty() || bankId.empty()) return;
if (!book()->replaceSample(newId, oldId, bankId)) return; // pool-guard reject: NO-OP
persistBankOp("ReaSampler: replace sample");
persistBankOp(*g_panel.session, "ReaSampler: replace sample");
g_panel.selection = Selection{};
invalidatePanel();
}
+3 -2
View File
@@ -18,7 +18,7 @@
#include "shell/panel/panel_input.h"
#include "shell/actions/bank_actions.h" // bankPruneCommandId — the footer Prune dispatch (R3)
#include "persist.h" // ReaSamplerSession — view/tail reads + mutation
#include "shell/persist/session.h" // ReaSamplerSession — view/tail reads + mutation
#include "core/view/view_mode_model.h" // autoTagNewContent / NewItem / AutoTag (D2 Wave 2)
#include "shell/capture/item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B)
#include "shell/capture/track_guid.h" // guidString — canonical track GUID key (D2 Wave 2)
@@ -157,7 +157,8 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
//
// INTENTIONAL: membership mutation happens OUTSIDE any Undo block. Auto-tag is a
// background metadata update (like setting a label), not a destructive project edit.
// persist.cpp writes it on the next project save alongside the bank and view state, the
// the persist shell (ext_state_io.cpp) writes it on the next project save alongside the
// bank and view state, the
// same way an action-driven tag is persisted. Wrapping this in an Undo block would flood
// the REAPER undo history with a new entry for every timer tick that sees new content.
// Returns true iff this tick tagged at least one new GUID into a mode — the signal the
+1 -1
View File
@@ -20,7 +20,7 @@
#include "shell/panel/panel_state.h"
#include "shell/panel/panel_layout.h"
#include "persist.h" // ReaSamplerSession — mode/view reads
#include "shell/persist/session.h" // ReaSamplerSession — mode/view reads
#include "core/view/view_mode_model.h" // ViewModeModel — modes()/activeModeId()
// Action-trigger buttons (M11): resolve each button's command id at runtime from the
+1 -1
View File
@@ -18,7 +18,7 @@
#include "shell/panel/panel_state.h"
#include "shell/panel/draw_kit.h" // kit text()/fillSurface/drawButton/drawWaveform (L1)
#include "persist.h" // ReaSamplerSession — mode/view/tail reads
#include "shell/persist/session.h" // ReaSamplerSession — mode/view/tail reads
#include "core/view/view_mode_model.h" // ViewModeModel / Mode — the footer toggle's model
namespace reasampler::panel {
+7 -11
View File
@@ -14,13 +14,9 @@
// plain free function — direct call-through, no interface, no virtual dispatch
// (T4-28: the audition path and the per-mouse-move path must stay direct calls).
// * Explicit using-declarations pulling the pure modules' symbols into
// reasampler::panel from their REAL namespace homes (Q-W1 sub-namespaces) — this
// header itself does not directly include the interim core/namespaces.h shim
// (Q-W2 retires that direct dependency for this module; Q-W4 retired the
// actions.h carrier with the actions split). Several panel TUs still pull the
// shim in TRANSITIVELY via persist.h/ingest.h/draw_kit.h/view.h; only
// panel_thumbnails.cpp and panel_audition.cpp are shim-free end to end.
// Nothing HERE depends on it either way.
// reasampler::panel from their REAL namespace homes (Q-W1 sub-namespaces). The
// interim core/namespaces.h shim is GONE (deleted in Q-W6 with the last split);
// every symbol below names its true home.
//
// REFERENCE-INVALIDATION GUARDRAIL (CONTEXT.md §Multi-bank): a bank-structural
// mutation (create/delete/evacuate/activate/move) can reallocate the book's vector,
@@ -84,10 +80,10 @@ namespace reasampler::panel {
// --- Real-namespace-home using-declarations -----------------------------------
//
// The panel's pre-split internals reference the pure modules' symbols unqualified;
// these explicit per-symbol usings (NOT the core/namespaces.h shim) keep those
// references valid while documenting each symbol's Q-W1 home. Flat-`reasampler`
// symbols (BankBook / ViewModeModel / the draw_kit shell / persistBankOp / ...)
// resolve via the enclosing namespace and need no using.
// these explicit per-symbol usings keep those references valid while documenting
// each symbol's Q-W1 home. Flat-`reasampler` symbols (BankBook / ViewModeModel /
// the draw_kit shell / the shell/bank_ops verbs / ...) resolve via the enclosing
// namespace and need no using.
// core/ui
using ui::ActionBarRect;
+5 -5
View File
@@ -36,7 +36,7 @@
#include "core/capture/capture_paths.h" // projectDirOfRpp (pure path arithmetic)
#include "core/instrument/map/bank_sync.h" // parseBankGeneration / formatBankGeneration (SHARED with the instrument reader)
#include "core/instrument/map/bridge_marshal.h" // readProjExtStateGrowing (T2-04: the ONE grow-loop policy)
#include "core/wire/ext_state_read.h" // readProjExtStateGrowing (T2-04: the ONE grow-loop policy)
#include "core/version/app_version.h"
#define REAPERAPI_MINIMAL
@@ -77,15 +77,15 @@ std::string projectDirOf(const std::string& 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
// instrument::map::readProjExtStateGrowing (Q-W5 rider T2-04 — the same policy the
// usage_scan and VST-bridge reads run); this wrapper binds the REAPER call and
// 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 instrument::map::GrowingExtStateRead;
const GrowingExtStateRead read = instrument::map::readProjExtStateGrowing(
using wire::GrowingExtStateRead;
const GrowingExtStateRead read = wire::readProjExtStateGrowing(
[&](char* buf, int cap) {
return GetProjExtState(static_cast<ReaProject*>(proj), ns, key, buf, cap);
});
+2 -2
View File
@@ -29,8 +29,8 @@ std::string projectDirOf(const std::string& rppPath);
// Growing GetProjExtState read for `key` in namespace `ns` against `proj`. Returns
// "" when the key is absent (a valid empty bank, not an error) and warns on the
// console for a value exceeding the 16 MB read ceiling (unreadable whole, ignored).
// The retry policy itself is the shared pure instrument::map::readProjExtStateGrowing
// (Q-W5 rider, T2-04); this wrapper binds the REAPER call + persist's fold.
// The retry policy itself is the shared pure wire::readProjExtStateGrowing
// (T2-04; rehomed to core/wire in Q-W6); this wrapper binds the REAPER call + persist's fold.
std::string getProjExtStateString(void* proj, const char* ns, const char* key);
// The GUID we mint per project, formatted "{XXXXXXXX-....}" by guidToString.
+12 -6
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// usage_scan.cpp — see usage_scan.h. The REAPER reads behind the pS-usage prune
// protection; every decision is in the pure sample_usage module, this TU only reads.
//
@@ -27,7 +26,7 @@
#include <vector>
#include "core/version/app_version.h" // vstPluginName / vstOutputName (channel name needles)
#include "core/instrument/map/bridge_marshal.h" // readProjExtStateGrowing (T2-04: the ONE grow-loop policy)
#include "core/wire/ext_state_read.h" // readProjExtStateGrowing (T2-04: the ONE grow-loop policy)
#include "ext_keys.h" // kProjExtNamespace / kProjExtUsageKeyPrefix
#include "core/wire/instrument_drop.h" // vstClassIdHex — the frozen channel class-UID hex
#include "core/wire/sample_usage.h" // identityMatches, foldUsageRecords (the pure decisions)
@@ -53,6 +52,14 @@
namespace reasampler {
// Real-namespace-home using-directive (Q-W6: the namespaces.h shim is retired):
// this TU speaks the sample_usage wire vocabulary wholesale (UsageRecord /
// decodeUsageRecord / foldUsageRecords / identityMatches / toUpperAscii) plus the
// channel-identity accessors + the preset class-id hex.
using namespace reasampler::wire;
using version::vstOutputName;
using version::vstPluginName;
namespace {
// The three UPPERCASED channel needles identityMatches (pure, sample_usage) checks
@@ -169,16 +176,15 @@ bool itemHasInstance(MediaItem* item, const FxIdentityNeedles& id) {
// Growing GetProjExtState read: the usage record scales with the hold count, so a
// fixed buffer risks a truncated decode. The retry policy is the SHARED pure
// instrument::map::readProjExtStateGrowing (Q-W5 rider T2-04 — one loop for persist,
// this prune-safety-adjacent read, and the VST bridge; the rules cannot drift).
// wire::readProjExtStateGrowing (T2-04 — one loop for persist, this
// prune-safety-adjacent read, and the VST bridge; the rules cannot drift).
// Returns nullopt when the key cannot be read WHOLE — absent-after-enumeration
// (rv <= 0) or pathologically large (> 16 MB give-up). The caller only queries keys
// the enumeration just listed, so a nullopt here is a PRESENT-BUT-UNREADABLE record:
// it folds to abortPrune (fail-safe — silently reduced protection is the delete
// direction).
std::optional<std::string> readExtStateValue(ReaProject* proj, const char* key) {
using instrument::map::GrowingExtStateRead;
const GrowingExtStateRead read = instrument::map::readProjExtStateGrowing(
const GrowingExtStateRead read = readProjExtStateGrowing(
[&](char* buf, int cap) {
return GetProjExtState(proj, kProjExtNamespace(), key, buf, cap);
});
-1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// usage_scan — the EXTENSION-side shell of the pS-usage seam (see sample_usage.h for
// the pure core, the fail-safe folds, and the full design note). At prune-scan time it
// answers ONE question: which project-relative bank paths are held by a LIVE ReaSampler
+7 -1
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// view.cpp — REAPER-facing Design View shell (Phase D2). See view.h.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
@@ -48,6 +47,13 @@
namespace reasampler {
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
using view::buildFolderTree;
using view::isOnManualLane;
using view::managedLaneKey;
using view::modeIdFromLaneName;
using view::TrackFolderEntry;
namespace {
// Track fixed-lane mode value (I_FREEMODE=2). See SDK: 0=normal, 1=free item
-1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// view — the REAPER-facing shell of the Design View feature (Phase D2). It is the
// mirror of the capture shell: the ViewModeModel (pure, D1) holds the mode/
// membership/snapshot state and emits the toggle plan; this shell reads the live