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:
+25
-14
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user