#pragma once // render_settings — the REAPER-free logic behind the capture action family. // // PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO // vendor/ includes. Standard library only. The capture shell (capture.cpp) and // action layer (main.cpp) read the actual DAW state (time selection, selected // tracks/items, razor strings, the ancestor-track chain) and hand the raw values // here so the genuinely-pure, easy-to-get-wrong pieces are unit-tested outside // the DAW: // // 1. sourceMode -> the RENDER_SETTINGS integer bit value (wet only). // 2. a P_RAZOREDITS string -> the list of (start,end) ranges + their union bound. // 3. range inference: razor-present -> razor union, else time selection. Range // is a SOURCE choice orthogonal to the capture scope. // 4. the FX-scope bypass plan: given a scope + an ancestor-chain length, which // tracks' FX to bypass so each scope hears only the FX it should (the M7 // "items captured through parent FX" defect is corrected here). // 5. the capture-action table (id string, description, scope) — the taxonomy, // in one place so main.cpp iterates it instead of hand-listing. // // The RENDER_SETTINGS bit MEANINGS are transcribed verbatim from // reaper_plugin_functions.h line ~3041 (see kRender* constants); the CHOICE of // which bits each source mode sets is this module's logic and is tested. #include #include #include "bank_model.h" // SourceMode (pure enum) namespace reasampler { // --- RENDER_SETTINGS source/processing bits (verbatim from SDK header ~3041) -- // // Only the bits this module actually uses are named. Values are the documented bit // weights; the DOC of each is the SDK header's, not a guess. inline constexpr int kRenderMasterMix = 0; // (&(1|2))==0, no source bits inline constexpr int kRenderSelItems = 32; // &32 selected media items inline constexpr int kRenderSelItemsViaMaster = 64; // &64 selected media items via master inline constexpr int kRenderSelTracksViaMaster = 128; // &128 selected tracks via master inline constexpr int kRenderRazorEdits = 4096; // &4096 render razor edits // NOTE: kRenderPreFaderStems (&8192) is NOT used. REAPER offline render has no // true pre-FX "dry" bit. FX scoping is done by the FX-bypass-around-render // mechanism (see fxBypassPlan below) — bypassing the FX-enable of the tracks that // fall outside a scope — NOT by any render bit. All capture actions render wet // (post the FX that remain enabled); the scope decides which FX remain enabled. inline constexpr int kRenderSingleFile = (4 << 16); // items/razor -> one file // --- Tail: RENDER_NORMALIZE / RENDER_TRIMEND bits + named constants ---------- // // The capture-tail feature (docs/product/capture-tail.md) preserves reverb/release // decay past the range end. Every offline capture renders custom-time-bounds, so // the only tail-flag bit that ever applies is &1 (RENDER_TAILFLAG, header ~3047). // These values are the pure part — mode -> (RENDER_* values) — unit-tested outside // the DAW exactly like renderSettingsFor; the backend just applies them. // // RENDER_NORMALIZE bit meanings (verbatim from SDK header ~3051): // &32768 = trim ending silence (the surgical Auto path) // &(4<<16) = disable all render postprocessing (the None/Manual path) inline constexpr int kNormalizeTrimEnd = 32768; // &32768 trim ending silence inline constexpr int kNormalizeDisableAll = (4 << 16); // &(4<<16) = 262144, disable all // RENDER_TAILFLAG &1 = apply tail for custom time bounds (header ~3047). We render // custom bounds unconditionally, so this is the only tail bit that ever applies. inline constexpr int kTailFlagNone = 0; inline constexpr int kTailFlagCustomBounds = 1; // &1 // Auto-trim trailing-silence threshold. -72 dB is quiet enough that the trimmed // region is inaudible decay, loud enough to not chase a reverb's infinite noise // floor. Daniel-set. Single source of truth: the RENDER_TRIMEND ratio derives from // this dB, never the reverse. inline constexpr double kAutoTrimThresholdDb = -72.0; // Max tail rendered past the range end. The runaway guard: a non-decaying or // looping signal never crosses the trim threshold, so this caps the render. // Daniel-set. Shared by the offline (T1) and future realtime (T2) tail paths. inline constexpr double kMaxTailSeconds = 8.0; inline constexpr double kMaxTailMs = 8000.0; // Derived linear amplitude ratio for RENDER_TRIMEND. The header (~3062) documents // RENDER_TRIMEND as an amplitude ratio ("0.5 means -6.02 dB"), i.e. 10^(dB/20). // Derived from kAutoTrimThresholdDb so the dB stays the single source of truth and // a future config change to the dB does not require hand-recomputing the ratio. // // std::pow is not constexpr before C++26, so this is a function, not a constant. // For -72 dB: 10^(-72/20) = 10^(-3.6) ~= 0.00025119 (the value the DAW confirm targets). double autoTrimEndRatio(); // The three tail states (docs/product/capture-tail.md §The three tail states): // None — exact bounds, no tail. Byte-identical to the pre-tail capture. The // default and the ONLY mode for null-test / verify captures. // Auto — generous 8 s tail then trim trailing silence to -72 dB (surgical // normalize). The user-facing tail-on option (panel toggle). // Manual — a fixed tail length (clamped to the 8 s cap), no trim. enum class TailMode { None, Auto, Manual, }; // The RENDER_* values a tail mode drives, in addition to the exact STARTPOS/ENDPOS // the backend already sets. `trimEnd` is meaningful only when the trim-end normalize // bit is set (Auto); it is 0 otherwise. This is the pure mapping — the backend reads // these four fields straight onto GetSetProjectInfo. struct TailRenderSettings { int tailFlag = kTailFlagNone; // RENDER_TAILFLAG (0 or &1) double tailMs = 0.0; // RENDER_TAILMS int normalize = kNormalizeDisableAll; // RENDER_NORMALIZE double trimEnd = 0.0; // RENDER_TRIMEND (only used when trim bit set) }; // Maps a tail mode (+ the requested manual tail ms) to its RENDER_* values. // `manualTailMs` is used ONLY for TailMode::Manual (ignored otherwise). Manual is // clamped to kMaxTailMs — the runaway guard applies whether the length came from // the Auto default or an explicit request (spec §Manual override). Pure + tested. TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs); // The RENDER_SETTINGS value for a given source mode. `supported` is false only // for SourceMode::Realtime (that is the M8 backend, not offline render). struct RenderSettingsChoice { int settings = kRenderMasterMix; bool supported = true; // false => not an offline-render source (e.g. Realtime) }; // Maps a source mode to its RENDER_SETTINGS value (which content the render // covers). FX scoping is orthogonal — done by fxBypassPlan, not by these bits. // `wetDry` is accepted but ignored for the mapping — retained in CaptureRequest // as the seam for future dry work (M10 null test). // // CONFIRMED (SDK header ~3041): // MasterMix / TimeSelection -> master mix (0). // SelectedTracks -> &128 selected tracks via master. // SelectedItems -> &32 | single-file (one wav, not one-per-item). // RazorArea -> &4096| single-file. RenderSettingsChoice renderSettingsFor(SourceMode mode, double wetDry); // --- Capture scope: the FX-scope invariant (Daniel, critical) ---------------- // // Two FX scopes. The render RANGE (razor-else-time) is orthogonal to the scope. // Item -> item/take FX ONLY (no track, no parent/folder, no master FX). // Track -> item FX + the selected track's OWN track FX (no parent/folder/master). // There is NO master scope: to capture the master you render a track instead. The // master track's FX/gain/pan are still NEUTRALIZED as part of the out-of-scope // chain for both item and track captures (bypassMaster below) — master is a // bypass target, not a capture scope. enum class CaptureScope { Item, Track, }; // The render source mode each scope drives. Item captures selected items, Track // captures selected tracks (via master). SourceMode sourceModeForScope(CaptureScope scope); // --- Range inference: razor-else-time (orthogonal to scope) ------------------- // // Every scope action infers its render range the same way: if a razor area is // present, use the razor union; otherwise use the time selection. Razor is a // range SOURCE, not a capture mode (the M7 four-mode model conflated them). enum class RangeSource { Razor, // a razor area is present -> use its union bound TimeSelection, // no razor -> use the time selection }; // Picks the range source. Pure so the "razor wins when present" rule is tested // without a DAW; the shell supplies whether any razor area was found. RangeSource inferRangeSource(bool hasRazorArea); // --- FX-bypass plan: which tracks' FX to bypass for a scope ------------------- // // Given a CaptureScope, returns three boolean flags: whether to bypass (a) the // captured track's OWN FX, (b) each of its ancestor (parent/folder) tracks' FX, // and (c) the master FX. The caller (FxBypassGuard) resolves these flags to // concrete MediaTrack* by walking the ancestor chain via GetParentTrack and // clears I_FXEN on each flagged track, snapshotting first (RAII restore). // // SCOPE BOUNDARY: I_FXEN bypasses a track's FX plugins but NOT its volume/pan. // The guard (FxBypassGuard, main.cpp) therefore ALSO neutralizes the fader GAIN // (D_VOL -> unity) of every track in this same bypass set, so a Track/Item // capture rendered via master does NOT bake in the parent/folder/master fader // level (Daniel: the capture is likely re-routed through that chain later). PAN // 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 { bool bypassSelfFx = false; // the captured track's own FX bool bypassAncestorFx = false; // every ancestor (parent/folder) track's FX bool bypassMaster = false; // the master track's FX }; FxBypassPlan fxBypassPlanFor(CaptureScope scope); // A single razor-edit area: a time range on one track (envelope GUID ignored — // razor captures target track-audio areas, not envelope lanes). struct RazorRange { double startSeconds = 0.0; double endSeconds = 0.0; }; // Parses ONE track's P_RAZOREDITS string (SDK header ~2899): space-separated // TRIPLES of . The envelope GUID is "" (an empty // quoted string, i.e. the literal two chars `""`) for a track-audio area and a // GUID like {…} for an envelope-lane area. // // Returns only the track-audio ranges (envelope-lane triples are skipped — razor // captures target track audio, not envelope lanes). Malformed/short trailing tokens are ignored, not fatal. // A range with end <= start is dropped (no negative/empty areas leak through). std::vector parseRazorEdits(const std::string& razorString); // The union bound (min start, max end) of a set of razor ranges — the exact // window the offline render must cover so every area is inside the rendered file. // Returns {0,0} for an empty input (caller treats that as "no razor area"). RazorRange razorUnionBounds(const std::vector& ranges); // --- Capture-action taxonomy (the bindable set main.cpp registers) ----------- // // One row per bindable SCOPE action: item and track. The range each captures // (razor-else-time) is inferred at fire time, not a mode. TAIL is NOT a per-action // variant — the tail MODE (None/Auto/Manual) is a panel SETTING the capture reads // at fire time (see tail_control + bank_panel), so a single pair of actions covers // every tail state. Bounded, discoverable, NO dialogs (the tool's no-clutter ethos). // // commandString is FOREVER-STABLE (user keybindings key off it) — never change a // shipped value. baseName feeds the file stem (sanitized by capture_paths). struct CaptureActionDef { const char* commandString; // CEREBELLUM_REASAMPLER_… FOREVER-STABLE id string const char* description; // Actions-list label const char* baseName; // file-stem base for this capture CaptureScope scope; // FX scope (item / track) }; // The capture-action table. Iterated by main.cpp to register the family and route // each fired command back to its definition. Kept here (pure) so the taxonomy is // one testable list, not scattered registration code. // // Two rows: CAPTURE_ITEM / CAPTURE_TRACK. There is no master capture — to capture // the master you render a track. Razor is an inferred range, not a mode, and each // scope enforces its FX-scope invariant via fxBypassPlanFor. The tail mode each // capture applies is read from the docked-panel setting, not baked into the row. const std::vector& captureActionTable(); } // namespace reasampler