refactor(capture): drop master scope; realtime taps selected track

Capture is now item + track only (master removed as a scope; still bypassed
as out-of-scope chain). Realtime records the selected track's own output via
per-track post-fader sends into a hidden temp track, fixing the silent file.
This commit is contained in:
2026-07-23 16:34:02 -04:00
parent 3791e6c119
commit 4ef41cf705
6 changed files with 215 additions and 192 deletions
+70 -61
View File
@@ -35,7 +35,7 @@
// Persistent action-id prefix for the ReaSampler action family.
// Every bindable action (capture / insert / slot / verify) mints its command id
// from a string beginning with this prefix, e.g. "CEREBELLUM_REASAMPLER_CAPTURE_MASTER".
// from a string beginning with this prefix, e.g. "CEREBELLUM_REASAMPLER_CAPTURE_TRACK".
// FOREVER-STABLE once shipped: user keybindings key off these strings, so the
// prefix and any minted id must never change after release.
#define REASAMPLER_ACTION_PREFIX "CEREBELLUM_REASAMPLER_"
@@ -44,17 +44,18 @@
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 (three FX scopes) -------------------------------
// Three bindable SCOPE actions from captureActionTable() (render_settings, pure):
// capture item / track / master. Each infers its range (razor-else-time) and
// enforces the FX-scope invariant via FX-bypass-around-render (FxBypassGuard):
// ---- 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).
// Master -> whole chain (bypass nothing).
// This REPLACES the retired M7 four-mode family (master / tracks / items / razor).
// The retired CAPTURE_TRACKS_WET / CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids are
// mirror-unregistered on unload so old keybindings clear cleanly; CAPTURE_MASTER's
// id string is preserved.
// 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-
@@ -62,14 +63,18 @@ reaper_plugin_info_t* g_rec = nullptr; // REAPER's dispatch struct
static std::vector<int> g_captureCmdIds;
static std::vector<gaccel_register_t> g_captureAccels;
// Retired capture-action command-id strings (M7 four-mode family). Kept ONLY to
// mirror-unregister them on unload so a user's stale keybindings are cleaned up.
// Never re-register these. CAPTURE_MASTER is NOT here — its id string carries over
// to the new master scope action unchanged.
// Retired capture-action command-id strings. Kept ONLY to mirror-unregister them on
// unload so a user's stale keybindings are cleaned up. Never re-register these.
// * 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.
static const char* const kRetiredCaptureCmdStrings[] = {
"CEREBELLUM_REASAMPLER_CAPTURE_TRACKS_WET",
"CEREBELLUM_REASAMPLER_CAPTURE_ITEMS_WET",
"CEREBELLUM_REASAMPLER_CAPTURE_RAZOR_WET",
"CEREBELLUM_REASAMPLER_CAPTURE_MASTER",
"CEREBELLUM_REASAMPLER_CAPTURE_MASTER_REALTIME",
};
// Command id for "ReaSampler: toggle bank panel" (M5). FOREVER-STABLE string.
@@ -85,12 +90,13 @@ static int g_cmdToggleBankPanel = 0;
static int g_cmdInsertSelected = 0;
static int g_cmdInsertSelectedConform = 0;
// Command id for the M8 "capture master (realtime)" action. FOREVER-STABLE string.
// Records the master 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_MASTER scope action: same range logic
// (razor-else-time), same bank/persist path, different backend. Dialog-free.
static int g_cmdCaptureMasterRealtime = 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 M8 "cancel realtime capture" action. FOREVER-STABLE string.
// Aborts the in-flight realtime capture (stop + restore, non-destructive) so a user
@@ -223,12 +229,13 @@ static void OnTimer()
// range); the caller reports it and writes nothing.
// The resolved source: exact bounds + the source tracks (for FX-bypass + Sample
// provenance GUIDs). `sourceTracks` is empty for Master scope.
// 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's/selected tracks; empty for master
std::vector<MediaTrack*> sourceTracks; // item-owning tracks / selected tracks
std::vector<std::string> trackGuids; // canonical GUIDs of sourceTracks
};
@@ -325,8 +332,8 @@ static bool collectSelectedItemTracks(ResolvedSource& out)
return !out.sourceTracks.empty();
}
// Resolves the source for a scope: the selection tracks (item/track) or none
// (master), plus the inferred range. Returns false with a reason on nothing to do.
// 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)
{
@@ -343,8 +350,6 @@ static bool ResolveScopeSource(reasampler::CaptureScope scope,
why = "select at least one track"; return false;
}
break;
case CaptureScope::Master:
break; // whole chain — no source-track collection
}
return resolveRange(out.startSeconds, out.endSeconds, why);
}
@@ -359,7 +364,7 @@ static bool ResolveScopeSource(reasampler::CaptureScope scope,
// 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-> nothing (full chain, unchanged).
// (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
@@ -545,22 +550,23 @@ static void RunCapture(const reasampler::CaptureActionDef& def)
ShowConsoleMsg(log.c_str());
}
// STARTS the M8 REALTIME master capture and returns immediately — the record runs
// across timer ticks (DriveRealtimeCapture), so REAPER's UI stays responsive. Infers
// the range (razor-else-time, the same orthogonal range logic as the offline scopes)
// and starts recording the master output into a hidden temp track via
// RealtimeRecordBackend::begin; OnTimer drives it to completion, then adds the Sample
// and persists. MASTER scope only this increment (track/item realtime routing is a
// surfaced fork — see capture_realtime.cpp §FORK). Dialog-free. Non-bit-identical by
// nature (it is realtime) — offline stays the deterministic default. FxBypassGuard is
// NOT used here (it neutralizes the live chain, altering the user's monitoring). The
// 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 RunCaptureRealtimeMaster()
static void RunCaptureRealtimeTrack()
{
if (g_rtCapture)
{
@@ -569,29 +575,32 @@ static void RunCaptureRealtimeMaster()
return;
}
double start = 0.0, end = 0.0;
// 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 (!resolveRange(start, end, why))
if (!ResolveScopeSource(reasampler::CaptureScope::Track, src, why))
{
ShowConsoleMsg(("ReaSampler realtime capture: " + why + ".\n").c_str());
return;
}
reasampler::CaptureRequest req;
req.sourceMode = reasampler::SourceMode::MasterMix; // realtime master scope
req.startSeconds = start; // exact bounds — no rounding
req.endSeconds = end;
req.wetDry = 1.0; // fully wet (post-fader tap)
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.renderTail = false;
req.tailMs = 0.0;
req.sampleRate = 0; // follow project rate
req.sampleRate = 0; // follow project rate
req.channelCount = 2;
req.bitDepth = reasampler::WavBitDepth::Float32;
req.baseName = "realtime";
// No trackGuids — master scope is not track-provenanced.
req.trackGuids = src.trackGuids; // provenance on the Sample
reasampler::CaptureResult failure;
reasampler::RealtimeCaptureHandle st = g_rtBackend.begin(req, failure);
reasampler::RealtimeCaptureHandle st =
g_rtBackend.begin(req, src.sourceTracks, failure);
if (!st)
{
// begin() validated/failed and already restored anything it touched.
@@ -685,7 +694,7 @@ static bool OnHookCommand(int command, int /*flag*/)
if (command == g_cmdToggleBankPanel) { reasampler::bankPanelToggle(); return true; }
if (command == g_cmdInsertSelected) { RunInsertSelected(false); return true; }
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); return true; }
if (command == g_cmdCaptureMasterRealtime) { RunCaptureRealtimeMaster(); return true; }
if (command == g_cmdCaptureTrackRealtime) { RunCaptureRealtimeTrack(); return true; }
if (command == g_cmdCancelRealtime) { RunCancelRealtime(); 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).
@@ -707,7 +716,7 @@ static int OnToggleAction(int command)
static gaccel_register_t g_accelToggleBankPanel{};
static gaccel_register_t g_accelInsertSelected{};
static gaccel_register_t g_accelInsertSelectedConform{};
static gaccel_register_t g_accelCaptureMasterRealtime{};
static gaccel_register_t g_accelCaptureTrackRealtime{};
static gaccel_register_t g_accelCancelRealtime{};
extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
@@ -740,9 +749,9 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
g_rec->Register("-gaccel", (void*)&g_accelCancelRealtime);
g_rec->Register("-command_id",
(void*)(REASAMPLER_ACTION_PREFIX "CANCEL_REALTIME_CAPTURE"));
g_rec->Register("-gaccel", (void*)&g_accelCaptureMasterRealtime);
g_rec->Register("-gaccel", (void*)&g_accelCaptureTrackRealtime);
g_rec->Register("-command_id",
(void*)(REASAMPLER_ACTION_PREFIX "CAPTURE_MASTER_REALTIME"));
(void*)(REASAMPLER_ACTION_PREFIX "CAPTURE_TRACK_REALTIME"));
g_rec->Register("-gaccel", (void*)&g_accelInsertSelectedConform);
g_rec->Register("-command_id",
(void*)(REASAMPLER_ACTION_PREFIX "INSERT_SELECTED_CONFORM"));
@@ -853,19 +862,19 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
rec->Register("gaccel", (void*)&g_accelInsertSelectedConform);
}
// Register the M8 "capture master (realtime)" action (command_id -> gaccel ->
// hookcommand). Realtime sibling of the offline CAPTURE_MASTER scope: records
// the master output in realtime into a hidden temp track, moves it into the
// bank. Dialog-free. FOREVER-STABLE id string.
g_cmdCaptureMasterRealtime = rec->Register(
// 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. NEW FOREVER-STABLE id string.
g_cmdCaptureTrackRealtime = rec->Register(
"command_id",
(void*)(REASAMPLER_ACTION_PREFIX "CAPTURE_MASTER_REALTIME"));
if (g_cmdCaptureMasterRealtime)
(void*)(REASAMPLER_ACTION_PREFIX "CAPTURE_TRACK_REALTIME"));
if (g_cmdCaptureTrackRealtime)
{
g_accelCaptureMasterRealtime.accel.cmd = g_cmdCaptureMasterRealtime;
g_accelCaptureMasterRealtime.desc =
"ReaSampler: capture master (realtime)";
rec->Register("gaccel", (void*)&g_accelCaptureMasterRealtime);
g_accelCaptureTrackRealtime.accel.cmd = g_cmdCaptureTrackRealtime;
g_accelCaptureTrackRealtime.desc =
"ReaSampler: capture selected track (realtime)";
rec->Register("gaccel", (void*)&g_accelCaptureTrackRealtime);
}
// Cancel-in-flight sibling: aborts a running realtime capture (stop + restore).