2a9ab65944
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.
220 lines
8.5 KiB
C++
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
|