// 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 #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 }