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
+25 -14
View File
@@ -21,6 +21,12 @@
#include "bank_model.h"
// MediaTrack is forward-declared (like track_guid.h) so this header stays
// REAPER-free while RealtimeRecordBackend::begin can take the resolved source
// MediaTrack* to tap. The pointers are opaque here — never dereferenced in a
// pure/header context; only the REAPER-facing capture_realtime.cpp touches them.
class MediaTrack;
namespace reasampler {
// Audio bit-depth for the rendered wav. 32-bit float is the M3 default —
@@ -150,8 +156,9 @@ struct RealtimeTickResult {
};
// The opaque in-flight capture state. Owns the snapshot of everything to restore
// (temp track, other tracks' I_RECARM, master send/routing, transport, edit cursor,
// time selection) and the record's own project handle. Defined in
// (temp track + its receive sends from the source tracks, other tracks' I_RECARM,
// transport, edit cursor, time selection) and the record's own project handle.
// Defined in
// capture_realtime.cpp; the header stays REAPER-free (no MediaTrack*/ReaProject*
// leaks here) by holding it behind a forward-declared type + unique_ptr.
//
@@ -182,21 +189,25 @@ using RealtimeCaptureHandle =
// harder here than offline because the record spans ticks: the snapshot + restore
// live on RealtimeCaptureState, not a function-scope RAII destructor.
//
// SCOPE (this increment): MASTER scope only — records the master-mix output, which
// does not need the FxBypassGuard scope isolation (the whole chain is in scope).
// Track/Item scopes are a genuine routing fork surfaced to Daniel, NOT silently
// built (see capture_realtime.cpp §FORK). A Track/Item request is refused.
// SCOPE (this increment): TRACK scope only — records the selected track's OWN
// output (item + that track's own FX + its own fader/pan, PRE-parent), matching
// offline's track scope. This needs NO FxBypassGuard: a send tapping a track's
// output is naturally PRE-parent (the parent has not summed it yet), so the tap is
// chain-independent by construction. Item realtime is deferred (UnsupportedMode).
class RealtimeRecordBackend {
public:
// Starts a realtime record: validates the request (master scope, non-empty
// range, active + saved project, transport idle), snapshots all state to
// restore, creates the hidden temp track, routes the master send, arms, and
// CSurf_OnRecord — then returns IMMEDIATELY (no wait, no UI block). On success
// the returned unique_ptr owns the in-flight state; drive it with tick(). On a
// validation/setup failure returns nullptr and fills `outFailure` with the
// CaptureStatus + message (nothing was left mutated — begin() restores on its
// own failure paths).
// Starts a realtime record: validates the request (track scope, non-empty range,
// at least one source track, active + saved project, transport idle), snapshots
// all state to restore, creates the hidden temp track, routes a send FROM each
// source track INTO the temp track, arms, and CSurf_OnRecord — then returns
// IMMEDIATELY (no wait, no UI block). `sourceTracks` are the selected tracks to
// tap (resolved by the action layer — the CaptureRequest itself stays REAPER-free,
// carrying only the provenance GUIDs). On success the returned unique_ptr owns the
// in-flight state; drive it with tick(). On a validation/setup failure returns
// nullptr and fills `outFailure` with the CaptureStatus + message (nothing was
// left mutated — begin() restores on its own failure paths).
RealtimeCaptureHandle begin(const CaptureRequest& request,
const std::vector<MediaTrack*>& sourceTracks,
CaptureResult& outFailure);
// Advances the in-flight record one tick. Reads the transport (bound to the
+92 -54
View File
@@ -6,9 +6,9 @@
//
// Captures the requested scope over the requested range by RECORDING in realtime
// (transport-driven) into a hidden temp track, then moves the recorded file into
// the bank as a Sample — non-destructively. This increment implements the MASTER
// scope only (records the master-mix output). Track/Item scopes are a genuine
// routing fork (see §FORK below) and are refused rather than silently half-built.
// the bank as a Sample — non-destructively. This increment implements the TRACK
// scope only (records the selected track's own output). Item realtime is deferred
// (UnsupportedMode) rather than silently half-built.
//
// ============================================================================
// §ASYNC — timer-driven, no UI block (M8 rework — Daniel: "do it right")
@@ -18,7 +18,7 @@
// REAPER's UI for the whole record. That is gone. The record is now driven across
// timer ticks:
// begin() — validate, snapshot ALL state to restore, create the temp track,
// route the master send, arm, CSurf_OnRecord, RETURN IMMEDIATELY.
// route the source-track tap, arm, CSurf_OnRecord, RETURN IMMEDIATELY.
// tick() — (from OnTimer, the same tick as session.poll()) read the transport,
// and on a terminal verdict stop + finalize/abort + RESTORE everything.
// abort() — force-terminate now (shutdown / project switch) + RESTORE everything.
@@ -33,25 +33,39 @@
// (unit-tested outside the DAW). This TU owns only the REAPER-bound recipe.
//
// ============================================================================
// §FORK — wet-master / per-scope routing (SURFACED, NOT SILENTLY BUILT)
// §TAP — track-output tap (selected track's own output, PRE-parent)
// ============================================================================
// The open design question (CONTEXT.md / PLAN.md "realtime wet-master routing"):
// tap the SCOPED output into the hidden record track WITHOUT altering the user's
// monitoring, with correct latency compensation.
// The recipe: the hidden temp track RECEIVES a send FROM each selected source track
// (CreateTrackSend(source, temp)). The temp track records its OWN output
// (I_RECMODE 3/6, latency-compensated) with B_MAINSEND=0 (it does NOT sum back into
// the master — no feedback, no monitoring double). Multiple selected tracks each get
// a send into the one temp track, so their outputs SUM in the temp track — matching
// how offline track scope handles a multi-track selection.
//
// This increment resolves it for MASTER scope with the cleanest header-verifiable
// recipe: a temp track carrying a SEND from the master track, recorded in
// output-record mode (I_RECMODE = stereo/mono-out w/latency comp). The temp
// track's own B_MAINSEND is cleared (it does NOT sum back into the master), so the
// user hears no change or double — the record tap is a pure branch off the master
// bus. Latency compensation is REAPER's (I_RECMODE 3/6 are the *latency-compensated*
// output modes), so the recorded file lines up with the source.
// WHY THIS FAITHFULLY CAPTURES THE TRACK'S OUTPUT — and why NO FxBypassGuard:
// A CreateTrackSend defaults to I_SENDMODE=0 (post-fader) with I_SRCCHAN=0
// (channel offset 0, (srcchan>>10)==0 => full stereo — SDK ~3302/3304). Post-fader
// taps the source track AFTER its own FX and AFTER its own fader/pan — i.e. exactly
// the track's OWN OUTPUT — but BEFORE the parent/folder/master sums it. The send is
// a branch off the signal at the track's output stage; the parent chain downstream
// of that branch is not in the tapped path AT ALL. So the tap is chain-independent
// BY CONSTRUCTION: there is nothing to neutralize, and FxBypassGuard (which mutates
// the live chain, altering the user's monitoring) is deliberately NOT used. This is
// the realtime analogue of offline track scope (item + the track's own FX + its own
// fader/pan; parent/folder/master excluded), reached without touching any live FX.
//
// Track/Item scopes DO NOT compose cleanly with this recipe (they need per-scope
// source-track routing + the send-isolation rule) — that is the fork the brief says
// to STOP before, and they are refused with UnsupportedMode. FxBypassGuard is NOT
// reused here — it alters live monitoring (wrong tool for realtime); the master-send
// recipe needs no chain neutralization.
// This ALSO fixes the earlier silent-file bug: that spike sent FROM the master INTO
// a temp track, which REAPER refuses to carry (master->track is a feedback loop), so
// the temp recorded silence. A regular track->track send has no feedback — it works.
//
// Non-destructive: the temp track is deleted on teardown, which removes every send we
// created INTO it (REAPER cannot leave a send dangling to a deleted destination) — so
// NO source track retains any routing change. We never mutate any existing track's
// persistent state; we only add sends FROM the source tracks that vanish with the
// temp track. The selected source tracks are UNCHANGED after capture.
//
// Item realtime is deferred (UnsupportedMode): item scope would need per-item take
// isolation on top of the tap, which is a separate increment.
#include "capture.h"
@@ -70,7 +84,6 @@
#define REAPERAPI_WANT_Main_SaveProject
#define REAPERAPI_WANT_Master_GetTempo
#define REAPERAPI_WANT_GetSetProjectInfo
#define REAPERAPI_WANT_GetMasterTrack
#define REAPERAPI_WANT_InsertTrackAtIndex
#define REAPERAPI_WANT_DeleteTrack
#define REAPERAPI_WANT_CountTracks
@@ -151,8 +164,8 @@ std::int64_t recordedFileSize(MediaTrack* temp) {
// ============================================================================
// RealtimeCaptureState — the in-flight snapshot + idempotent restore
// ============================================================================
// Holds EVERYTHING to restore across the many ticks the record spans (temp track,
// other tracks' I_RECARM, master send, transport, edit cursor, time selection),
// Holds EVERYTHING to restore across the many ticks the record spans (temp track +
// its receive-sum sends, other tracks' I_RECARM, transport, edit cursor, time selection),
// plus the request echo needed to finalize the Sample. restore() is idempotent
// (restored_ latch) and is the single teardown every terminal path calls.
class RealtimeCaptureState {
@@ -165,9 +178,11 @@ public:
BankPaths paths_;
std::string uniqueTag_;
// The transient sink + the send we made from the master into it.
// The transient sink. The sends we create (from each selected source track INTO
// temp_) live on those source tracks pointing AT temp_, and are removed automatically
// when temp_ is deleted — REAPER cannot leave a send dangling to a deleted
// destination. So there is no separate send handle to track here.
MediaTrack* temp_ = nullptr;
MediaTrack* master_ = nullptr;
// The record phase (pure state machine drives the transition). Starts Recording.
RecordPhase phase_ = RecordPhase::Recording;
@@ -229,7 +244,7 @@ public:
// completion, user stop, error, project switch, unload). Safe to call more than
// once — the restored_ latch makes every call after the first a no-op. Order:
// 1. stop the transport if anything is still running (we own it),
// 2. delete the temp track (drops its send + the recorded arrange item),
// 2. delete the temp track (drops its receive-sum sends + the recorded item),
// 3. restore every other track's arm,
// 4. restore the time selection + edit cursor.
// Stop the record's OWN project transport if it is still playing/recording. Uses
@@ -242,7 +257,7 @@ public:
}
// Is the captured project STILL OPEN? (review §1 — CRITICAL). If the captured
// project was CLOSED mid-record, proj_/temp_/master_ point at freed memory;
// project was CLOSED mid-record, proj_/temp_ point at freed memory;
// touching them (stopOwnTransport, DeleteTrack, arm restore) is a use-after-free.
// ValidatePtr2 with a null project validates the ReaProject* itself (the header:
// "proj is ignored if pointer is itself a project"). Every teardown that
@@ -259,7 +274,6 @@ public:
void dropWithoutRestore() {
restored_ = true;
temp_ = nullptr;
master_ = nullptr;
armSnaps_.clear();
}
@@ -271,8 +285,9 @@ public:
// by the terminal path's explicit stop-before-finalize — a safe no-op then).
stopOwnTransport();
// 2. Temp track: deleting it drops the master send AND the recorded arrange
// item in one move — nothing stays in the arrange (load-bearing principle).
// 2. Temp track: deleting it drops the source-track sends (REAPER removes every
// send whose destination is deleted — no source track is left mutated) AND the
// recorded arrange item in one move — nothing stays behind (load-bearing).
if (temp_) { DeleteTrack(temp_); temp_ = nullptr; }
// 3. Other tracks' record-arm.
@@ -367,12 +382,24 @@ void RealtimeCaptureStateDeleter::operator()(RealtimeCaptureState* p) const noex
}
RealtimeCaptureHandle
RealtimeRecordBackend::begin(const CaptureRequest& request, CaptureResult& outFailure) {
// Only the master scope is implemented this increment (see §FORK).
if (request.sourceMode != SourceMode::MasterMix) {
RealtimeRecordBackend::begin(const CaptureRequest& request,
const std::vector<MediaTrack*>& sourceTracks,
CaptureResult& outFailure) {
// Only the track scope is implemented this increment (see §TAP). Item realtime
// is deferred — it needs per-item take isolation on top of the track-output tap.
if (request.sourceMode != SourceMode::SelectedTracks) {
outFailure.status = CaptureStatus::UnsupportedMode;
outFailure.message = "RealtimeRecordBackend implements MASTER scope only this "
"increment (track/item realtime routing is a surfaced fork).";
outFailure.message = "RealtimeRecordBackend implements TRACK scope only this "
"increment (item realtime is deferred).";
return nullptr;
}
// Track scope needs at least one source track to tap. No selection -> refuse
// (matching offline track scope's no-op on an empty selection).
if (sourceTracks.empty()) {
outFailure.status = CaptureStatus::UnsupportedMode;
outFailure.message = "No track selected — realtime track capture needs at least "
"one selected track to tap.";
return nullptr;
}
@@ -431,8 +458,8 @@ RealtimeRecordBackend::begin(const CaptureRequest& request, CaptureResult& outFa
st->snapshotAndDisarmOthers();
// Hidden temp track at the end: no default FX/envelopes (clean sink), hidden from
// both panels, B_MAINSEND=0 so it does not sum back into the master (monitoring
// invariant — the record tap is a pure branch off the master bus).
// both panels, B_MAINSEND=0 so it does NOT sum back into the master (monitoring
// invariant — it would otherwise double the tapped tracks in the user's monitoring).
const int idx = CountTracks(proj);
InsertTrackAtIndex(idx, false);
st->temp_ = GetTrack(proj, idx);
@@ -446,28 +473,39 @@ RealtimeRecordBackend::begin(const CaptureRequest& request, CaptureResult& outFa
SetMediaTrackInfo_Value(st->temp_, "B_SHOWINMIXER", 0.0);
SetMediaTrackInfo_Value(st->temp_, "B_MAINSEND", 0.0);
// Route the MASTER output into the temp track (a send master -> temp). The temp
// track records this in output-record mode.
// Route the TRACK-OUTPUT tap: a send FROM each selected source track INTO the temp
// track (CreateTrackSend(source, temp)). The temp records its OWN output, so the
// sends' outputs SUM in it — multiple selected tracks are captured together (same as
// offline track scope). See §TAP for why this faithfully captures each track's own
// output and needs no FxBypassGuard.
//
// DAW-ONLY ASSUMPTION (flag): whether output-record mode (I_RECMODE 3/6) on a
// track fed only by a master send records THAT send's signal is the crux to
// verify live — named here so DAW testing targets it directly.
st->master_ = GetMasterTrack(proj);
if (!st->master_) {
outFailure.status = CaptureStatus::RenderFailed;
outFailure.message = "Could not resolve the master track for realtime routing.";
st->restore(); // temp track removed here
return nullptr;
// Sends default to post-fader (I_SENDMODE 0) and full-stereo (I_SRCCHAN default,
// (srcchan>>10)==0 — SDK ~3302/3304): post-fader = after the source track's FX and
// fader/pan = the track's OWN output, tapped BEFORE the parent sums it. Left at
// defaults deliberately — that IS the track-scope tap point.
//
// DAW-ONLY ASSUMPTION (flag): that a post-fader track->temp send + output-record
// reproduces the track's own output sample-for-sample (latency comp, pan law,
// mono/stereo folding) is the crux to verify live.
int sendsMade = 0;
for (MediaTrack* src : sourceTracks) {
if (!src || src == st->temp_) continue;
if (CreateTrackSend(src, st->temp_) >= 0) ++sendsMade;
}
const int sendIdx = CreateTrackSend(st->master_, st->temp_);
if (sendIdx < 0) {
if (sendsMade == 0) {
// Every send failed (should not happen for valid selected tracks). Refuse
// rather than record a guaranteed-silent file.
outFailure.status = CaptureStatus::RenderFailed;
outFailure.message = "Could not route the master output into the record track.";
st->restore(); // deleting the temp track drops any partial send too
outFailure.message = "Could not route any selected track into the record tap — "
"nothing to capture.";
st->restore(); // deleting the temp track drops any partial sends too
return nullptr;
}
// Record-mode values from the pure planner. Master mix is fully wet -> PostFader.
// Record-mode values from the pure planner. The temp track records its OWN output;
// it has no FX and unity fader, so its post-fader output equals the summed sends.
// Track scope is fully wet -> PostFader. (The actual track-scope tap point is the
// source sends' default post-fader mode; the temp's recmode only records the sum.)
const OutputTap tap = outputTapForWetDry(request.wetDry);
const RecordModePlan rec = recordModePlanFor(request.channelCount, tap);
SetMediaTrackInfo_Value(st->temp_, "I_RECMODE", static_cast<double>(rec.recMode));
@@ -581,7 +619,7 @@ RealtimeTickResult RealtimeRecordBackend::abort(RealtimeCaptureState& state) {
if (state.restored()) { out.status = RealtimeTickStatus::Failed; return out; }
// CRITICAL (review §1): if the captured project was CLOSED mid-record, proj_ /
// temp_ / master_ point at freed memory. The closed project already reclaimed its
// temp_ point at freed memory. The closed project already reclaimed its
// temp track, arms, and transport — so DROP the handle WITHOUT touching any REAPER
// state (no stop, no finalize, no DeleteTrack, no arm restore). Touching those
// freed pointers is the use-after-free bug this guard exists to prevent. This is
+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).
+4 -15
View File
@@ -55,9 +55,8 @@ SourceMode sourceModeForScope(CaptureScope scope) {
switch (scope) {
case CaptureScope::Item: return SourceMode::SelectedItems;
case CaptureScope::Track: return SourceMode::SelectedTracks;
case CaptureScope::Master: return SourceMode::MasterMix;
}
return SourceMode::MasterMix; // unreachable for a valid enum; fail to master
return SourceMode::SelectedItems; // unreachable for a valid enum; fail closed
}
RangeSource inferRangeSource(bool hasRazorArea) {
@@ -84,12 +83,6 @@ FxBypassPlan fxBypassPlanFor(CaptureScope scope) {
p.bypassAncestorFx = true;
p.bypassMaster = true;
return p;
case CaptureScope::Master:
// Master = whole chain. Bypass nothing.
p.bypassSelfFx = false;
p.bypassAncestorFx = false;
p.bypassMaster = false;
return p;
}
return p; // unreachable; bypass nothing (fail to full-chain, never over-bypass)
}
@@ -137,12 +130,13 @@ RazorRange razorUnionBounds(const std::vector<RazorRange>& ranges) {
}
const std::vector<CaptureActionDef>& captureActionTable() {
// Built once (function-local static): three SCOPE actions. Tail OFF for all
// Built once (function-local static): two SCOPE actions. Tail OFF for all
// (exact bounds). Ids are FOREVER-STABLE — never edit a shipped string. Each
// action infers its range (razor-else-time) at fire time and enforces its
// FX-scope invariant via fxBypassPlanFor. The M7 CAPTURE_TRACKS_WET /
// CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids are RETIRED (mirror-unregistered in
// main.cpp); CAPTURE_MASTER keeps its shipped id string.
// main.cpp); the CAPTURE_MASTER scope action is REMOVED (its id is likewise
// mirror-unregistered) — to capture the master you render a track.
static const std::vector<CaptureActionDef> table = {
// Item scope — item/take FX only. NEW forever-stable id.
{"CEREBELLUM_REASAMPLER_CAPTURE_ITEM",
@@ -153,11 +147,6 @@ const std::vector<CaptureActionDef>& captureActionTable() {
{"CEREBELLUM_REASAMPLER_CAPTURE_TRACK",
"ReaSampler: capture selected track(s)", "track",
CaptureScope::Track},
// Master scope — whole chain. Id string unchanged from M7 (already shipped).
{"CEREBELLUM_REASAMPLER_CAPTURE_MASTER",
"ReaSampler: capture master", "master",
CaptureScope::Master},
};
return table;
}
+11 -10
View File
@@ -66,18 +66,20 @@ RenderSettingsChoice renderSettingsFor(SourceMode mode, double wetDry);
// --- Capture scope: the FX-scope invariant (Daniel, critical) ----------------
//
// Three FX scopes. The render RANGE (razor-else-time) is orthogonal to the scope.
// Two FX scopes. The render RANGE (razor-else-time) is orthogonal to the scope.
// Item -> item/take FX ONLY (no track, no parent/folder, no master FX).
// Track -> item FX + the selected track's OWN track FX (no parent/folder/master).
// Master -> the whole chain (nothing bypassed).
// There is NO master scope: to capture the master you render a track instead. The
// master track's FX/gain/pan are still NEUTRALIZED as part of the out-of-scope
// chain for both item and track captures (bypassMaster below) — master is a
// bypass target, not a capture scope.
enum class CaptureScope {
Item,
Track,
Master,
};
// The render source mode each scope drives. Item captures selected items, Track
// captures selected tracks (via master), Master captures the master mix.
// captures selected tracks (via master).
SourceMode sourceModeForScope(CaptureScope scope);
// --- Range inference: razor-else-time (orthogonal to scope) -------------------
@@ -142,7 +144,7 @@ RazorRange razorUnionBounds(const std::vector<RazorRange>& ranges);
// --- Capture-action taxonomy (the bindable set main.cpp registers) -----------
//
// One row per bindable SCOPE action. Three scopes (item / track / master); the
// One row per bindable SCOPE action. Two scopes (item / track); the
// range each captures (razor-else-time) is inferred at fire time, not a mode.
// Tail is OFF for every row (exact bounds); a tail-on variant is a later opt-in,
// YAGNI now. Bounded, discoverable, NO dialogs (the tool's no-clutter ethos).
@@ -153,17 +155,16 @@ struct CaptureActionDef {
const char* commandString; // CEREBELLUM_REASAMPLER_… FOREVER-STABLE id string
const char* description; // Actions-list label
const char* baseName; // file-stem base for this capture
CaptureScope scope; // FX scope (item / track / master)
CaptureScope scope; // FX scope (item / track)
};
// The capture-action table. Iterated by main.cpp to register the family and route
// each fired command back to its definition. Kept here (pure) so the taxonomy is
// one testable list, not scattered registration code.
//
// Three scope rows: CAPTURE_ITEM, CAPTURE_TRACK, CAPTURE_MASTER. This replaces the
// M7 four-mode table (master / tracks / items / razor) — razor is now an inferred
// range, not a mode, and each scope enforces its FX-scope invariant via
// fxBypassPlanFor.
// Two scope rows: CAPTURE_ITEM, CAPTURE_TRACK. There is no master capture — to
// capture the master you render a track. Razor is an inferred range, not a mode,
// and each scope enforces its FX-scope invariant via fxBypassPlanFor.
const std::vector<CaptureActionDef>& captureActionTable();
} // namespace reasampler