df03b5e759
Replace the four capture modes with item/track/master scope actions. Each infers its range (razor-else-time) and enforces FX scope via non-destructive FX-bypass-around-render (RAII I_FXEN snapshot/restore over ancestors + master). Corrects the defect of items captured through parent FX.
598 lines
28 KiB
C++
598 lines
28 KiB
C++
// main.cpp — the SINGLE translation unit that OWNS the REAPER API pointers.
|
|
//
|
|
// This file is the entire contract between REAPER and the extension:
|
|
// * At startup REAPER scans UserPlugins/ for reaper_*.dll|dylib|so and
|
|
// dlopen()s each one, then looks up ONE exported symbol: ReaperPluginEntry
|
|
// (that name is produced by the REAPER_PLUGIN_ENTRYPOINT macro).
|
|
// * REAPER calls it, handing over `rec` — a small dispatch struct.
|
|
// - rec->GetFunc(name) resolves any REAPER API function to a pointer
|
|
// - rec->Register(what,ptr) plugs OUR callbacks into REAPER
|
|
// * REAPERAPI_LoadAPI(rec->GetFunc) walks reaper_plugin_functions.h and
|
|
// fills in every global function pointer (ShowConsoleMsg, InsertMedia...).
|
|
//
|
|
// Exactly ONE .cpp defines REAPERAPI_IMPLEMENT (this one) — that allocates
|
|
// storage for those global pointers. Every other .cpp includes
|
|
// reaper_plugin_functions.h WITHOUT the define and gets `extern` declarations.
|
|
|
|
#define REAPERAPI_IMPLEMENT
|
|
#include "reaper_plugin.h"
|
|
#include "reaper_plugin_functions.h"
|
|
|
|
#include <string>
|
|
|
|
#include <vector>
|
|
|
|
#include "actions.h"
|
|
#include "bank_model.h"
|
|
#include "bank_panel.h"
|
|
#include "capture.h"
|
|
#include "insert.h"
|
|
#include "persist.h"
|
|
#include "render_settings.h"
|
|
#include "track_guid.h"
|
|
#include "view.h"
|
|
|
|
// Persistent action-id prefix for the ReaSampler action family.
|
|
// Every bindable action (capture / insert / slot / verify) mints its command id
|
|
// from a string beginning with this prefix, e.g. "CEREBELLUM_REASAMPLER_CAPTURE_MASTER".
|
|
// FOREVER-STABLE once shipped: user keybindings key off these strings, so the
|
|
// prefix and any minted id must never change after release.
|
|
#define REASAMPLER_ACTION_PREFIX "CEREBELLUM_REASAMPLER_"
|
|
|
|
// 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 (three FX scopes) -------------------------------
|
|
// Three bindable SCOPE actions from captureActionTable() (render_settings, pure):
|
|
// capture item / track / master. Each infers its range (razor-else-time) and
|
|
// enforces the FX-scope invariant via FX-bypass-around-render (FxBypassGuard):
|
|
// Item -> take/item FX only (bypass the item's track + ancestors + master).
|
|
// Track -> item FX + track's own FX (bypass ancestors + master).
|
|
// Master -> whole chain (bypass nothing).
|
|
// This REPLACES the retired M7 four-mode family (master / tracks / items / razor).
|
|
// The retired CAPTURE_TRACKS_WET / CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids are
|
|
// mirror-unregistered on unload so old keybindings clear cleanly; CAPTURE_MASTER's
|
|
// id string is preserved.
|
|
//
|
|
// 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;
|
|
|
|
// Retired capture-action command-id strings (M7 four-mode family). Kept ONLY to
|
|
// mirror-unregister them on unload so a user's stale keybindings are cleaned up.
|
|
// Never re-register these. CAPTURE_MASTER is NOT here — its id string carries over
|
|
// to the new master scope action unchanged.
|
|
static const char* const kRetiredCaptureCmdStrings[] = {
|
|
"CEREBELLUM_REASAMPLER_CAPTURE_TRACKS_WET",
|
|
"CEREBELLUM_REASAMPLER_CAPTURE_ITEMS_WET",
|
|
"CEREBELLUM_REASAMPLER_CAPTURE_RAZOR_WET",
|
|
};
|
|
|
|
// 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;
|
|
|
|
// 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
|
|
// serialize the bank back into the active project's ext state so it travels with
|
|
// the .rpp. Replaces the M3 session-only g_bank.
|
|
static reasampler::ReaSamplerSession g_session;
|
|
|
|
// 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).
|
|
static void OnTimer()
|
|
{
|
|
g_session.poll();
|
|
|
|
// D4 reapply-on-open glue. persist stays MODEL-ONLY (it loads the saved view
|
|
// model but deliberately does NOT apply visibility — that would couple persist
|
|
// to the view shell). Instead poll() raises a one-shot load signal; here — the
|
|
// integration layer that already drives both persist and the view shell — we
|
|
// drain it and reapply the SAVED active mode's visibility/processing so opening a
|
|
// project saved in Design mode parks the Arrange tracks automatically, no manual
|
|
// toggle. Fires exactly once per load (consumeLoadSignal clears it); idle ticks
|
|
// skip it. proj = nullptr -> REAPER's active project (the one poll just loaded).
|
|
if (g_session.consumeLoadSignal())
|
|
reasampler::applyMode(g_session.view(), g_session.view().activeModeId(), nullptr);
|
|
|
|
// Reflect a live bank change (capture / project load) in the docked grid.
|
|
// Cheap when the bank is unchanged (a fingerprint compare); repaints only on
|
|
// an actual change. No-op when the panel is closed.
|
|
reasampler::bankPanelRefresh();
|
|
}
|
|
|
|
// --- Scope-action source resolution -----------------------------------------
|
|
// The three scope actions (item / track / master) each resolve to (1) an exact
|
|
// render range in project seconds — razor-else-time, inferred here — and (2) the
|
|
// set of source TRACKS whose ancestor chains drive the FX-bypass plan. All reads
|
|
// are non-destructive: selection, razor, and time selection are read, never
|
|
// mutated. Returning false means "nothing to capture" (empty selection / no
|
|
// range); the caller reports it and writes nothing.
|
|
|
|
// The resolved source: exact bounds + the source tracks (for FX-bypass + Sample
|
|
// provenance GUIDs). `sourceTracks` is empty for Master scope.
|
|
struct ResolvedSource
|
|
{
|
|
double startSeconds = 0.0;
|
|
double endSeconds = 0.0;
|
|
std::vector<MediaTrack*> sourceTracks; // item's/selected tracks; empty for master
|
|
std::vector<std::string> trackGuids; // canonical GUIDs of sourceTracks
|
|
};
|
|
|
|
// Time selection -> exact bounds (no rounding). GetSet_LoopTimeRange(isSet=false,
|
|
// isLoop=false) reads the current time selection.
|
|
static bool resolveTimeSelection(double& start, double& end)
|
|
{
|
|
start = 0.0; end = 0.0;
|
|
GetSet_LoopTimeRange(false, false, &start, &end, false);
|
|
return end > start;
|
|
}
|
|
|
|
// Reads every track's P_RAZOREDITS (SDK header ~2899: space-separated triples of
|
|
// start, end, envGuidString), parses the track-audio areas (pure parseRazorEdits),
|
|
// and returns the union bound. Reads only — never clears the razor selection.
|
|
// Returns false when no track-audio razor area exists on any track.
|
|
static bool resolveRazorRange(double& start, double& end)
|
|
{
|
|
std::vector<reasampler::RazorRange> allRanges;
|
|
const int n = CountTracks(nullptr);
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
MediaTrack* tr = GetTrack(nullptr, i);
|
|
if (!tr) continue;
|
|
std::vector<char> buf(8192, '\0');
|
|
if (!GetSetMediaTrackInfo_String(tr, "P_RAZOREDITS", buf.data(), false))
|
|
continue;
|
|
std::vector<reasampler::RazorRange> ranges =
|
|
reasampler::parseRazorEdits(std::string(buf.data()));
|
|
for (auto& r : ranges) allRanges.push_back(r);
|
|
}
|
|
if (allRanges.empty()) return false;
|
|
reasampler::RazorRange u = reasampler::razorUnionBounds(allRanges);
|
|
start = u.startSeconds;
|
|
end = u.endSeconds;
|
|
return end > start;
|
|
}
|
|
|
|
// Infers the render RANGE for any scope: razor union when a razor area is present,
|
|
// else the time selection (pure inferRangeSource decides which). Orthogonal to
|
|
// scope. Returns false (with a reason) when neither yields a non-empty range.
|
|
static bool resolveRange(double& start, double& end, std::string& why)
|
|
{
|
|
double rzStart = 0.0, rzEnd = 0.0;
|
|
const bool hasRazor = resolveRazorRange(rzStart, rzEnd);
|
|
if (reasampler::inferRangeSource(hasRazor) == reasampler::RangeSource::Razor)
|
|
{
|
|
start = rzStart; end = rzEnd;
|
|
return true; // resolveRazorRange already verified end > start
|
|
}
|
|
if (resolveTimeSelection(start, end)) return true;
|
|
why = "make a razor area or a time selection first";
|
|
return false;
|
|
}
|
|
|
|
// Collects the selected tracks (Track scope) into out.sourceTracks + GUIDs.
|
|
static bool collectSelectedTracks(ResolvedSource& out)
|
|
{
|
|
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
|
if (n <= 0) return false;
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
MediaTrack* tr = GetSelectedTrack(nullptr, i);
|
|
if (!tr) continue;
|
|
out.sourceTracks.push_back(tr);
|
|
std::string g = reasampler::guidString(tr);
|
|
if (!g.empty()) out.trackGuids.push_back(std::move(g));
|
|
}
|
|
return !out.sourceTracks.empty();
|
|
}
|
|
|
|
// Collects the tracks that own the selected items (Item scope) into
|
|
// out.sourceTracks (deduped) — these are the tracks whose FX must be bypassed so an
|
|
// item capture hears take/item FX only. GetMediaItem_Track(item) gives the owning
|
|
// track (SDK header, verify). GUIDs recorded for provenance.
|
|
static bool collectSelectedItemTracks(ResolvedSource& out)
|
|
{
|
|
const int n = CountSelectedMediaItems(nullptr);
|
|
if (n <= 0) return false;
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
|
if (!it) continue;
|
|
MediaTrack* tr = GetMediaItem_Track(it);
|
|
if (!tr) continue;
|
|
// Dedup: several selected items can share a track.
|
|
bool seen = false;
|
|
for (MediaTrack* t : out.sourceTracks) if (t == tr) { seen = true; break; }
|
|
if (seen) continue;
|
|
out.sourceTracks.push_back(tr);
|
|
std::string g = reasampler::guidString(tr);
|
|
if (!g.empty()) out.trackGuids.push_back(std::move(g));
|
|
}
|
|
return !out.sourceTracks.empty();
|
|
}
|
|
|
|
// Resolves the source for a scope: the selection tracks (item/track) or none
|
|
// (master), plus the inferred range. Returns false with a reason on nothing to do.
|
|
static bool ResolveScopeSource(reasampler::CaptureScope scope,
|
|
ResolvedSource& out, std::string& why)
|
|
{
|
|
using reasampler::CaptureScope;
|
|
switch (scope)
|
|
{
|
|
case CaptureScope::Item:
|
|
if (!collectSelectedItemTracks(out)) {
|
|
why = "select at least one media item"; return false;
|
|
}
|
|
break;
|
|
case CaptureScope::Track:
|
|
if (!collectSelectedTracks(out)) {
|
|
why = "select at least one track"; return false;
|
|
}
|
|
break;
|
|
case CaptureScope::Master:
|
|
break; // whole chain — no source-track collection
|
|
}
|
|
return resolveRange(out.startSeconds, out.endSeconds, why);
|
|
}
|
|
|
|
// --- FX-bypass-around-render (RAII, non-destructive) ------------------------
|
|
// Snapshots and clears I_FXEN on the tracks a scope must NOT hear the FX of, then
|
|
// restores every snapshotted value on EVERY exit path (including the render's).
|
|
// I_FXEN bypasses a track's FX plugins only — NOT its volume/pan/routing (so a
|
|
// Track capture rendered via master still carries parent/master GAIN; documented
|
|
// boundary, DAW-confirm). Structurally non-destructive: no takes, no items, no
|
|
// project restructuring — only a transient FX-enable toggle, always restored.
|
|
class FxBypassGuard
|
|
{
|
|
public:
|
|
// scope drives fxBypassPlanFor; sourceTracks are the captured tracks whose
|
|
// ancestor chains (walked via GetParentTrack) + the master are bypassed per the
|
|
// plan. proj is the active project (for GetMasterTrack).
|
|
FxBypassGuard(reasampler::CaptureScope scope,
|
|
const std::vector<MediaTrack*>& sourceTracks,
|
|
ReaProject* proj)
|
|
{
|
|
const reasampler::FxBypassPlan plan = reasampler::fxBypassPlanFor(scope);
|
|
|
|
for (MediaTrack* tr : sourceTracks)
|
|
{
|
|
if (!tr) continue;
|
|
if (plan.bypassSelfFx) bypass(tr);
|
|
if (plan.bypassAncestorFx)
|
|
{
|
|
// Walk parents to the top: GetParentTrack returns the immediate
|
|
// parent (folder) track, nullptr at the outermost level (SDK
|
|
// header ~2407). The master is NOT returned here — handled below.
|
|
for (MediaTrack* p = GetParentTrack(tr); p; p = GetParentTrack(p))
|
|
bypass(p);
|
|
}
|
|
}
|
|
if (plan.bypassMaster)
|
|
{
|
|
// GetMasterTrack(proj) -> the master track (SDK header ~1925); I_FXEN
|
|
// on it bypasses the master FX chain, leaving master gain/routing live.
|
|
if (MediaTrack* master = GetMasterTrack(proj)) bypass(master);
|
|
}
|
|
}
|
|
|
|
~FxBypassGuard()
|
|
{
|
|
// Restore in reverse for symmetry (order is not load-bearing — each track
|
|
// appears once, snapshots are independent).
|
|
for (auto it = snapshots_.rbegin(); it != snapshots_.rend(); ++it)
|
|
SetMediaTrackInfo_Value(it->track, "I_FXEN", it->fxen);
|
|
}
|
|
|
|
FxBypassGuard(const FxBypassGuard&) = delete;
|
|
FxBypassGuard& operator=(const FxBypassGuard&) = delete;
|
|
|
|
private:
|
|
struct Snap { MediaTrack* track; double fxen; };
|
|
std::vector<Snap> snapshots_;
|
|
|
|
// Snapshot I_FXEN once per track (dedup: an ancestor shared by two selected
|
|
// tracks must be restored to its ORIGINAL value, not a re-snapshot of the
|
|
// already-bypassed 0), then clear it.
|
|
void bypass(MediaTrack* tr)
|
|
{
|
|
for (const Snap& s : snapshots_) if (s.track == tr) return; // already done
|
|
const double fxen = GetMediaTrackInfo_Value(tr, "I_FXEN");
|
|
snapshots_.push_back({tr, fxen});
|
|
SetMediaTrackInfo_Value(tr, "I_FXEN", 0.0); // 0 = bypassed (SDK ~2194)
|
|
}
|
|
};
|
|
|
|
// Runs one capture-action-table row: resolve its scope source + range, snapshot &
|
|
// clear the out-of-scope FX (RAII), render via the offline backend, add the Sample
|
|
// to the bank, 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. Non-destructive: FX-enable is fully restored on
|
|
// every path by FxBypassGuard, and the backend restores every RENDER_* setting.
|
|
static void RunCapture(const reasampler::CaptureActionDef& def)
|
|
{
|
|
ResolvedSource src;
|
|
std::string why;
|
|
if (!ResolveScopeSource(def.scope, src, why))
|
|
{
|
|
ShowConsoleMsg(("ReaSampler capture: " + why + ".\n").c_str());
|
|
return;
|
|
}
|
|
|
|
reasampler::CaptureRequest req;
|
|
req.sourceMode = reasampler::sourceModeForScope(def.scope);
|
|
req.startSeconds = src.startSeconds; // exact bounds — no rounding
|
|
req.endSeconds = src.endSeconds;
|
|
req.wetDry = 1.0; // wet post the FX left enabled by the scope
|
|
req.renderTail = false; // exact bounds, no tail (default)
|
|
req.tailMs = 0.0;
|
|
req.sampleRate = 0; // follow project rate
|
|
req.channelCount = 2;
|
|
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
|
|
req.baseName = def.baseName;
|
|
req.trackGuids = src.trackGuids; // recorded on the Sample (provenance)
|
|
|
|
// Bypass the out-of-scope FX for the duration of the render. Restored on EVERY
|
|
// exit path below (RAII), including backend failures. proj = active project.
|
|
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
|
FxBypassGuard fxGuard(def.scope, src.sourceTracks, proj);
|
|
|
|
reasampler::OfflineRenderBackend backend;
|
|
reasampler::CaptureResult res = backend.capture(req);
|
|
|
|
if (res.status != reasampler::CaptureStatus::Ok)
|
|
{
|
|
ShowConsoleMsg(("ReaSampler capture failed: " + res.message + "\n").c_str());
|
|
return;
|
|
}
|
|
|
|
reasampler::AddResult added = g_session.bank().add(res.sample);
|
|
// Persist the updated bank into the active project's ext state so the capture
|
|
// survives Save / close+reopen (M4) and travels with the .rpp. saveToActiveProject
|
|
// also calls MarkProjectDirty. Non-destructive: writes only our own ext-state key.
|
|
g_session.saveToActiveProject();
|
|
|
|
std::string log = "ReaSampler: " + res.message + "\n";
|
|
log += " bank size now " + std::to_string(g_session.bank().size()) +
|
|
(added == reasampler::AddResult::Added ? " (added)\n"
|
|
: added == reasampler::AddResult::Collapsed ? " (collapsed on hash)\n"
|
|
: " (rejected)\n");
|
|
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;
|
|
// target defaults to CurrentTrack (InsertOptions::target) — inserts onto the
|
|
// user's currently-selected track(s) at the edit cursor.
|
|
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 onto " + std::to_string(res.inserted) +
|
|
(res.inserted == 1 ? " track" : " tracks") +
|
|
(conform ? " (conformed to tempo)" : " (native length)") + "\n";
|
|
break;
|
|
case reasampler::InsertStatus::NoSelection:
|
|
// "select a track first" is printed by runInsert when no track is
|
|
// selected; this branch covers the no-panel-selection case.
|
|
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;
|
|
// 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])
|
|
{
|
|
RunCapture(reasampler::captureActionTable()[i]);
|
|
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;
|
|
return false;
|
|
}
|
|
|
|
// REAPER polls this to render each of OUR actions' checked state in menus/toolbars.
|
|
// 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)
|
|
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_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)
|
|
{
|
|
if (!rec)
|
|
{
|
|
// rec == nullptr => REAPER is UNLOADING us. Mirror-unregister every
|
|
// callback with the same strings prefixed '-' (per the contract).
|
|
if (g_rec)
|
|
{
|
|
g_rec->Register("-timer", (void*)&OnTimer);
|
|
g_rec->Register("-toggleaction", (void*)&OnToggleAction);
|
|
g_rec->Register("-hookcommand", (void*)&OnHookCommand);
|
|
// 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"));
|
|
// Mirror-unregister the capture family: gaccel + command_id per row,
|
|
// with '-'-prefixed strings (per the contract). The FOREVER-STABLE id
|
|
// strings come from the same table used to register them.
|
|
{
|
|
const auto& table = reasampler::captureActionTable();
|
|
for (std::size_t i = 0; i < table.size(); ++i)
|
|
{
|
|
if (i < g_captureAccels.size())
|
|
g_rec->Register("-gaccel", (void*)&g_captureAccels[i]);
|
|
g_rec->Register("-command_id", (void*)table[i].commandString);
|
|
}
|
|
}
|
|
// Retire the removed M7 command ids (command_id only — we never held a
|
|
// gaccel for them this session). Clears stale user keybindings on unload.
|
|
for (const char* id : kRetiredCaptureCmdStrings)
|
|
g_rec->Register("-command_id", (void*)id);
|
|
}
|
|
// Destroy the docked window and release cached thumbnails before we drop
|
|
// the API pointers (DockWindowRemove/DestroyWindow need them live).
|
|
reasampler::bankPanelShutdown();
|
|
g_rec = nullptr;
|
|
return 0;
|
|
}
|
|
|
|
// ABI guard: the struct layout we compiled against must match this REAPER.
|
|
if (rec->caller_version != REAPER_PLUGIN_VERSION)
|
|
return 0;
|
|
|
|
// Resolve every REAPER API function pointer. Returns the number that FAILED
|
|
// to load; 0 == success. Non-zero usually means REAPER is older than our SDK.
|
|
if (REAPERAPI_LoadAPI(rec->GetFunc) != 0)
|
|
return 0;
|
|
|
|
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 = reasampler::captureActionTable();
|
|
g_captureCmdIds.assign(table.size(), 0);
|
|
g_captureAccels.assign(table.size(), gaccel_register_t{});
|
|
for (std::size_t i = 0; i < table.size(); ++i)
|
|
{
|
|
const int cmd =
|
|
rec->Register("command_id", (void*)table[i].commandString);
|
|
g_captureCmdIds[i] = cmd;
|
|
if (cmd)
|
|
{
|
|
g_captureAccels[i].accel.cmd = cmd;
|
|
g_captureAccels[i].desc = table[i].description;
|
|
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).
|
|
g_cmdToggleBankPanel = rec->Register(
|
|
"command_id",
|
|
(void*)(REASAMPLER_ACTION_PREFIX "TOGGLE_BANK_PANEL"));
|
|
if (g_cmdToggleBankPanel)
|
|
{
|
|
g_accelToggleBankPanel.accel.cmd = g_cmdToggleBankPanel;
|
|
g_accelToggleBankPanel.desc = "ReaSampler: toggle bank panel";
|
|
rec->Register("gaccel", (void*)&g_accelToggleBankPanel);
|
|
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
|
|
// the hook so every id is minted first.
|
|
reasampler::designViewRegisterActions(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);
|
|
|
|
// Drive project-load / Save-As detection (M4 persist). The timer polls the
|
|
// active project each tick; on a project load it reloads the bank from ext
|
|
// state, on a Save-As it relocates the bank folder under the new .rpp.
|
|
rec->Register("timer", (void*)&OnTimer);
|
|
|
|
ShowConsoleMsg("ReaSampler loaded.\n");
|
|
|
|
return 1; // success — REAPER keeps us loaded
|
|
}
|