feat(S8): ingest through the bank — capture/import/drop into bank + assign to active instance

This commit is contained in:
2026-07-26 21:29:13 -04:00
parent bf1bf5283c
commit 8074e21057
11 changed files with 1009 additions and 6 deletions
+101 -5
View File
@@ -32,6 +32,7 @@
#include "bank_panel.h"
#include "batch_capture.h"
#include "capture.h"
#include "ingest.h"
#include "insert.h"
#include "persist.h"
#include "provenance.h"
@@ -151,6 +152,16 @@ static int g_cmdCaptureTrackRealtime = 0;
// explicit action, allowed by the console policy).
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 — same razor-else-time range, same
// FX-scope neutralize, same bank/persist landing — then writes an S8 assignment request
// so the active sampler instance plays the just-captured sample on its next reload. NEVER
// inserts a timeline item (the capture/placement separation holds; assign is a bank-index
// + instance-selection act). Lives in the capture family (not the ingest family) because
// it leans on main.cpp's capture render machinery, which is not exposed cross-module.
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) so a user
// who started a long capture can bail without waiting for the range end or hunting for
@@ -738,6 +749,11 @@ static reasampler::CaptureResult renderOffline(
// a bank index entry ONLY; never touches the arrange/timeline. Non-destructive: the
// out-of-scope FX/fader/pan chain is fully restored on every path (FxBypassGuard),
// and the backend restores every RENDER_* setting.
//
// On success, res.sample.id carries the LANDED bank-index id (S8): the newly-added id
// on a fresh add, or the EXISTING entry's id on a hash-dedup collapse — so the S8
// capture+assign path can target the sample actually in the bank. Batch callers ignore
// it; the plain capture actions are unaffected.
static reasampler::CaptureResult captureAndIndexOne(
reasampler::CaptureScope scope,
const ResolvedSource& src,
@@ -780,13 +796,26 @@ static reasampler::CaptureResult captureAndIndexOne(
// resample-from-sample; otherwise the optional stays empty, per M1's contract).
res.sample.provenance = prov;
// Add to the ACTIVE bank: g_session.bank() resolves to book.activeIndex() (B2).
g_session.bank().add(res.sample);
// Add to the ACTIVE bank: g_session.bank() resolves to book.activeIndex() (B2). The
// AddResult tells a fresh add from a hash-dedup collapse, so the assign path (S8) can
// target the sample actually in the bank (the existing entry on a collapse).
const reasampler::AddResult addResult = g_session.bank().add(res.sample);
// B-cap: record the created file in the owned-file manifest, at the same point the
// Sample is added. Recorded regardless of the index AddResult — even a hash-collapse
// still WROTE a file the tool owns, and the manifest dedups a repeat path itself
// (Phase R prune reconciles manifest vs index later).
g_session.owned().add(res.sample.relativePath);
// Resolve the LANDED bank-index id into res.sample.id for the S8 assign path: the new
// id on a fresh Added (already in res.sample.id); the EXISTING entry's id on a
// Collapsed (the file we just rendered deduped onto an already-present sample — assign
// THAT one). Batch/plain-capture callers ignore this field; behaviour unchanged.
if (addResult == reasampler::AddResult::Collapsed && !res.sample.contentHash.empty())
{
if (const reasampler::Sample* existing =
g_session.bank().findByHash(res.sample.contentHash))
res.sample.id = existing->id;
}
return res;
}
@@ -794,14 +823,19 @@ static reasampler::CaptureResult captureAndIndexOne(
// record via captureAndIndexOne, then persist + mark dirty. The load-bearing principle
// holds structurally — this path writes a file + a bank index entry ONLY; it never
// calls InsertMedia or touches the arrange/timeline.
static void RunCapture(const reasampler::CaptureActionDef& def)
// Returns the bank-index id of the sample the capture landed on: the newly-added id on a
// fresh capture, or the EXISTING id on a hash-dedup collapse (so an ingest-with-assign
// targets the sample actually in the bank). Empty on any failure / no-op. The S8 arrange
// capture+assign path reads this to write an assignment request; the plain capture actions
// ignore it (their behaviour is unchanged — capture still writes a file + index entry only).
static std::string RunCapture(const reasampler::CaptureActionDef& def)
{
ResolvedSource src;
std::string why;
if (!ResolveScopeSource(def.scope, src, why))
{
ShowConsoleMsg(("ReaSampler capture: " + why + ".\n").c_str());
return;
return {};
}
reasampler::CaptureResult res =
@@ -809,14 +843,42 @@ static void RunCapture(const reasampler::CaptureActionDef& def)
if (res.status != reasampler::CaptureStatus::Ok)
{
ShowConsoleMsg(("ReaSampler capture failed: " + res.message + "\n").c_str());
return;
return {};
}
// captureAndIndexOne has already stamped provenance, added the Sample to the ACTIVE
// bank, and recorded the created file in the owned-file manifest (WITHOUT persisting).
// Persist the updated book AND manifest into the active project's ext state (the
// `banks` + `owned_files` keys) so the capture survives Save / close+reopen (M4) and
// travels with the .rpp. saveToActiveProject also clears the retired legacy key and
// calls MarkProjectDirty. Non-destructive: writes only our own ext-state keys.
g_session.saveToActiveProject();
// Hand the LANDED bank-index id back to the assign path (S8): captureAndIndexOne
// resolved res.sample.id to the fresh id on a new add or the existing entry's id on a
// hash-dedup collapse. Empty on any reject (unreachable here — status was Ok above).
return res.sample.id;
}
// S8 arrange ingest: capture the selected item / time-selection into the active bank
// (reusing the Item-scope capture path verbatim) and, on success, write an assignment
// request so the active sampler instance plays the new sample on its next reload. The
// capture itself is unchanged — RunCapture writes a file + an index entry and NEVER
// inserts a timeline item (load-bearing principle); the only addition here is the
// bank-index-id -> assignment-request write after the sample lands. If the capture
// failed / no-op'd (empty id), no assignment is written (nothing to assign).
static void RunCaptureItemAssign()
{
// Reuse the Item-scope def from the capture table (index 0) — same range logic, same
// FX-scope neutralize, same bank/persist landing as the plain "capture item" action.
const std::string sampleId =
RunCapture(reasampler::captureActionTable()[0]);
if (sampleId.empty()) return; // capture failed / no-op — RunCapture already reported
reasampler::ingestAssignActiveInstance(g_session.book().activeBankId(), sampleId);
reasampler::bankPanelRefresh();
ShowConsoleMsg("ReaSampler ingest: captured into the bank and assigned to the active "
"instance.\n");
}
// --- M11: batch capture (per selected item / per razor area) ----------------
@@ -1422,6 +1484,7 @@ static bool OnHookCommand(int command, int /*flag*/)
return true;
}
if (command == g_cmdToggleBankPanel) { reasampler::bankPanelToggle(); return true; }
if (command == g_cmdCaptureItemAssign) { RunCaptureItemAssign(); return true; }
if (command == g_cmdInsertSelected) { RunInsertSelected(false); return true; }
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); return true; }
if (command == g_cmdCaptureBatchItems) { RunBatchCaptureItems(); return true; }
@@ -1440,6 +1503,8 @@ static bool OnHookCommand(int command, int /*flag*/)
if (reasampler::designViewHandleCommand(command)) return true;
// Multi-bank action family (B3). Same contract: claims only its own ids.
if (reasampler::bankHandleCommand(command)) return true;
// S8 ingest action family (Media-Explorer import). Same contract.
if (reasampler::ingestHandleCommand(command)) return true;
return false;
}
@@ -1455,6 +1520,7 @@ static int OnToggleAction(int command)
// 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{};
@@ -1468,6 +1534,7 @@ static gaccel_register_t g_accelShowVersion{};
// (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;
@@ -1480,6 +1547,7 @@ 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;
@@ -1519,6 +1587,8 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
reasampler::designViewUnregisterActions(g_rec);
// Tear down the multi-bank action family (B3) — same mirror-unregister.
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);
@@ -1537,6 +1607,8 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
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
@@ -1627,6 +1699,24 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
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 here; 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 = reasampler::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.
@@ -1745,6 +1835,12 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
// by the same hookcommand via bankHandleCommand. Registered before the hook.
reasampler::bankRegisterActions(rec, &g_session);
// 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.)
reasampler::ingestRegisterActions(rec, &g_session);
// One hookcommand routes every ReaSampler action (spike + toggle + Design View).
// Registered once, after all command ids are minted.
rec->Register("hookcommand", (void*)&OnHookCommand);