// 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 #include #include 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 = kTailFlagTimeSelection; 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 = kTailFlagTimeSelection; 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; } void applySecondPassRenderSettings(const bool secondPass, RenderSettingsChoice& choice) { if (secondPass) { choice.settings |= kSecondPassRender; } } const char* renderSourceLabel(SourceMode mode) { switch (mode) { // MasterMix and TimeSelection share this label because they ARE the same // render — see the header. case SourceMode::MasterMix: case SourceMode::TimeSelection: return "master mix"; case SourceMode::SelectedTracks: return "selected tracks via master"; case SourceMode::SelectedItems: return "selected media items"; case SourceMode::RazorArea: return "razor edits"; case SourceMode::Realtime: return "realtime record"; } return "unknown"; // unreachable for a valid enum; never claim a source } 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 parseRazorEdits(const std::string& razorString) { std::vector 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& 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& 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 table = { {"CAPTURE_ITEM", "capture selected item(s)", "item", CaptureScope::Item}, {"CAPTURE_TRACK", "capture selected track(s)", "track", CaptureScope::Track}, }; return table; } } // namespace reasampler::capture