363 lines
19 KiB
C++
363 lines
19 KiB
C++
// 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 <cstddef>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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 "shell/actions/package_export_action.h" // bank-package export action body
|
|
#include "shell/actions/package_import_action.h" // bank-package import action body
|
|
#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
|
|
#include "shell/view/view_fx_park.h" // the mode switch's deferred FX park
|
|
|
|
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<std::size_t>(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 RunExportBankPackage(int) { reasampler::doBankPackageExport(g_session, g_session.book().activeBankId()); }
|
|
static void RunImportBankPackage(int) { reasampler::doImportBankPackage(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<reasampler::ActionTableRow> buildMainActionTable() {
|
|
using reasampler::ActionTableRow;
|
|
std::vector<ActionTableRow> 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<int>(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({"EXPORT_BANK_PACKAGE", "export active bank as package",
|
|
&RunExportBankPackage});
|
|
rows.push_back({"IMPORT_BANK_PACKAGE", "import bank package (.rsbank)",
|
|
&RunImportBankPackage});
|
|
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()) {
|
|
// Every path that raises this signal — open, project switch, recycled
|
|
// pointer, undo/redo state restore — replaced the model the pending FX
|
|
// intents were planned against, so they are discarded rather than applied
|
|
// to the project that replaced it. This is the queue's ONLY guard against
|
|
// a recycled ReaProject*, which a pointer compare cannot see.
|
|
reasampler::discardDeferredFxParks();
|
|
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);
|
|
} else {
|
|
// A mode switch applies its visibility/routing writes synchronously and
|
|
// leaves the per-FX offline work here, so the new mode paints before the
|
|
// plugins unload. Idle cost is one empty test. Skipped on a load tick so
|
|
// the reapply's own intents defer one tick like any other switch's.
|
|
reasampler::drainDeferredFxParks();
|
|
}
|
|
|
|
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 <EXTSTATE> 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<reasampler::ActionTableRow> 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
|
|
}
|