Rework capture into three FX-scope actions with inferred range
Replace the four capture modes with item/track/master scope actions. Each infers its range (razor-else-time) and enforces FX scope via non-destructive FX-bypass-around-render (RAII I_FXEN snapshot/restore over ancestors + master). Corrects the defect of items captured through parent FX.
This commit is contained in:
+6
-3
@@ -1,11 +1,14 @@
|
||||
// capture.cpp — REAPER-facing offline-render backend (M3 spike).
|
||||
// capture.cpp — REAPER-facing offline-render backend (OfflineRenderBackend).
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes
|
||||
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU
|
||||
// that defines the API pointers; here they are extern (CLAUDE.md §contract).
|
||||
//
|
||||
// Scope (M7): the full offline source family — master mix / time selection,
|
||||
// selected tracks, selected items, razor area — all wet-only with optional tail.
|
||||
// Renders a CaptureRequest's source over its requested range. The full three-scope
|
||||
// capture family (item / track / master, each over a razor-else-time range) is
|
||||
// driven here — all wet-only with optional tail. FX scope is enforced by the
|
||||
// caller (via FX-bypass-around-render / FxBypassGuard) before invoking capture;
|
||||
// this backend is source-agnostic and does not itself read the DAW selection.
|
||||
// Drives the RENDER_* project settings via GetSetProjectInfo / _String
|
||||
// (the source-selection bits come from render_settings.cpp, the pure mapping),
|
||||
// snapshots and restores every setting it changes (non-destructive), triggers a
|
||||
|
||||
+7
-7
@@ -41,7 +41,7 @@ struct CaptureRequest {
|
||||
double startSeconds = 0.0;
|
||||
double endSeconds = 0.0;
|
||||
|
||||
// 1.0 = fully wet, 0.0 = fully dry. All M7 actions set this to 1.0 (wet).
|
||||
// 1.0 = fully wet, 0.0 = fully dry. All three-scope capture actions set this to 1.0 (wet).
|
||||
// The field is kept as the seam for future true-dry work (M10 null test):
|
||||
// true pre-FX dry offline is NOT available via RENDER_SETTINGS — it requires
|
||||
// FX-bypass-around-render or the M8 realtime pre-FX path, and will be
|
||||
@@ -101,12 +101,12 @@ public:
|
||||
virtual CaptureResult capture(const CaptureRequest& request) = 0;
|
||||
};
|
||||
|
||||
// Deterministic offline-render backend. M7 implements the full offline source
|
||||
// family — master mix / time selection, selected tracks, selected items, razor
|
||||
// area — all wet-only (render_settings.h) with optional tail. The source
|
||||
// selection + range are resolved by the caller (the action layer) and handed in
|
||||
// via the CaptureRequest; the backend drives RENDER_* and never reads the DAW
|
||||
// selection itself. SourceMode::Realtime returns UnsupportedMode (that is M8).
|
||||
// Deterministic offline-render backend. Drives the full offline source family —
|
||||
// master mix / time selection, selected tracks, selected items, razor area — all
|
||||
// wet-only (render_settings.h) with optional tail. The source selection + range
|
||||
// are resolved by the caller (the action layer) and handed in via the
|
||||
// CaptureRequest; the backend drives RENDER_* and never reads the DAW selection
|
||||
// itself. SourceMode::Realtime returns UnsupportedMode (that is the M8 backend).
|
||||
class OfflineRenderBackend : public ICaptureBackend {
|
||||
public:
|
||||
CaptureResult capture(const CaptureRequest& request) override;
|
||||
|
||||
+209
-118
@@ -43,12 +43,17 @@
|
||||
REAPER_PLUGIN_HINSTANCE g_hInst = nullptr; // this module's instance handle
|
||||
reaper_plugin_info_t* g_rec = nullptr; // REAPER's dispatch struct
|
||||
|
||||
// ---- M7 capture action family ----------------------------------------------
|
||||
// Four wet-only bindable actions from captureActionTable() (render_settings, pure):
|
||||
// master mix, selected tracks, selected items, razor area — all wet (post-FX).
|
||||
// Tail is OFF for every row (exact bounds); a tail-on variant is a later opt-in
|
||||
// (YAGNI). The M3 "capture master mix (spike)" action is RETIRED and replaced by
|
||||
// this family. Dry variants are deferred to M10 (null-test work).
|
||||
// ---- 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):
|
||||
// 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.
|
||||
//
|
||||
// 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-
|
||||
@@ -56,6 +61,16 @@ 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.
|
||||
static const char* const kRetiredCaptureCmdStrings[] = {
|
||||
"CEREBELLUM_REASAMPLER_CAPTURE_TRACKS_WET",
|
||||
"CEREBELLUM_REASAMPLER_CAPTURE_ITEMS_WET",
|
||||
"CEREBELLUM_REASAMPLER_CAPTURE_RAZOR_WET",
|
||||
};
|
||||
|
||||
// Command id for "ReaSampler: toggle bank panel" (M5). FOREVER-STABLE string.
|
||||
// The docked grid window is display-only this wave (Wave A) — the action just
|
||||
// shows/hides it; it never captures, inserts, or mutates the bank.
|
||||
@@ -100,24 +115,26 @@ static void OnTimer()
|
||||
reasampler::bankPanelRefresh();
|
||||
}
|
||||
|
||||
// --- M7 source resolvers ----------------------------------------------------
|
||||
// Each resolves a source mode to (1) the exact render range in project seconds and
|
||||
// (2) the track GUIDs, when track-scoped. They ONLY READ DAW state (selection, time
|
||||
// selection, razor strings) — they never mutate it (non-destructive). Returning
|
||||
// false means "nothing to capture" (empty selection / no razor / empty range); the
|
||||
// caller reports it and writes nothing.
|
||||
// --- Scope-action source resolution -----------------------------------------
|
||||
// The three scope actions (item / track / master) each resolve to (1) an exact
|
||||
// render range in project seconds — razor-else-time, inferred here — and (2) the
|
||||
// set of source TRACKS whose ancestor chains drive the FX-bypass plan. All reads
|
||||
// are non-destructive: selection, razor, and time selection are read, never
|
||||
// mutated. Returning false means "nothing to capture" (empty selection / no
|
||||
// range); the caller reports it and writes nothing.
|
||||
|
||||
// The resolved source: exact bounds + optional track GUIDs.
|
||||
// The resolved source: exact bounds + the source tracks (for FX-bypass + Sample
|
||||
// provenance GUIDs). `sourceTracks` is empty for Master scope.
|
||||
struct ResolvedSource
|
||||
{
|
||||
double startSeconds = 0.0;
|
||||
double endSeconds = 0.0;
|
||||
std::vector<std::string> trackGuids; // populated only for SelectedTracks
|
||||
double startSeconds = 0.0;
|
||||
double endSeconds = 0.0;
|
||||
std::vector<MediaTrack*> sourceTracks; // item's/selected tracks; empty for master
|
||||
std::vector<std::string> trackGuids; // canonical GUIDs of sourceTracks
|
||||
};
|
||||
|
||||
// Time selection -> exact bounds (no rounding). GetSet_LoopTimeRange(isSet=false,
|
||||
// isLoop=false) reads the current time selection. Used by master mix (the range is
|
||||
// the time selection) and as the time window for selected-track captures.
|
||||
// isLoop=false) reads the current time selection.
|
||||
static bool resolveTimeSelection(double& start, double& end)
|
||||
{
|
||||
start = 0.0; end = 0.0;
|
||||
@@ -125,62 +142,11 @@ static bool resolveTimeSelection(double& start, double& end)
|
||||
return end > start;
|
||||
}
|
||||
|
||||
// Master mix / time selection: bounds = the time selection; no track GUIDs.
|
||||
static bool resolveMaster(ResolvedSource& out)
|
||||
{
|
||||
return resolveTimeSelection(out.startSeconds, out.endSeconds);
|
||||
}
|
||||
|
||||
// Selected tracks: the render time window is the time selection (RENDER_SETTINGS
|
||||
// selects WHICH tracks; the custom bounds select the WHEN). We also collect the
|
||||
// selected tracks' GUIDs for the Sample's provenance. Requires both a non-empty
|
||||
// track selection AND a time selection (the bounds come from the latter).
|
||||
static bool resolveSelectedTracks(ResolvedSource& out)
|
||||
{
|
||||
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
||||
if (n <= 0) return false;
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MediaTrack* tr = GetSelectedTrack(nullptr, i);
|
||||
if (!tr) continue;
|
||||
std::string g = reasampler::guidString(tr);
|
||||
if (!g.empty()) out.trackGuids.push_back(std::move(g));
|
||||
}
|
||||
return resolveTimeSelection(out.startSeconds, out.endSeconds);
|
||||
}
|
||||
|
||||
// Selected items: bounds = the union [min position, max position+length] across
|
||||
// the selected items (D_POSITION / D_LENGTH — SDK header ~1990/1991). Exact, no
|
||||
// rounding. RENDER_SETTINGS selects the items; the bounds keep the render window
|
||||
// tight around them.
|
||||
static bool resolveSelectedItems(ResolvedSource& out)
|
||||
{
|
||||
const int n = CountSelectedMediaItems(nullptr);
|
||||
if (n <= 0) return false;
|
||||
bool any = false;
|
||||
double lo = 0.0, hi = 0.0;
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
||||
if (!it) continue;
|
||||
const double pos = GetMediaItemInfo_Value(it, "D_POSITION");
|
||||
const double len = GetMediaItemInfo_Value(it, "D_LENGTH");
|
||||
const double end = pos + len;
|
||||
if (!any) { lo = pos; hi = end; any = true; }
|
||||
else { if (pos < lo) lo = pos; if (end > hi) hi = end; }
|
||||
}
|
||||
if (!any) return false;
|
||||
out.startSeconds = lo;
|
||||
out.endSeconds = hi;
|
||||
return out.endSeconds > out.startSeconds;
|
||||
}
|
||||
|
||||
// Razor area: razor edits live PER TRACK (P_RAZOREDITS — SDK header ~2899:
|
||||
// space-separated triples of start, end, envGuidString). We read every track's
|
||||
// razor string, parse the track-audio areas (pure parseRazorEdits), and take the
|
||||
// union bound as the render window. RENDER_SETTINGS&4096 selects the razor content;
|
||||
// the bounds keep the window tight. Reads only — never clears the razor selection.
|
||||
static bool resolveRazorArea(ResolvedSource& out)
|
||||
// Reads every track's P_RAZOREDITS (SDK header ~2899: space-separated triples of
|
||||
// start, end, envGuidString), parses the track-audio areas (pure parseRazorEdits),
|
||||
// and returns the union bound. Reads only — never clears the razor selection.
|
||||
// Returns false when no track-audio razor area exists on any track.
|
||||
static bool resolveRazorRange(double& start, double& end)
|
||||
{
|
||||
std::vector<reasampler::RazorRange> allRanges;
|
||||
const int n = CountTracks(nullptr);
|
||||
@@ -188,8 +154,6 @@ static bool resolveRazorArea(ResolvedSource& out)
|
||||
{
|
||||
MediaTrack* tr = GetTrack(nullptr, i);
|
||||
if (!tr) continue;
|
||||
// GetSetMediaTrackInfo_String(tr, "P_RAZOREDITS", buf, false) reads the
|
||||
// razor string into buf. Big buffer: many areas can accumulate.
|
||||
std::vector<char> buf(8192, '\0');
|
||||
if (!GetSetMediaTrackInfo_String(tr, "P_RAZOREDITS", buf.data(), false))
|
||||
continue;
|
||||
@@ -199,70 +163,193 @@ static bool resolveRazorArea(ResolvedSource& out)
|
||||
}
|
||||
if (allRanges.empty()) return false;
|
||||
reasampler::RazorRange u = reasampler::razorUnionBounds(allRanges);
|
||||
out.startSeconds = u.startSeconds;
|
||||
out.endSeconds = u.endSeconds;
|
||||
return out.endSeconds > out.startSeconds;
|
||||
start = u.startSeconds;
|
||||
end = u.endSeconds;
|
||||
return end > start;
|
||||
}
|
||||
|
||||
// Dispatches to the right resolver for a source mode. Returns false with a reason
|
||||
// in `why` when there is nothing to capture (so the action can log precisely).
|
||||
static bool ResolveSource(reasampler::SourceMode mode, ResolvedSource& out,
|
||||
std::string& why)
|
||||
// Infers the render RANGE for any scope: razor union when a razor area is present,
|
||||
// else the time selection (pure inferRangeSource decides which). Orthogonal to
|
||||
// scope. Returns false (with a reason) when neither yields a non-empty range.
|
||||
static bool resolveRange(double& start, double& end, std::string& why)
|
||||
{
|
||||
using reasampler::SourceMode;
|
||||
switch (mode)
|
||||
double rzStart = 0.0, rzEnd = 0.0;
|
||||
const bool hasRazor = resolveRazorRange(rzStart, rzEnd);
|
||||
if (reasampler::inferRangeSource(hasRazor) == reasampler::RangeSource::Razor)
|
||||
{
|
||||
case SourceMode::MasterMix:
|
||||
case SourceMode::TimeSelection:
|
||||
if (resolveMaster(out)) return true;
|
||||
why = "no time selection (make a time selection first)";
|
||||
return false;
|
||||
case SourceMode::SelectedTracks:
|
||||
if (resolveSelectedTracks(out)) return true;
|
||||
why = "select at least one track AND make a time selection";
|
||||
return false;
|
||||
case SourceMode::SelectedItems:
|
||||
if (resolveSelectedItems(out)) return true;
|
||||
why = "select at least one media item";
|
||||
return false;
|
||||
case SourceMode::RazorArea:
|
||||
if (resolveRazorArea(out)) return true;
|
||||
why = "no razor edit area found on any track";
|
||||
return false;
|
||||
case SourceMode::Realtime:
|
||||
why = "realtime capture is the M8 backend, not offline render";
|
||||
return false;
|
||||
start = rzStart; end = rzEnd;
|
||||
return true; // resolveRazorRange already verified end > start
|
||||
}
|
||||
why = "unknown source mode";
|
||||
if (resolveTimeSelection(start, end)) return true;
|
||||
why = "make a razor area or a time selection first";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Runs one capture-action-table row: resolve its source, build a CaptureRequest,
|
||||
// hand it to the offline backend, add the Sample to the bank, persist + mark dirty.
|
||||
// The load-bearing principle holds structurally — this path writes a file + a bank
|
||||
// index entry ONLY; it never calls InsertMedia or touches the arrange/timeline.
|
||||
// Collects the selected tracks (Track scope) into out.sourceTracks + GUIDs.
|
||||
static bool collectSelectedTracks(ResolvedSource& out)
|
||||
{
|
||||
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
||||
if (n <= 0) return false;
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MediaTrack* tr = GetSelectedTrack(nullptr, i);
|
||||
if (!tr) continue;
|
||||
out.sourceTracks.push_back(tr);
|
||||
std::string g = reasampler::guidString(tr);
|
||||
if (!g.empty()) out.trackGuids.push_back(std::move(g));
|
||||
}
|
||||
return !out.sourceTracks.empty();
|
||||
}
|
||||
|
||||
// Collects the tracks that own the selected items (Item scope) into
|
||||
// out.sourceTracks (deduped) — these are the tracks whose FX must be bypassed so an
|
||||
// item capture hears take/item FX only. GetMediaItem_Track(item) gives the owning
|
||||
// track (SDK header, verify). GUIDs recorded for provenance.
|
||||
static bool collectSelectedItemTracks(ResolvedSource& out)
|
||||
{
|
||||
const int n = CountSelectedMediaItems(nullptr);
|
||||
if (n <= 0) return false;
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
||||
if (!it) continue;
|
||||
MediaTrack* tr = GetMediaItem_Track(it);
|
||||
if (!tr) continue;
|
||||
// Dedup: several selected items can share a track.
|
||||
bool seen = false;
|
||||
for (MediaTrack* t : out.sourceTracks) if (t == tr) { seen = true; break; }
|
||||
if (seen) continue;
|
||||
out.sourceTracks.push_back(tr);
|
||||
std::string g = reasampler::guidString(tr);
|
||||
if (!g.empty()) out.trackGuids.push_back(std::move(g));
|
||||
}
|
||||
return !out.sourceTracks.empty();
|
||||
}
|
||||
|
||||
// Resolves the source for a scope: the selection tracks (item/track) or none
|
||||
// (master), plus the inferred range. Returns false with a reason on nothing to do.
|
||||
static bool ResolveScopeSource(reasampler::CaptureScope scope,
|
||||
ResolvedSource& out, std::string& why)
|
||||
{
|
||||
using reasampler::CaptureScope;
|
||||
switch (scope)
|
||||
{
|
||||
case CaptureScope::Item:
|
||||
if (!collectSelectedItemTracks(out)) {
|
||||
why = "select at least one media item"; return false;
|
||||
}
|
||||
break;
|
||||
case CaptureScope::Track:
|
||||
if (!collectSelectedTracks(out)) {
|
||||
why = "select at least one track"; return false;
|
||||
}
|
||||
break;
|
||||
case CaptureScope::Master:
|
||||
break; // whole chain — no source-track collection
|
||||
}
|
||||
return resolveRange(out.startSeconds, out.endSeconds, why);
|
||||
}
|
||||
|
||||
// --- FX-bypass-around-render (RAII, non-destructive) ------------------------
|
||||
// Snapshots and clears I_FXEN on the tracks a scope must NOT hear the FX of, then
|
||||
// restores every snapshotted value on EVERY exit path (including the render's).
|
||||
// I_FXEN bypasses a track's FX plugins only — NOT its volume/pan/routing (so a
|
||||
// Track capture rendered via master still carries parent/master GAIN; documented
|
||||
// boundary, DAW-confirm). Structurally non-destructive: no takes, no items, no
|
||||
// project restructuring — only a transient FX-enable toggle, always restored.
|
||||
class FxBypassGuard
|
||||
{
|
||||
public:
|
||||
// scope drives fxBypassPlanFor; sourceTracks are the captured tracks whose
|
||||
// ancestor chains (walked via GetParentTrack) + the master are bypassed per the
|
||||
// plan. proj is the active project (for GetMasterTrack).
|
||||
FxBypassGuard(reasampler::CaptureScope scope,
|
||||
const std::vector<MediaTrack*>& sourceTracks,
|
||||
ReaProject* proj)
|
||||
{
|
||||
const reasampler::FxBypassPlan plan = reasampler::fxBypassPlanFor(scope);
|
||||
|
||||
for (MediaTrack* tr : sourceTracks)
|
||||
{
|
||||
if (!tr) continue;
|
||||
if (plan.bypassSelfFx) bypass(tr);
|
||||
if (plan.bypassAncestorFx)
|
||||
{
|
||||
// Walk parents to the top: GetParentTrack returns the immediate
|
||||
// parent (folder) track, nullptr at the outermost level (SDK
|
||||
// header ~2407). The master is NOT returned here — handled below.
|
||||
for (MediaTrack* p = GetParentTrack(tr); p; p = GetParentTrack(p))
|
||||
bypass(p);
|
||||
}
|
||||
}
|
||||
if (plan.bypassMaster)
|
||||
{
|
||||
// GetMasterTrack(proj) -> the master track (SDK header ~1925); I_FXEN
|
||||
// on it bypasses the master FX chain, leaving master gain/routing live.
|
||||
if (MediaTrack* master = GetMasterTrack(proj)) bypass(master);
|
||||
}
|
||||
}
|
||||
|
||||
~FxBypassGuard()
|
||||
{
|
||||
// Restore in reverse for symmetry (order is not load-bearing — each track
|
||||
// appears once, snapshots are independent).
|
||||
for (auto it = snapshots_.rbegin(); it != snapshots_.rend(); ++it)
|
||||
SetMediaTrackInfo_Value(it->track, "I_FXEN", it->fxen);
|
||||
}
|
||||
|
||||
FxBypassGuard(const FxBypassGuard&) = delete;
|
||||
FxBypassGuard& operator=(const FxBypassGuard&) = delete;
|
||||
|
||||
private:
|
||||
struct Snap { MediaTrack* track; double fxen; };
|
||||
std::vector<Snap> snapshots_;
|
||||
|
||||
// Snapshot I_FXEN once per track (dedup: an ancestor shared by two selected
|
||||
// tracks must be restored to its ORIGINAL value, not a re-snapshot of the
|
||||
// already-bypassed 0), then clear it.
|
||||
void bypass(MediaTrack* tr)
|
||||
{
|
||||
for (const Snap& s : snapshots_) if (s.track == tr) return; // already done
|
||||
const double fxen = GetMediaTrackInfo_Value(tr, "I_FXEN");
|
||||
snapshots_.push_back({tr, fxen});
|
||||
SetMediaTrackInfo_Value(tr, "I_FXEN", 0.0); // 0 = bypassed (SDK ~2194)
|
||||
}
|
||||
};
|
||||
|
||||
// Runs one capture-action-table row: resolve its scope source + range, snapshot &
|
||||
// clear the out-of-scope FX (RAII), render via the offline backend, add the Sample
|
||||
// to the bank, persist + mark dirty. The load-bearing principle holds structurally
|
||||
// — this path writes a file + a bank index entry ONLY; it never calls InsertMedia
|
||||
// or touches the arrange/timeline. Non-destructive: FX-enable is fully restored on
|
||||
// every path by FxBypassGuard, and the backend restores every RENDER_* setting.
|
||||
static void RunCapture(const reasampler::CaptureActionDef& def)
|
||||
{
|
||||
ResolvedSource src;
|
||||
std::string why;
|
||||
if (!ResolveSource(def.sourceMode, src, why))
|
||||
if (!ResolveScopeSource(def.scope, src, why))
|
||||
{
|
||||
ShowConsoleMsg(("ReaSampler capture: " + why + ".\n").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
reasampler::CaptureRequest req;
|
||||
req.sourceMode = def.sourceMode;
|
||||
req.sourceMode = reasampler::sourceModeForScope(def.scope);
|
||||
req.startSeconds = src.startSeconds; // exact bounds — no rounding
|
||||
req.endSeconds = src.endSeconds;
|
||||
req.wetDry = def.wetDry; // 1.0 wet (all M7 actions are wet-only)
|
||||
req.renderTail = false; // exact bounds, no tail (M7 default)
|
||||
req.wetDry = 1.0; // wet post the FX left enabled by the scope
|
||||
req.renderTail = false; // exact bounds, no tail (default)
|
||||
req.tailMs = 0.0;
|
||||
req.sampleRate = 0; // follow project rate
|
||||
req.channelCount = 2;
|
||||
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
|
||||
req.baseName = def.baseName;
|
||||
req.trackGuids = src.trackGuids; // recorded on the Sample (track captures)
|
||||
req.trackGuids = src.trackGuids; // recorded on the Sample (provenance)
|
||||
|
||||
// Bypass the out-of-scope FX for the duration of the render. Restored on EVERY
|
||||
// exit path below (RAII), including backend failures. proj = active project.
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
FxBypassGuard fxGuard(def.scope, src.sourceTracks, proj);
|
||||
|
||||
reasampler::OfflineRenderBackend backend;
|
||||
reasampler::CaptureResult res = backend.capture(req);
|
||||
@@ -332,7 +419,7 @@ static void RunInsertSelected(bool conform)
|
||||
static bool OnHookCommand(int command, int /*flag*/)
|
||||
{
|
||||
if (command == 0) return false;
|
||||
// M7 capture family: command ids parallel captureActionTable() 1:1 by index.
|
||||
// Three-scope capture family: command ids parallel captureActionTable() 1:1 by index.
|
||||
// Claim the fired id if it is one of ours and route to its table row.
|
||||
for (std::size_t i = 0; i < g_captureCmdIds.size(); ++i)
|
||||
if (command == g_captureCmdIds[i])
|
||||
@@ -388,7 +475,7 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
g_rec->Register("-gaccel", (void*)&g_accelToggleBankPanel);
|
||||
g_rec->Register("-command_id",
|
||||
(void*)(REASAMPLER_ACTION_PREFIX "TOGGLE_BANK_PANEL"));
|
||||
// Mirror-unregister the M7 capture family: gaccel + command_id per row,
|
||||
// Mirror-unregister the capture family: gaccel + command_id per row,
|
||||
// with '-'-prefixed strings (per the contract). The FOREVER-STABLE id
|
||||
// strings come from the same table used to register them.
|
||||
{
|
||||
@@ -400,6 +487,10 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
g_rec->Register("-command_id", (void*)table[i].commandString);
|
||||
}
|
||||
}
|
||||
// Retire the removed M7 command ids (command_id only — we never held a
|
||||
// gaccel for them this session). Clears stale user keybindings on unload.
|
||||
for (const char* id : kRetiredCaptureCmdStrings)
|
||||
g_rec->Register("-command_id", (void*)id);
|
||||
}
|
||||
// Destroy the docked window and release cached thumbnails before we drop
|
||||
// the API pointers (DockWindowRemove/DestroyWindow need them live).
|
||||
@@ -420,7 +511,7 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
g_hInst = hInstance;
|
||||
g_rec = rec;
|
||||
|
||||
// Register the M7 capture action family (command_id -> gaccel per table row).
|
||||
// Register the three-scope capture action family (command_id -> gaccel per table row).
|
||||
// The single hookcommand below routes every fired id back to its row by index.
|
||||
// g_captureAccels must be sized BEFORE the loop and never reallocated after —
|
||||
// REAPER holds a pointer to each element until we mirror-unregister it.
|
||||
|
||||
+67
-25
@@ -1,4 +1,4 @@
|
||||
// render_settings.cpp — pure logic for the M7 capture action family. See header.
|
||||
// render_settings.cpp — pure logic for the three-scope capture action family. See header.
|
||||
// NO REAPER types; unit-tested by tests/test_render_settings.cpp.
|
||||
|
||||
#include "render_settings.h"
|
||||
@@ -8,8 +8,9 @@
|
||||
namespace reasampler {
|
||||
|
||||
RenderSettingsChoice renderSettingsFor(SourceMode mode, double /*wetDry*/) {
|
||||
// All M7 actions are wet-only. `wetDry` is accepted so CaptureRequest.wetDry
|
||||
// remains the seam for future M10 dry work, but it does not affect the mapping.
|
||||
// `wetDry` is accepted so CaptureRequest.wetDry remains the seam for future
|
||||
// dry work (M10 null test), but it does not affect this mapping. FX scoping is
|
||||
// handled by fxBypassPlanFor, not by these render bits.
|
||||
RenderSettingsChoice c;
|
||||
|
||||
switch (mode) {
|
||||
@@ -50,6 +51,49 @@ RenderSettingsChoice renderSettingsFor(SourceMode mode, double /*wetDry*/) {
|
||||
return c;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
RangeSource inferRangeSource(bool hasRazorArea) {
|
||||
// Razor wins when present; otherwise the time selection. Orthogonal to scope.
|
||||
return hasRazorArea ? RangeSource::Razor : RangeSource::TimeSelection;
|
||||
}
|
||||
|
||||
FxBypassPlan fxBypassPlanFor(CaptureScope scope) {
|
||||
FxBypassPlan p;
|
||||
switch (scope) {
|
||||
case CaptureScope::Item:
|
||||
// Item = take/item FX ONLY. Bypass the item's own track FX, every
|
||||
// ancestor's FX, and the master's FX. (Take FX live in the item and
|
||||
// are always rendered — there is no track to bypass them from.)
|
||||
p.bypassSelfFx = true;
|
||||
p.bypassAncestorFx = true;
|
||||
p.bypassMaster = true;
|
||||
return p;
|
||||
case CaptureScope::Track:
|
||||
// Track = item FX + the selected track's OWN FX. Keep self FX; bypass
|
||||
// every ancestor (parent/folder) and the master. Parent/master GAIN
|
||||
// still applies (I_FXEN is FX-only) — documented boundary.
|
||||
p.bypassSelfFx = false;
|
||||
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)
|
||||
}
|
||||
|
||||
std::vector<RazorRange> parseRazorEdits(const std::string& razorString) {
|
||||
std::vector<RazorRange> ranges;
|
||||
std::istringstream in(razorString);
|
||||
@@ -59,7 +103,7 @@ std::vector<RazorRange> parseRazorEdits(const std::string& razorString) {
|
||||
// envelope-lane area's is a GUID `{…}`. We keep only track-audio triples.
|
||||
std::string startTok, endTok, guidTok;
|
||||
while (in >> startTok >> endTok >> guidTok) {
|
||||
// Envelope-lane areas carry a real GUID; skip them (M7 = track audio).
|
||||
// Envelope-lane areas carry a real GUID; skip them (razor captures track audio only).
|
||||
// A track-audio area's GUID token is the empty quoted string `""`.
|
||||
if (guidTok != "\"\"") continue;
|
||||
|
||||
@@ -93,29 +137,27 @@ RazorRange razorUnionBounds(const std::vector<RazorRange>& ranges) {
|
||||
}
|
||||
|
||||
const std::vector<CaptureActionDef>& captureActionTable() {
|
||||
// Built once (function-local static): four wet-only actions. Tail OFF for all
|
||||
// (exact bounds). Ids are FOREVER-STABLE — never edit a shipped string.
|
||||
// Dry variants deferred to M10; see kRenderPreFaderStems note in the header.
|
||||
// Built once (function-local static): three 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.
|
||||
static const std::vector<CaptureActionDef> table = {
|
||||
// Master mix — wet only (master IS the mix; no pre-FX concept applies).
|
||||
// Item scope — item/take FX only. NEW forever-stable id.
|
||||
{"CEREBELLUM_REASAMPLER_CAPTURE_ITEM",
|
||||
"ReaSampler: capture selected item(s)", "item",
|
||||
CaptureScope::Item},
|
||||
|
||||
// Track scope — item FX + the track's own FX. NEW forever-stable id.
|
||||
{"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 mix", "master_mix",
|
||||
SourceMode::MasterMix, 1.0},
|
||||
|
||||
// Selected tracks — wet (via master, &128).
|
||||
{"CEREBELLUM_REASAMPLER_CAPTURE_TRACKS_WET",
|
||||
"ReaSampler: capture selected tracks", "tracks_wet",
|
||||
SourceMode::SelectedTracks, 1.0},
|
||||
|
||||
// Selected items — wet, single file (&32 | single-file).
|
||||
{"CEREBELLUM_REASAMPLER_CAPTURE_ITEMS_WET",
|
||||
"ReaSampler: capture selected items", "items_wet",
|
||||
SourceMode::SelectedItems, 1.0},
|
||||
|
||||
// Razor area — wet, single file (&4096 | single-file).
|
||||
{"CEREBELLUM_REASAMPLER_CAPTURE_RAZOR_WET",
|
||||
"ReaSampler: capture razor area", "razor_wet",
|
||||
SourceMode::RazorArea, 1.0},
|
||||
"ReaSampler: capture master", "master",
|
||||
CaptureScope::Master},
|
||||
};
|
||||
return table;
|
||||
}
|
||||
|
||||
+95
-36
@@ -1,16 +1,22 @@
|
||||
#pragma once
|
||||
// render_settings — the REAPER-free logic behind the M7 capture action family.
|
||||
// render_settings — the REAPER-free logic behind the capture action family.
|
||||
//
|
||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
||||
// vendor/ includes. Standard library only. The capture shell (capture.cpp) reads
|
||||
// the actual DAW state (time selection, selected tracks/items, razor strings) and
|
||||
// hands the raw values here so the three genuinely-pure, easy-to-get-wrong pieces
|
||||
// are unit-tested outside the DAW:
|
||||
// vendor/ includes. Standard library only. The capture shell (capture.cpp) and
|
||||
// action layer (main.cpp) read the actual DAW state (time selection, selected
|
||||
// tracks/items, razor strings, the ancestor-track chain) and hand the raw values
|
||||
// here so the genuinely-pure, easy-to-get-wrong pieces are unit-tested outside
|
||||
// the DAW:
|
||||
//
|
||||
// 1. sourceMode -> the RENDER_SETTINGS integer bit value (wet only; M7 scope).
|
||||
// 1. sourceMode -> the RENDER_SETTINGS integer bit value (wet only).
|
||||
// 2. a P_RAZOREDITS string -> the list of (start,end) ranges + their union bound.
|
||||
// 3. the capture-action table (id string, description, source mode, wet/dry) —
|
||||
// the taxonomy, in one place so main.cpp iterates it instead of hand-listing.
|
||||
// 3. range inference: razor-present -> razor union, else time selection. Range
|
||||
// is a SOURCE choice orthogonal to the capture scope.
|
||||
// 4. the FX-scope bypass plan: given a scope + an ancestor-chain length, which
|
||||
// tracks' FX to bypass so each scope hears only the FX it should (the M7
|
||||
// "items captured through parent FX" defect is corrected here).
|
||||
// 5. the capture-action table (id string, description, scope) — the taxonomy,
|
||||
// in one place so main.cpp iterates it instead of hand-listing.
|
||||
//
|
||||
// The RENDER_SETTINGS bit MEANINGS are transcribed verbatim from
|
||||
// reaper_plugin_functions.h line ~3041 (see kRender* constants); the CHOICE of
|
||||
@@ -25,30 +31,31 @@ namespace reasampler {
|
||||
|
||||
// --- RENDER_SETTINGS source/processing bits (verbatim from SDK header ~3041) --
|
||||
//
|
||||
// Only the bits M7 actually uses are named. Values are the documented bit
|
||||
// Only the bits this module actually uses are named. Values are the documented bit
|
||||
// weights; the DOC of each is the SDK header's, not a guess.
|
||||
inline constexpr int kRenderMasterMix = 0; // (&(1|2))==0, no source bits
|
||||
inline constexpr int kRenderSelItems = 32; // &32 selected media items
|
||||
inline constexpr int kRenderSelItemsViaMaster = 64; // &64 selected media items via master
|
||||
inline constexpr int kRenderSelTracksViaMaster = 128; // &128 selected tracks via master
|
||||
inline constexpr int kRenderRazorEdits = 4096; // &4096 render razor edits
|
||||
// NOTE: kRenderPreFaderStems (&8192) is NOT used in M7. REAPER offline render has
|
||||
// no true pre-FX "dry" bit. Pre-fader stems are post-FX/pre-fader-volume — an
|
||||
// approximation, not a dry capture. True pre-FX dry requires FX-bypass-around-
|
||||
// render or the M8 realtime pre-FX path; it will be designed with the M10 null
|
||||
// test. All M7 capture actions are wet-only.
|
||||
// NOTE: kRenderPreFaderStems (&8192) is NOT used. REAPER offline render has no
|
||||
// true pre-FX "dry" bit. FX scoping is done by the FX-bypass-around-render
|
||||
// mechanism (see fxBypassPlan below) — bypassing the FX-enable of the tracks that
|
||||
// fall outside a scope — NOT by any render bit. All capture actions render wet
|
||||
// (post the FX that remain enabled); the scope decides which FX remain enabled.
|
||||
inline constexpr int kRenderSingleFile = (4 << 16); // items/razor -> one file
|
||||
|
||||
// The RENDER_SETTINGS value for a given source mode. `supported` is false only
|
||||
// for SourceMode::Realtime (that is the M8 backend, not offline render).
|
||||
struct RenderSettingsChoice {
|
||||
int settings = kRenderMasterMix;
|
||||
bool supported = true; // false => not an offline-render source in M7
|
||||
bool supported = true; // false => not an offline-render source (e.g. Realtime)
|
||||
};
|
||||
|
||||
// Maps a source mode to the wet RENDER_SETTINGS value for M7.
|
||||
// All M7 actions are wet-only (post-FX). `wetDry` is accepted but ignored for
|
||||
// the mapping — retained in CaptureRequest as the seam for future M10 dry work.
|
||||
// Maps a source mode to its RENDER_SETTINGS value (which content the render
|
||||
// covers). FX scoping is orthogonal — done by fxBypassPlan, not by these bits.
|
||||
// `wetDry` is accepted but ignored for the mapping — retained in CaptureRequest
|
||||
// as the seam for future dry work (M10 null test).
|
||||
//
|
||||
// CONFIRMED (SDK header ~3041):
|
||||
// MasterMix / TimeSelection -> master mix (0).
|
||||
@@ -57,8 +64,60 @@ struct RenderSettingsChoice {
|
||||
// RazorArea -> &4096| single-file.
|
||||
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.
|
||||
// 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).
|
||||
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.
|
||||
SourceMode sourceModeForScope(CaptureScope scope);
|
||||
|
||||
// --- Range inference: razor-else-time (orthogonal to scope) -------------------
|
||||
//
|
||||
// Every scope action infers its render range the same way: if a razor area is
|
||||
// present, use the razor union; otherwise use the time selection. Razor is a
|
||||
// range SOURCE, not a capture mode (the M7 four-mode model conflated them).
|
||||
enum class RangeSource {
|
||||
Razor, // a razor area is present -> use its union bound
|
||||
TimeSelection, // no razor -> use the time selection
|
||||
};
|
||||
|
||||
// Picks the range source. Pure so the "razor wins when present" rule is tested
|
||||
// without a DAW; the shell supplies whether any razor area was found.
|
||||
RangeSource inferRangeSource(bool hasRazorArea);
|
||||
|
||||
// --- FX-bypass plan: which tracks' FX to bypass for a scope -------------------
|
||||
//
|
||||
// Given a CaptureScope, returns three boolean flags: whether to bypass (a) the
|
||||
// captured track's OWN FX, (b) each of its ancestor (parent/folder) tracks' FX,
|
||||
// and (c) the master FX. The caller (FxBypassGuard) resolves these flags to
|
||||
// concrete MediaTrack* by walking the ancestor chain via GetParentTrack and
|
||||
// clears I_FXEN on each flagged track, snapshotting first (RAII restore).
|
||||
//
|
||||
// SCOPE BOUNDARY (documented, DAW-confirm): I_FXEN bypasses a track's FX plugins
|
||||
// but NOT its volume/pan/routing. "No parent FX" is satisfied by bypassing parent
|
||||
// FX only — parent/master GAIN still applies to a Track capture rendered via
|
||||
// master (&128). Neutralizing parent gain would be a larger, surprising mutation
|
||||
// (and is not what "FX scope" means); the least-surprising default is FX-only
|
||||
// bypass. Flagged for Daniel's DAW confirmation.
|
||||
struct FxBypassPlan {
|
||||
bool bypassSelfFx = false; // the captured track's own FX
|
||||
bool bypassAncestorFx = false; // every ancestor (parent/folder) track's FX
|
||||
bool bypassMaster = false; // the master track's FX
|
||||
};
|
||||
|
||||
FxBypassPlan fxBypassPlanFor(CaptureScope scope);
|
||||
|
||||
// A single razor-edit area: a time range on one track (envelope GUID ignored —
|
||||
// M7 captures track-audio razor areas, not envelope lanes).
|
||||
// razor captures target track-audio areas, not envelope lanes).
|
||||
struct RazorRange {
|
||||
double startSeconds = 0.0;
|
||||
double endSeconds = 0.0;
|
||||
@@ -69,8 +128,8 @@ struct RazorRange {
|
||||
// quoted string, i.e. the literal two chars `""`) for a track-audio area and a
|
||||
// GUID like {…} for an envelope-lane area.
|
||||
//
|
||||
// Returns only the track-audio ranges (envelope-lane triples are skipped — M7
|
||||
// renders track audio). Malformed/short trailing tokens are ignored, not fatal.
|
||||
// Returns only the track-audio ranges (envelope-lane triples are skipped — razor
|
||||
// captures target track audio, not envelope lanes). Malformed/short trailing tokens are ignored, not fatal.
|
||||
// A range with end <= start is dropped (no negative/empty areas leak through).
|
||||
std::vector<RazorRange> parseRazorEdits(const std::string& razorString);
|
||||
|
||||
@@ -81,28 +140,28 @@ RazorRange razorUnionBounds(const std::vector<RazorRange>& ranges);
|
||||
|
||||
// --- Capture-action taxonomy (the bindable set main.cpp registers) -----------
|
||||
//
|
||||
// One row per bindable action. All M7 rows are wet-only (post-FX). Tail is OFF
|
||||
// for every row (exact bounds); a tail-on variant is a later opt-in, YAGNI now.
|
||||
// Yields a bounded, discoverable set with NO dialogs (the tool's no-clutter ethos).
|
||||
// One row per bindable SCOPE action. Three scopes (item / track / master); 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).
|
||||
//
|
||||
// commandString is FOREVER-STABLE (user keybindings key off it) — never change a
|
||||
// shipped value. baseName feeds the file stem (sanitized by capture_paths).
|
||||
// wetDry is retained as the M10 seam; all M7 rows set it to 1.0.
|
||||
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
|
||||
SourceMode sourceMode;
|
||||
double wetDry; // 1.0 (wet) for all M7 rows; seam for M10 dry
|
||||
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)
|
||||
};
|
||||
|
||||
// The full M7 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.
|
||||
// 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.
|
||||
//
|
||||
// Four wet-only rows: CAPTURE_MASTER, CAPTURE_TRACKS_WET, CAPTURE_ITEMS_WET,
|
||||
// CAPTURE_RAZOR_WET. Dry variants are deferred to M10 (null-test work) — see the
|
||||
// kRenderPreFaderStems note above for why offline dry is non-trivial.
|
||||
// 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.
|
||||
const std::vector<CaptureActionDef>& captureActionTable();
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
Reference in New Issue
Block a user