1899 lines
97 KiB
C++
1899 lines
97 KiB
C++
#include "core/namespaces.h"
|
|
// 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 <cstddef>
|
|
#include <deque>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
#include <vector>
|
|
|
|
#include "actions.h"
|
|
#include "core/version/app_version.h"
|
|
#include "core/model/bank_model.h"
|
|
#include "bank_panel.h"
|
|
#include "core/capture/batch_capture.h"
|
|
#include "shell/capture/capture.h"
|
|
#include "ingest.h"
|
|
#include "shell/capture/insert.h"
|
|
#include "persist.h"
|
|
#include "core/model/provenance.h"
|
|
#include "shell/capture/provenance_shell.h"
|
|
#include "core/capture/render_settings.h"
|
|
#include "shell/capture/track_guid.h"
|
|
#include "shell/view/view.h"
|
|
|
|
#include <filesystem> // project-dir derivation for provenance parent resolution
|
|
|
|
// Persistent action-id family (Phase V, V4 — channel-qualified). Every bindable action
|
|
// mints its command id from commandIdPrefix() + a per-action SUFFIX, and its Actions-list
|
|
// name from actionDisplayPrefix() + a phrase, both derived from the ONE channel bit in the
|
|
// pure app_version module (channelCommandId / channelActionName). Stable rebuilds the exact
|
|
// shipped id ("CEREBELLUM_REASAMPLER_CAPTURE_TRACK"); beta yields the isolated forever-
|
|
// family id ("CEREBELLUM_REASAMPLER_BETA_CAPTURE_TRACK"). FOREVER-STABLE per channel: a
|
|
// shipped suffix is as permanent as the prefix; user keybindings key off the composed id.
|
|
//
|
|
// The composed id strings are held here for the module's lifetime (idStore) so both the
|
|
// register call and the mirroring '-command_id' unregister pass the SAME stable pointer.
|
|
// A std::deque (NOT vector) is used deliberately: it never invalidates references to
|
|
// existing elements on push_back, so a c_str() handed out early stays valid after later
|
|
// interning — the unload path re-presents these same pointers.
|
|
static std::deque<std::string> g_idStore;
|
|
|
|
// Interns a composed command-id string for the module lifetime and returns its C string.
|
|
// Appended-to only during startup registration and read on unload; never cleared until
|
|
// process exit, and deque guarantees the returned pointer stays valid.
|
|
static const char* internCmdId(const std::string& suffix) {
|
|
g_idStore.push_back(reasampler::channelCommandId(suffix));
|
|
return g_idStore.back().c_str();
|
|
}
|
|
|
|
// 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
|
|
|
|
// ---- Capture action family (two FX scopes) ---------------------------------
|
|
// Two bindable SCOPE actions from captureActionTable() (render_settings, pure):
|
|
// capture item / track. Each infers its range (razor-else-time) and enforces the
|
|
// FX-scope invariant via FX-bypass-around-render (FxBypassGuard):
|
|
// Item -> take/item FX only (bypass the item's track + ancestors + master).
|
|
// Track -> item FX + track's own FX (bypass ancestors + master).
|
|
// There is NO master scope — to capture the master you render a track. (The master
|
|
// track's FX/gain/pan are STILL neutralized for both scopes as the out-of-scope
|
|
// chain — master is a bypass target, not a capture scope.) The retired M7
|
|
// CAPTURE_TRACKS_WET / CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids AND the removed
|
|
// CAPTURE_MASTER / CAPTURE_MASTER_REALTIME ids are mirror-unregistered on unload so
|
|
// old keybindings clear cleanly.
|
|
//
|
|
// The minted command ids parallel the table rows 1:1 (same index). gaccel storage
|
|
// must outlive registration (REAPER holds each pointer), so both vectors are file-
|
|
// scope and sized to the table. FOREVER-STABLE id strings live in the table.
|
|
static std::vector<int> g_captureCmdIds;
|
|
static std::vector<gaccel_register_t> g_captureAccels;
|
|
// Channel-qualified capture-action labels, one per table row. REAPER holds each gaccel's
|
|
// `desc` pointer, so the composed strings live here for the module lifetime (parallel to
|
|
// g_captureAccels; never resized after the registration loop sets it).
|
|
static std::vector<std::string> g_captureDescs;
|
|
|
|
// Retired capture-action 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 (channelCommandId) 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. Retired so
|
|
// old keybindings clear.
|
|
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",
|
|
};
|
|
|
|
// 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;
|
|
|
|
// Command ids for the M6 insert actions. FOREVER-STABLE strings. Two variants that
|
|
// differ ONLY in the InsertOptions they build: the default inserts at native length
|
|
// (no stretch, no conform); the "conform" variant is the EXPLICIT opt-in to REAPER's
|
|
// try-to-match-project-tempo path (CONTEXT.md §insert: conform is opt-in, never
|
|
// silent). Both read the bank panel's current selection and place at the edit cursor.
|
|
static int g_cmdInsertSelected = 0;
|
|
static int g_cmdInsertSelectedConform = 0;
|
|
|
|
// Command ids for the M11 batch-capture actions. NEW FOREVER-STABLE strings. One action
|
|
// fires N captures: CAPTURE_BATCH_ITEMS -> one bank sample per selected item (item scope);
|
|
// CAPTURE_BATCH_RAZOR -> one bank sample per razor area (track scope, each area's range).
|
|
// Each unit honors every precision invariant; the original selection is restored on every
|
|
// exit path. Bank-only, never places on the timeline (load-bearing principle).
|
|
static int g_cmdCaptureBatchItems = 0;
|
|
static int g_cmdCaptureBatchRazor = 0;
|
|
|
|
// Command id for the "capture selected track (realtime)" action. NEW FOREVER-STABLE
|
|
// string. Records the selected track's OWN output in realtime (transport-driven) into
|
|
// a hidden temp track via RealtimeRecordBackend, then moves the recorded file into the
|
|
// bank. The realtime SIBLING of the offline CAPTURE_TRACK scope action: same range
|
|
// logic (razor-else-time), same track selection, same bank/persist path, different
|
|
// backend. Dialog-free. (Replaces the removed CAPTURE_MASTER_REALTIME action.)
|
|
static int g_cmdCaptureTrackRealtime = 0;
|
|
|
|
// Command id for the M10 "re-capture from source" action. NEW FOREVER-STABLE string
|
|
// (suffix RECAPTURE_FROM_SOURCE). Regenerates the bank panel's selected PROVENANCED
|
|
// sample from its recorded source's current state and updates the Sample in place —
|
|
// BANK-ONLY, never places on the timeline (load-bearing principle). Reports the no-
|
|
// provenance / vanished-source / drift cases to the console (a direct response to an
|
|
// explicit action, allowed by the console policy).
|
|
static int g_cmdRecaptureFromSource = 0;
|
|
|
|
// Command id for the S8 "capture selected item / time-selection into bank + assign"
|
|
// action. NEW FOREVER-STABLE string (suffix CAPTURE_ITEM_ASSIGN). Reuses the offline
|
|
// Item-scope capture path (RunCapture) verbatim — same razor-else-time range, same
|
|
// FX-scope neutralize, same bank/persist landing — then writes an S8 assignment request
|
|
// so the active sampler instance plays the just-captured sample on its next reload. NEVER
|
|
// inserts a timeline item (the capture/placement separation holds; assign is a bank-index
|
|
// + instance-selection act). Lives in the capture family (not the ingest family) because
|
|
// it leans on main.cpp's capture render machinery, which is not exposed cross-module.
|
|
static int g_cmdCaptureItemAssign = 0;
|
|
|
|
// Command id for the M8 "cancel realtime capture" action. FOREVER-STABLE string.
|
|
// Aborts the in-flight realtime capture (stop + restore, non-destructive) so a user
|
|
// who started a long capture can bail without waiting for the range end or hunting for
|
|
// the transport-stop. No-op (with a note) when nothing is in flight.
|
|
static int g_cmdCancelRealtime = 0;
|
|
|
|
// Command id for the Phase V "show version" action. FOREVER-STABLE string. On demand
|
|
// ONLY — prints the CMake-sourced version string to the console when fired. This is the
|
|
// SOLE new console output the versioning wave adds; there is no unconditional startup
|
|
// version print (routine console chatter was deliberately removed — it pops the console
|
|
// window). The user copies this line into a bug report.
|
|
static int g_cmdShowVersion = 0;
|
|
|
|
// 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;
|
|
|
|
// --- M8 in-flight realtime capture (async, timer-driven) --------------------
|
|
// A realtime record spans many timer ticks (it takes end-start wall-clock seconds
|
|
// and must NOT block REAPER's UI). The action STARTS it (g_rtBackend.begin), which
|
|
// returns immediately with the in-flight state owned here; OnTimer drives it
|
|
// (g_rtBackend.tick) each tick until a terminal verdict; then this pointer is
|
|
// cleared. Non-null == a capture is in progress (used to reject a second one, and to
|
|
// abort on project switch / unload).
|
|
static reasampler::RealtimeRecordBackend g_rtBackend;
|
|
static reasampler::RealtimeCaptureHandle g_rtCapture;
|
|
|
|
// The ReaProject* the in-flight capture belongs to (opaque, compare-only) — lets
|
|
// OnTimer detect a project switch mid-capture and abort+restore rather than leak the
|
|
// temp track/arm/transport into or across projects. Only meaningful when
|
|
// g_rtCapture != nullptr.
|
|
static ReaProject* g_rtCaptureProject = nullptr;
|
|
|
|
// Commit a finished realtime capture (a Done tick/abort with an Ok result): add the
|
|
// Sample to the ACTIVE bank (g_session.bank() resolves to book.activeIndex() — B2),
|
|
// persist + MarkProjectDirty. Shared by the tick-completion path and the abort
|
|
// paths. On a non-Ok result, logs the failure only.
|
|
static void CommitRealtimeResult(const reasampler::CaptureResult& res)
|
|
{
|
|
if (res.status != reasampler::CaptureStatus::Ok)
|
|
{
|
|
ShowConsoleMsg(("ReaSampler realtime capture failed: " + res.message + "\n").c_str());
|
|
return;
|
|
}
|
|
g_session.bank().add(res.sample);
|
|
// B-cap: record the file the capture created in the owned-file manifest, at the same
|
|
// point the Sample is added and before the same persist. Recorded regardless of the
|
|
// index AddResult — even a hash-collapse still WROTE a file the tool owns, and the
|
|
// manifest dedups a repeat path itself (Phase R prune reconciles manifest vs index).
|
|
g_session.owned().add(res.sample.relativePath);
|
|
// S9: a capture add changes what a live instance could play (a new sample landed in the
|
|
// active bank) -> bump before the persist so the stamped generation refreshes instances.
|
|
g_session.bumpBankGeneration();
|
|
g_session.saveToActiveProject(); // persist book + manifest + generation + MarkProjectDirty (travels with .rpp)
|
|
}
|
|
|
|
// Advance any in-flight realtime capture one tick. Cheap when none is running (a
|
|
// null check) and fast even mid-record (tick() only reads the transport until the
|
|
// terminal tick). Detects a project switch mid-capture and aborts+restores so the
|
|
// capture never leaks across projects. Called from OnTimer BEFORE session.poll() so
|
|
// poll's project-switch handling sees a cleaned-up project.
|
|
static void DriveRealtimeCapture()
|
|
{
|
|
if (!g_rtCapture) return;
|
|
|
|
// Project switch guard: if the active project is no longer the one the capture
|
|
// belongs to, a new/other project became active mid-record — abort + restore
|
|
// (into the ORIGINAL project the state is bound to) and drop it. Do NOT finalize
|
|
// into the new project.
|
|
ReaProject* active = EnumProjects(-1, nullptr, 0);
|
|
if (active != g_rtCaptureProject)
|
|
{
|
|
reasampler::RealtimeTickResult r = g_rtBackend.abort(*g_rtCapture);
|
|
// Only commit if the ORIGINAL project is still open and active would be it —
|
|
// on a switch we restored into the original but must not persist into the
|
|
// now-active foreign project. Log the outcome without persisting. On a Failed
|
|
// abort surface abort()'s own message — it distinguishes a clean tab-switch
|
|
// abort from the closed-project DROP (the captured project was closed mid-record,
|
|
// review §1: nothing restored because the pointers were already freed).
|
|
if (r.status == reasampler::RealtimeTickStatus::Done)
|
|
ShowConsoleMsg("ReaSampler realtime capture: project switched mid-record -- "
|
|
"captured audio restored into the original project; not "
|
|
"persisted to avoid crossing projects.\n");
|
|
else
|
|
ShowConsoleMsg(("ReaSampler realtime capture: project changed mid-record -- " +
|
|
r.result.message + "\n").c_str());
|
|
g_rtCapture.reset();
|
|
g_rtCaptureProject = nullptr;
|
|
return;
|
|
}
|
|
|
|
reasampler::RealtimeTickResult r = g_rtBackend.tick(*g_rtCapture);
|
|
if (r.status == reasampler::RealtimeTickStatus::InProgress) return;
|
|
|
|
// Terminal (Done or Failed): commit/log and drop the in-flight state.
|
|
CommitRealtimeResult(r.result);
|
|
g_rtCapture.reset();
|
|
g_rtCaptureProject = nullptr;
|
|
}
|
|
|
|
// 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.
|
|
DriveRealtimeCapture();
|
|
|
|
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 <EXTSTATE> 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 <EXTSTATE> 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
|
|
};
|
|
|
|
// --- Scope-action source resolution -----------------------------------------
|
|
// The three scope actions (item / track / master) each resolve to (1) an exact
|
|
// render range in project seconds — razor-else-time, inferred here — and (2) the
|
|
// set of source TRACKS whose ancestor chains drive the FX-bypass plan. All reads
|
|
// are non-destructive: selection, razor, and time selection are read, never
|
|
// mutated. Returning false means "nothing to capture" (empty selection / no
|
|
// range); the caller reports it and writes nothing.
|
|
|
|
// The resolved source: exact bounds + the source tracks (for FX-bypass + Sample
|
|
// provenance GUIDs). `sourceTracks` holds the item-owning tracks (Item scope) or the
|
|
// selected tracks (Track scope).
|
|
struct ResolvedSource
|
|
{
|
|
double startSeconds = 0.0;
|
|
double endSeconds = 0.0;
|
|
std::vector<MediaTrack*> sourceTracks; // item-owning tracks / selected tracks
|
|
std::vector<std::string> trackGuids; // canonical GUIDs of sourceTracks
|
|
};
|
|
|
|
// Time selection -> exact bounds (no rounding). GetSet_LoopTimeRange(isSet=false,
|
|
// isLoop=false) reads the current time selection.
|
|
static bool resolveTimeSelection(double& start, double& end)
|
|
{
|
|
start = 0.0; end = 0.0;
|
|
GetSet_LoopTimeRange(false, false, &start, &end, false);
|
|
return end > start;
|
|
}
|
|
|
|
// Reads every track's P_RAZOREDITS (SDK header ~2899: space-separated triples of
|
|
// start, end, envGuidString), parses the track-audio areas (pure parseRazorEdits),
|
|
// and returns the union bound. Reads only — never clears the razor selection.
|
|
// Returns false when no track-audio razor area exists on any track.
|
|
static bool resolveRazorRange(double& start, double& end)
|
|
{
|
|
std::vector<reasampler::RazorRange> allRanges;
|
|
const int n = CountTracks(nullptr);
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
MediaTrack* tr = GetTrack(nullptr, i);
|
|
if (!tr) continue;
|
|
std::vector<char> buf(8192, '\0');
|
|
if (!GetSetMediaTrackInfo_String(tr, "P_RAZOREDITS", buf.data(), false))
|
|
continue;
|
|
std::vector<reasampler::RazorRange> ranges =
|
|
reasampler::parseRazorEdits(std::string(buf.data()));
|
|
for (auto& r : ranges) allRanges.push_back(r);
|
|
}
|
|
if (allRanges.empty()) return false;
|
|
reasampler::RazorRange u = reasampler::razorUnionBounds(allRanges);
|
|
start = u.startSeconds;
|
|
end = u.endSeconds;
|
|
return end > start;
|
|
}
|
|
|
|
// Infers the render RANGE for any scope: razor union when a razor area is present,
|
|
// else the time selection (pure inferRangeSource decides which). Orthogonal to
|
|
// scope. Returns false (with a reason) when neither yields a non-empty range.
|
|
static bool resolveRange(double& start, double& end, std::string& why)
|
|
{
|
|
double rzStart = 0.0, rzEnd = 0.0;
|
|
const bool hasRazor = resolveRazorRange(rzStart, rzEnd);
|
|
if (reasampler::inferRangeSource(hasRazor) == reasampler::RangeSource::Razor)
|
|
{
|
|
start = rzStart; end = rzEnd;
|
|
return true; // resolveRazorRange already verified end > start
|
|
}
|
|
if (resolveTimeSelection(start, end)) return true;
|
|
why = "make a razor area or a time selection first";
|
|
return false;
|
|
}
|
|
|
|
// Current project's directory (parent of its .rpp), forward-slashed, no trailing
|
|
// slash — the same derivation capture.cpp does internally, needed here so M10 can
|
|
// resolve the bank's relative paths to absolute for parent detection. Empty for an
|
|
// unsaved project (EnumProjects writes an empty .rpp path), which makes every bank
|
|
// file resolve empty -> no false parentage. Read-only; mutates nothing.
|
|
static std::string currentProjectDir()
|
|
{
|
|
std::vector<char> buf(4096, '\0');
|
|
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
|
const std::string rpp(buf.data());
|
|
if (rpp.empty()) return {};
|
|
namespace fs = std::filesystem;
|
|
std::string dir = fs::path(rpp).parent_path().string();
|
|
for (char& c : dir) if (c == '\\') c = '/';
|
|
if (dir.size() > 1 && dir.back() == '/') dir.pop_back();
|
|
return dir;
|
|
}
|
|
|
|
// Maps a capture FX scope onto the pure provenance scope (kept decoupled so the
|
|
// pure provenance module does not depend on render_settings).
|
|
static reasampler::ProvenanceScope provenanceScopeFor(reasampler::CaptureScope scope)
|
|
{
|
|
return scope == reasampler::CaptureScope::Item ? reasampler::ProvenanceScope::Item
|
|
: reasampler::ProvenanceScope::Track;
|
|
}
|
|
|
|
// Builds the M10 provenance for a capture IF it genuinely resamples from a bank
|
|
// sample, else returns nullopt (the common, non-resample case). Detection rule
|
|
// (stated honestly): the capture's source item media file(s) must all resolve, by
|
|
// exact normalized absolute path, to ONE bank sample's file (detectParent). On a
|
|
// match, records that sample's id as the parent plus a THIN capture-recipe
|
|
// fingerprint (P1=a) — scope + source mode + exact range + tail + rate + channels +
|
|
// source track GUIDs + the in-scope source FX-chain identity — so "re-capture from
|
|
// source" can replay the request and report drift. NEVER a serialized chain to
|
|
// restore. Item scope reads the active take's TakeFX chain (via TakeFX_*) per
|
|
// selected item, combined in item order; Track scope reads the track FX chain.
|
|
static std::optional<reasampler::Provenance> buildCaptureProvenance(
|
|
const reasampler::CaptureRequest& req,
|
|
reasampler::CaptureScope scope,
|
|
const ResolvedSource& src)
|
|
{
|
|
const std::string projectDir = currentProjectDir();
|
|
const std::vector<reasampler::BankFileRef> bankFiles =
|
|
reasampler::bankFileRefs(g_session.book(), projectDir);
|
|
|
|
// The "what audio is being captured" source set depends on scope: item scope uses
|
|
// the SELECTED items (the user picked them); track scope uses the range-overlapping
|
|
// items ON the source tracks (the user picked the track, not the item).
|
|
const std::vector<std::string> sourceFiles =
|
|
scope == reasampler::CaptureScope::Item
|
|
? reasampler::selectedItemSourceFiles()
|
|
: reasampler::trackItemSourceFiles(src.sourceTracks, req.startSeconds,
|
|
req.endSeconds);
|
|
|
|
const std::optional<std::string> parentId =
|
|
reasampler::detectParent(sourceFiles, bankFiles);
|
|
if (!parentId) return std::nullopt; // not a resample-from-sample — no provenance
|
|
|
|
reasampler::CaptureRecipe recipe;
|
|
recipe.scope = provenanceScopeFor(scope);
|
|
recipe.sourceMode = static_cast<int>(req.sourceMode);
|
|
recipe.startSeconds = req.startSeconds;
|
|
recipe.endSeconds = req.endSeconds;
|
|
recipe.tailMode = static_cast<int>(req.tailMode);
|
|
recipe.tailMs = req.tailMs;
|
|
recipe.sampleRate = req.sampleRate;
|
|
recipe.channelCount = req.channelCount;
|
|
recipe.trackGuids = req.trackGuids;
|
|
// The in-scope FX-chain identity:
|
|
// Track scope — per-track chains combined in track order (TrackFX_*).
|
|
// Item scope — per-item active-take chains combined in item order (TakeFX_*);
|
|
// the owning track's FX chain is OUT OF SCOPE for an item capture and must
|
|
// not be fingerprinted here (it is bypassed during render, not heard).
|
|
if (scope == reasampler::CaptureScope::Item) {
|
|
const int n = CountSelectedMediaItems(nullptr);
|
|
std::vector<MediaItem*> items;
|
|
items.reserve(static_cast<std::size_t>(n < 0 ? 0 : n));
|
|
for (int i = 0; i < n; ++i) {
|
|
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
|
if (it) items.push_back(it);
|
|
}
|
|
recipe.fxChainIdentity = reasampler::fxChainIdentityForItems(items);
|
|
} else {
|
|
std::vector<std::string> perTrack;
|
|
perTrack.reserve(src.sourceTracks.size());
|
|
for (MediaTrack* tr : src.sourceTracks)
|
|
perTrack.push_back(reasampler::fxChainIdentityForTrack(tr));
|
|
recipe.fxChainIdentity = reasampler::combineChainIdentities(perTrack);
|
|
}
|
|
|
|
reasampler::Provenance prov;
|
|
prov.parentSampleId = *parentId;
|
|
prov.fxChainSnapshot = reasampler::buildFingerprint(recipe);
|
|
return prov;
|
|
}
|
|
|
|
// Collects the selected tracks (Track scope) into out.sourceTracks + GUIDs.
|
|
static bool collectSelectedTracks(ResolvedSource& out)
|
|
{
|
|
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
|
if (n <= 0) return false;
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
MediaTrack* tr = GetSelectedTrack(nullptr, i);
|
|
if (!tr) continue;
|
|
out.sourceTracks.push_back(tr);
|
|
std::string g = reasampler::guidString(tr);
|
|
if (!g.empty()) out.trackGuids.push_back(std::move(g));
|
|
}
|
|
return !out.sourceTracks.empty();
|
|
}
|
|
|
|
// Collects the tracks that own the selected items (Item scope) into
|
|
// out.sourceTracks (deduped) — these are the tracks whose FX must be bypassed so an
|
|
// item capture hears take/item FX only. GetMediaItem_Track(item) gives the owning
|
|
// track (SDK header, verify). GUIDs recorded for provenance.
|
|
static bool collectSelectedItemTracks(ResolvedSource& out)
|
|
{
|
|
const int n = CountSelectedMediaItems(nullptr);
|
|
if (n <= 0) return false;
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
|
if (!it) continue;
|
|
MediaTrack* tr = GetMediaItem_Track(it);
|
|
if (!tr) continue;
|
|
// Dedup: several selected items can share a track.
|
|
bool seen = false;
|
|
for (MediaTrack* t : out.sourceTracks) if (t == tr) { seen = true; break; }
|
|
if (seen) continue;
|
|
out.sourceTracks.push_back(tr);
|
|
std::string g = reasampler::guidString(tr);
|
|
if (!g.empty()) out.trackGuids.push_back(std::move(g));
|
|
}
|
|
return !out.sourceTracks.empty();
|
|
}
|
|
|
|
// Resolves the source for a scope: the selection tracks (item/track), plus the
|
|
// inferred range. Returns false with a reason on nothing to do.
|
|
static bool ResolveScopeSource(reasampler::CaptureScope scope,
|
|
ResolvedSource& out, std::string& why)
|
|
{
|
|
using reasampler::CaptureScope;
|
|
switch (scope)
|
|
{
|
|
case CaptureScope::Item:
|
|
if (!collectSelectedItemTracks(out)) {
|
|
why = "select at least one media item"; return false;
|
|
}
|
|
break;
|
|
case CaptureScope::Track:
|
|
if (!collectSelectedTracks(out)) {
|
|
why = "select at least one track"; return false;
|
|
}
|
|
break;
|
|
}
|
|
return resolveRange(out.startSeconds, out.endSeconds, why);
|
|
}
|
|
|
|
// --- FX-bypass + full parent-chain neutralize around render (RAII, non-destr.) --
|
|
// For every track a scope must NOT hear the FX of, this ALSO neutralizes that
|
|
// track's fader gain AND its full pan chain (pan/width/law/mode) for the render —
|
|
// because a Track/Item capture renders via master and would otherwise sum through
|
|
// the parent/folder/master FADERS and PAN/WIDTH/LAW, printing their gain and pan
|
|
// coloring into the file (Daniel: the capture is likely re-routed through that
|
|
// same chain later, so parent/master level and pan must not be baked in). The
|
|
// neutralize set is IDENTICAL to the FX-bypass set:
|
|
// Item -> own track + all ancestors + master (take vol/pan kept: item content).
|
|
// Track -> all ancestors + master (selected track's OWN vol/pan kept).
|
|
// (Master is a bypass TARGET for both scopes — never a scope of its own.)
|
|
//
|
|
// Per track in that set we snapshot & set the full parent-chain-independence set,
|
|
// so a Track/Item capture is uncolored by the parent/folder/master it renders
|
|
// through — no FX, no fader, and no pan/width/law/mode coloring:
|
|
// I_FXEN -> 0 (FX bypassed; SDK ~2194)
|
|
// D_VOL -> 1.0 (unity trim volume; SDK ~2226 "1=+0dB")
|
|
// D_PAN -> 0.0 (center; SDK ~2227 "trim pan of track, -1..1")
|
|
// D_WIDTH -> 1.0 (full/neutral stereo width; SDK ~2228 "width, -1..1",
|
|
// 1.0 = full width = no narrowing/collapse)
|
|
// D_PANLAW -> 1.0 (no coloring; SDK ~2232 "1=+0dB" — pan-law applies no gain)
|
|
// I_PANMODE -> 5 (stereo pan; SDK ~2231 "0=classic,3=balance,5=stereo,6=dual")
|
|
// All are restored to their ORIGINAL values on EVERY exit path (RAII).
|
|
//
|
|
// Why also force I_PANMODE (pan mode). D_PAN's effect is mode-dependent. In modes
|
|
// 0/3/5, D_PAN=0 + D_WIDTH=1 is a provable pass-through. But in mode 6 (dual pan)
|
|
// D_PAN/D_WIDTH are ignored — routing is governed instead by D_DUALPANL/D_DUALPANR
|
|
// (SDK ~2229-2230, live only when I_PANMODE==6), whose neutral pass-through the
|
|
// header does not state as such. Rather than snapshot two more mode-conditional
|
|
// params and infer their neutral values, we force I_PANMODE=5 (stereo pan) for the
|
|
// render, where D_PAN=0 + D_WIDTH=1 is unambiguously uncolored, then restore the
|
|
// original mode. This fully neutralizes pan for every original mode with no
|
|
// residual — the "handle it fully" the brief requires. (See Snap dual-pan note.)
|
|
//
|
|
// Structurally non-destructive: no takes, no items, no project restructuring —
|
|
// only transient FX-enable + trim-volume toggles, always restored.
|
|
class FxBypassGuard
|
|
{
|
|
public:
|
|
// scope drives fxBypassPlanFor; sourceTracks are the captured tracks whose
|
|
// ancestor chains (walked via GetParentTrack) + the master are bypassed per the
|
|
// plan. proj is the active project (for GetMasterTrack).
|
|
FxBypassGuard(reasampler::CaptureScope scope,
|
|
const std::vector<MediaTrack*>& sourceTracks,
|
|
ReaProject* proj)
|
|
{
|
|
const reasampler::FxBypassPlan plan = reasampler::fxBypassPlanFor(scope);
|
|
|
|
for (MediaTrack* tr : sourceTracks)
|
|
{
|
|
if (!tr) continue;
|
|
if (plan.bypassSelfFx) bypass(tr);
|
|
if (plan.bypassAncestorFx)
|
|
{
|
|
// Walk parents to the top: GetParentTrack returns the immediate
|
|
// parent (folder) track, nullptr at the outermost level (SDK
|
|
// header ~2407). The master is NOT returned here — handled below.
|
|
for (MediaTrack* p = GetParentTrack(tr); p; p = GetParentTrack(p))
|
|
bypass(p);
|
|
}
|
|
}
|
|
if (plan.bypassMaster)
|
|
{
|
|
// GetMasterTrack(proj) -> the master track (SDK header ~1925). bypass()
|
|
// neutralizes its FX (I_FXEN), gain (D_VOL) AND pan/width/law/mode on it
|
|
// just like any other in-scope track; only the master's summing/routing
|
|
// topology (the mix bus itself) remains — that is not a per-track param.
|
|
if (MediaTrack* master = GetMasterTrack(proj)) bypass(master);
|
|
}
|
|
}
|
|
|
|
~FxBypassGuard()
|
|
{
|
|
// Restore in reverse for symmetry (order is not load-bearing — each track
|
|
// appears once, snapshots are independent). EVERY snapshotted param is
|
|
// restored to its ORIGINAL value on this (every) exit path. Restore
|
|
// I_PANMODE before the pan values so any mode-conditional params (e.g. dual
|
|
// pan) settle under the original mode.
|
|
for (auto it = snapshots_.rbegin(); it != snapshots_.rend(); ++it)
|
|
{
|
|
SetMediaTrackInfo_Value(it->track, "I_FXEN", it->fxen);
|
|
SetMediaTrackInfo_Value(it->track, "D_VOL", it->vol);
|
|
SetMediaTrackInfo_Value(it->track, "I_PANMODE", it->panmode);
|
|
SetMediaTrackInfo_Value(it->track, "D_PAN", it->pan);
|
|
SetMediaTrackInfo_Value(it->track, "D_WIDTH", it->width);
|
|
SetMediaTrackInfo_Value(it->track, "D_PANLAW", it->panlaw);
|
|
}
|
|
}
|
|
|
|
FxBypassGuard(const FxBypassGuard&) = delete;
|
|
FxBypassGuard& operator=(const FxBypassGuard&) = delete;
|
|
|
|
private:
|
|
// One snapshot per bypassed track: all params we neutralize, at their originals.
|
|
// panmode captures I_PANMODE so we can force stereo-pan for the render and put
|
|
// the original mode back — which also makes D_DUALPANL/D_DUALPANR (live only when
|
|
// I_PANMODE==6, SDK ~2229-2230) irrelevant during the render without us having to
|
|
// touch or guess neutral values for them.
|
|
struct Snap
|
|
{
|
|
MediaTrack* track;
|
|
double fxen;
|
|
double vol;
|
|
double pan;
|
|
double width;
|
|
double panlaw;
|
|
double panmode;
|
|
};
|
|
std::vector<Snap> snapshots_;
|
|
|
|
// Snapshot every neutralized param once per track (dedup: an ancestor shared by
|
|
// two selected tracks must be restored to its ORIGINAL values, not to a
|
|
// re-snapshot of the already-neutralized state), then read ALL originals, push
|
|
// one Snap, and set all to neutral — bypass FX, unity gain, uncolored pan chain.
|
|
void bypass(MediaTrack* tr)
|
|
{
|
|
for (const Snap& s : snapshots_) if (s.track == tr) return; // already done
|
|
// Read ALL originals first (atomic snapshot), then push, then neutralize.
|
|
const double fxen = GetMediaTrackInfo_Value(tr, "I_FXEN");
|
|
const double vol = GetMediaTrackInfo_Value(tr, "D_VOL");
|
|
const double pan = GetMediaTrackInfo_Value(tr, "D_PAN");
|
|
const double width = GetMediaTrackInfo_Value(tr, "D_WIDTH");
|
|
const double panlaw = GetMediaTrackInfo_Value(tr, "D_PANLAW");
|
|
const double panmode = GetMediaTrackInfo_Value(tr, "I_PANMODE");
|
|
snapshots_.push_back({tr, fxen, vol, pan, width, panlaw, panmode});
|
|
SetMediaTrackInfo_Value(tr, "I_FXEN", 0.0); // 0 = bypassed (SDK ~2194)
|
|
SetMediaTrackInfo_Value(tr, "D_VOL", 1.0); // 1.0 = unity gain (SDK ~2226)
|
|
SetMediaTrackInfo_Value(tr, "I_PANMODE", 5.0); // 5 = stereo pan (SDK ~2231)
|
|
SetMediaTrackInfo_Value(tr, "D_PAN", 0.0); // 0.0 = center (SDK ~2227)
|
|
SetMediaTrackInfo_Value(tr, "D_WIDTH", 1.0); // 1.0 = full width (SDK ~2228)
|
|
SetMediaTrackInfo_Value(tr, "D_PANLAW", 1.0); // 1.0 = +0dB, no law (SDK ~2232)
|
|
}
|
|
};
|
|
|
|
// Renders one CaptureRequest through the offline backend under the scope's
|
|
// FX-bypass guard, returning the backend's CaptureResult. Shared by RunCapture and
|
|
// RunRecaptureFromSource so the FX-scope neutralize + render recipe lives in ONE
|
|
// place: the out-of-scope FX / fader / pan chain is snapshotted, neutralized for the
|
|
// render, and fully restored on every path (RAII). Non-destructive; touches no
|
|
// timeline item (load-bearing principle) — it writes a file only.
|
|
static reasampler::CaptureResult renderOffline(
|
|
reasampler::CaptureScope scope,
|
|
const std::vector<MediaTrack*>& sourceTracks,
|
|
const reasampler::CaptureRequest& req)
|
|
{
|
|
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
|
FxBypassGuard fxGuard(scope, sourceTracks, proj);
|
|
reasampler::OfflineRenderBackend backend;
|
|
return backend.capture(req);
|
|
}
|
|
|
|
// Renders ONE capture request under the scope's FX-bypass guard, stamps provenance,
|
|
// and adds the resulting Sample to the ACTIVE bank + records the created file in the
|
|
// owned-file manifest — WITHOUT persisting. The caller persists once (single-capture:
|
|
// right after; batch: once at the end) so a batch does not write ext state N times.
|
|
//
|
|
// Provenance is read from the LIVE selection here, so a batch that transiently
|
|
// selects exactly one item per unit gets per-unit-correct provenance. `src` supplies
|
|
// the source tracks (FX bypass + Sample GUIDs); `scope` drives the bypass plan and
|
|
// provenance scope. Returns the backend's CaptureResult (status + message) so the
|
|
// caller can report success/failure. Load-bearing principle holds: writes a file +
|
|
// a bank index entry ONLY; never touches the arrange/timeline. Non-destructive: the
|
|
// out-of-scope FX/fader/pan chain is fully restored on every path (FxBypassGuard),
|
|
// and the backend restores every RENDER_* setting.
|
|
//
|
|
// On success, res.sample.id carries the LANDED bank-index id (S8): the newly-added id
|
|
// on a fresh add, or the EXISTING entry's id on a hash-dedup collapse — so the S8
|
|
// capture+assign path can target the sample actually in the bank. Batch callers ignore
|
|
// it; the plain capture actions are unaffected.
|
|
static reasampler::CaptureResult captureAndIndexOne(
|
|
reasampler::CaptureScope scope,
|
|
const ResolvedSource& src,
|
|
const std::string& baseName,
|
|
double startSeconds,
|
|
double endSeconds)
|
|
{
|
|
// The tail mode is a PANEL SETTING (docked bank panel's toggle), not a per-action
|
|
// variant: the capture actions apply whatever the panel is set to. Default is None
|
|
// (exact bounds / byte-identical to today) until the user opts in via the toggle.
|
|
const reasampler::TailSetting tail = reasampler::bankPanelTailSetting();
|
|
|
|
reasampler::CaptureRequest req;
|
|
req.sourceMode = reasampler::sourceModeForScope(scope);
|
|
req.startSeconds = startSeconds; // exact bounds — no rounding
|
|
req.endSeconds = endSeconds;
|
|
req.wetDry = 1.0; // wet post the FX left enabled by the scope
|
|
req.tailMode = tail.mode; // None / Auto / Manual, from the panel toggle
|
|
req.tailMs = tail.manualMs; // Manual-only (clamped); ignored for None/Auto
|
|
req.sampleRate = 0; // follow project rate
|
|
req.channelCount = 2;
|
|
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
|
|
req.baseName = baseName;
|
|
req.trackGuids = src.trackGuids; // recorded on the Sample (provenance)
|
|
|
|
// M10: compute provenance BEFORE the FxBypassGuard neutralizes the in-scope chain —
|
|
// the source FX-chain identity must be read from the LIVE (un-bypassed) chain, and
|
|
// the source selection is still live here. Returns nullopt unless this capture
|
|
// genuinely resamples from a bank sample (detectParent). Read-only.
|
|
const std::optional<reasampler::Provenance> prov =
|
|
buildCaptureProvenance(req, scope, src);
|
|
|
|
// Render under the scope's FX-bypass guard (out-of-scope FX / fader / pan chain
|
|
// neutralized for the render, fully restored on every path). Writes a file only.
|
|
reasampler::CaptureResult res = renderOffline(scope, src.sourceTracks, req);
|
|
if (res.status != reasampler::CaptureStatus::Ok)
|
|
return res;
|
|
|
|
// Stamp provenance onto the captured Sample (only set when this was a genuine
|
|
// resample-from-sample; otherwise the optional stays empty, per M1's contract).
|
|
res.sample.provenance = prov;
|
|
|
|
// Add to the ACTIVE bank: g_session.bank() resolves to book.activeIndex() (B2). The
|
|
// AddResult tells a fresh add from a hash-dedup collapse, so the assign path (S8) can
|
|
// target the sample actually in the bank (the existing entry on a collapse).
|
|
const reasampler::AddResult addResult = g_session.bank().add(res.sample);
|
|
// B-cap: record the created file in the owned-file manifest, at the same point the
|
|
// Sample is added. Recorded regardless of the index AddResult — even a hash-collapse
|
|
// still WROTE a file the tool owns, and the manifest dedups a repeat path itself
|
|
// (Phase R prune reconciles manifest vs index later).
|
|
g_session.owned().add(res.sample.relativePath);
|
|
|
|
// Resolve the LANDED bank-index id into res.sample.id for the S8 assign path: the new
|
|
// id on a fresh Added (already in res.sample.id); the EXISTING entry's id on a
|
|
// Collapsed (the file we just rendered deduped onto an already-present sample — assign
|
|
// THAT one). Batch/plain-capture callers ignore this field; behaviour unchanged.
|
|
if (addResult == reasampler::AddResult::Collapsed && !res.sample.contentHash.empty())
|
|
{
|
|
if (const reasampler::Sample* existing =
|
|
g_session.bank().findByHash(res.sample.contentHash))
|
|
res.sample.id = existing->id;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// Runs one capture-action-table row: resolve its scope source + range, render + add +
|
|
// record via captureAndIndexOne, then persist + mark dirty. The load-bearing principle
|
|
// holds structurally — this path writes a file + a bank index entry ONLY; it never
|
|
// calls InsertMedia or touches the arrange/timeline.
|
|
// Returns the bank-index id of the sample the capture landed on: the newly-added id on a
|
|
// fresh capture, or the EXISTING id on a hash-dedup collapse (so an ingest-with-assign
|
|
// targets the sample actually in the bank). Empty on any failure / no-op. The S8 arrange
|
|
// capture+assign path reads this to write an assignment request; the plain capture actions
|
|
// ignore it (their behaviour is unchanged — capture still writes a file + index entry only).
|
|
static std::string RunCapture(const reasampler::CaptureActionDef& def)
|
|
{
|
|
ResolvedSource src;
|
|
std::string why;
|
|
if (!ResolveScopeSource(def.scope, src, why))
|
|
{
|
|
ShowConsoleMsg(("ReaSampler capture: " + why + ".\n").c_str());
|
|
return {};
|
|
}
|
|
|
|
reasampler::CaptureResult res =
|
|
captureAndIndexOne(def.scope, src, def.baseName, src.startSeconds, src.endSeconds);
|
|
if (res.status != reasampler::CaptureStatus::Ok)
|
|
{
|
|
ShowConsoleMsg(("ReaSampler capture failed: " + res.message + "\n").c_str());
|
|
return {};
|
|
}
|
|
|
|
// captureAndIndexOne has already stamped provenance, added the Sample to the ACTIVE
|
|
// bank, and recorded the created file in the owned-file manifest (WITHOUT persisting).
|
|
// Persist the updated book AND manifest into the active project's ext state (the
|
|
// `banks` + `owned_files` keys) so the capture survives Save / close+reopen (M4) and
|
|
// travels with the .rpp. saveToActiveProject also clears the retired legacy key and
|
|
// calls MarkProjectDirty. Non-destructive: writes only our own ext-state keys.
|
|
// S9: a capture add is a bank-content change -> bump before the persist so an assigned
|
|
// live instance refreshes hands-free (the S8 capture+assign path builds on this).
|
|
g_session.bumpBankGeneration();
|
|
g_session.saveToActiveProject();
|
|
|
|
// Hand the LANDED bank-index id back to the assign path (S8): captureAndIndexOne
|
|
// resolved res.sample.id to the fresh id on a new add or the existing entry's id on a
|
|
// hash-dedup collapse. Empty on any reject (unreachable here — status was Ok above).
|
|
return res.sample.id;
|
|
}
|
|
|
|
// S8 arrange ingest: capture the selected item / time-selection into the active bank
|
|
// (reusing the Item-scope capture path verbatim) and, on success, write an assignment
|
|
// request so the active sampler instance plays the new sample on its next reload. The
|
|
// capture itself is unchanged — RunCapture writes a file + an index entry and NEVER
|
|
// inserts a timeline item (load-bearing principle); the only addition here is the
|
|
// bank-index-id -> assignment-request write after the sample lands. If the capture
|
|
// failed / no-op'd (empty id), no assignment is written (nothing to assign).
|
|
//
|
|
// UNDO GROUPING: both the bank mutation (RunCapture -> saveToActiveProject) AND the
|
|
// assignment-request write (ingestAssignActiveInstance -> writeAssignmentRequest) are
|
|
// wrapped in a single undo block so Ctrl-Z rolls back both ext-state keys atomically.
|
|
// An undo that removes the captured sample also clears the assign_request that named it,
|
|
// preventing a stale request from pointing at a removed sample. The block uses the house
|
|
// pattern (UNDO_STATE_MISCCFG, discarded on an unsaved project with empty label + zero
|
|
// flag) matching the bank-op family in actions.cpp.
|
|
static void RunCaptureItemAssign()
|
|
{
|
|
// Reuse the Item-scope def from the capture table (index 0) — same range logic, same
|
|
// FX-scope neutralize, same bank/persist landing as the plain "capture item" action.
|
|
Undo_BeginBlock2(nullptr);
|
|
|
|
const std::string sampleId =
|
|
RunCapture(reasampler::captureActionTable()[0]);
|
|
if (sampleId.empty())
|
|
{
|
|
// Capture failed or no-op'd — RunCapture already reported. Discard the empty point.
|
|
Undo_EndBlock2(nullptr, "", 0);
|
|
return;
|
|
}
|
|
|
|
// Assign inside the same block so undo clears both keys together.
|
|
reasampler::ingestAssignActiveInstance(g_session.book().activeBankId(), sampleId);
|
|
Undo_EndBlock2(nullptr, "ReaSampler: capture + assign to active instance",
|
|
UNDO_STATE_MISCCFG);
|
|
|
|
reasampler::bankPanelRefresh();
|
|
ShowConsoleMsg("ReaSampler ingest: captured into the bank and assigned to the active "
|
|
"instance.\n");
|
|
}
|
|
|
|
// --- M11: batch capture (per selected item / per razor area) ----------------
|
|
//
|
|
// One action fires N captures — one bank sample per selected item (item scope) or per
|
|
// razor area (track scope, each area's own range). Each individual capture honors every
|
|
// precision invariant via captureAndIndexOne (exact bounds, non-destructive FX/fader/pan
|
|
// neutralize, relative paths, channel preservation) and M10 provenance stamping applies
|
|
// per capture where its detection rule matches. The load-bearing principle holds: each
|
|
// unit writes a file + a bank index entry ONLY; nothing lands in the arrange.
|
|
//
|
|
// Per-unit FILE NAMING: the offline backend's unique tag is 1-second-granular. The
|
|
// per-item render already takes real wall-clock time (REAPER's offline-render dialog per
|
|
// unit), so consecutive units naturally land in distinct seconds; belt-and-braces, each
|
|
// unit's baseName also carries its ordinal ("item-1", "item-2", ...) so two units are
|
|
// never asked to write the same stem within one batch. (Residual, DAW-verify: two BATCHES
|
|
// fired within the same wall-clock second with identical ordinals could still collide —
|
|
// unreachable in practice given the per-unit render latency, noted for completeness.)
|
|
|
|
// RAII snapshot/restore of the project's media-item selection. Batch item capture must
|
|
// transiently select exactly one item per render (RENDER_SETTINGS &32 renders whatever is
|
|
// selected); the user's ORIGINAL selection must be restored on EVERY exit path — including
|
|
// a mid-batch failure or early return — because selection restoration is part of the
|
|
// non-destructive invariant. Snapshot on construct (the currently-selected item set),
|
|
// restore on destruct (deselect everything, then re-select exactly the snapshot).
|
|
class ItemSelectionGuard
|
|
{
|
|
public:
|
|
ItemSelectionGuard()
|
|
{
|
|
const int n = CountSelectedMediaItems(nullptr);
|
|
for (int i = 0; i < n; ++i)
|
|
if (MediaItem* it = GetSelectedMediaItem(nullptr, i))
|
|
selected_.push_back(it);
|
|
}
|
|
|
|
~ItemSelectionGuard()
|
|
{
|
|
// Deselect every item in the project, then re-select the snapshot — restoring the
|
|
// exact original set regardless of what the batch selected in between. Iterate ALL
|
|
// items (not just the currently-selected) so any transient selection is cleared.
|
|
const int total = CountMediaItems(nullptr);
|
|
for (int i = 0; i < total; ++i)
|
|
if (MediaItem* it = GetMediaItem(nullptr, i))
|
|
SetMediaItemSelected(it, false);
|
|
for (MediaItem* it : selected_)
|
|
SetMediaItemSelected(it, true);
|
|
UpdateArrange(); // reflect the restored selection in the arrange view
|
|
}
|
|
|
|
ItemSelectionGuard(const ItemSelectionGuard&) = delete;
|
|
ItemSelectionGuard& operator=(const ItemSelectionGuard&) = delete;
|
|
|
|
private:
|
|
std::vector<MediaItem*> selected_;
|
|
};
|
|
|
|
// Selects exactly `item` (deselect-all then select-one) so the offline render's
|
|
// selected-items bit (&32) captures a single item. Used inside the batch loop under the
|
|
// ItemSelectionGuard, which restores the user's original selection afterward.
|
|
static void selectOnlyItem(MediaItem* item)
|
|
{
|
|
const int total = CountMediaItems(nullptr);
|
|
for (int i = 0; i < total; ++i)
|
|
if (MediaItem* it = GetMediaItem(nullptr, i))
|
|
SetMediaItemSelected(it, it == item);
|
|
}
|
|
|
|
// Batch item capture: one bank sample per SELECTED item, item scope. Snapshots the
|
|
// selection (RAII restore on every path), then for each selected item transiently selects
|
|
// only it, renders its exact [pos, pos+len] range under item-scope FX neutralize, adds the
|
|
// Sample, and records a per-unit verdict. Persists ONCE at the end (one ext-state write for
|
|
// the whole batch). Reports a mixed-result summary (explicit-action response — allowed).
|
|
static void RunBatchCaptureItems()
|
|
{
|
|
// Read the selected items up front (pointers stay valid — batch mutates only selection
|
|
// flags, never adds/removes items). Also capture each item's exact bounds and owning
|
|
// track NOW, while the full selection is live, before any transient re-selection.
|
|
struct ItemUnit { MediaItem* item; MediaTrack* track; double start; double end; };
|
|
std::vector<ItemUnit> itemUnits;
|
|
{
|
|
const int n = CountSelectedMediaItems(nullptr);
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
|
if (!it) continue;
|
|
MediaTrack* tr = GetMediaItem_Track(it);
|
|
if (!tr) continue;
|
|
const double pos = GetMediaItemInfo_Value(it, "D_POSITION");
|
|
const double len = GetMediaItemInfo_Value(it, "D_LENGTH");
|
|
itemUnits.push_back({it, tr, pos, pos + len});
|
|
}
|
|
}
|
|
if (itemUnits.empty())
|
|
{
|
|
ShowConsoleMsg("ReaSampler batch capture: select at least one media item.\n");
|
|
return;
|
|
}
|
|
|
|
// Plan the exact source ranges -> validated, ordinal-assigned units (pure). Empty/
|
|
// inverted item ranges (a zero-length item) are dropped here so no stray render runs.
|
|
std::vector<reasampler::BatchRange> ranges;
|
|
ranges.reserve(itemUnits.size());
|
|
for (const ItemUnit& u : itemUnits)
|
|
ranges.push_back({u.start, u.end});
|
|
const std::vector<reasampler::CaptureUnit> plan = reasampler::planCaptureUnits(ranges);
|
|
|
|
reasampler::BatchOutcome outcome;
|
|
bool anyAdded = false;
|
|
{
|
|
// Restore the user's ORIGINAL item selection on every exit path (incl. early
|
|
// return / mid-batch failure) — non-destructive invariant.
|
|
ItemSelectionGuard selGuard;
|
|
|
|
// The plan and itemUnits are parallel over the KEPT units. Walk itemUnits, but only
|
|
// for those whose range survived planning (same drop rule), matching by ordinal.
|
|
std::size_t planIdx = 0;
|
|
for (const ItemUnit& u : itemUnits)
|
|
{
|
|
if (!(u.end > u.start)) continue; // dropped by planCaptureUnits — skip in lockstep
|
|
const reasampler::CaptureUnit& unit = plan[planIdx++];
|
|
|
|
// Transiently select ONLY this item so the item-scope render captures exactly it.
|
|
selectOnlyItem(u.item);
|
|
|
|
ResolvedSource src;
|
|
src.startSeconds = unit.startSeconds;
|
|
src.endSeconds = unit.endSeconds;
|
|
src.sourceTracks.push_back(u.track);
|
|
if (std::string g = reasampler::guidString(u.track); !g.empty())
|
|
src.trackGuids.push_back(std::move(g));
|
|
|
|
const std::string baseName = "item-" + std::to_string(unit.ordinal);
|
|
reasampler::CaptureResult res = captureAndIndexOne(
|
|
reasampler::CaptureScope::Item, src, baseName,
|
|
unit.startSeconds, unit.endSeconds);
|
|
|
|
const bool ok = (res.status == reasampler::CaptureStatus::Ok);
|
|
outcome.record(unit.ordinal, ok, ok ? std::string{} : res.message);
|
|
if (ok) anyAdded = true;
|
|
}
|
|
} // selGuard restores the original selection here, on every path
|
|
|
|
// Persist ONCE for the whole batch (one ext-state write) — only if something landed.
|
|
// S9: one bump for the whole batch (coalesced) — the counter is monotonic, not per-sample,
|
|
// so a single increment past the last-seen value is enough to trigger one instance reload.
|
|
if (anyAdded) {
|
|
g_session.bumpBankGeneration();
|
|
g_session.saveToActiveProject();
|
|
}
|
|
|
|
ShowConsoleMsg((outcome.summaryLine("item") + "\n").c_str());
|
|
}
|
|
|
|
// Collects every track's razor AUDIO areas as (owning track, range) pairs, preserving
|
|
// track order then area order — the batch analog of resolveRazorRange, which unions them.
|
|
// Read-only (never clears the razor selection). Reuses the pure parseRazorEdits parser.
|
|
static std::vector<std::pair<MediaTrack*, reasampler::RazorRange>> collectRazorAreas()
|
|
{
|
|
std::vector<std::pair<MediaTrack*, reasampler::RazorRange>> areas;
|
|
const int n = CountTracks(nullptr);
|
|
for (int i = 0; i < n; ++i)
|
|
{
|
|
MediaTrack* tr = GetTrack(nullptr, i);
|
|
if (!tr) continue;
|
|
std::vector<char> buf(8192, '\0');
|
|
if (!GetSetMediaTrackInfo_String(tr, "P_RAZOREDITS", buf.data(), false))
|
|
continue;
|
|
for (const reasampler::RazorRange& r :
|
|
reasampler::parseRazorEdits(std::string(buf.data())))
|
|
areas.push_back({tr, r});
|
|
}
|
|
return areas;
|
|
}
|
|
|
|
// RAII snapshot/restore of the project's TRACK selection. Batch razor capture must
|
|
// transiently select exactly the area's owning track per render (track scope's &128 bit
|
|
// renders whatever TRACKS are selected); the user's original track selection is restored
|
|
// on EVERY exit path (part of the non-destructive invariant). Mirror of ItemSelectionGuard.
|
|
class TrackSelectionGuard
|
|
{
|
|
public:
|
|
TrackSelectionGuard()
|
|
{
|
|
const int n = CountSelectedTracks(nullptr);
|
|
for (int i = 0; i < n; ++i)
|
|
if (MediaTrack* tr = GetSelectedTrack(nullptr, i))
|
|
selected_.push_back(tr);
|
|
}
|
|
|
|
~TrackSelectionGuard()
|
|
{
|
|
// Deselect every track, then re-select the snapshot — the exact original set.
|
|
const int total = CountTracks(nullptr);
|
|
for (int i = 0; i < total; ++i)
|
|
if (MediaTrack* tr = GetTrack(nullptr, i))
|
|
SetTrackSelected(tr, false);
|
|
for (MediaTrack* tr : selected_)
|
|
SetTrackSelected(tr, true);
|
|
}
|
|
|
|
TrackSelectionGuard(const TrackSelectionGuard&) = delete;
|
|
TrackSelectionGuard& operator=(const TrackSelectionGuard&) = delete;
|
|
|
|
private:
|
|
std::vector<MediaTrack*> selected_;
|
|
};
|
|
|
|
// Batch razor capture: one bank sample per razor AREA, track scope over that area's own
|
|
// range (the area's owning track is the source track). Track scope renders the selected
|
|
// TRACKS via master (&128), so each unit transiently selects ONLY its owning track
|
|
// (SetOnlyTrackSelected) under the TrackSelectionGuard, which restores the user's original
|
|
// track selection on every path. The razor selection itself is read-only and left intact.
|
|
// Persists ONCE at the end. Reports a mixed-result summary.
|
|
static void RunBatchCaptureRazor()
|
|
{
|
|
const std::vector<std::pair<MediaTrack*, reasampler::RazorRange>> areas =
|
|
collectRazorAreas();
|
|
if (areas.empty())
|
|
{
|
|
ShowConsoleMsg("ReaSampler batch capture: make at least one razor area first.\n");
|
|
return;
|
|
}
|
|
|
|
std::vector<reasampler::BatchRange> ranges;
|
|
ranges.reserve(areas.size());
|
|
for (const auto& a : areas)
|
|
ranges.push_back({a.second.startSeconds, a.second.endSeconds});
|
|
const std::vector<reasampler::CaptureUnit> plan = reasampler::planCaptureUnits(ranges);
|
|
|
|
reasampler::BatchOutcome outcome;
|
|
bool anyAdded = false;
|
|
{
|
|
// Restore the user's ORIGINAL track selection on every exit path.
|
|
TrackSelectionGuard selGuard;
|
|
|
|
std::size_t planIdx = 0;
|
|
for (const auto& a : areas)
|
|
{
|
|
if (!(a.second.endSeconds > a.second.startSeconds)) continue; // dropped — lockstep
|
|
const reasampler::CaptureUnit& unit = plan[planIdx++];
|
|
MediaTrack* tr = a.first;
|
|
|
|
// Transiently select ONLY this track so the track-scope render (&128) captures
|
|
// exactly it via master (over the custom time bounds we set per unit).
|
|
SetOnlyTrackSelected(tr);
|
|
|
|
ResolvedSource src;
|
|
src.startSeconds = unit.startSeconds;
|
|
src.endSeconds = unit.endSeconds;
|
|
src.sourceTracks.push_back(tr);
|
|
if (std::string g = reasampler::guidString(tr); !g.empty())
|
|
src.trackGuids.push_back(std::move(g));
|
|
|
|
const std::string baseName = "razor-" + std::to_string(unit.ordinal);
|
|
reasampler::CaptureResult res = captureAndIndexOne(
|
|
reasampler::CaptureScope::Track, src, baseName,
|
|
unit.startSeconds, unit.endSeconds);
|
|
|
|
const bool ok = (res.status == reasampler::CaptureStatus::Ok);
|
|
outcome.record(unit.ordinal, ok, ok ? std::string{} : res.message);
|
|
if (ok) anyAdded = true;
|
|
}
|
|
} // selGuard restores the original track selection here, on every path
|
|
|
|
// S9: one coalesced bump for the whole razor batch (see the item-batch note above).
|
|
if (anyAdded) {
|
|
g_session.bumpBankGeneration();
|
|
g_session.saveToActiveProject();
|
|
}
|
|
|
|
ShowConsoleMsg((outcome.summaryLine("razor area") + "\n").c_str());
|
|
}
|
|
|
|
// --- M10: re-capture from source --------------------------------------------
|
|
//
|
|
// Regenerates a PROVENANCED bank sample's file from its recorded source's CURRENT
|
|
// state, then updates the bank Sample IN PLACE. BANK-ONLY — it renders a file and
|
|
// refreshes the index entry; it NEVER calls InsertMedia / touches the timeline (the
|
|
// load-bearing capture-never-places line, structurally visible: this function has no
|
|
// insert path at all). Non-destructive to the source (FxBypassGuard snapshot/restore
|
|
// via renderOffline). Fork P2=a: refresh the bank entry only; the user re-places
|
|
// manually if they want the new version on the timeline.
|
|
//
|
|
// Failure modes are handled explicitly and reported to the user (a direct response
|
|
// to an explicit action is allowed by the console policy):
|
|
// * the selected sample has no provenance (not a resample) -> reported, no-op.
|
|
// * the recorded fingerprint is unparseable (legacy/corrupt) -> reported, no-op.
|
|
// * the recorded source track(s) no longer exist -> reported, no-op.
|
|
// * the render itself fails to satisfy the recorded request -> reported, no-op.
|
|
// On success, if the source FX chain drifted since capture (recorded vs current
|
|
// identity differ) the user is told — the re-capture still reflects the source AS IT
|
|
// IS NOW (P1=a: the fingerprint detects drift, it does not freeze the source).
|
|
static void RunRecaptureFromSource()
|
|
{
|
|
const std::vector<std::string> selected = reasampler::bankPanelSelectedSampleIds();
|
|
if (selected.empty())
|
|
{
|
|
ShowConsoleMsg("ReaSampler re-capture: select a sample in the bank panel first.\n");
|
|
return;
|
|
}
|
|
if (selected.size() > 1)
|
|
{
|
|
ShowConsoleMsg("ReaSampler re-capture: select a single sample to re-capture.\n");
|
|
return;
|
|
}
|
|
const std::string sampleId = selected.front();
|
|
|
|
// Resolve the sample from the bank it lives in (the focused region's displayed bank).
|
|
const std::string srcBankId = reasampler::bankPanelSelectedSourceBankId();
|
|
const reasampler::Bank* bank = g_session.book().bank(srcBankId);
|
|
const reasampler::Sample* orig = bank ? bank->index.query(sampleId) : nullptr;
|
|
if (!orig)
|
|
{
|
|
ShowConsoleMsg("ReaSampler re-capture: the selected sample is no longer in the bank.\n");
|
|
return;
|
|
}
|
|
if (!orig->provenance)
|
|
{
|
|
ShowConsoleMsg("ReaSampler re-capture: this sample has no provenance "
|
|
"(it was not resampled from a bank sample).\n");
|
|
return;
|
|
}
|
|
|
|
// Parse the recorded capture recipe from the fingerprint. A legacy / corrupt
|
|
// string fails gracefully — never a partial re-capture.
|
|
const std::string recordedParentId = orig->provenance->parentSampleId;
|
|
const std::string recordedFingerprint = orig->provenance->fxChainSnapshot;
|
|
const std::optional<reasampler::CaptureRecipe> recipe =
|
|
reasampler::parseFingerprint(recordedFingerprint);
|
|
if (!recipe)
|
|
{
|
|
ShowConsoleMsg("ReaSampler re-capture: this sample's provenance is unreadable "
|
|
"(recorded by an older/incompatible build); cannot re-capture.\n");
|
|
return;
|
|
}
|
|
|
|
// Resolve the recorded source track GUID(s) to live tracks. Any missing track is a
|
|
// hard failure — we will not silently re-capture a different source.
|
|
std::vector<MediaTrack*> sourceTracks;
|
|
for (const std::string& g : recipe->trackGuids)
|
|
{
|
|
MediaTrack* tr = reasampler::trackByGuid(g);
|
|
if (!tr)
|
|
{
|
|
ShowConsoleMsg("ReaSampler re-capture: a recorded source track no longer "
|
|
"exists in this project; cannot re-capture from source.\n");
|
|
return;
|
|
}
|
|
sourceTracks.push_back(tr);
|
|
}
|
|
if (sourceTracks.empty())
|
|
{
|
|
// The recipe recorded no source tracks (e.g. an item-scope capture whose source
|
|
// tracks were not track-scoped). Without a resolvable source we cannot re-run.
|
|
ShowConsoleMsg("ReaSampler re-capture: no resolvable recorded source for this "
|
|
"sample; cannot re-capture from source.\n");
|
|
return;
|
|
}
|
|
|
|
const reasampler::CaptureScope scope =
|
|
recipe->scope == reasampler::ProvenanceScope::Item
|
|
? reasampler::CaptureScope::Item
|
|
: reasampler::CaptureScope::Track;
|
|
|
|
// Rebuild the capture request verbatim from the recorded recipe — the SAME request,
|
|
// re-run against the source's CURRENT state (P1=a). Exact bounds, tail, rate,
|
|
// channels, bit depth all match the original so an unchanged source produces a
|
|
// byte-identical file (bit-identical-repeats invariant, consumed as a feature).
|
|
reasampler::CaptureRequest req;
|
|
req.sourceMode = static_cast<reasampler::SourceMode>(recipe->sourceMode);
|
|
req.startSeconds = recipe->startSeconds;
|
|
req.endSeconds = recipe->endSeconds;
|
|
req.wetDry = 1.0;
|
|
req.tailMode = static_cast<reasampler::TailMode>(recipe->tailMode);
|
|
req.tailMs = recipe->tailMs;
|
|
req.sampleRate = recipe->sampleRate;
|
|
req.channelCount = recipe->channelCount;
|
|
req.bitDepth = reasampler::WavBitDepth::Float32;
|
|
req.baseName = orig->displayName.empty() ? "recapture" : orig->displayName;
|
|
req.trackGuids = recipe->trackGuids;
|
|
|
|
// Read the CURRENT source FX-chain identity BEFORE the render bypasses it, to
|
|
// compare against the recorded identity for drift reporting. Mirror the same
|
|
// scope split as buildCaptureProvenance: item scope reads take FX via TakeFX_*;
|
|
// track scope reads the track FX chain via TrackFX_*.
|
|
std::string currentIdentity;
|
|
if (scope == reasampler::CaptureScope::Item) {
|
|
const int n = CountSelectedMediaItems(nullptr);
|
|
std::vector<MediaItem*> items;
|
|
items.reserve(static_cast<std::size_t>(n < 0 ? 0 : n));
|
|
for (int i = 0; i < n; ++i) {
|
|
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
|
if (it) items.push_back(it);
|
|
}
|
|
currentIdentity = reasampler::fxChainIdentityForItems(items);
|
|
} else {
|
|
std::vector<std::string> perTrackNow;
|
|
perTrackNow.reserve(sourceTracks.size());
|
|
for (MediaTrack* tr : sourceTracks)
|
|
perTrackNow.push_back(reasampler::fxChainIdentityForTrack(tr));
|
|
currentIdentity = reasampler::combineChainIdentities(perTrackNow);
|
|
}
|
|
const bool drifted = (currentIdentity != recipe->fxChainIdentity);
|
|
|
|
// Render (bank-only; renderOffline never touches the timeline).
|
|
reasampler::CaptureResult res = renderOffline(scope, sourceTracks, req);
|
|
if (res.status != reasampler::CaptureStatus::Ok)
|
|
{
|
|
ShowConsoleMsg(("ReaSampler re-capture failed: " + res.message + "\n").c_str());
|
|
return;
|
|
}
|
|
|
|
// Update the Sample IN PLACE: keep its identity (id) and its provenance thread
|
|
// (same parent + a REFRESHED fingerprint reflecting the source as re-captured), but
|
|
// adopt the regenerated file's path / hash / length / rate / timestamp. The
|
|
// fingerprint is rebuilt from the recipe with the CURRENT FX identity so a
|
|
// subsequent re-capture measures drift from this point, not the original.
|
|
reasampler::CaptureRecipe refreshed = *recipe;
|
|
refreshed.fxChainIdentity = currentIdentity;
|
|
|
|
reasampler::Sample updated = *orig; // copy: preserves id, displayName, tier, key
|
|
updated.relativePath = res.sample.relativePath;
|
|
updated.contentHash = res.sample.contentHash;
|
|
updated.sourceMode = res.sample.sourceMode;
|
|
updated.sourceRange = res.sample.sourceRange;
|
|
updated.channelCount = res.sample.channelCount;
|
|
updated.sampleRate = res.sample.sampleRate;
|
|
updated.lengthSeconds = res.sample.lengthSeconds;
|
|
updated.captureTempo = res.sample.captureTempo;
|
|
updated.captureTimeSigNum = res.sample.captureTimeSigNum; // L7 F1: refresh meter stamp
|
|
updated.captureTimeSigDenom = res.sample.captureTimeSigDenom; // to the re-capture's meter
|
|
updated.trackGuids = res.sample.trackGuids;
|
|
updated.createdTimestamp = res.sample.createdTimestamp;
|
|
// NOTE: levels, clipped, and lengthBeats are carried from the original (via the
|
|
// *orig copy above) because the offline backend does not populate them today
|
|
// (res.sample leaves them at defaults). If a later milestone populates these
|
|
// fields at capture time, refresh them here from res.sample instead.
|
|
reasampler::Provenance prov;
|
|
prov.parentSampleId = recordedParentId;
|
|
prov.fxChainSnapshot = reasampler::buildFingerprint(refreshed);
|
|
updated.provenance = prov;
|
|
|
|
// Single batched undo point around the in-place bank mutation (mirrors the bank
|
|
// action family's R-B pattern). The mutation is index-only ext-state; the render
|
|
// wrote a new file but placed nothing on the timeline.
|
|
Undo_BeginBlock2(nullptr);
|
|
const bool changed = g_session.book().updateSampleInPlace(sampleId, updated);
|
|
if (changed)
|
|
{
|
|
// Record the regenerated file in the owned manifest (a new file the tool wrote);
|
|
// the superseded old file becomes an orphan reclaimed by Phase R prune.
|
|
g_session.owned().add(updated.relativePath);
|
|
// S9: re-capture-in-place regenerates the SAME id's audio — the exact case the
|
|
// hands-free refresh exists for (an instance referencing this id keeps playing the
|
|
// OLD audio until it reloads). Bump inside the undo block so undo rolls back the
|
|
// generation with the rest of the blob.
|
|
g_session.bumpBankGeneration();
|
|
const bool persisted = g_session.saveToActiveProject(); // book + manifest + generation + MarkProjectDirty
|
|
Undo_EndBlock2(nullptr, persisted ? "ReaSampler: re-capture from source" : "",
|
|
persisted ? UNDO_STATE_MISCCFG : 0);
|
|
}
|
|
else
|
|
{
|
|
Undo_EndBlock2(nullptr, "", 0); // nothing mutated -> discard the empty point
|
|
}
|
|
|
|
reasampler::bankPanelRefresh(); // reflect the regenerated file in the docked grid
|
|
|
|
if (drifted)
|
|
ShowConsoleMsg("ReaSampler re-capture: the source FX chain changed since the "
|
|
"original capture -- the sample was regenerated from the source's "
|
|
"current state.\n");
|
|
}
|
|
|
|
// STARTS the REALTIME track capture and returns immediately — the record runs across
|
|
// timer ticks (DriveRealtimeCapture), so REAPER's UI stays responsive. Resolves the
|
|
// selected tracks + the range (razor-else-time, the same orthogonal range logic as the
|
|
// offline scopes) and starts recording each selected track's OWN output into a hidden
|
|
// temp track via RealtimeRecordBackend::begin (a send FROM each source track INTO the
|
|
// temp — see capture_realtime.cpp §TAP); OnTimer drives it to completion, then adds the
|
|
// Sample and persists. TRACK scope only this increment (item realtime is deferred).
|
|
// Dialog-free. Non-bit-identical by nature (it is realtime) — offline stays the
|
|
// deterministic default. FxBypassGuard is NOT used here — the track-output tap is
|
|
// PRE-parent by construction (§TAP), so there is no live chain to neutralize. The
|
|
// load-bearing principle holds structurally — this writes a file + a bank entry ONLY;
|
|
// the temp track is a transient sink removed by the backend, nothing lands in arrange.
|
|
//
|
|
// A SECOND realtime capture requested while one is in progress is REJECTED — the
|
|
// first keeps running (we own the transport for its window; starting a second would
|
|
// collide on the transport and the temp-track/arm snapshot).
|
|
static void RunCaptureRealtimeTrack()
|
|
{
|
|
if (g_rtCapture)
|
|
{
|
|
ShowConsoleMsg("ReaSampler realtime capture: a capture is already in "
|
|
"progress -- let it finish (or stop the transport) first.\n");
|
|
return;
|
|
}
|
|
|
|
// Resolve the selected tracks + range exactly as the offline Track scope does.
|
|
// No track selected -> refuse (same no-op as offline track scope).
|
|
ResolvedSource src;
|
|
std::string why;
|
|
if (!ResolveScopeSource(reasampler::CaptureScope::Track, src, why))
|
|
{
|
|
ShowConsoleMsg(("ReaSampler realtime capture: " + why + ".\n").c_str());
|
|
return;
|
|
}
|
|
|
|
// The tail mode is the SAME panel setting the offline capture actions read (the
|
|
// docked bank panel's toggle). Realtime honors it via a parallel path: the backend
|
|
// records a generous window past the range end, then trims by PCM decay-scan (T2 /
|
|
// capture-tail.md §The realtime path) — it does NOT drive RENDER_*. Default None
|
|
// keeps realtime exact-bounds / byte-identical to today.
|
|
const reasampler::TailSetting tail = reasampler::bankPanelTailSetting();
|
|
|
|
reasampler::CaptureRequest req;
|
|
req.sourceMode = reasampler::SourceMode::SelectedTracks; // realtime track scope
|
|
req.startSeconds = src.startSeconds; // exact bounds — no rounding
|
|
req.endSeconds = src.endSeconds;
|
|
req.wetDry = 1.0; // fully wet (post-fader tap)
|
|
req.tailMode = tail.mode; // None / Auto / Manual, from the panel toggle
|
|
req.tailMs = tail.manualMs; // Manual-only (pre-clamped); ignored for None/Auto
|
|
req.sampleRate = 0; // follow project rate
|
|
req.channelCount = 2;
|
|
req.bitDepth = reasampler::WavBitDepth::Float32;
|
|
req.baseName = "realtime";
|
|
req.trackGuids = src.trackGuids; // provenance on the Sample
|
|
|
|
reasampler::CaptureResult failure;
|
|
reasampler::RealtimeCaptureHandle st =
|
|
g_rtBackend.begin(req, src.sourceTracks, failure);
|
|
if (!st)
|
|
{
|
|
// begin() validated/failed and already restored anything it touched.
|
|
ShowConsoleMsg(("ReaSampler realtime capture failed: " + failure.message + "\n").c_str());
|
|
return;
|
|
}
|
|
|
|
// Started. Store the in-flight state + its project; OnTimer drives it to
|
|
// completion across ticks (UI stays responsive).
|
|
g_rtCaptureProject = EnumProjects(-1, nullptr, 0);
|
|
g_rtCapture = std::move(st);
|
|
}
|
|
|
|
// Cancels the in-flight realtime capture on demand (bindable action). Force-terminates
|
|
// via abort() — stop the transport + restore ALL snapshotted state (non-destructive),
|
|
// committing whatever audio was already captured (best effort) so a cancel near the end
|
|
// still keeps the take. Runs only against the record's OWN project (abort() self-guards
|
|
// the closed-project case, review §1). No-op with a note when nothing is in flight.
|
|
static void RunCancelRealtime()
|
|
{
|
|
if (!g_rtCapture)
|
|
{
|
|
ShowConsoleMsg("ReaSampler: no realtime capture in progress to cancel.\n");
|
|
return;
|
|
}
|
|
reasampler::RealtimeTickResult r = g_rtBackend.abort(*g_rtCapture);
|
|
if (r.status == reasampler::RealtimeTickStatus::Done)
|
|
CommitRealtimeResult(r.result); // Ok: keep what was captured up to the cancel
|
|
else
|
|
ShowConsoleMsg(("ReaSampler realtime capture cancelled -- " +
|
|
r.result.message + "\n").c_str());
|
|
g_rtCapture.reset();
|
|
g_rtCaptureProject = nullptr;
|
|
}
|
|
|
|
// Runs the M6 insert: place the bank panel's selected sample(s) at the edit cursor
|
|
// via InsertMedia, undo-wrapped. `conform` selects the explicit opt-in tempo-match
|
|
// variant (never silent — it fires only from the distinct "conform" action). This
|
|
// is the INTENDED placement path: it adds items to the arrange on purpose
|
|
// (CONTEXT.md §load-bearing principle) and runs only from a user-invoked action.
|
|
static void RunInsertSelected(bool conform)
|
|
{
|
|
reasampler::InsertRequest req;
|
|
// target defaults to CurrentTrack (InsertOptions::target) — inserts onto the
|
|
// user's currently-selected track(s) at the edit cursor.
|
|
req.options.conform =
|
|
conform ? reasampler::TempoConform::Ratio1x : reasampler::TempoConform::None;
|
|
// preservePitch stays true: a tempo conform matches tempo without varispeeding
|
|
// pitch. (A pitch-shifting variant is a later opt-in if wanted — YAGNI now.)
|
|
|
|
reasampler::InsertResult res = reasampler::runInsert(&g_session, req);
|
|
|
|
switch (res.status)
|
|
{
|
|
case reasampler::InsertStatus::Ok:
|
|
break; // success — no console chatter
|
|
case reasampler::InsertStatus::NoSelection:
|
|
// "select a track first" is printed by runInsert when no track is
|
|
// selected; this branch covers the no-panel-selection case.
|
|
ShowConsoleMsg("ReaSampler insert: nothing selected in the bank panel.\n");
|
|
break;
|
|
case reasampler::InsertStatus::NoProject:
|
|
ShowConsoleMsg("ReaSampler insert: no saved project, so the bank has no location.\n");
|
|
break;
|
|
case reasampler::InsertStatus::NothingResolved:
|
|
ShowConsoleMsg("ReaSampler insert: selected sample(s) could not be resolved to a file.\n");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
// Three-scope capture family: command ids parallel captureActionTable() 1:1 by index.
|
|
// Claim the fired id if it is one of ours and route to its table row.
|
|
for (std::size_t i = 0; i < g_captureCmdIds.size(); ++i)
|
|
if (command == g_captureCmdIds[i])
|
|
{
|
|
RunCapture(reasampler::captureActionTable()[i]);
|
|
return true;
|
|
}
|
|
if (command == g_cmdToggleBankPanel) { reasampler::bankPanelToggle(); return true; }
|
|
if (command == g_cmdCaptureItemAssign) { RunCaptureItemAssign(); return true; }
|
|
if (command == g_cmdInsertSelected) { RunInsertSelected(false); return true; }
|
|
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); return true; }
|
|
if (command == g_cmdCaptureBatchItems) { RunBatchCaptureItems(); return true; }
|
|
if (command == g_cmdCaptureBatchRazor) { RunBatchCaptureRazor(); return true; }
|
|
if (command == g_cmdCaptureTrackRealtime) { RunCaptureRealtimeTrack(); return true; }
|
|
if (command == g_cmdCancelRealtime) { RunCancelRealtime(); return true; }
|
|
if (command == g_cmdRecaptureFromSource) { RunRecaptureFromSource(); return true; }
|
|
if (command == g_cmdShowVersion)
|
|
{
|
|
// On-demand version readout — the ONLY version output on any path.
|
|
ShowConsoleMsg(("ReaSampler " + reasampler::appVersion() + "\n").c_str());
|
|
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 == g_cmdToggleBankPanel)
|
|
return reasampler::bankPanelIsOpen() ? 1 : 0;
|
|
return -1; // not ours / non-toggling
|
|
}
|
|
|
|
// gaccel storage must outlive registration — REAPER holds the pointer.
|
|
// (The capture family's accels live in g_captureAccels, sized to the table.)
|
|
static gaccel_register_t g_accelToggleBankPanel{};
|
|
static gaccel_register_t g_accelCaptureItemAssign{};
|
|
static gaccel_register_t g_accelInsertSelected{};
|
|
static gaccel_register_t g_accelInsertSelectedConform{};
|
|
static gaccel_register_t g_accelCaptureBatchItems{};
|
|
static gaccel_register_t g_accelCaptureBatchRazor{};
|
|
static gaccel_register_t g_accelCaptureTrackRealtime{};
|
|
static gaccel_register_t g_accelCancelRealtime{};
|
|
static gaccel_register_t g_accelRecaptureFromSource{};
|
|
static gaccel_register_t g_accelShowVersion{};
|
|
|
|
// gaccel desc storage. The Actions-list label is channel-qualified at runtime
|
|
// (channelActionName) so it cannot be a string literal; REAPER holds the gaccel's `desc`
|
|
// pointer, so each label lives here for the module lifetime. Composed once at registration.
|
|
static std::string g_descToggleBankPanel;
|
|
static std::string g_descCaptureItemAssign;
|
|
static std::string g_descInsertSelected;
|
|
static std::string g_descInsertSelectedConform;
|
|
static std::string g_descCaptureBatchItems;
|
|
static std::string g_descCaptureBatchRazor;
|
|
static std::string g_descCaptureTrackRealtime;
|
|
static std::string g_descCancelRealtime;
|
|
static std::string g_descRecaptureFromSource;
|
|
static std::string g_descShowVersion;
|
|
|
|
// Composed command-id strings (channel-qualified), interned so register and the mirroring
|
|
// '-command_id' unregister pass the SAME pointer. Set during registration; read on unload.
|
|
static const char* g_idToggleBankPanel = nullptr;
|
|
static const char* g_idCaptureItemAssign = nullptr;
|
|
static const char* g_idInsertSelected = nullptr;
|
|
static const char* g_idInsertSelectedConform = nullptr;
|
|
static const char* g_idCaptureBatchItems = nullptr;
|
|
static const char* g_idCaptureBatchRazor = nullptr;
|
|
static const char* g_idCaptureTrackRealtime = nullptr;
|
|
static const char* g_idCancelRealtime = nullptr;
|
|
static const char* g_idRecaptureFromSource = nullptr;
|
|
static const char* g_idShowVersion = nullptr;
|
|
|
|
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.
|
|
if (g_rtCapture)
|
|
{
|
|
reasampler::RealtimeTickResult r = g_rtBackend.abort(*g_rtCapture);
|
|
CommitRealtimeResult(r.result);
|
|
g_rtCapture.reset();
|
|
g_rtCaptureProject = nullptr;
|
|
}
|
|
|
|
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);
|
|
// Each '-command_id' re-presents the SAME interned, channel-qualified pointer
|
|
// used at register (g_id*), so the mirror-unregister matches exactly.
|
|
g_rec->Register("-gaccel", (void*)&g_accelShowVersion);
|
|
g_rec->Register("-command_id", (void*)g_idShowVersion);
|
|
g_rec->Register("-gaccel", (void*)&g_accelRecaptureFromSource);
|
|
g_rec->Register("-command_id", (void*)g_idRecaptureFromSource);
|
|
g_rec->Register("-gaccel", (void*)&g_accelCancelRealtime);
|
|
g_rec->Register("-command_id", (void*)g_idCancelRealtime);
|
|
g_rec->Register("-gaccel", (void*)&g_accelCaptureTrackRealtime);
|
|
g_rec->Register("-command_id", (void*)g_idCaptureTrackRealtime);
|
|
g_rec->Register("-gaccel", (void*)&g_accelCaptureBatchRazor);
|
|
g_rec->Register("-command_id", (void*)g_idCaptureBatchRazor);
|
|
g_rec->Register("-gaccel", (void*)&g_accelCaptureBatchItems);
|
|
g_rec->Register("-command_id", (void*)g_idCaptureBatchItems);
|
|
g_rec->Register("-gaccel", (void*)&g_accelInsertSelectedConform);
|
|
g_rec->Register("-command_id", (void*)g_idInsertSelectedConform);
|
|
g_rec->Register("-gaccel", (void*)&g_accelInsertSelected);
|
|
g_rec->Register("-command_id", (void*)g_idInsertSelected);
|
|
g_rec->Register("-gaccel", (void*)&g_accelCaptureItemAssign);
|
|
g_rec->Register("-command_id", (void*)g_idCaptureItemAssign);
|
|
g_rec->Register("-gaccel", (void*)&g_accelToggleBankPanel);
|
|
g_rec->Register("-command_id", (void*)g_idToggleBankPanel);
|
|
// Mirror-unregister the capture family: gaccel + command_id per row, with
|
|
// '-'-prefixed strings (per the contract). The command id is re-composed from
|
|
// the same suffix + channel prefix used at register — identical string.
|
|
{
|
|
const auto& table = reasampler::captureActionTable();
|
|
for (std::size_t i = 0; i < table.size(); ++i)
|
|
{
|
|
if (i < g_captureAccels.size())
|
|
g_rec->Register("-gaccel", (void*)&g_captureAccels[i]);
|
|
const std::string id =
|
|
reasampler::channelCommandId(table[i].commandSuffix);
|
|
g_rec->Register("-command_id", (void*)id.c_str());
|
|
}
|
|
}
|
|
// Retire the removed M7 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 clears its own.
|
|
for (const char* suffix : kRetiredCaptureCmdSuffixes)
|
|
{
|
|
const std::string id = reasampler::channelCommandId(suffix);
|
|
g_rec->Register("-command_id", (void*)id.c_str());
|
|
}
|
|
}
|
|
// 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 three-scope capture action family (command_id -> gaccel per table row).
|
|
// The single hookcommand below routes every fired id back to its row by index.
|
|
// g_captureAccels must be sized BEFORE the loop and never reallocated after —
|
|
// REAPER holds a pointer to each element until we mirror-unregister it.
|
|
{
|
|
const auto& table = reasampler::captureActionTable();
|
|
g_captureCmdIds.assign(table.size(), 0);
|
|
g_captureAccels.assign(table.size(), gaccel_register_t{});
|
|
g_captureDescs.assign(table.size(), std::string{});
|
|
for (std::size_t i = 0; i < table.size(); ++i)
|
|
{
|
|
// Compose the channel-qualified id (prefix + suffix) and label
|
|
// ("ReaSampler[ beta]: " + phrase). The id is interned so unregister re-presents
|
|
// the same pointer; the label lives in g_captureDescs for the gaccel's lifetime.
|
|
const int cmd =
|
|
rec->Register("command_id", (void*)internCmdId(table[i].commandSuffix));
|
|
g_captureCmdIds[i] = cmd;
|
|
if (cmd)
|
|
{
|
|
g_captureDescs[i] =
|
|
reasampler::channelActionName(table[i].descriptionPhrase);
|
|
g_captureAccels[i].accel.cmd = cmd;
|
|
g_captureAccels[i].desc = g_captureDescs[i].c_str();
|
|
rec->Register("gaccel", (void*)&g_captureAccels[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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). Id + label are channel-qualified.
|
|
g_idToggleBankPanel = internCmdId("TOGGLE_BANK_PANEL");
|
|
g_cmdToggleBankPanel = rec->Register("command_id", (void*)g_idToggleBankPanel);
|
|
if (g_cmdToggleBankPanel)
|
|
{
|
|
g_descToggleBankPanel = reasampler::channelActionName("toggle bank panel");
|
|
g_accelToggleBankPanel.accel.cmd = g_cmdToggleBankPanel;
|
|
g_accelToggleBankPanel.desc = g_descToggleBankPanel.c_str();
|
|
rec->Register("gaccel", (void*)&g_accelToggleBankPanel);
|
|
rec->Register("toggleaction", (void*)&OnToggleAction);
|
|
}
|
|
|
|
// Register the S8 "capture selected item / time-selection into bank + assign" action
|
|
// (command_id -> gaccel -> hookcommand). Reuses the Item-scope offline capture path and
|
|
// writes an assignment request so the active instance plays the new sample. Channel-
|
|
// qualified FOREVER-STABLE id (suffix CAPTURE_ITEM_ASSIGN). MIDI-bindable like every
|
|
// capture action. Registered in the capture family (main.cpp) because it leans on the
|
|
// capture render machinery here; the other two ingest surfaces live in the ingest family
|
|
// (Media-Explorer import) and the panel drop callback.
|
|
g_idCaptureItemAssign = internCmdId("CAPTURE_ITEM_ASSIGN");
|
|
g_cmdCaptureItemAssign = rec->Register("command_id", (void*)g_idCaptureItemAssign);
|
|
if (g_cmdCaptureItemAssign)
|
|
{
|
|
g_descCaptureItemAssign = reasampler::channelActionName(
|
|
"capture selected item into bank + assign to active instance");
|
|
g_accelCaptureItemAssign.accel.cmd = g_cmdCaptureItemAssign;
|
|
g_accelCaptureItemAssign.desc = g_descCaptureItemAssign.c_str();
|
|
rec->Register("gaccel", (void*)&g_accelCaptureItemAssign);
|
|
}
|
|
|
|
// Register the M6 insert actions (command_id -> gaccel -> hookcommand). Two
|
|
// variants: native-length (default, no stretch) and the EXPLICIT conform-to-
|
|
// tempo opt-in. Both read the bank panel selection and place at the edit cursor.
|
|
g_idInsertSelected = internCmdId("INSERT_SELECTED");
|
|
g_cmdInsertSelected = rec->Register("command_id", (void*)g_idInsertSelected);
|
|
if (g_cmdInsertSelected)
|
|
{
|
|
g_descInsertSelected =
|
|
reasampler::channelActionName("insert selected sample at edit cursor");
|
|
g_accelInsertSelected.accel.cmd = g_cmdInsertSelected;
|
|
g_accelInsertSelected.desc = g_descInsertSelected.c_str();
|
|
rec->Register("gaccel", (void*)&g_accelInsertSelected);
|
|
}
|
|
|
|
g_idInsertSelectedConform = internCmdId("INSERT_SELECTED_CONFORM");
|
|
g_cmdInsertSelectedConform = rec->Register("command_id", (void*)g_idInsertSelectedConform);
|
|
if (g_cmdInsertSelectedConform)
|
|
{
|
|
g_descInsertSelectedConform = reasampler::channelActionName(
|
|
"insert selected sample at edit cursor (conform to tempo)");
|
|
g_accelInsertSelectedConform.accel.cmd = g_cmdInsertSelectedConform;
|
|
g_accelInsertSelectedConform.desc = g_descInsertSelectedConform.c_str();
|
|
rec->Register("gaccel", (void*)&g_accelInsertSelectedConform);
|
|
}
|
|
|
|
// Register the M11 batch-capture actions (command_id -> gaccel -> hookcommand). Each
|
|
// fires N captures (one bank sample per selected item / per razor area), honoring every
|
|
// precision invariant per unit and restoring the original selection on every path.
|
|
// Channel-qualified FOREVER-STABLE ids.
|
|
g_idCaptureBatchItems = internCmdId("CAPTURE_BATCH_ITEMS");
|
|
g_cmdCaptureBatchItems = rec->Register("command_id", (void*)g_idCaptureBatchItems);
|
|
if (g_cmdCaptureBatchItems)
|
|
{
|
|
g_descCaptureBatchItems =
|
|
reasampler::channelActionName("batch capture selected items (one per item)");
|
|
g_accelCaptureBatchItems.accel.cmd = g_cmdCaptureBatchItems;
|
|
g_accelCaptureBatchItems.desc = g_descCaptureBatchItems.c_str();
|
|
rec->Register("gaccel", (void*)&g_accelCaptureBatchItems);
|
|
}
|
|
|
|
g_idCaptureBatchRazor = internCmdId("CAPTURE_BATCH_RAZOR");
|
|
g_cmdCaptureBatchRazor = rec->Register("command_id", (void*)g_idCaptureBatchRazor);
|
|
if (g_cmdCaptureBatchRazor)
|
|
{
|
|
g_descCaptureBatchRazor =
|
|
reasampler::channelActionName("batch capture razor areas (one per area)");
|
|
g_accelCaptureBatchRazor.accel.cmd = g_cmdCaptureBatchRazor;
|
|
g_accelCaptureBatchRazor.desc = g_descCaptureBatchRazor.c_str();
|
|
rec->Register("gaccel", (void*)&g_accelCaptureBatchRazor);
|
|
}
|
|
|
|
// Register the "capture selected track (realtime)" action (command_id -> gaccel ->
|
|
// hookcommand). Realtime sibling of the offline CAPTURE_TRACK scope: records the
|
|
// selected track's own output in realtime into a hidden temp track, moves it into
|
|
// the bank. Dialog-free. Channel-qualified FOREVER-STABLE id.
|
|
g_idCaptureTrackRealtime = internCmdId("CAPTURE_TRACK_REALTIME");
|
|
g_cmdCaptureTrackRealtime = rec->Register("command_id", (void*)g_idCaptureTrackRealtime);
|
|
if (g_cmdCaptureTrackRealtime)
|
|
{
|
|
g_descCaptureTrackRealtime =
|
|
reasampler::channelActionName("capture selected track (realtime)");
|
|
g_accelCaptureTrackRealtime.accel.cmd = g_cmdCaptureTrackRealtime;
|
|
g_accelCaptureTrackRealtime.desc = g_descCaptureTrackRealtime.c_str();
|
|
rec->Register("gaccel", (void*)&g_accelCaptureTrackRealtime);
|
|
}
|
|
|
|
// Cancel-in-flight sibling: aborts a running realtime capture (stop + restore).
|
|
// Channel-qualified FOREVER-STABLE id.
|
|
g_idCancelRealtime = internCmdId("CANCEL_REALTIME_CAPTURE");
|
|
g_cmdCancelRealtime = rec->Register("command_id", (void*)g_idCancelRealtime);
|
|
if (g_cmdCancelRealtime)
|
|
{
|
|
g_descCancelRealtime = reasampler::channelActionName("cancel realtime capture");
|
|
g_accelCancelRealtime.accel.cmd = g_cmdCancelRealtime;
|
|
g_accelCancelRealtime.desc = g_descCancelRealtime.c_str();
|
|
rec->Register("gaccel", (void*)&g_accelCancelRealtime);
|
|
}
|
|
|
|
// Register the M10 "re-capture from source" action (command_id -> gaccel ->
|
|
// hookcommand). Regenerates the selected provenanced sample from its recorded
|
|
// source's current state; bank-only, never places on the timeline. Channel-
|
|
// qualified FOREVER-STABLE id (suffix RECAPTURE_FROM_SOURCE).
|
|
g_idRecaptureFromSource = internCmdId("RECAPTURE_FROM_SOURCE");
|
|
g_cmdRecaptureFromSource = rec->Register("command_id", (void*)g_idRecaptureFromSource);
|
|
if (g_cmdRecaptureFromSource)
|
|
{
|
|
g_descRecaptureFromSource = reasampler::channelActionName("re-capture from source");
|
|
g_accelRecaptureFromSource.accel.cmd = g_cmdRecaptureFromSource;
|
|
g_accelRecaptureFromSource.desc = g_descRecaptureFromSource.c_str();
|
|
rec->Register("gaccel", (void*)&g_accelRecaptureFromSource);
|
|
}
|
|
|
|
// Register the Phase V "show version" action (command_id -> gaccel -> hookcommand).
|
|
// On-demand only — prints the CMake-sourced version to the console when fired; no
|
|
// startup print. Channel-qualified FOREVER-STABLE id; label carries the channel prefix
|
|
// so a beta's "show version" is distinguishable from stable's in the Actions list.
|
|
g_idShowVersion = internCmdId("SHOW_VERSION");
|
|
g_cmdShowVersion = rec->Register("command_id", (void*)g_idShowVersion);
|
|
if (g_cmdShowVersion)
|
|
{
|
|
g_descShowVersion = reasampler::channelActionName("show version");
|
|
g_accelShowVersion.accel.cmd = g_cmdShowVersion;
|
|
g_accelShowVersion.desc = g_descShowVersion.c_str();
|
|
rec->Register("gaccel", (void*)&g_accelShowVersion);
|
|
}
|
|
|
|
// 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 registered in the capture
|
|
// family above; the drop path is a bank_panel callback, not a bindable action.)
|
|
reasampler::ingestRegisterActions(rec, &g_session);
|
|
|
|
// One hookcommand routes every ReaSampler action (spike + toggle + Design View).
|
|
// 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
|
|
}
|