Files
reasampler/src/core/capture/render_settings.cpp
T
daniel 2a9ab65944 Ψ-W3 remediation: fix the verify doc's wrong render source, add missing content checks, soften unverified claims
Corrected the Render-dialog source name the refusal's evidence depends on, added a by-ear content check and a file-size channel proxy, and stopped two comments from overclaiming.
2026-08-01 23:01:50 -04:00

220 lines
8.5 KiB
C++

// 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 "core/capture/render_settings.h"
#include <algorithm>
#include <cmath>
#include <sstream>
namespace reasampler::capture {
double autoTrimEndRatio() {
// Amplitude ratio = 10^(dB/20) (header ~3062). For -72 dB this is ~0.00025119.
return std::pow(10.0, kAutoTrimThresholdDb / 20.0);
}
TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs) {
TailRenderSettings t;
switch (mode) {
case TailMode::None:
// Exact bounds — byte-identical to the pre-tail capture.
t.tailFlag = kTailFlagNone;
t.tailMs = 0.0;
t.normalize = kNormalizeDisableAll;
t.trimEnd = 0.0;
return t;
case TailMode::Auto:
// Surgical normalize: only the trim-ending-silence bit set, every other
// postprocessing bit clear. A fixed-threshold trim scales/limits/fades
// nothing, so identical requests trim at the identical sample -> holds
// the bit-identical-repeats invariant.
t.tailFlag = kTailFlagCustomBounds;
t.tailMs = kMaxTailMs;
t.normalize = kNormalizeTrimEnd;
t.trimEnd = autoTrimEndRatio();
return t;
case TailMode::Manual:
// Clamped to the cap regardless of source; negative floors to 0.
t.tailFlag = kTailFlagCustomBounds;
t.tailMs = std::clamp(manualTailMs, 0.0, kMaxTailMs);
t.normalize = kNormalizeDisableAll;
t.trimEnd = 0.0;
return t;
}
// Unreachable for a valid enum; fail closed to exact bounds (never a stray tail).
return t;
}
double realtimeRecordWindowEnd(TailMode mode, double rangeEndSeconds,
double manualTailMs) {
switch (mode) {
case TailMode::None:
return rangeEndSeconds; // exact, no extra recording
case TailMode::Auto:
return rangeEndSeconds + kMaxTailSeconds; // runaway cap; decay-trim shortens later
case TailMode::Manual:
return rangeEndSeconds + std::clamp(manualTailMs, 0.0, kMaxTailMs) / 1000.0;
}
// Unreachable for a valid enum; fail closed to exact bounds (never a stray tail).
return rangeEndSeconds;
}
RenderSettingsChoice renderSettingsFor(SourceMode mode, double /*wetDry*/) {
// wetDry doesn't affect this mapping (seam for future dry work); FX scoping
// is handled by fxBypassPlanFor, not by these render bits.
RenderSettingsChoice c;
switch (mode) {
case SourceMode::MasterMix:
case SourceMode::TimeSelection:
c.settings = kRenderMasterMix; // wet-only, no source bits
c.supported = true;
return c;
case SourceMode::SelectedTracks:
c.settings = kRenderSelTracksViaMaster;
c.supported = true;
return c;
case SourceMode::SelectedItems:
// Single-file bit so a multi-item selection yields one bank entry.
c.settings = kRenderSelItems | kRenderSingleFile;
c.supported = true;
return c;
case SourceMode::RazorArea:
c.settings = kRenderRazorEdits | kRenderSingleFile;
c.supported = true;
return c;
case SourceMode::Realtime:
c.settings = kRenderMasterMix;
c.supported = false; // not an offline-render source
return c;
}
// Unreachable for a valid enum; fail closed (unsupported) rather than render.
c.supported = false;
return c;
}
SourceMode sourceModeForScope(CaptureScope scope, bool itemExtentIsWindow) {
switch (scope) {
case CaptureScope::Item:
return itemExtentIsWindow ? SourceMode::SelectedItems
: SourceMode::SelectedTracks;
case CaptureScope::Track: return SourceMode::SelectedTracks;
}
// Unreachable for a valid enum; fail closed to the time-bounded render, which
// honors the requested bounds whatever the selection is.
return SourceMode::SelectedTracks;
}
bool isMultiTrackStemRender(SourceMode mode, int sourceTrackCount) {
return mode == SourceMode::SelectedTracks && sourceTrackCount > 1;
}
std::string multiTrackRefusalMessage(CaptureScope scope) {
// Deliberately does not name realtime capture as a way out, though it is the one
// action that sums correctly here: realtime is non-deterministic (hardware/performed
// FX, no bit-identical-repeats guarantee), so pointing an offline refusal at it would
// trade one invariant for another rather than just naming a substitute. A stated
// choice, not an oversight.
switch (scope) {
case CaptureScope::Item:
return "This range is narrower than the selected items, so it renders "
"through their tracks -- and those items span more than one track, "
"which this shape cannot land as a single file. Capture one track's "
"items at a time, or make the range match the items' extent.";
case CaptureScope::Track:
return "A track capture renders the selected tracks through the master, "
"and more than one track cannot land as a single file. Capture one "
"track at a time, or route them into a folder/bus track and capture "
"that (a folder's own output is its children summed).";
}
// Unreachable for a valid enum; a refusal with no way out is still better than a
// silent one, so fail closed to the scope-agnostic half of the message.
return "This selection spans more than one track, which cannot land as a single "
"file. Capture one track at a time.";
}
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:
// Take FX live in the item and are always rendered — bypass everything else.
p.bypassSelfFx = true;
p.bypassAncestorFx = true;
p.bypassMaster = true;
return p;
case CaptureScope::Track:
// Keep self FX; bypass every ancestor (parent/folder) and the master.
p.bypassSelfFx = false;
p.bypassAncestorFx = true;
p.bypassMaster = true;
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);
std::string startTok, endTok, guidTok;
while (in >> startTok >> endTok >> guidTok) {
// Skip envelope-lane areas (real GUID); keep only track-audio (`""`).
if (guidTok != "\"\"") continue;
// std::stod throws on garbage — guard so one malformed triple doesn't
// abort the whole parse.
double start = 0.0, end = 0.0;
try {
std::size_t sp = 0, ep = 0;
start = std::stod(startTok, &sp);
end = std::stod(endTok, &ep);
// Reject trailing garbage (e.g. "1.0x") — a partial parse is malformed.
if (sp != startTok.size() || ep != endTok.size()) continue;
} catch (...) {
continue;
}
if (end > start) ranges.push_back({start, end}); // drop empty/inverted
}
return ranges;
}
RazorRange razorUnionBounds(const std::vector<RazorRange>& ranges) {
if (ranges.empty()) return {0.0, 0.0};
RazorRange u = ranges.front();
for (const RazorRange& r : ranges) {
if (r.startSeconds < u.startSeconds) u.startSeconds = r.startSeconds;
if (r.endSeconds > u.endSeconds) u.endSeconds = r.endSeconds;
}
return u;
}
const std::vector<CaptureActionDef>& captureActionTable() {
// FOREVER-STABLE ids — never edit a shipped string. No master capture
// action (its id was retired; do not reintroduce it).
static const std::vector<CaptureActionDef> table = {
{"CAPTURE_ITEM",
"capture selected item(s)", "item",
CaptureScope::Item},
{"CAPTURE_TRACK",
"capture selected track(s)", "track",
CaptureScope::Track},
};
return table;
}
} // namespace reasampler::capture