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);
|
||||
}
|
||||
|
||||
// --- 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.
|
||||
// --- FX-bypass + full parent-chain neutralize around render (RAII, non-destr.) --
|
||||
// For every track a scope must NOT hear the FX of, this ALSO neutralizes that
|
||||
// track's fader gain AND its full pan chain (pan/width/law/mode) for the render —
|
||||
// because a Track/Item capture renders via master and would otherwise sum through
|
||||
// the parent/folder/master FADERS and PAN/WIDTH/LAW, printing their gain and pan
|
||||
// coloring into the file (Daniel: the capture is likely re-routed through that
|
||||
// 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
|
||||
{
|
||||
public:
|
||||
@@ -284,8 +313,10 @@ public:
|
||||
}
|
||||
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.
|
||||
// GetMasterTrack(proj) -> the master track (SDK header ~1925). bypass()
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -293,36 +324,73 @@ public:
|
||||
~FxBypassGuard()
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
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& operator=(const FxBypassGuard&) = delete;
|
||||
|
||||
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_;
|
||||
|
||||
// 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.
|
||||
// Snapshot every neutralized param once per track (dedup: an ancestor shared by
|
||||
// two selected tracks must be restored to its ORIGINAL values, not to a
|
||||
// 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)
|
||||
{
|
||||
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)
|
||||
// Read ALL originals first (atomic snapshot), then push, then neutralize.
|
||||
const double fxen = GetMediaTrackInfo_Value(tr, "I_FXEN");
|
||||
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 &
|
||||
// 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.
|
||||
// clear the out-of-scope FX AND neutralize their fader gain + pan chain (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 + 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)
|
||||
{
|
||||
ResolvedSource src;
|
||||
@@ -346,8 +414,10 @@ static void RunCapture(const reasampler::CaptureActionDef& def)
|
||||
req.baseName = def.baseName;
|
||||
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.
|
||||
// Bypass the out-of-scope FX and neutralize their fader gain to unity for the
|
||||
// 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);
|
||||
FxBypassGuard fxGuard(def.scope, src.sourceTracks, proj);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user