// 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. // // Since Q-W3 this TU is ONLY pointers + entry + dispatch; since Q-W6 its own // action family registers through the DATA-DRIVEN TABLE below (kMainActionRows + // action_registry's registerActionTable/actionTableHandleCommand/ // unregisterActionTable) — adding a bindable action here means adding ONE row and // its handler function, nothing else (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" // channelCommandId / appVersion #include "ingest.h" #include "shell/actions/action_registry.h" // the Q-W6 registration table #include "shell/actions/bank_actions.h" // multi-bank action family (B3; Q-W4 home) #include "shell/actions/design_view_actions.h" // Design View action family (D4; Q-W4 home) #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/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; // this module's instance handle reaper_plugin_info_t* g_rec = nullptr; // REAPER's dispatch struct // Retired command-id SUFFIXES. Kept ONLY to mirror-unregister them on unload so a // user's stale keybindings are cleaned up. Never re-register these. Composed through // the channel prefix at unload (channelIdFor) so a beta unload clears beta-qualified // retired ids and a stable unload clears stable's — each channel cleans up only its // own family. // * The M7 four-mode ids (tracks/items/razor WET). // * CAPTURE_MASTER and CAPTURE_MASTER_REALTIME — the master offline scope and the // master realtime action are REMOVED (capture is now item + track only; realtime // taps the selected track). Their shipped ids are retired so old keybindings clear. // * CAPTURE_ITEM_TAIL and CAPTURE_TRACK_TAIL — the former per-action tail variants // are REMOVED; tail is now a panel-setting 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", }; // The persistence session (M4): 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() — which (B2) resolves to // the ACTIVE bank's index inside the session's BankBook; after a capture we serialize // the book back into the active project's ext state (the `banks` key) so it travels // with the .rpp. Replaces the M3 session-only g_bank. 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; // --- Action handlers (the table's function pointers) -------------------------- // // Each is a thin stateless routing shim: (session, per-row arg) -> the action body // hoisted in Q-W3/Q-W4 (shell/capture/, shell/panel/). The bodies own all behavior; // these exist only so the table rows can be plain data with flat function pointers. // Capture scope family: `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 (never a hand-kept parallel list). 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); } // Insert: `arg` != 0 is the EXPLICIT conform-to-project-tempo opt-in (CONTEXT.md // §insert: conform is 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 RunShowVersion(int) { // On-demand version readout — the ONLY version output on any path (Phase V: no // unconditional startup print; routine console chatter pops the console window). ShowConsoleMsg(("ReaSampler " + reasampler::version::appVersion() + "\n").c_str()); } // --- The registration table (Q-W6) -------------------------------------------- // // ONE row per bindable action this TU owns: FOREVER-STABLE id suffix (channel prefix // composed at register — stable rebuilds the exact shipped id, e.g. // "CEREBELLUM_REASAMPLER_CAPTURE_TRACK"; beta its isolated forever-family), the // Actions-list phrase (after the "ReaSampler[ beta]: " lead), the handler, and its // per-row arg. Registration, hookcommand dispatch, and the unload mirror-unregister // all iterate this data — adding an action = adding a row + a handler above. // // The capture scope rows (CAPTURE_ITEM / CAPTURE_TRACK) come first, sourced from the // pure captureActionTable() taxonomy (render_settings) — suffix/phrase live in that // one testable list, and `arg` carries the row index back to RunCapture. The // remaining rows are this TU's singles, in the pre-table registration order. 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)}); // M5: show/hide the docked bank panel (display-only; never captures/inserts). rows.push_back({"TOGGLE_BANK_PANEL", "toggle bank panel", &RunToggleBankPanel}); // S8: Item-scope capture + assignment-request write (capture family because it // leans on the capture render machinery; the other ingest surfaces live in the // ingest family and the panel drop callback). rows.push_back({"CAPTURE_ITEM_ASSIGN", "capture selected item into bank + assign to active instance", &RunCaptureItemAssign}); // M6: place the panel's selected sample at the edit cursor. Two variants that // differ ONLY in InsertOptions — native length vs the explicit 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}); // M11: 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}); // M8: realtime sibling of the offline CAPTURE_TRACK scope — records the selected // track's own output into a hidden temp track, dialog-free — 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}); // M10: regenerate the selected PROVENANCED sample from its recorded source's // current state, in place. Bank-only, never places on the timeline. rows.push_back({"RECAPTURE_FROM_SOURCE", "re-capture from source", &RunRecaptureFromSource}); // Phase V: on-demand version readout for bug reports. 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 // the capture torn down/restored before session.poll() reacts to that switch. // LOAD-BEARING (CONTEXT.md §Phase Q): the idle fast-path is a SINGLE POINTER // TEST — the cross-TU drive call is made only when a capture is in flight. if (capture::g_rtCapture) capture::DriveRealtimeCapture(g_session); 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). // // The SAME signal re-arms the bank panel's new-content detector: a load must // re-baseline the detector against the just-loaded project's content so its // pre-existing tracks are never mis-detected as "new" and mass-tagged into the // active mode (the reload-mis-tag bug). Notify BEFORE the reapply so the detector's // re-arm and the model restore ride the one authoritative load event. if (g_session.consumeLoadSignal()) { reasampler::bankPanelNotifyProjectLoaded(); // Reconcile the restored lane-ownership index against the live project's lanes // FIRST (via REAPER's durable P_LANENAME — the cross-session source of truth), // so a saved lane-split project's managed/manual classification is correct // before the active mode's lane visibility is reapplied. Never re-mints, never // mass-tags — it only records managed ownership recovered from lane names. reasampler::reconcileManagedLanes(g_session.view(), nullptr); 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(); } // --- projectconfig hook: reload the session on undo/redo (R-B) --------------- // A Ctrl-Z / Ctrl-Shift-Z rolls back / forward the "reasampler" project ext state on // disk but keeps the SAME project identity (ReaProject*/GUID/.rpp path), so the timer's // identity poll reads it as NoOp and never re-reads ext state — the in-memory book/view // would stay stale until close+reopen. REAPER's projectconfig extension fires // BeginLoadProjectState on every project-state (re)load, INCLUDING an undo/redo restore // (isUndo == true for both). We hook it to drive a session reload. // // TIMING (the crux): BeginLoadProjectState is documented (reaper_plugin.h ~1203) as // firing BEFORE any state restore. Reading GetProjExtState synchronously here would // return the PRE-undo value. So we do NOT read here — we raise a one-shot reload request // (g_session.requestReload()) that OnTimer's poll() drains on the NEXT tick, by which // point REAPER has finished restoring the block and GetProjExtState returns // the POST-undo value. Deterministic, event-driven — NOT ext-state content polling. // // GATED ON isUndo: a normal project open also fires BeginLoadProjectState (isUndo=false); // we ignore that here so a normal open flows solely through the timer's identity-transition // Load path (no double load). Only undo/redo (isUndo=true) requests the reload. static void OnBeginLoadProjectState(bool isUndo, project_config_extension_t* /*reg*/) { if (isUndo) g_session.requestReload(); } // ProcessExtensionLine / SaveExtensionConfig are intentional no-ops: ReaSampler stores // its state via project EXT STATE (SetProjExtState/GetProjExtState under "reasampler"), // which REAPER persists in its own RPP block — NOT via this extension's own // project lines. We register the struct ONLY for the BeginLoadProjectState undo/redo // notification. Returning false from ProcessExtensionLine means "not our line" so REAPER // keeps dispatching (we claim none). SaveExtensionConfig writes nothing. 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*/) { // Nothing to write: our data rides in ext state, not project lines. } // 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 anywhere; claim only our own id, // return false otherwise so REAPER keeps looking. This TU's own family dispatches // through the registration table; the Q-W4 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; // 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; // Multi-bank action family (B3). Same contract: claims only its own ids. if (reasampler::bankHandleCommand(command)) return true; // S8 ingest action family (Media-Explorer import). Same contract. if (reasampler::ingestHandleCommand(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 != 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. Mirror-unregister every // callback with the same strings prefixed '-' (per the contract). if (g_rec) { // Abort any in-flight realtime capture FIRST, while the API pointers are // still live — finalize-or-abort + restore so we never leave a temp track, // an armed track, or an altered transport/cursor in the user's project on // unload. Commit whatever was captured (best effort) before tearing down. 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); // 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); // Tear down the multi-bank action family (B3) — same mirror-unregister. reasampler::bankUnregisterActions(g_rec); // Tear down the S8 ingest action family — same mirror-unregister. reasampler::ingestUnregisterActions(g_rec); // Tear down this TU's own family from the registration table (reverse // table order; each '-command_id' re-presents the SAME interned, // channel-qualified 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). Clears stale user keybindings on unload. Composed // per channel so a beta clears beta-qualified retired ids, stable its own. for (const char* suffix : kRetiredCaptureCmdSuffixes) g_rec->Register("-command_id", (void*)reasampler::channelIdFor(suffix)); } // 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; // 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 this TU's whole action family from the table: command_id -> gaccel // per row, all channel-qualified, all FOREVER-STABLE per channel. { 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); // 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); // Register the multi-bank action family (B3): create/rename/delete/evacuate bank, // activate (cycle + pool), move/copy selected samples to a bank, and the two // full-height layout toggles. Shares g_session with the Design View family; routed // by the same hookcommand via bankHandleCommand. Registered before the hook. reasampler::bankRegisterActions(rec, &g_session); // Register the S8 ingest action family: the Media-Explorer import-into-bank+assign // action. Shares g_session with the other families; routed by the same hookcommand via // ingestHandleCommand. (The arrange capture+assign action is a table row above; the // drop path is a bank_panel callback, not a bindable action.) reasampler::ingestRegisterActions(rec, &g_session); // One hookcommand routes every ReaSampler action (table + the three families). // 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); // Register the projectconfig hook so an UNDO/REDO state restore reloads the // session's book + view from the restored ext state (R-B). The timer's identity // poll cannot see an undo (same project identity), so this hook owns undo/redo; it // requests a deferred reload that the next timer tick drains (see the hook comment). rec->Register("projectconfig", (void*)&g_projectConfig); return 1; // success — REAPER keeps us loaded }