// main.cpp — the SINGLE translation unit that OWNS the REAPER API pointers. // // REAPER dlopen()s reaper_*.dll|dylib|so from UserPlugins/ and calls the exported // ReaperPluginEntry, handing over `rec` (rec->GetFunc resolves API pointers, // rec->Register plugs our callbacks in). Exactly ONE .cpp defines // REAPERAPI_IMPLEMENT (this one) — that allocates storage for the global API // pointers every other TU gets `extern`. Never let a second TU define it. // // This TU is ONLY pointers + entry + dispatch. Its own action family registers // through the data-driven table below (buildMainActionTable + action_registry) — // adding a bindable action means adding ONE row and its handler function (OCP). The // design_view / bank / ingest families keep their own register/handle/unregister // triples, called from entry. #define REAPERAPI_IMPLEMENT #include "reaper_plugin.h" #include "reaper_plugin_functions.h" #include #include #include #include "core/capture/render_settings.h" // captureActionTable #include "core/version/app_version.h" // appVersion #include "shell/actions/ingest.h" #include "shell/actions/action_registry.h" // the registration table #include "shell/actions/bank_actions.h" // multi-bank action family #include "shell/actions/design_view_actions.h" // Design View action family #include "core/wire/bake_wire.h" // kBakeActionSuffix (the shared action id) #include "shell/capture/bake_land.h" // resample-bake landing action body #include "shell/capture/capture_batch.h" // batch + recapture action bodies #include "shell/capture/capture_orchestrator.h" // single-capture / realtime / insert action bodies #include "shell/capture/realtime_lifecycle.h" // in-flight realtime state + tick driver #include "shell/capture/render_in_place.h" // render-in-place action body #include "shell/panel/panel_input.h" // bankPanelRefresh / bankPanelNotifyProjectLoaded #include "shell/panel/panel_window.h" // panel lifecycle (init/toggle/open-query/shutdown) #include "shell/persist/session.h" // ReaSamplerSession #include "shell/view/view.h" // reconcileManagedLanes / applyMode namespace capture = reasampler::capture; // Globals other files reference via `extern`. REAPER_PLUGIN_HINSTANCE g_hInst = nullptr; reaper_plugin_info_t* g_rec = nullptr; // Retired command-id SUFFIXES: kept ONLY to mirror-unregister on unload so a user's // stale keybindings are cleaned up. Never re-register these. The four-mode WET ids, // the removed master scope/realtime actions, and the removed per-action tail variants // (tail is now a panel toggle, not a paired action). static const char* const kRetiredCaptureCmdSuffixes[] = { "CAPTURE_TRACKS_WET", "CAPTURE_ITEMS_WET", "CAPTURE_RAZOR_WET", "CAPTURE_MASTER", "CAPTURE_MASTER_REALTIME", "CAPTURE_ITEM_TAIL", "CAPTURE_TRACK_TAIL", }; // Owns the in-memory BankModel 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() (resolves to the active bank's index), and we serialize the book // back into the active project's ext state (the `banks` key) so it travels with the .rpp. static reasampler::ReaSamplerSession g_session; // Command id of the TOGGLE_BANK_PANEL row, resolved from the table once at load so // OnToggleAction's checked-state poll is a single int compare (no per-poll lookup). static int g_cmdToggleBankPanel = 0; // Each handler is a thin stateless routing shim: (session, per-row arg) -> the // action body in shell/capture/ or shell/panel/, existing only so table rows can be // plain data with flat function pointers. // `arg` is the captureActionTable() row index — the table rows below are built by // iterating that pure taxonomy, so the routing stays 1:1 by construction. static void RunCaptureScopeRow(int arg) { capture::RunCapture(g_session, capture::captureActionTable()[static_cast(arg)]); } static void RunToggleBankPanel(int) { reasampler::bankPanelToggle(); } static void RunCaptureItemAssign(int) { capture::RunCaptureItemAssign(g_session); } // `arg` != 0 is the EXPLICIT conform-to-project-tempo opt-in (never silent); 0 // inserts at native length. static void RunInsertSelected(int arg) { capture::RunInsertSelected(g_session, arg != 0); } static void RunBatchCaptureItems(int) { capture::RunBatchCaptureItems(g_session); } static void RunBatchCaptureRazor(int) { capture::RunBatchCaptureRazor(g_session); } static void RunCaptureRealtime(int) { capture::RunCaptureRealtimeTrack(g_session); } static void RunCancelRealtime(int) { capture::RunCancelRealtime(g_session); } static void RunRecaptureFromSource(int) { capture::RunRecaptureFromSource(g_session); } static void RunRenderTrackInPlace(int) { capture::RunRenderTrackInPlace(g_session); } static void RunResampleBake(int) { capture::RunResampleBake(g_session); } static void RunShowVersion(int) { // On-demand only — no unconditional startup print (routine console chatter pops // the console window). ShowConsoleMsg(("ReaSampler " + reasampler::version::appVersion() + "\n").c_str()); } // ONE row per bindable action this TU owns: FOREVER-STABLE id suffix, Actions-list // phrase, handler, per-row arg. Registration, hookcommand dispatch, and the unload // mirror-unregister all iterate this data. The capture scope rows come first, // sourced from the pure captureActionTable() taxonomy; the rest are this TU's singles. static std::vector buildMainActionTable() { using reasampler::ActionTableRow; std::vector rows; const auto& cap = capture::captureActionTable(); for (std::size_t i = 0; i < cap.size(); ++i) rows.push_back(ActionTableRow{cap[i].commandSuffix, cap[i].descriptionPhrase, &RunCaptureScopeRow, static_cast(i)}); // Show/hide the docked bank panel (display-only; never captures/inserts). rows.push_back({"TOGGLE_BANK_PANEL", "toggle bank panel", &RunToggleBankPanel}); rows.push_back({"CAPTURE_ITEM_ASSIGN", "capture selected item into bank + assign to active instance", &RunCaptureItemAssign}); // Two variants differing ONLY in InsertOptions — native length vs conform opt-in. rows.push_back({"INSERT_SELECTED", "insert selected sample at edit cursor", &RunInsertSelected, 0}); rows.push_back({"INSERT_SELECTED_CONFORM", "insert selected sample at edit cursor (conform to tempo)", &RunInsertSelected, 1}); // One action fires N captures (per selected item / per razor area); the original // selection is restored on every exit path. Bank-only, never places. rows.push_back({"CAPTURE_BATCH_ITEMS", "batch capture selected items (one per item)", &RunBatchCaptureItems}); rows.push_back({"CAPTURE_BATCH_RAZOR", "batch capture razor areas (one per area)", &RunBatchCaptureRazor}); // Realtime sibling of the offline CAPTURE_TRACK scope, plus its cancel-in-flight // companion (stop + restore, non-destructive). rows.push_back({"CAPTURE_TRACK_REALTIME", "capture selected track (realtime)", &RunCaptureRealtime}); rows.push_back({"CANCEL_REALTIME_CAPTURE", "cancel realtime capture", &RunCancelRealtime}); rows.push_back({"RECAPTURE_FROM_SOURCE", "re-capture from source", &RunRecaptureFromSource}); // A RENDER_*, not a CAPTURE_*: the id is permanent and is the most durable // statement the codebase makes about which pillar a feature belongs to. rows.push_back({"RENDER_TRACK_IN_PLACE", "render selected track to a new track (source moves to Design)", &RunRenderTrackInPlace}); // Invoked by a ReaSampler 9000 instance over the VST3 host bridge (and bindable, so a // stranded request can be landed by hand). The suffix is the wire contract itself — // core/wire/bake_wire owns the spelling both artifacts read. rows.push_back({reasampler::wire::kBakeActionSuffix, "land pending ReaSampler 9000 resample bake", &RunResampleBake}); rows.push_back({"SHOW_VERSION", "show version", &RunShowVersion}); return rows; } // 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() { // Advance any in-flight realtime capture FIRST, so a project switch is caught and // torn down/restored before session.poll() reacts to that switch. LOAD-BEARING: // the idle fast-path is a SINGLE POINTER TEST — drive only when a capture is live. if (capture::g_rtCapture) capture::DriveRealtimeCapture(g_session); g_session.poll(); // persist stays MODEL-ONLY (loads the saved view model but does not apply // visibility, to avoid coupling persist to the view shell); poll() raises a // one-shot load signal that we drain here to reapply the SAVED active mode so a // project saved in Design mode parks Arrange tracks automatically. The same // signal re-arms the bank panel's new-content detector — notified BEFORE the // reapply so re-arm and model restore ride the one load event (otherwise // pre-existing tracks can be mis-detected as "new" and mass-tagged). if (g_session.consumeLoadSignal()) { reasampler::bankPanelNotifyProjectLoaded(); // Reconcile lane ownership against the live project's lanes (P_LANENAME, // the cross-session source of truth) BEFORE reapplying visibility. Never // re-mints, never mass-tags. reasampler::reconcileManagedLanes(g_session.view(), nullptr); reasampler::applyMode(g_session.view(), g_session.view().activeModeId(), nullptr); } reasampler::bankPanelRefresh(); // cheap fingerprint compare; no-op when unchanged/closed } // A Ctrl-Z/Ctrl-Shift-Z rolls back/forward the "reasampler" project ext state on disk // but keeps the SAME project identity, so the timer's identity poll never re-reads // ext state on undo/redo — the in-memory book/view would stay stale until // close+reopen. REAPER's projectconfig fires BeginLoadProjectState on every // project-state (re)load INCLUDING undo/redo (isUndo == true for both); we hook it. // // TIMING: BeginLoadProjectState fires BEFORE any state restore, so reading // GetProjExtState here would return the PRE-undo value. Instead we raise a one-shot // reload request that OnTimer's poll() drains on the NEXT tick, once REAPER has // finished restoring the block. A normal project open also fires this // (isUndo=false); ignored here so a normal open flows solely through the timer's // identity-transition Load path (no double load). static void OnBeginLoadProjectState(bool isUndo, project_config_extension_t* /*reg*/) { if (isUndo) g_session.requestReload(); } // Intentional no-ops: ReaSampler stores state via project EXT STATE, not this // extension's own project lines. The struct is registered ONLY for the // BeginLoadProjectState undo/redo notification. static bool OnProcessExtensionLine(const char* /*line*/, ProjectStateContext* /*ctx*/, bool /*isUndo*/, project_config_extension_t* /*reg*/) { return false; // we own no project lines — ext state carries our data } static void OnSaveExtensionConfig(ProjectStateContext* /*ctx*/, bool /*isUndo*/, project_config_extension_t* /*reg*/) { } // Storage must outlive registration — REAPER holds this pointer until we unregister it. static project_config_extension_t g_projectConfig{ &OnProcessExtensionLine, &OnSaveExtensionConfig, &OnBeginLoadProjectState, nullptr, // userData }; // REAPER calls this for every action fired in the MAIN section; claim only our own id, // return false otherwise so REAPER keeps looking. This TU's own family dispatches through // the registration table; the other families claim their own ids after it. static bool OnHookCommand(int command, int /*flag*/) { if (command == 0) return false; if (reasampler::actionTableHandleCommand(command)) return true; if (reasampler::designViewHandleCommand(command)) return true; if (reasampler::bankHandleCommand(command)) return true; if (reasampler::ingestHandleCommand(command)) return true; return false; } // "hookcommand" covers the main section only, so actions we published into another // section arrive here instead. Partitioning contract: root `CLAUDE.md` §"REAPER // extension contract". static bool OnHookCommand2(KbdSectionInfo* /*sec*/, int command, int /*val*/, int /*val2*/, int /*relmode*/, HWND /*hwnd*/) { if (command == 0) return false; return reasampler::ingestHandleSectionCommand(command); } // 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 != 0 && command == g_cmdToggleBankPanel) return reasampler::bankPanelIsOpen() ? 1 : 0; return -1; // not ours / non-toggling } 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. if (g_rec) { // Abort any in-flight realtime capture FIRST, while the API pointers are // still live, so we never leave a temp track, an armed track, or an // altered transport/cursor in the user's project on unload. capture::AbortRealtimeCaptureForUnload(g_session); g_rec->Register("-timer", (void*)&OnTimer); g_rec->Register("-projectconfig", (void*)&g_projectConfig); g_rec->Register("-toggleaction", (void*)&OnToggleAction); g_rec->Register("-hookcommand", (void*)&OnHookCommand); g_rec->Register("-hookcommand2", (void*)&OnHookCommand2); reasampler::designViewUnregisterActions(g_rec); reasampler::bankUnregisterActions(g_rec); reasampler::ingestUnregisterActions(g_rec); // This TU's own family, reverse table order; each '-command_id' // re-presents the SAME interned pointer used at register. reasampler::unregisterActionTable(g_rec); // Retire the REMOVED command ids (command_id only — we never held a gaccel // for them this session). for (const char* suffix : kRetiredCaptureCmdSuffixes) g_rec->Register("-command_id", (void*)reasampler::channelIdFor(suffix)); } // Before dropping 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; // Point the bank panel at the live session BEFORE registering its action, so a // toggle firing immediately has a session to read. Does not open the window. reasampler::bankPanelInit(&g_session); { const std::vector rows = buildMainActionTable(); reasampler::registerActionTable(rec, rows.data(), rows.size()); } // The panel toggle renders a checked state — resolve its minted id once and // register the toggleaction hook that reports it. g_cmdToggleBankPanel = reasampler::actionTableCommandId("TOGGLE_BANK_PANEL"); if (g_cmdToggleBankPanel) rec->Register("toggleaction", (void*)&OnToggleAction); // Each family mints its own command_id + gaccel, shares g_session, and is routed // by the same hookcommand below. Registered before the hook so every id is // minted first. reasampler::designViewRegisterActions(rec, &g_session); reasampler::bankRegisterActions(rec, &g_session); reasampler::ingestRegisterActions(rec, &g_session); rec->Register("hookcommand", (void*)&OnHookCommand); rec->Register("hookcommand2", (void*)&OnHookCommand2); // Drives project-load / Save-As detection: 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); // An UNDO/REDO state restore reloads the session's book + view from the restored // ext state. The timer's identity poll cannot see an undo (same project // identity), so this hook owns it (see OnBeginLoadProjectState). rec->Register("projectconfig", (void*)&g_projectConfig); return 1; // success — REAPER keeps us loaded }