5c0e3a8ccf
Compile-time channel flag forks a fully isolated reaper_reasampler_beta (namespace, command-id prefix, action names, dock ident, -beta render) from one auditable app_version definition. Stable identity byte-unchanged.
264 lines
15 KiB
C++
264 lines
15 KiB
C++
#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 <string>
|
|
#include <vector>
|
|
|
|
#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 REALTIME record-window end (in project seconds) a tail mode records to, given
|
|
// the request's exact range end (docs/product/capture-tail.md §The realtime path).
|
|
// Realtime does NOT drive RENDER_*; it records a generous window and trims later, so
|
|
// the window end is where the transport actually stops:
|
|
// None -> rangeEndSeconds (exact — no extra recording).
|
|
// Auto -> rangeEndSeconds + kMaxTailSeconds (the 8 s runaway cap; trimmed later).
|
|
// Manual -> rangeEndSeconds + clamp(manualTailMs, kMaxTailMs)/1000 (fixed, no trim).
|
|
// `manualTailMs` is used ONLY for Manual. Pure so the mode->window arithmetic (and
|
|
// the Manual clamp) is unit-tested outside the DAW; the backend applies the returned
|
|
// end to the record time selection. Shared -72 dB / 8 s constants are the same ones
|
|
// the offline tail uses (single source of truth).
|
|
double realtimeRecordWindowEnd(TailMode mode, double rangeEndSeconds,
|
|
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 <start> <end> <envGuidString>. 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<RazorRange> 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<RazorRange>& 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).
|
|
//
|
|
// Phase V (V4): the row stores the channel-AGNOSTIC pieces — a command-id SUFFIX (the
|
|
// tail after the family prefix) and a description PHRASE (the label after the "ReaSampler:
|
|
// " lead). The registering shell composes the full, channel-qualified id/name via
|
|
// app_version's channelCommandId / channelActionName (commandIdPrefix + suffix /
|
|
// actionDisplayPrefix + phrase). This keeps the pure table free of any channel branch:
|
|
// stable rebuilds the exact shipped id "CEREBELLUM_REASAMPLER_CAPTURE_TRACK" from
|
|
// prefix + "CAPTURE_TRACK"; beta yields "CEREBELLUM_REASAMPLER_BETA_CAPTURE_TRACK".
|
|
//
|
|
// commandSuffix is FOREVER-STABLE (user keybindings key off the composed id) — never
|
|
// change a shipped value. baseName feeds the file stem (sanitized by capture_paths).
|
|
struct CaptureActionDef {
|
|
const char* commandSuffix; // e.g. "CAPTURE_TRACK" — FOREVER-STABLE (composed w/ prefix)
|
|
const char* descriptionPhrase; // e.g. "capture selected track(s)" — Actions-list phrase
|
|
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<CaptureActionDef>& captureActionTable();
|
|
|
|
} // namespace reasampler
|