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:
+120
@@ -0,0 +1,120 @@
|
||||
// insert.cpp — REAPER-facing placement shell (M6). See insert.h.
|
||||
//
|
||||
// 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).
|
||||
//
|
||||
// THIS IS THE INTENDED PLACEMENT PATH. Unlike capture / bank_panel (which never
|
||||
// touch the arrange), insert deliberately adds items to the arrange — that is its
|
||||
// whole job (CONTEXT.md §load-bearing principle). It runs ONLY from its own action.
|
||||
//
|
||||
// FLAGGED RUNTIME ASSUMPTIONS (semantics the header does not fully specify — must
|
||||
// be DAW-verified by Daniel post-merge; see the handoff):
|
||||
// A. InsertMedia base modes 0/1 insert AT THE EDIT CURSOR. The header names the
|
||||
// base targets ("add to current track" / "add new track") but does not spell
|
||||
// out an explicit "at edit cursor" bit — placement at the edit cursor is
|
||||
// REAPER's documented convention for these modes, relied on here.
|
||||
// B. InsertMedia ADVANCES the edit cursor to the end of the inserted media. This
|
||||
// is the behavior that makes sequential multi-insert lay items end-to-end. It
|
||||
// is REAPER's long-standing behavior but is not stated in the header — flagged.
|
||||
// We do NOT re-read/patch the cursor between inserts (we trust B); if B proved
|
||||
// false in the DAW, the fix is to advance the cursor ourselves by the inserted
|
||||
// item length. Not done now (no evidence it is needed, and item length is not
|
||||
// returned by InsertMedia).
|
||||
// C. New-track insert (mode base 1) creates the track and leaves it selected;
|
||||
// current-track insert (base 0) targets the current/last-selected track. We do
|
||||
// not force a track selection — the user's current selection is the target for
|
||||
// base 0, matching REAPER's drag-to-track semantics.
|
||||
|
||||
#include "insert.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "bank_model.h"
|
||||
#include "bank_panel.h"
|
||||
#include "capture_paths.h"
|
||||
#include "persist.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetCursorPosition
|
||||
#define REAPERAPI_WANT_InsertMedia
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
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"
|
||||
// REAPER helper is a clean small refactor now that a fourth consumer exists — out
|
||||
// of scope for M6.
|
||||
std::string currentProjectDir() {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
std::string rpp(buf.data());
|
||||
if (rpp.empty()) return {}; // unsaved project: no resolvable bank
|
||||
return normalizeSlashes(fs::path(rpp).parent_path().string());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
InsertResult runInsert(ReaSamplerSession* session, const InsertRequest& request) {
|
||||
InsertResult result;
|
||||
if (!session) { result.status = InsertStatus::NoSelection; return result; }
|
||||
|
||||
// WHAT to place: the panel's current selection (ids, in bank order).
|
||||
const std::vector<std::string> ids = bankPanelSelectedSampleIds();
|
||||
if (ids.empty()) { result.status = InsertStatus::NoSelection; return result; }
|
||||
|
||||
// WHERE the bank lives on disk. An unsaved project has no resolvable bank dir;
|
||||
// insert is a no-op rather than resolving against CWD (CLAUDE.md invariant).
|
||||
const std::string projectDir = currentProjectDir();
|
||||
if (projectDir.empty()) { result.status = InsertStatus::NoProject; return result; }
|
||||
|
||||
const BankIndex& bank = session->bank();
|
||||
const int mode = computeInsertMode(request.options);
|
||||
|
||||
// Wrap the whole placement in ONE undo block so a single undo removes every
|
||||
// item inserted by this action (proj=nullptr -> active project). Opened before
|
||||
// the first InsertMedia and closed after the last, unconditionally, so the block
|
||||
// is always balanced even if nothing resolves (an empty block is harmless).
|
||||
Undo_BeginBlock2(nullptr);
|
||||
|
||||
for (const std::string& id : ids) {
|
||||
const Sample* sample = bank.query(id);
|
||||
if (!sample) { ++result.skipped; continue; } // id no longer in the bank
|
||||
|
||||
const std::string abs = resolveBankFile(projectDir, sample->relativePath);
|
||||
if (abs.empty()) { ++result.skipped; continue; } // unresolvable relative path
|
||||
if (!fs::exists(fs::path(abs))) { ++result.skipped; continue; } // file missing
|
||||
|
||||
// Insert AT THE EDIT CURSOR (assumption A). InsertMedia advances the cursor
|
||||
// to the end of the inserted media (assumption B), so the next iteration
|
||||
// lands contiguously — no manual cursor math needed. Non-destructive to the
|
||||
// bank: this references abs, it does not modify the file or the index.
|
||||
InsertMedia(abs.c_str(), mode);
|
||||
++result.inserted;
|
||||
}
|
||||
|
||||
// Label reflects the count and the conform choice so the undo history reads
|
||||
// clearly ("ReaSampler: insert 2 samples" etc.). extraflags 0 = default scope.
|
||||
const std::string label =
|
||||
"ReaSampler: insert " + std::to_string(result.inserted) +
|
||||
(result.inserted == 1 ? " sample" : " samples") +
|
||||
(request.options.conform == TempoConform::None ? "" : " (conform)");
|
||||
Undo_EndBlock2(nullptr, label.c_str(), -1);
|
||||
|
||||
if (result.inserted == 0) result.status = InsertStatus::NothingResolved;
|
||||
else result.status = InsertStatus::Ok;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user