39b995e824
BeginLoadProjectState now routes unbranched through fxReclaimAbandonForStateLoad; only the session's deferred reload stays undo-only.
408 lines
22 KiB
C++
408 lines
22 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/insert_fx_action.h" // insert-as-FX action body
|
|
#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/fx_reclaim.h" // the FX reclaimer's tick
|
|
#include "shell/view/view.h" // applyMode
|
|
#include "shell/view/view_lanes.h" // reconcileManagedLanes
|
|
#include "shell/view/view_reconcile.h" // reconcileAppliedFlags
|
|
|
|
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);
|
|
}
|
|
// Placement of the PLAYER, not of an item: adds a ReaSampler 9000 to the selected track's
|
|
// FX chain. Sessionless — it reads the panel's selection, exactly like the drag it mirrors.
|
|
static void RunInsertAsFx(int) { reasampler::doInsertAsFx(); }
|
|
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});
|
|
// A LOAD_*, not an INSERT_*: INSERT_SELECTED/_CONFORM place a timeline item;
|
|
// this loads an instrument onto a track's FX chain. The id is permanent, so
|
|
// keeping the two apart here is the most durable statement of which verb this is.
|
|
rows.push_back({"LOAD_INSTRUMENT_ON_TRACK",
|
|
"insert selected sample as ReaSampler 9000 on the selected track",
|
|
&RunInsertAsFx});
|
|
// 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);
|
|
|
|
// poll()'s undo/redo-reload and project-switch Load branches call
|
|
// loadFromProject, replacing g_session.view() wholesale — deferred here while
|
|
// an applyMode is on the stack (reachable if a pumped message loop re-enters
|
|
// this timer mid-apply), same retry-next-tick shape as the consume guard below.
|
|
// Deferred WHOLE, not just the reload branches: poll()'s identity tracking is
|
|
// cheap to skip for one tick and resumes correctly, unlatched, on the next.
|
|
// The reclaim drain is latched for the same reason and is the same window: it
|
|
// holds that view model for the length of a plugin unload, which pumps.
|
|
if (!reasampler::modeApplyInProgress() && !reasampler::fxReclaimDrainInProgress())
|
|
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).
|
|
// Both latches are tested BEFORE the consume, not after the applyMode call: the load
|
|
// signal is one-shot, a re-entered applyMode fails closed, and a reapply dropped here
|
|
// is the saved mode never applied at all. Holding the whole block back one tick also
|
|
// keeps the panel re-arm ahead of the reapply.
|
|
if (!reasampler::modeApplyInProgress() && !reasampler::fxReclaimDrainInProgress() &&
|
|
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);
|
|
// An undo/redo reload also arrives here, and REAPER's undo does not restore
|
|
// the visibility flags a park drove (UNDO_STATE_TRACKCFG does not carry them).
|
|
// Repair the tracks the reloaded model no longer records BEFORE the reapply,
|
|
// which only ever drives tracks the model still records.
|
|
reasampler::reconcileAppliedFlags(g_session.view(), nullptr);
|
|
reasampler::applyMode(g_session.view(), g_session.view().activeModeId(), nullptr);
|
|
}
|
|
|
|
// Converge the FX chains toward what the view state says they should be — the
|
|
// 1897ms of TrackFX_SetOffline a mode switch no longer pays synchronously. Same
|
|
// idle discipline as the realtime fast path above: ONE bool read when there is
|
|
// nothing to converge. Ordered AFTER the load block deliberately — a reload replaces
|
|
// the view model wholesale, and the applyMode above is what re-derives the queue
|
|
// from it, so draining first would spend a tick against the outgoing model.
|
|
if (reasampler::fxReclaimPending())
|
|
reasampler::fxReclaimTick(g_session, 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 <EXTSTATE> block. Only the RELOAD REQUEST is undo-only: a
|
|
// normal open's reload belongs to the timer's identity-transition Load path, whose
|
|
// forked-sibling re-GUID branch requesting a reload here would bypass.
|
|
static void OnBeginLoadProjectState(bool isUndo, project_config_extension_t* /*reg*/)
|
|
{
|
|
// UNBRANCHED, and the reason the terminator is a hook at all: this is REAPER's one
|
|
// entry for a state load — "project load/undo", plus a new project
|
|
// (reaper_plugin.h:1203-1205) — and the last moment before the restore. Why that
|
|
// matters, and why isUndo does not narrow it: fxReclaimAbandonForStateLoad.
|
|
reasampler::fxReclaimAbandonForStateLoad();
|
|
|
|
if (isUndo)
|
|
g_session.requestReload();
|
|
}
|
|
|
|
// Intentional no-op: ReaSampler stores state via project EXT STATE, not this
|
|
// extension's own project lines.
|
|
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
|
|
}
|
|
|
|
// We write no lines into `ctx` — this is the FX reclaimer's one durability point. Its
|
|
// spent park records ride the user's save rather than dirtying the project on the
|
|
// reclaimer's own initiative (fx_reclaim.h's ReleaseFlush).
|
|
//
|
|
// A save only: an isUndo pass is REAPER capturing an undo point, not writing the file,
|
|
// and clearing the pending flush on one would leave the record unspent on disk for good.
|
|
static void OnSaveExtensionConfig(ProjectStateContext* /*ctx*/, bool isUndo,
|
|
project_config_extension_t* /*reg*/)
|
|
{
|
|
if (!isUndo)
|
|
reasampler::fxReclaimFlushRelease(g_session);
|
|
}
|
|
|
|
// 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);
|
|
|
|
// Same reason, for the reclaimer's undo span: a block left open here has no
|
|
// owner left to close it and would swallow every edit the user makes after.
|
|
reasampler::fxReclaimEndUndoSpan();
|
|
|
|
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
|
|
}
|