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" #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 { namespace reasampler {
// Audio bit-depth for the rendered wav. 32-bit float is the M3 default — // 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 // 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, // (temp track + its receive sends from the source tracks, other tracks' I_RECARM,
// time selection) and the record's own project handle. Defined in // 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* // capture_realtime.cpp; the header stays REAPER-free (no MediaTrack*/ReaProject*
// leaks here) by holding it behind a forward-declared type + unique_ptr. // 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 // harder here than offline because the record spans ticks: the snapshot + restore
// live on RealtimeCaptureState, not a function-scope RAII destructor. // live on RealtimeCaptureState, not a function-scope RAII destructor.
// //
// SCOPE (this increment): MASTER scope only — records the master-mix output, which // SCOPE (this increment): TRACK scope only — records the selected track's OWN
// does not need the FxBypassGuard scope isolation (the whole chain is in scope). // output (item + that track's own FX + its own fader/pan, PRE-parent), matching
// Track/Item scopes are a genuine routing fork surfaced to Daniel, NOT silently // offline's track scope. This needs NO FxBypassGuard: a send tapping a track's
// built (see capture_realtime.cpp §FORK). A Track/Item request is refused. // 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 { class RealtimeRecordBackend {
public: public:
// Starts a realtime record: validates the request (master scope, non-empty // Starts a realtime record: validates the request (track scope, non-empty range,
// range, active + saved project, transport idle), snapshots all state to // at least one source track, active + saved project, transport idle), snapshots
// restore, creates the hidden temp track, routes the master send, arms, and // all state to restore, creates the hidden temp track, routes a send FROM each
// CSurf_OnRecord — then returns IMMEDIATELY (no wait, no UI block). On success // source track INTO the temp track, arms, and CSurf_OnRecord — then returns
// the returned unique_ptr owns the in-flight state; drive it with tick(). On a // IMMEDIATELY (no wait, no UI block). `sourceTracks` are the selected tracks to
// validation/setup failure returns nullptr and fills `outFailure` with the // tap (resolved by the action layer — the CaptureRequest itself stays REAPER-free,
// CaptureStatus + message (nothing was left mutated — begin() restores on its // carrying only the provenance GUIDs). On success the returned unique_ptr owns the
// own failure paths). // 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, RealtimeCaptureHandle begin(const CaptureRequest& request,
const std::vector<MediaTrack*>& sourceTracks,
CaptureResult& outFailure); CaptureResult& outFailure);
// Advances the in-flight record one tick. Reads the transport (bound to the // 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 // 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 // (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 // the bank as a Sample — non-destructively. This increment implements the TRACK
// scope only (records the master-mix output). Track/Item scopes are a genuine // scope only (records the selected track's own output). Item realtime is deferred
// routing fork (see §FORK below) and are refused rather than silently half-built. // (UnsupportedMode) rather than silently half-built.
// //
// ============================================================================ // ============================================================================
// §ASYNC — timer-driven, no UI block (M8 rework — Daniel: "do it right") // §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 // REAPER's UI for the whole record. That is gone. The record is now driven across
// timer ticks: // timer ticks:
// begin() — validate, snapshot ALL state to restore, create the temp track, // 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, // tick() — (from OnTimer, the same tick as session.poll()) read the transport,
// and on a terminal verdict stop + finalize/abort + RESTORE everything. // and on a terminal verdict stop + finalize/abort + RESTORE everything.
// abort() — force-terminate now (shutdown / project switch) + 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. // (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"): // The recipe: the hidden temp track RECEIVES a send FROM each selected source track
// tap the SCOPED output into the hidden record track WITHOUT altering the user's // (CreateTrackSend(source, temp)). The temp track records its OWN output
// monitoring, with correct latency compensation. // (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 // WHY THIS FAITHFULLY CAPTURES THE TRACK'S OUTPUT — and why NO FxBypassGuard:
// recipe: a temp track carrying a SEND from the master track, recorded in // A CreateTrackSend defaults to I_SENDMODE=0 (post-fader) with I_SRCCHAN=0
// output-record mode (I_RECMODE = stereo/mono-out w/latency comp). The temp // (channel offset 0, (srcchan>>10)==0 => full stereo — SDK ~3302/3304). Post-fader
// track's own B_MAINSEND is cleared (it does NOT sum back into the master), so the // taps the source track AFTER its own FX and AFTER its own fader/pan — i.e. exactly
// user hears no change or double — the record tap is a pure branch off the master // the track's OWN OUTPUT — but BEFORE the parent/folder/master sums it. The send is
// bus. Latency compensation is REAPER's (I_RECMODE 3/6 are the *latency-compensated* // a branch off the signal at the track's output stage; the parent chain downstream
// output modes), so the recorded file lines up with the source. // 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 // This ALSO fixes the earlier silent-file bug: that spike sent FROM the master INTO
// source-track routing + the send-isolation rule) — that is the fork the brief says // a temp track, which REAPER refuses to carry (master->track is a feedback loop), so
// to STOP before, and they are refused with UnsupportedMode. FxBypassGuard is NOT // the temp recorded silence. A regular track->track send has no feedback — it works.
// reused here — it alters live monitoring (wrong tool for realtime); the master-send //
// recipe needs no chain neutralization. // 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" #include "capture.h"
@@ -70,7 +84,6 @@
#define REAPERAPI_WANT_Main_SaveProject #define REAPERAPI_WANT_Main_SaveProject
#define REAPERAPI_WANT_Master_GetTempo #define REAPERAPI_WANT_Master_GetTempo
#define REAPERAPI_WANT_GetSetProjectInfo #define REAPERAPI_WANT_GetSetProjectInfo
#define REAPERAPI_WANT_GetMasterTrack
#define REAPERAPI_WANT_InsertTrackAtIndex #define REAPERAPI_WANT_InsertTrackAtIndex
#define REAPERAPI_WANT_DeleteTrack #define REAPERAPI_WANT_DeleteTrack
#define REAPERAPI_WANT_CountTracks #define REAPERAPI_WANT_CountTracks
@@ -151,8 +164,8 @@ std::int64_t recordedFileSize(MediaTrack* temp) {
// ============================================================================ // ============================================================================
// RealtimeCaptureState — the in-flight snapshot + idempotent restore // RealtimeCaptureState — the in-flight snapshot + idempotent restore
// ============================================================================ // ============================================================================
// Holds EVERYTHING to restore across the many ticks the record spans (temp track, // Holds EVERYTHING to restore across the many ticks the record spans (temp track +
// other tracks' I_RECARM, master send, transport, edit cursor, time selection), // 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 // plus the request echo needed to finalize the Sample. restore() is idempotent
// (restored_ latch) and is the single teardown every terminal path calls. // (restored_ latch) and is the single teardown every terminal path calls.
class RealtimeCaptureState { class RealtimeCaptureState {
@@ -165,9 +178,11 @@ public:
BankPaths paths_; BankPaths paths_;
std::string uniqueTag_; 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* temp_ = nullptr;
MediaTrack* master_ = nullptr;
// The record phase (pure state machine drives the transition). Starts Recording. // The record phase (pure state machine drives the transition). Starts Recording.
RecordPhase phase_ = RecordPhase::Recording; RecordPhase phase_ = RecordPhase::Recording;
@@ -229,7 +244,7 @@ public:
// completion, user stop, error, project switch, unload). Safe to call more than // 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: // 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), // 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, // 3. restore every other track's arm,
// 4. restore the time selection + edit cursor. // 4. restore the time selection + edit cursor.
// Stop the record's OWN project transport if it is still playing/recording. Uses // 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 // 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. // touching them (stopOwnTransport, DeleteTrack, arm restore) is a use-after-free.
// ValidatePtr2 with a null project validates the ReaProject* itself (the header: // ValidatePtr2 with a null project validates the ReaProject* itself (the header:
// "proj is ignored if pointer is itself a project"). Every teardown that // "proj is ignored if pointer is itself a project"). Every teardown that
@@ -259,7 +274,6 @@ public:
void dropWithoutRestore() { void dropWithoutRestore() {
restored_ = true; restored_ = true;
temp_ = nullptr; temp_ = nullptr;
master_ = nullptr;
armSnaps_.clear(); armSnaps_.clear();
} }
@@ -271,8 +285,9 @@ public:
// by the terminal path's explicit stop-before-finalize — a safe no-op then). // by the terminal path's explicit stop-before-finalize — a safe no-op then).
stopOwnTransport(); stopOwnTransport();
// 2. Temp track: deleting it drops the master send AND the recorded arrange // 2. Temp track: deleting it drops the source-track sends (REAPER removes every
// item in one move — nothing stays in the arrange (load-bearing principle). // 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; } if (temp_) { DeleteTrack(temp_); temp_ = nullptr; }
// 3. Other tracks' record-arm. // 3. Other tracks' record-arm.
@@ -367,12 +382,24 @@ void RealtimeCaptureStateDeleter::operator()(RealtimeCaptureState* p) const noex
} }
RealtimeCaptureHandle RealtimeCaptureHandle
RealtimeRecordBackend::begin(const CaptureRequest& request, CaptureResult& outFailure) { RealtimeRecordBackend::begin(const CaptureRequest& request,
// Only the master scope is implemented this increment (see §FORK). const std::vector<MediaTrack*>& sourceTracks,
if (request.sourceMode != SourceMode::MasterMix) { 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.status = CaptureStatus::UnsupportedMode;
outFailure.message = "RealtimeRecordBackend implements MASTER scope only this " outFailure.message = "RealtimeRecordBackend implements TRACK scope only this "
"increment (track/item realtime routing is a surfaced fork)."; "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; return nullptr;
} }
@@ -431,8 +458,8 @@ RealtimeRecordBackend::begin(const CaptureRequest& request, CaptureResult& outFa
st->snapshotAndDisarmOthers(); st->snapshotAndDisarmOthers();
// Hidden temp track at the end: no default FX/envelopes (clean sink), hidden from // 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 // 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). // invariant — it would otherwise double the tapped tracks in the user's monitoring).
const int idx = CountTracks(proj); const int idx = CountTracks(proj);
InsertTrackAtIndex(idx, false); InsertTrackAtIndex(idx, false);
st->temp_ = GetTrack(proj, idx); 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_SHOWINMIXER", 0.0);
SetMediaTrackInfo_Value(st->temp_, "B_MAINSEND", 0.0); SetMediaTrackInfo_Value(st->temp_, "B_MAINSEND", 0.0);
// Route the MASTER output into the temp track (a send master -> temp). The temp // Route the TRACK-OUTPUT tap: a send FROM each selected source track INTO the temp
// track records this in output-record mode. // 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 // Sends default to post-fader (I_SENDMODE 0) and full-stereo (I_SRCCHAN default,
// track fed only by a master send records THAT send's signal is the crux to // (srcchan>>10)==0 — SDK ~3302/3304): post-fader = after the source track's FX and
// verify live — named here so DAW testing targets it directly. // fader/pan = the track's OWN output, tapped BEFORE the parent sums it. Left at
st->master_ = GetMasterTrack(proj); // defaults deliberately — that IS the track-scope tap point.
if (!st->master_) { //
outFailure.status = CaptureStatus::RenderFailed; // DAW-ONLY ASSUMPTION (flag): that a post-fader track->temp send + output-record
outFailure.message = "Could not resolve the master track for realtime routing."; // reproduces the track's own output sample-for-sample (latency comp, pan law,
st->restore(); // temp track removed here // mono/stereo folding) is the crux to verify live.
return nullptr; 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 (sendsMade == 0) {
if (sendIdx < 0) { // Every send failed (should not happen for valid selected tracks). Refuse
// rather than record a guaranteed-silent file.
outFailure.status = CaptureStatus::RenderFailed; outFailure.status = CaptureStatus::RenderFailed;
outFailure.message = "Could not route the master output into the record track."; outFailure.message = "Could not route any selected track into the record tap — "
st->restore(); // deleting the temp track drops any partial send too "nothing to capture.";
st->restore(); // deleting the temp track drops any partial sends too
return nullptr; 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 OutputTap tap = outputTapForWetDry(request.wetDry);
const RecordModePlan rec = recordModePlanFor(request.channelCount, tap); const RecordModePlan rec = recordModePlanFor(request.channelCount, tap);
SetMediaTrackInfo_Value(st->temp_, "I_RECMODE", static_cast<double>(rec.recMode)); 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; } if (state.restored()) { out.status = RealtimeTickStatus::Failed; return out; }
// CRITICAL (review §1): if the captured project was CLOSED mid-record, proj_ / // 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 // 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 // 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 // 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. // Persistent action-id prefix for the ReaSampler action family.
// Every bindable action (capture / insert / slot / verify) mints its command id // 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 // FOREVER-STABLE once shipped: user keybindings key off these strings, so the
// prefix and any minted id must never change after release. // prefix and any minted id must never change after release.
#define REASAMPLER_ACTION_PREFIX "CEREBELLUM_REASAMPLER_" #define REASAMPLER_ACTION_PREFIX "CEREBELLUM_REASAMPLER_"
@@ -44,17 +44,18 @@
REAPER_PLUGIN_HINSTANCE g_hInst = nullptr; // this module's instance handle REAPER_PLUGIN_HINSTANCE g_hInst = nullptr; // this module's instance handle
reaper_plugin_info_t* g_rec = nullptr; // REAPER's dispatch struct reaper_plugin_info_t* g_rec = nullptr; // REAPER's dispatch struct
// ---- Capture action family (three FX scopes) ------------------------------- // ---- Capture action family (two FX scopes) ---------------------------------
// Three bindable SCOPE actions from captureActionTable() (render_settings, pure): // Two bindable SCOPE actions from captureActionTable() (render_settings, pure):
// capture item / track / master. Each infers its range (razor-else-time) and // capture item / track. Each infers its range (razor-else-time) and enforces the
// enforces the FX-scope invariant via FX-bypass-around-render (FxBypassGuard): // FX-scope invariant via FX-bypass-around-render (FxBypassGuard):
// Item -> take/item FX only (bypass the item's track + ancestors + master). // Item -> take/item FX only (bypass the item's track + ancestors + master).
// Track -> item FX + track's own FX (bypass ancestors + master). // Track -> item FX + track's own FX (bypass ancestors + master).
// Master -> whole chain (bypass nothing). // There is NO master scope — to capture the master you render a track. (The master
// This REPLACES the retired M7 four-mode family (master / tracks / items / razor). // track's FX/gain/pan are STILL neutralized for both scopes as the out-of-scope
// The retired CAPTURE_TRACKS_WET / CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids are // chain — master is a bypass target, not a capture scope.) The retired M7
// mirror-unregistered on unload so old keybindings clear cleanly; CAPTURE_MASTER's // CAPTURE_TRACKS_WET / CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids AND the removed
// id string is preserved. // 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 // 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- // 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<int> g_captureCmdIds;
static std::vector<gaccel_register_t> g_captureAccels; static std::vector<gaccel_register_t> g_captureAccels;
// Retired capture-action command-id strings (M7 four-mode family). Kept ONLY to // Retired capture-action command-id strings. Kept ONLY to mirror-unregister them on
// mirror-unregister them on unload so a user's stale keybindings are cleaned up. // unload so a user's stale keybindings are cleaned up. Never re-register these.
// Never re-register these. CAPTURE_MASTER is NOT here — its id string carries over // * The M7 four-mode ids (tracks/items/razor WET).
// to the new master scope action unchanged. // * 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[] = { static const char* const kRetiredCaptureCmdStrings[] = {
"CEREBELLUM_REASAMPLER_CAPTURE_TRACKS_WET", "CEREBELLUM_REASAMPLER_CAPTURE_TRACKS_WET",
"CEREBELLUM_REASAMPLER_CAPTURE_ITEMS_WET", "CEREBELLUM_REASAMPLER_CAPTURE_ITEMS_WET",
"CEREBELLUM_REASAMPLER_CAPTURE_RAZOR_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. // 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_cmdInsertSelected = 0;
static int g_cmdInsertSelectedConform = 0; static int g_cmdInsertSelectedConform = 0;
// Command id for the M8 "capture master (realtime)" action. FOREVER-STABLE string. // Command id for the "capture selected track (realtime)" action. NEW FOREVER-STABLE
// Records the master output in realtime (transport-driven) into a hidden temp track // string. Records the selected track's OWN output in realtime (transport-driven) into
// via RealtimeRecordBackend, then moves the recorded file into the bank. The // a hidden temp track via RealtimeRecordBackend, then moves the recorded file into the
// realtime SIBLING of the offline CAPTURE_MASTER scope action: same range logic // bank. The realtime SIBLING of the offline CAPTURE_TRACK scope action: same range
// (razor-else-time), same bank/persist path, different backend. Dialog-free. // logic (razor-else-time), same track selection, same bank/persist path, different
static int g_cmdCaptureMasterRealtime = 0; // 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. // 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 // 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. // range); the caller reports it and writes nothing.
// The resolved source: exact bounds + the source tracks (for FX-bypass + Sample // 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 struct ResolvedSource
{ {
double startSeconds = 0.0; double startSeconds = 0.0;
double endSeconds = 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 std::vector<std::string> trackGuids; // canonical GUIDs of sourceTracks
}; };
@@ -325,8 +332,8 @@ static bool collectSelectedItemTracks(ResolvedSource& out)
return !out.sourceTracks.empty(); return !out.sourceTracks.empty();
} }
// Resolves the source for a scope: the selection tracks (item/track) or none // Resolves the source for a scope: the selection tracks (item/track), plus the
// (master), plus the inferred range. Returns false with a reason on nothing to do. // inferred range. Returns false with a reason on nothing to do.
static bool ResolveScopeSource(reasampler::CaptureScope scope, static bool ResolveScopeSource(reasampler::CaptureScope scope,
ResolvedSource& out, std::string& why) ResolvedSource& out, std::string& why)
{ {
@@ -343,8 +350,6 @@ static bool ResolveScopeSource(reasampler::CaptureScope scope,
why = "select at least one track"; return false; why = "select at least one track"; return false;
} }
break; break;
case CaptureScope::Master:
break; // whole chain — no source-track collection
} }
return resolveRange(out.startSeconds, out.endSeconds, why); 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: // neutralize set is IDENTICAL to the FX-bypass set:
// Item -> own track + all ancestors + master (take vol/pan kept: item content). // Item -> own track + all ancestors + master (take vol/pan kept: item content).
// Track -> all ancestors + master (selected track's OWN vol/pan kept). // 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, // 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 // 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()); ShowConsoleMsg(log.c_str());
} }
// STARTS the M8 REALTIME master capture and returns immediately — the record runs // STARTS the REALTIME track capture and returns immediately — the record runs across
// across timer ticks (DriveRealtimeCapture), so REAPER's UI stays responsive. Infers // timer ticks (DriveRealtimeCapture), so REAPER's UI stays responsive. Resolves the
// the range (razor-else-time, the same orthogonal range logic as the offline scopes) // selected tracks + the range (razor-else-time, the same orthogonal range logic as the
// and starts recording the master output into a hidden temp track via // offline scopes) and starts recording each selected track's OWN output into a hidden
// RealtimeRecordBackend::begin; OnTimer drives it to completion, then adds the Sample // temp track via RealtimeRecordBackend::begin (a send FROM each source track INTO the
// and persists. MASTER scope only this increment (track/item realtime routing is a // temp — see capture_realtime.cpp §TAP); OnTimer drives it to completion, then adds the
// surfaced fork — see capture_realtime.cpp §FORK). Dialog-free. Non-bit-identical by // Sample and persists. TRACK scope only this increment (item realtime is deferred).
// nature (it is realtime) — offline stays the deterministic default. FxBypassGuard is // Dialog-free. Non-bit-identical by nature (it is realtime) — offline stays the
// NOT used here (it neutralizes the live chain, altering the user's monitoring). 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; // 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. // 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 // 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 // first keeps running (we own the transport for its window; starting a second would
// collide on the transport and the temp-track/arm snapshot). // collide on the transport and the temp-track/arm snapshot).
static void RunCaptureRealtimeMaster() static void RunCaptureRealtimeTrack()
{ {
if (g_rtCapture) if (g_rtCapture)
{ {
@@ -569,29 +575,32 @@ static void RunCaptureRealtimeMaster()
return; 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; std::string why;
if (!resolveRange(start, end, why)) if (!ResolveScopeSource(reasampler::CaptureScope::Track, src, why))
{ {
ShowConsoleMsg(("ReaSampler realtime capture: " + why + ".\n").c_str()); ShowConsoleMsg(("ReaSampler realtime capture: " + why + ".\n").c_str());
return; return;
} }
reasampler::CaptureRequest req; reasampler::CaptureRequest req;
req.sourceMode = reasampler::SourceMode::MasterMix; // realtime master scope req.sourceMode = reasampler::SourceMode::SelectedTracks; // realtime track scope
req.startSeconds = start; // exact bounds — no rounding req.startSeconds = src.startSeconds; // exact bounds — no rounding
req.endSeconds = end; req.endSeconds = src.endSeconds;
req.wetDry = 1.0; // fully wet (post-fader tap) req.wetDry = 1.0; // fully wet (post-fader tap)
req.renderTail = false; req.renderTail = false;
req.tailMs = 0.0; req.tailMs = 0.0;
req.sampleRate = 0; // follow project rate req.sampleRate = 0; // follow project rate
req.channelCount = 2; req.channelCount = 2;
req.bitDepth = reasampler::WavBitDepth::Float32; req.bitDepth = reasampler::WavBitDepth::Float32;
req.baseName = "realtime"; req.baseName = "realtime";
// No trackGuids — master scope is not track-provenanced. req.trackGuids = src.trackGuids; // provenance on the Sample
reasampler::CaptureResult failure; reasampler::CaptureResult failure;
reasampler::RealtimeCaptureHandle st = g_rtBackend.begin(req, failure); reasampler::RealtimeCaptureHandle st =
g_rtBackend.begin(req, src.sourceTracks, failure);
if (!st) if (!st)
{ {
// begin() validated/failed and already restored anything it touched. // 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_cmdToggleBankPanel) { reasampler::bankPanelToggle(); return true; }
if (command == g_cmdInsertSelected) { RunInsertSelected(false); return true; } if (command == g_cmdInsertSelected) { RunInsertSelected(false); return true; }
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); 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; } if (command == g_cmdCancelRealtime) { RunCancelRealtime(); return true; }
// Design View action family (D4). Claims only its own ids; returns false for the // Design View action family (D4). Claims only its own ids; returns false for the
// rest so this hook keeps looking (per the contract). // 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_accelToggleBankPanel{};
static gaccel_register_t g_accelInsertSelected{}; static gaccel_register_t g_accelInsertSelected{};
static gaccel_register_t g_accelInsertSelectedConform{}; static gaccel_register_t g_accelInsertSelectedConform{};
static gaccel_register_t g_accelCaptureMasterRealtime{}; static gaccel_register_t g_accelCaptureTrackRealtime{};
static gaccel_register_t g_accelCancelRealtime{}; static gaccel_register_t g_accelCancelRealtime{};
extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT( 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("-gaccel", (void*)&g_accelCancelRealtime);
g_rec->Register("-command_id", g_rec->Register("-command_id",
(void*)(REASAMPLER_ACTION_PREFIX "CANCEL_REALTIME_CAPTURE")); (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", 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("-gaccel", (void*)&g_accelInsertSelectedConform);
g_rec->Register("-command_id", g_rec->Register("-command_id",
(void*)(REASAMPLER_ACTION_PREFIX "INSERT_SELECTED_CONFORM")); (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); rec->Register("gaccel", (void*)&g_accelInsertSelectedConform);
} }
// Register the M8 "capture master (realtime)" action (command_id -> gaccel -> // Register the "capture selected track (realtime)" action (command_id -> gaccel ->
// hookcommand). Realtime sibling of the offline CAPTURE_MASTER scope: records // hookcommand). Realtime sibling of the offline CAPTURE_TRACK scope: records the
// the master output in realtime into a hidden temp track, moves it into the // selected track's own output in realtime into a hidden temp track, moves it into
// bank. Dialog-free. FOREVER-STABLE id string. // the bank. Dialog-free. NEW FOREVER-STABLE id string.
g_cmdCaptureMasterRealtime = rec->Register( g_cmdCaptureTrackRealtime = rec->Register(
"command_id", "command_id",
(void*)(REASAMPLER_ACTION_PREFIX "CAPTURE_MASTER_REALTIME")); (void*)(REASAMPLER_ACTION_PREFIX "CAPTURE_TRACK_REALTIME"));
if (g_cmdCaptureMasterRealtime) if (g_cmdCaptureTrackRealtime)
{ {
g_accelCaptureMasterRealtime.accel.cmd = g_cmdCaptureMasterRealtime; g_accelCaptureTrackRealtime.accel.cmd = g_cmdCaptureTrackRealtime;
g_accelCaptureMasterRealtime.desc = g_accelCaptureTrackRealtime.desc =
"ReaSampler: capture master (realtime)"; "ReaSampler: capture selected track (realtime)";
rec->Register("gaccel", (void*)&g_accelCaptureMasterRealtime); rec->Register("gaccel", (void*)&g_accelCaptureTrackRealtime);
} }
// Cancel-in-flight sibling: aborts a running realtime capture (stop + restore). // 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) { switch (scope) {
case CaptureScope::Item: return SourceMode::SelectedItems; case CaptureScope::Item: return SourceMode::SelectedItems;
case CaptureScope::Track: return SourceMode::SelectedTracks; 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) { RangeSource inferRangeSource(bool hasRazorArea) {
@@ -84,12 +83,6 @@ FxBypassPlan fxBypassPlanFor(CaptureScope scope) {
p.bypassAncestorFx = true; p.bypassAncestorFx = true;
p.bypassMaster = true; p.bypassMaster = true;
return p; 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) 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() { 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 // (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 // action infers its range (razor-else-time) at fire time and enforces its
// FX-scope invariant via fxBypassPlanFor. The M7 CAPTURE_TRACKS_WET / // FX-scope invariant via fxBypassPlanFor. The M7 CAPTURE_TRACKS_WET /
// CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids are RETIRED (mirror-unregistered in // 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 = { static const std::vector<CaptureActionDef> table = {
// Item scope — item/take FX only. NEW forever-stable id. // Item scope — item/take FX only. NEW forever-stable id.
{"CEREBELLUM_REASAMPLER_CAPTURE_ITEM", {"CEREBELLUM_REASAMPLER_CAPTURE_ITEM",
@@ -153,11 +147,6 @@ const std::vector<CaptureActionDef>& captureActionTable() {
{"CEREBELLUM_REASAMPLER_CAPTURE_TRACK", {"CEREBELLUM_REASAMPLER_CAPTURE_TRACK",
"ReaSampler: capture selected track(s)", "track", "ReaSampler: capture selected track(s)", "track",
CaptureScope::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; return table;
} }
+11 -10
View File
@@ -66,18 +66,20 @@ RenderSettingsChoice renderSettingsFor(SourceMode mode, double wetDry);
// --- Capture scope: the FX-scope invariant (Daniel, critical) ---------------- // --- 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). // 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). // 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 { enum class CaptureScope {
Item, Item,
Track, Track,
Master,
}; };
// The render source mode each scope drives. Item captures selected items, Track // 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); SourceMode sourceModeForScope(CaptureScope scope);
// --- Range inference: razor-else-time (orthogonal to 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) ----------- // --- 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. // 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, // 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). // 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* commandString; // CEREBELLUM_REASAMPLER_… FOREVER-STABLE id string
const char* description; // Actions-list label const char* description; // Actions-list label
const char* baseName; // file-stem base for this capture 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 // 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 // each fired command back to its definition. Kept here (pure) so the taxonomy is
// one testable list, not scattered registration code. // one testable list, not scattered registration code.
// //
// Three scope rows: CAPTURE_ITEM, CAPTURE_TRACK, CAPTURE_MASTER. This replaces the // Two scope rows: CAPTURE_ITEM, CAPTURE_TRACK. There is no master capture — to
// M7 four-mode table (master / tracks / items / razor) — razor is now an inferred // capture the master you render a track. Razor is an inferred range, not a mode,
// range, not a mode, and each scope enforces its FX-scope invariant via // and each scope enforces its FX-scope invariant via fxBypassPlanFor.
// fxBypassPlanFor.
const std::vector<CaptureActionDef>& captureActionTable(); const std::vector<CaptureActionDef>& captureActionTable();
} // namespace reasampler } // namespace reasampler
+13 -38
View File
@@ -1,5 +1,5 @@
// Standalone tests for reasampler::render_settings — no REAPER, no framework. // Standalone tests for reasampler::render_settings — no REAPER, no framework.
// Covers the pure pieces behind the three-scope capture family: the source-mode -> // Covers the pure pieces behind the two-scope capture family: the source-mode ->
// RENDER_SETTINGS bit mapping, P_RAZOREDITS parsing -> ranges + union, scope -> // RENDER_SETTINGS bit mapping, P_RAZOREDITS parsing -> ranges + union, scope ->
// source mode, range inference (razor-else-time), the FX-bypass plan (corrects // source mode, range inference (razor-else-time), the FX-bypass plan (corrects
// the "items captured through parent FX" defect), and the capture-action taxonomy // the "items captured through parent FX" defect), and the capture-action taxonomy
@@ -111,15 +111,14 @@ static void testRazorUnionBounds() {
// --- sourceModeForScope: scope -> render source mode ------------------------- // --- sourceModeForScope: scope -> render source mode -------------------------
static void testScopeSourceModes() { static void testScopeSourceModes() {
// Each scope drives a distinct render source. Item -> items, Track -> tracks, // Each scope drives a distinct render source. Item -> items, Track -> tracks.
// Master -> master mix. These feed renderSettingsFor and must be supported. // (There is no master scope — to capture the master you render a track.) These
// feed renderSettingsFor and must be supported.
CHECK(sourceModeForScope(CaptureScope::Item) == SourceMode::SelectedItems); CHECK(sourceModeForScope(CaptureScope::Item) == SourceMode::SelectedItems);
CHECK(sourceModeForScope(CaptureScope::Track) == SourceMode::SelectedTracks); CHECK(sourceModeForScope(CaptureScope::Track) == SourceMode::SelectedTracks);
CHECK(sourceModeForScope(CaptureScope::Master) == SourceMode::MasterMix);
// Every scope's source mode is an offline-supported render source. // Every scope's source mode is an offline-supported render source.
CHECK(renderSettingsFor(sourceModeForScope(CaptureScope::Item), 1.0).supported); CHECK(renderSettingsFor(sourceModeForScope(CaptureScope::Item), 1.0).supported);
CHECK(renderSettingsFor(sourceModeForScope(CaptureScope::Track), 1.0).supported); CHECK(renderSettingsFor(sourceModeForScope(CaptureScope::Track), 1.0).supported);
CHECK(renderSettingsFor(sourceModeForScope(CaptureScope::Master), 1.0).supported);
} }
// --- inferRangeSource: razor-else-time (orthogonal to scope) ----------------- // --- inferRangeSource: razor-else-time (orthogonal to scope) -----------------
@@ -144,29 +143,22 @@ static void testItemScopeBypassesEverythingButTake() {
static void testTrackScopeKeepsSelfBypassesAncestorsAndMaster() { static void testTrackScopeKeepsSelfBypassesAncestorsAndMaster() {
// Track = item FX + the track's OWN FX. Keep self FX; bypass ancestors + master. // Track = item FX + the track's OWN FX. Keep self FX; bypass ancestors + master.
// Master stays a bypass target even though it is no longer a capture scope.
FxBypassPlan p = fxBypassPlanFor(CaptureScope::Track); FxBypassPlan p = fxBypassPlanFor(CaptureScope::Track);
CHECK(!p.bypassSelfFx); // the whole point: the track's own FX stays live CHECK(!p.bypassSelfFx); // the whole point: the track's own FX stays live
CHECK(p.bypassAncestorFx); // no parent/folder FX CHECK(p.bypassAncestorFx); // no parent/folder FX
CHECK(p.bypassMaster); // no master FX CHECK(p.bypassMaster); // no master FX
} }
static void testMasterScopeBypassesNothing() { // --- captureActionTable: the two-scope taxonomy ------------------------------
// Master = whole chain. Nothing bypassed — the full signal path renders.
FxBypassPlan p = fxBypassPlanFor(CaptureScope::Master);
CHECK(!p.bypassSelfFx);
CHECK(!p.bypassAncestorFx);
CHECK(!p.bypassMaster);
}
// --- captureActionTable: the three-scope taxonomy ---------------------------- static void testTableHasTwoScopeRows() {
static void testTableHasThreeScopeRows() {
const auto& table = captureActionTable(); const auto& table = captureActionTable();
// Exactly 3 scope rows: item, track, master. // Exactly 2 scope rows: item, track. There is no master scope.
CHECK(table.size() == 3); CHECK(table.size() == 2);
std::set<std::string> ids; std::set<std::string> ids;
bool sawItem = false, sawTrack = false, sawMaster = false; bool sawItem = false, sawTrack = false;
for (const auto& def : table) { for (const auto& def : table) {
// Every id is a non-empty CEREBELLUM_REASAMPLER_ string and is UNIQUE // Every id is a non-empty CEREBELLUM_REASAMPLER_ string and is UNIQUE
// (duplicate ids would collide on registration). // (duplicate ids would collide on registration).
@@ -176,26 +168,11 @@ static void testTableHasThreeScopeRows() {
// Every scope resolves to a supported offline source. // Every scope resolves to a supported offline source.
CHECK(renderSettingsFor(sourceModeForScope(def.scope), 1.0).supported); CHECK(renderSettingsFor(sourceModeForScope(def.scope), 1.0).supported);
if (def.scope == CaptureScope::Item) sawItem = true; if (def.scope == CaptureScope::Item) sawItem = true;
if (def.scope == CaptureScope::Track) sawTrack = true; if (def.scope == CaptureScope::Track) sawTrack = true;
if (def.scope == CaptureScope::Master) sawMaster = true;
} }
CHECK(sawItem); CHECK(sawItem);
CHECK(sawTrack); CHECK(sawTrack);
CHECK(sawMaster);
}
static void testMasterCommandIdIsPreserved() {
// The master scope keeps its shipped M7 id string (user keybindings depend on
// it). Item/track mint NEW ids; master's must be exactly the old value.
bool foundMaster = false;
for (const auto& def : captureActionTable())
if (def.scope == CaptureScope::Master) {
CHECK(std::string(def.commandString) ==
"CEREBELLUM_REASAMPLER_CAPTURE_MASTER");
foundMaster = true;
}
CHECK(foundMaster);
} }
int main() { int main() {
@@ -213,9 +190,7 @@ int main() {
testRangeInference(); testRangeInference();
testItemScopeBypassesEverythingButTake(); testItemScopeBypassesEverythingButTake();
testTrackScopeKeepsSelfBypassesAncestorsAndMaster(); testTrackScopeKeepsSelfBypassesAncestorsAndMaster();
testMasterScopeBypassesNothing(); testTableHasTwoScopeRows();
testTableHasThreeScopeRows();
testMasterCommandIdIsPreserved();
if (g_fail == 0) std::printf("render_settings: all tests passed\n"); if (g_fail == 0) std::printf("render_settings: all tests passed\n");
else std::printf("render_settings: %d CHECK(s) FAILED\n", g_fail); else std::printf("render_settings: %d CHECK(s) FAILED\n", g_fail);