feat(capture): neutralize parent/master gain, pan, width, law in FX-scope capture
FxBypassGuard now snapshots/restores D_VOL, D_PAN, D_WIDTH, D_PANLAW, I_PANMODE alongside I_FXEN for every bypassed track (forcing stereo-pan mode for an unambiguous render), so Track/Item captures are uncolored by parent/folder/master fader and routing. All restored on every path.
This commit is contained in:
+94
-24
@@ -250,13 +250,42 @@ static bool ResolveScopeSource(reasampler::CaptureScope scope,
|
|||||||
return resolveRange(out.startSeconds, out.endSeconds, why);
|
return resolveRange(out.startSeconds, out.endSeconds, why);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- FX-bypass-around-render (RAII, non-destructive) ------------------------
|
// --- FX-bypass + full parent-chain neutralize around render (RAII, non-destr.) --
|
||||||
// Snapshots and clears I_FXEN on the tracks a scope must NOT hear the FX of, then
|
// For every track a scope must NOT hear the FX of, this ALSO neutralizes that
|
||||||
// restores every snapshotted value on EVERY exit path (including the render's).
|
// track's fader gain AND its full pan chain (pan/width/law/mode) for the render —
|
||||||
// I_FXEN bypasses a track's FX plugins only — NOT its volume/pan/routing (so a
|
// because a Track/Item capture renders via master and would otherwise sum through
|
||||||
// Track capture rendered via master still carries parent/master GAIN; documented
|
// the parent/folder/master FADERS and PAN/WIDTH/LAW, printing their gain and pan
|
||||||
// boundary, DAW-confirm). Structurally non-destructive: no takes, no items, no
|
// coloring into the file (Daniel: the capture is likely re-routed through that
|
||||||
// project restructuring — only a transient FX-enable toggle, always restored.
|
// same chain later, so parent/master level and pan must not be baked in). The
|
||||||
|
// 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).
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// through — no FX, no fader, and no pan/width/law/mode coloring:
|
||||||
|
// I_FXEN -> 0 (FX bypassed; SDK ~2194)
|
||||||
|
// D_VOL -> 1.0 (unity trim volume; SDK ~2226 "1=+0dB")
|
||||||
|
// D_PAN -> 0.0 (center; SDK ~2227 "trim pan of track, -1..1")
|
||||||
|
// D_WIDTH -> 1.0 (full/neutral stereo width; SDK ~2228 "width, -1..1",
|
||||||
|
// 1.0 = full width = no narrowing/collapse)
|
||||||
|
// D_PANLAW -> 1.0 (no coloring; SDK ~2232 "1=+0dB" — pan-law applies no gain)
|
||||||
|
// I_PANMODE -> 5 (stereo pan; SDK ~2231 "0=classic,3=balance,5=stereo,6=dual")
|
||||||
|
// All are restored to their ORIGINAL values on EVERY exit path (RAII).
|
||||||
|
//
|
||||||
|
// Why also force I_PANMODE (pan mode). D_PAN's effect is mode-dependent. In modes
|
||||||
|
// 0/3/5, D_PAN=0 + D_WIDTH=1 is a provable pass-through. But in mode 6 (dual pan)
|
||||||
|
// D_PAN/D_WIDTH are ignored — routing is governed instead by D_DUALPANL/D_DUALPANR
|
||||||
|
// (SDK ~2229-2230, live only when I_PANMODE==6), whose neutral pass-through the
|
||||||
|
// header does not state as such. Rather than snapshot two more mode-conditional
|
||||||
|
// params and infer their neutral values, we force I_PANMODE=5 (stereo pan) for the
|
||||||
|
// render, where D_PAN=0 + D_WIDTH=1 is unambiguously uncolored, then restore the
|
||||||
|
// original mode. This fully neutralizes pan for every original mode with no
|
||||||
|
// residual — the "handle it fully" the brief requires. (See Snap dual-pan note.)
|
||||||
|
//
|
||||||
|
// Structurally non-destructive: no takes, no items, no project restructuring —
|
||||||
|
// only transient FX-enable + trim-volume toggles, always restored.
|
||||||
class FxBypassGuard
|
class FxBypassGuard
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -284,8 +313,10 @@ public:
|
|||||||
}
|
}
|
||||||
if (plan.bypassMaster)
|
if (plan.bypassMaster)
|
||||||
{
|
{
|
||||||
// GetMasterTrack(proj) -> the master track (SDK header ~1925); I_FXEN
|
// GetMasterTrack(proj) -> the master track (SDK header ~1925). bypass()
|
||||||
// on it bypasses the master FX chain, leaving master gain/routing live.
|
// neutralizes its FX (I_FXEN), gain (D_VOL) AND pan/width/law/mode on it
|
||||||
|
// just like any other in-scope track; only the master's summing/routing
|
||||||
|
// topology (the mix bus itself) remains — that is not a per-track param.
|
||||||
if (MediaTrack* master = GetMasterTrack(proj)) bypass(master);
|
if (MediaTrack* master = GetMasterTrack(proj)) bypass(master);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -293,36 +324,73 @@ public:
|
|||||||
~FxBypassGuard()
|
~FxBypassGuard()
|
||||||
{
|
{
|
||||||
// Restore in reverse for symmetry (order is not load-bearing — each track
|
// Restore in reverse for symmetry (order is not load-bearing — each track
|
||||||
// appears once, snapshots are independent).
|
// appears once, snapshots are independent). EVERY snapshotted param is
|
||||||
|
// restored to its ORIGINAL value on this (every) exit path. Restore
|
||||||
|
// I_PANMODE before the pan values so any mode-conditional params (e.g. dual
|
||||||
|
// pan) settle under the original mode.
|
||||||
for (auto it = snapshots_.rbegin(); it != snapshots_.rend(); ++it)
|
for (auto it = snapshots_.rbegin(); it != snapshots_.rend(); ++it)
|
||||||
|
{
|
||||||
SetMediaTrackInfo_Value(it->track, "I_FXEN", it->fxen);
|
SetMediaTrackInfo_Value(it->track, "I_FXEN", it->fxen);
|
||||||
|
SetMediaTrackInfo_Value(it->track, "D_VOL", it->vol);
|
||||||
|
SetMediaTrackInfo_Value(it->track, "I_PANMODE", it->panmode);
|
||||||
|
SetMediaTrackInfo_Value(it->track, "D_PAN", it->pan);
|
||||||
|
SetMediaTrackInfo_Value(it->track, "D_WIDTH", it->width);
|
||||||
|
SetMediaTrackInfo_Value(it->track, "D_PANLAW", it->panlaw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FxBypassGuard(const FxBypassGuard&) = delete;
|
FxBypassGuard(const FxBypassGuard&) = delete;
|
||||||
FxBypassGuard& operator=(const FxBypassGuard&) = delete;
|
FxBypassGuard& operator=(const FxBypassGuard&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Snap { MediaTrack* track; double fxen; };
|
// One snapshot per bypassed track: all params we neutralize, at their originals.
|
||||||
|
// panmode captures I_PANMODE so we can force stereo-pan for the render and put
|
||||||
|
// the original mode back — which also makes D_DUALPANL/D_DUALPANR (live only when
|
||||||
|
// I_PANMODE==6, SDK ~2229-2230) irrelevant during the render without us having to
|
||||||
|
// touch or guess neutral values for them.
|
||||||
|
struct Snap
|
||||||
|
{
|
||||||
|
MediaTrack* track;
|
||||||
|
double fxen;
|
||||||
|
double vol;
|
||||||
|
double pan;
|
||||||
|
double width;
|
||||||
|
double panlaw;
|
||||||
|
double panmode;
|
||||||
|
};
|
||||||
std::vector<Snap> snapshots_;
|
std::vector<Snap> snapshots_;
|
||||||
|
|
||||||
// Snapshot I_FXEN once per track (dedup: an ancestor shared by two selected
|
// Snapshot every neutralized param once per track (dedup: an ancestor shared by
|
||||||
// tracks must be restored to its ORIGINAL value, not a re-snapshot of the
|
// two selected tracks must be restored to its ORIGINAL values, not to a
|
||||||
// already-bypassed 0), then clear it.
|
// re-snapshot of the already-neutralized state), then read ALL originals, push
|
||||||
|
// one Snap, and set all to neutral — bypass FX, unity gain, uncolored pan chain.
|
||||||
void bypass(MediaTrack* tr)
|
void bypass(MediaTrack* tr)
|
||||||
{
|
{
|
||||||
for (const Snap& s : snapshots_) if (s.track == tr) return; // already done
|
for (const Snap& s : snapshots_) if (s.track == tr) return; // already done
|
||||||
const double fxen = GetMediaTrackInfo_Value(tr, "I_FXEN");
|
// Read ALL originals first (atomic snapshot), then push, then neutralize.
|
||||||
snapshots_.push_back({tr, fxen});
|
const double fxen = GetMediaTrackInfo_Value(tr, "I_FXEN");
|
||||||
SetMediaTrackInfo_Value(tr, "I_FXEN", 0.0); // 0 = bypassed (SDK ~2194)
|
const double vol = GetMediaTrackInfo_Value(tr, "D_VOL");
|
||||||
|
const double pan = GetMediaTrackInfo_Value(tr, "D_PAN");
|
||||||
|
const double width = GetMediaTrackInfo_Value(tr, "D_WIDTH");
|
||||||
|
const double panlaw = GetMediaTrackInfo_Value(tr, "D_PANLAW");
|
||||||
|
const double panmode = GetMediaTrackInfo_Value(tr, "I_PANMODE");
|
||||||
|
snapshots_.push_back({tr, fxen, vol, pan, width, panlaw, panmode});
|
||||||
|
SetMediaTrackInfo_Value(tr, "I_FXEN", 0.0); // 0 = bypassed (SDK ~2194)
|
||||||
|
SetMediaTrackInfo_Value(tr, "D_VOL", 1.0); // 1.0 = unity gain (SDK ~2226)
|
||||||
|
SetMediaTrackInfo_Value(tr, "I_PANMODE", 5.0); // 5 = stereo pan (SDK ~2231)
|
||||||
|
SetMediaTrackInfo_Value(tr, "D_PAN", 0.0); // 0.0 = center (SDK ~2227)
|
||||||
|
SetMediaTrackInfo_Value(tr, "D_WIDTH", 1.0); // 1.0 = full width (SDK ~2228)
|
||||||
|
SetMediaTrackInfo_Value(tr, "D_PANLAW", 1.0); // 1.0 = +0dB, no law (SDK ~2232)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Runs one capture-action-table row: resolve its scope source + range, snapshot &
|
// 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
|
// clear the out-of-scope FX AND neutralize their fader gain + pan chain (RAII),
|
||||||
// to the bank, persist + mark dirty. The load-bearing principle holds structurally
|
// render via the offline backend, add the Sample to the bank, persist + mark dirty.
|
||||||
// — this path writes a file + a bank index entry ONLY; it never calls InsertMedia
|
// The load-bearing principle holds structurally — this path writes a file + a bank
|
||||||
// or touches the arrange/timeline. Non-destructive: FX-enable is fully restored on
|
// index entry ONLY; it never calls InsertMedia or touches the arrange/timeline.
|
||||||
// every path by FxBypassGuard, and the backend restores every RENDER_* setting.
|
// Non-destructive: FX-enable + fader gain + pan/width/law/mode are fully restored
|
||||||
|
// on every path by FxBypassGuard, and the backend restores every RENDER_* setting.
|
||||||
static void RunCapture(const reasampler::CaptureActionDef& def)
|
static void RunCapture(const reasampler::CaptureActionDef& def)
|
||||||
{
|
{
|
||||||
ResolvedSource src;
|
ResolvedSource src;
|
||||||
@@ -346,8 +414,10 @@ static void RunCapture(const reasampler::CaptureActionDef& def)
|
|||||||
req.baseName = def.baseName;
|
req.baseName = def.baseName;
|
||||||
req.trackGuids = src.trackGuids; // recorded on the Sample (provenance)
|
req.trackGuids = src.trackGuids; // recorded on the Sample (provenance)
|
||||||
|
|
||||||
// Bypass the out-of-scope FX for the duration of the render. Restored on EVERY
|
// Bypass the out-of-scope FX and neutralize their fader gain to unity for the
|
||||||
// exit path below (RAII), including backend failures. proj = active project.
|
// duration of the render (so parent/master fader level is not baked into the
|
||||||
|
// file). Restored on EVERY exit path below (RAII), including backend failures.
|
||||||
|
// proj = active project.
|
||||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||||
FxBypassGuard fxGuard(def.scope, src.sourceTracks, proj);
|
FxBypassGuard fxGuard(def.scope, src.sourceTracks, proj);
|
||||||
|
|
||||||
|
|||||||
@@ -102,12 +102,14 @@ RangeSource inferRangeSource(bool hasRazorArea);
|
|||||||
// concrete MediaTrack* by walking the ancestor chain via GetParentTrack and
|
// concrete MediaTrack* by walking the ancestor chain via GetParentTrack and
|
||||||
// clears I_FXEN on each flagged track, snapshotting first (RAII restore).
|
// clears I_FXEN on each flagged track, snapshotting first (RAII restore).
|
||||||
//
|
//
|
||||||
// SCOPE BOUNDARY (documented, DAW-confirm): I_FXEN bypasses a track's FX plugins
|
// SCOPE BOUNDARY: I_FXEN bypasses a track's FX plugins but NOT its volume/pan.
|
||||||
// but NOT its volume/pan/routing. "No parent FX" is satisfied by bypassing parent
|
// The guard (FxBypassGuard, main.cpp) therefore ALSO neutralizes the fader GAIN
|
||||||
// FX only — parent/master GAIN still applies to a Track capture rendered via
|
// (D_VOL -> unity) of every track in this same bypass set, so a Track/Item
|
||||||
// master (&128). Neutralizing parent gain would be a larger, surprising mutation
|
// capture rendered via master does NOT bake in the parent/folder/master fader
|
||||||
// (and is not what "FX scope" means); the least-surprising default is FX-only
|
// level (Daniel: the capture is likely re-routed through that chain later). PAN
|
||||||
// bypass. Flagged for Daniel's DAW confirmation.
|
// is deliberately left untouched (D_PAN is coupled to D_WIDTH/D_PANLAW — a clean
|
||||||
|
// neutralize is non-trivial; flagged as a follow-up, not half-done). This plan
|
||||||
|
// selects the SET; the guard applies both the FX bypass and the gain neutralize.
|
||||||
struct FxBypassPlan {
|
struct FxBypassPlan {
|
||||||
bool bypassSelfFx = false; // the captured track's own FX
|
bool bypassSelfFx = false; // the captured track's own FX
|
||||||
bool bypassAncestorFx = false; // every ancestor (parent/folder) track's FX
|
bool bypassAncestorFx = false; // every ancestor (parent/folder) track's FX
|
||||||
|
|||||||
Reference in New Issue
Block a user