feat: add M6 insert — place selected bank sample at edit cursor

InsertMedia-driven placement reading the bank panel selection, undo-wrapped.
Native length by default; conform-to-tempo is an explicit second action, never
a silent stretch. Pure mode-bit logic (insert_plan) unit-tested.
This commit is contained in:
2026-07-23 05:23:37 -04:00
parent 60add4afc7
commit 172d5902c8
9 changed files with 557 additions and 5 deletions
+86 -2
View File
@@ -24,6 +24,7 @@
#include "bank_model.h"
#include "bank_panel.h"
#include "capture.h"
#include "insert.h"
#include "persist.h"
#include "view.h"
@@ -52,6 +53,14 @@ static int g_cmdCaptureMasterSpike = 0;
// 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;
// The persistence session (M4): owns the in-memory BankIndex and bridges it to
// project ext state. A timer tick drives g_session.poll() to detect project
// load / Save-As; capture adds Samples to g_session.bank(); after a capture we
@@ -125,13 +134,55 @@ static void RunCaptureMasterSpike()
ShowConsoleMsg(log.c_str());
}
// Runs the M6 insert: place the bank panel's selected sample(s) at the edit cursor
// via InsertMedia, undo-wrapped. `conform` selects the explicit opt-in tempo-match
// variant (never silent — it fires only from the distinct "conform" action). This
// is the INTENDED placement path: it adds items to the arrange on purpose
// (CONTEXT.md §load-bearing principle) and runs only from a user-invoked action.
static void RunInsertSelected(bool conform)
{
reasampler::InsertRequest req;
req.options.target = reasampler::InsertTarget::NewTrack; // sensible default: own track
req.options.conform =
conform ? reasampler::TempoConform::Ratio1x : reasampler::TempoConform::None;
// preservePitch stays true: a tempo conform matches tempo without varispeeding
// pitch. (A pitch-shifting variant is a later opt-in if wanted — YAGNI now.)
reasampler::InsertResult res = reasampler::runInsert(&g_session, req);
std::string msg;
switch (res.status)
{
case reasampler::InsertStatus::Ok:
msg = "ReaSampler: inserted " + std::to_string(res.inserted) +
(res.inserted == 1 ? " sample" : " samples") +
(conform ? " (conformed to tempo)" : " (native length)");
if (res.skipped > 0)
msg += ", skipped " + std::to_string(res.skipped) + " unresolvable";
msg += "\n";
break;
case reasampler::InsertStatus::NoSelection:
msg = "ReaSampler insert: nothing selected in the bank panel.\n";
break;
case reasampler::InsertStatus::NoProject:
msg = "ReaSampler insert: no saved project, so the bank has no location.\n";
break;
case reasampler::InsertStatus::NothingResolved:
msg = "ReaSampler insert: selected sample(s) could not be resolved to a file.\n";
break;
}
ShowConsoleMsg(msg.c_str());
}
// REAPER calls this for EVERY action fired anywhere; claim only our own id,
// return false otherwise so REAPER keeps looking.
static bool OnHookCommand(int command, int /*flag*/)
{
if (command == 0) return false;
if (command == g_cmdCaptureMasterSpike) { RunCaptureMasterSpike(); return true; }
if (command == g_cmdToggleBankPanel) { reasampler::bankPanelToggle(); return true; }
if (command == g_cmdCaptureMasterSpike) { RunCaptureMasterSpike(); return true; }
if (command == g_cmdToggleBankPanel) { reasampler::bankPanelToggle(); return true; }
if (command == g_cmdInsertSelected) { RunInsertSelected(false); return true; }
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); 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;
@@ -150,6 +201,8 @@ static int OnToggleAction(int command)
// gaccel storage must outlive registration — REAPER holds the pointer.
static gaccel_register_t g_accelCaptureMaster{};
static gaccel_register_t g_accelToggleBankPanel{};
static gaccel_register_t g_accelInsertSelected{};
static gaccel_register_t g_accelInsertSelectedConform{};
extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t* rec)
@@ -166,6 +219,12 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
// Tear down the Design View action family (D4) — mirror-unregisters each
// gaccel + command_id with '-'-prefixed strings. After the hook is gone.
reasampler::designViewUnregisterActions(g_rec);
g_rec->Register("-gaccel", (void*)&g_accelInsertSelectedConform);
g_rec->Register("-command_id",
(void*)(REASAMPLER_ACTION_PREFIX "INSERT_SELECTED_CONFORM"));
g_rec->Register("-gaccel", (void*)&g_accelInsertSelected);
g_rec->Register("-command_id",
(void*)(REASAMPLER_ACTION_PREFIX "INSERT_SELECTED"));
g_rec->Register("-gaccel", (void*)&g_accelToggleBankPanel);
g_rec->Register("-command_id",
(void*)(REASAMPLER_ACTION_PREFIX "TOGGLE_BANK_PANEL"));
@@ -221,6 +280,31 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
rec->Register("toggleaction", (void*)&OnToggleAction);
}
// 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_cmdInsertSelected = rec->Register(
"command_id",
(void*)(REASAMPLER_ACTION_PREFIX "INSERT_SELECTED"));
if (g_cmdInsertSelected)
{
g_accelInsertSelected.accel.cmd = g_cmdInsertSelected;
g_accelInsertSelected.desc =
"ReaSampler: insert selected sample at edit cursor";
rec->Register("gaccel", (void*)&g_accelInsertSelected);
}
g_cmdInsertSelectedConform = rec->Register(
"command_id",
(void*)(REASAMPLER_ACTION_PREFIX "INSERT_SELECTED_CONFORM"));
if (g_cmdInsertSelectedConform)
{
g_accelInsertSelectedConform.accel.cmd = g_cmdInsertSelectedConform;
g_accelInsertSelectedConform.desc =
"ReaSampler: insert selected sample at edit cursor (conform to tempo)";
rec->Register("gaccel", (void*)&g_accelInsertSelectedConform);
}
// 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
// hookcommand below routes them via designViewHandleCommand. Registered before