// 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 mode 0 ("add to current track") targets the track that is // currently the ONLY selected track. The header names the base target but does // not spell out how "current track" resolves at runtime. We force exactly one // selected track via SetOnlyTrackSelected before each InsertMedia call, which // is the most defensible interpretation; if REAPER uses a different notion of // "current" (e.g. last-focused, not last-selected), DAW-verify and adjust. // B. InsertMedia mode 0 inserts AT THE EDIT CURSOR. Placement at the edit cursor // is REAPER's documented convention for base modes 0/1 (the header does not // spell out an explicit "at edit cursor" bit). Flagged for DAW-verification. // C. InsertMedia ADVANCES the edit cursor to the end of the inserted media. We // reset the cursor to the snapshot position before EACH track's insert, so // assumption C's truth or falsity is irrelevant: we own the cursor reset. // D. SetEditCurPos(time, false, false) moves the cursor without scrolling the view // and without seeking the transport. The header lists the args as // (time, moveview, seekplay) — moveview=false and seekplay=false are the // non-disruptive choice; flagged in case the DAW shows otherwise. // E. SetOnlyTrackSelected deselects all tracks and selects exactly one. The header // doc-comment says "Set exactly one track selected, deselect all others" — // this is the strongest confirmation we have; flagged for DAW-verification. #include "insert.h" #include #include #include #include "bank_model.h" #include "bank_panel.h" #include "capture_paths.h" #include "persist.h" #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_CountSelectedTracks #define REAPERAPI_WANT_EnumProjects #define REAPERAPI_WANT_GetCursorPosition #define REAPERAPI_WANT_GetSelectedTrack #define REAPERAPI_WANT_InsertMedia #define REAPERAPI_WANT_SetEditCurPos #define REAPERAPI_WANT_SetOnlyTrackSelected #define REAPERAPI_WANT_SetTrackSelected #define REAPERAPI_WANT_ShowConsoleMsg #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 buf(4096, '\0'); EnumProjects(-1, buf.data(), static_cast(buf.size())); std::string rpp(buf.data()); if (rpp.empty()) return {}; // unsaved project: no resolvable bank return normalizeSlashes(fs::path(rpp).parent_path().string()); } // Snapshot the user's currently-selected track set (ignores master, matches // CountSelectedTracks / GetSelectedTrack which both skip master). Returns the // tracks in selection order so we can restore the original state afterward. std::vector snapshotSelectedTracks() { const int n = CountSelectedTracks(nullptr); // nullptr = active project std::vector tracks; tracks.reserve(static_cast(n)); for (int i = 0; i < n; ++i) tracks.push_back(GetSelectedTrack(nullptr, i)); return tracks; } // Restore a previously-snapshotted track selection: deselect all (by setting the // first track alone) then re-select the full set. If the snapshot is empty we // leave all tracks deselected; no-op guard handles a completely empty project. void restoreSelectedTracks(const std::vector& tracks) { if (tracks.empty()) return; // Deselect all via the first track, then re-add the rest. SetOnlyTrackSelected(tracks[0]); for (size_t i = 1; i < tracks.size(); ++i) SetTrackSelected(tracks[i], true); } } // namespace InsertResult runInsert(ReaSamplerSession* session, const InsertRequest& request) { InsertResult result; if (!session) { result.status = InsertStatus::NoSelection; return result; } // WHO to target: the user's currently-selected track set. No-op (with a clear // console message) when nothing is selected — inserting without a target track // would create an unintended new track or behave unpredictably. const std::vector selectedTracks = snapshotSelectedTracks(); if (selectedTracks.empty()) { ShowConsoleMsg("ReaSampler insert: select a track first.\n"); result.status = InsertStatus::NoSelection; return result; } // WHAT to place: the single focused sample from the panel. Multi-select is // deprioritized; take the first (or only) selected id. An empty panel selection // is a no-op — nothing to place. const std::vector ids = bankPanelSelectedSampleIds(); if (ids.empty()) { result.status = InsertStatus::NoSelection; return result; } const std::string& id = ids.front(); // focused / first selected — single sample // 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 Sample* sample = bank.query(id); if (!sample) { result.status = InsertStatus::NothingResolved; return result; } const std::string abs = resolveBankFile(projectDir, sample->relativePath); if (abs.empty() || !fs::exists(fs::path(abs))) { result.status = InsertStatus::NothingResolved; return result; } const int mode = computeInsertMode(request.options); // Snapshot the edit cursor position up front so we can restore it to the same // position for each track insert (and after the whole operation). const double cursorPos = GetCursorPosition(); // Wrap the whole placement (all tracks + selection/cursor save-restore) in ONE // undo block so a single undo removes every item and restores the state before // the action. Opened before the first InsertMedia, closed after the restore, // unconditionally — the block is always balanced. Undo_BeginBlock2(nullptr); // Insert onto EACH selected track at the SAME edit-cursor position (assumption B). // For each track: isolate it as the only selection so InsertMedia mode 0 targets // it unambiguously (assumption A + E), reset the cursor to the snapshot position // (assumption C cursor advance is irrelevant — we own the reset), then insert. for (MediaTrack* track : selectedTracks) { SetOnlyTrackSelected(track); // assumption A + E SetEditCurPos(cursorPos, false, false); // assumption D InsertMedia(abs.c_str(), mode); ++result.inserted; } // Restore the user's original track selection and cursor position so the action // is non-destructive to their DAW state (non-negotiable per the brief). restoreSelectedTracks(selectedTracks); SetEditCurPos(cursorPos, false, false); // Label reflects the count and the conform choice so the undo history reads // clearly ("ReaSampler: insert on 2 tracks" etc.). extraflags -1 = UNDO_STATE_ALL // (superset: tracks, items, envelope points, project state). const std::string label = "ReaSampler: insert on " + std::to_string(result.inserted) + (result.inserted == 1 ? " track" : " tracks") + (request.options.conform == TempoConform::None ? "" : " (conform)"); Undo_EndBlock2(nullptr, label.c_str(), -1); result.status = InsertStatus::Ok; return result; } } // namespace reasampler