71a3b26a47
Docked SWELL window toggled by a new action, drawing the current bank as per-sample min/max thumbnails (PCM_source + peaks) with an in-memory cache. Pure grid-layout/cache-key math in bank_grid (tested). Renames peaks::Sample -> AudioSample to avoid colliding with bank_model::Sample.
217 lines
9.6 KiB
C++
217 lines
9.6 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 "bank_model.h"
|
|
#include "bank_panel.h"
|
|
#include "capture.h"
|
|
#include "persist.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
|
|
|
|
// ---- Action registration seam (M3 spike: capture master mix) ---------------
|
|
// First live use of the action pattern the seam left templated. The full capture
|
|
// action family (selected tracks/items/razor, wet/dry, tail) is M7; this is ONE
|
|
// temporary action driving the M3 offline-render spike.
|
|
|
|
// Command id for "ReaSampler: capture master mix (spike)". FOREVER-STABLE string
|
|
// (user keybindings key off it) — see the prefix note above.
|
|
static int g_cmdCaptureMasterSpike = 0;
|
|
|
|
// 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;
|
|
|
|
// 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();
|
|
// 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();
|
|
}
|
|
|
|
// Runs the M3 spike: render the time-selection master mix, add the Sample, log.
|
|
static void RunCaptureMasterSpike()
|
|
{
|
|
// Time selection -> exact render bounds (no rounding). GetSet_LoopTimeRange
|
|
// with isSet=false reads the current time selection (isLoop=false).
|
|
double start = 0.0, end = 0.0;
|
|
GetSet_LoopTimeRange(false, false, &start, &end, false);
|
|
|
|
reasampler::CaptureRequest req;
|
|
req.sourceMode = reasampler::SourceMode::TimeSelection;
|
|
req.startSeconds = start;
|
|
req.endSeconds = end;
|
|
req.wetDry = 1.0; // wet master mix
|
|
req.renderTail = false; // exact bounds, no tail
|
|
req.sampleRate = 0; // follow project rate
|
|
req.channelCount = 2;
|
|
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
|
|
req.baseName = "master_mix";
|
|
|
|
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). Non-destructive: writes only our own
|
|
// ext-state key. No-ops on an unsaved project (nothing to store into yet).
|
|
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());
|
|
}
|
|
|
|
// 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; }
|
|
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.
|
|
static gaccel_register_t g_accelCaptureMaster{};
|
|
static gaccel_register_t g_accelToggleBankPanel{};
|
|
|
|
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);
|
|
g_rec->Register("-gaccel", (void*)&g_accelToggleBankPanel);
|
|
g_rec->Register("-command_id",
|
|
(void*)(REASAMPLER_ACTION_PREFIX "TOGGLE_BANK_PANEL"));
|
|
g_rec->Register("-gaccel", (void*)&g_accelCaptureMaster);
|
|
g_rec->Register("-command_id",
|
|
(void*)(REASAMPLER_ACTION_PREFIX "CAPTURE_MASTER_SPIKE"));
|
|
}
|
|
// 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 M3 spike action (command_id -> gaccel -> hookcommand).
|
|
g_cmdCaptureMasterSpike = rec->Register(
|
|
"command_id",
|
|
(void*)(REASAMPLER_ACTION_PREFIX "CAPTURE_MASTER_SPIKE"));
|
|
if (g_cmdCaptureMasterSpike)
|
|
{
|
|
g_accelCaptureMaster.accel.cmd = g_cmdCaptureMasterSpike;
|
|
g_accelCaptureMaster.desc = "ReaSampler: capture master mix (spike)";
|
|
rec->Register("gaccel", (void*)&g_accelCaptureMaster);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// One hookcommand routes every ReaSampler action (spike + toggle). Registered
|
|
// once, after both 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
|
|
}
|