#pragma once // The shared capture seam: CaptureRequest/CaptureResult (types both backends // speak), OfflineRenderBackend, and the helpers both backends share (naming, the // mono collapse, the Sample stamp). // Realtime's async begin/tick/abort surface lives in capture_realtime_shell.h. // // REAPER-free on purpose (bank_model only) so callers can depend on the seam // without dragging the SDK into every include site; the .cpp is the REAPER TU. #include #include #include "core/model/bank_model.h" #include "core/capture/capture_name.h" // CaptureName — label + file-stem base #include "core/capture/render_settings.h" // TailMode — the three-state tail contract #include "core/capture/wav_codec.h" // MonoCollapseOutcome — the collapse's report // Forward-declared, never dereferenced here — only the REAPER-facing .cpp touches these. class MediaTrack; class ReaProject; namespace reasampler::capture { using model::Sample; // 32-bit float is the default; rationale lives in capture.cpp next to the sink-config bytes. enum class WavBitDepth { Int16, Int24, Float32, }; // Where the render lands. TWO VALUES, never a caller-supplied path string: the // backend resolves each to a directory itself, which is what makes "write into the // bank folder" inexpressible from the ProjectMedia side and vice versa. enum class CaptureDestination { Bank, // /reasampler_bank — every capture path ProjectMedia, // the project's recording path — the render-in-place verb only }; // One capture, independent of source mode. struct CaptureRequest { SourceMode sourceMode = SourceMode::MasterMix; // Sample-accurate render bounds in project seconds — NO rounding. double startSeconds = 0.0; double endSeconds = 0.0; // 1.0 = fully wet, 0.0 = fully dry. Every current capture action sets 1.0; // true pre-FX dry isn't available via RENDER_SETTINGS (needs FX-bypass-around-render // or the realtime pre-FX path) so this stays a seam for that future work. double wetDry = 1.0; // Track GUID(s) when source mode is track-scoped (SelectedTracks); empty otherwise. // The backend only copies these onto the Sample — it never reads selection itself. std::vector trackGuids; // Render tail (docs/product/capture-tail.md §The three tail states). None = exact // bounds, no added silence — the only mode valid for null-test/verify captures. // tailMs applies only to Manual (clamped to 8s by the pure mapping); Auto uses // the 8s cap + -72 dB trim internally, None ignores it. TailMode tailMode = TailMode::None; double tailMs = 0.0; // 0 sampleRate => follow project rate. int sampleRate = 0; // What the RENDER is asked for (RENDER_CHANNELS / the realtime record mode), not // what the capture lands as: a dual-mono render is collapsed to 1 channel after // the fact, and the Sample's count comes from the produced file. int channelCount = 2; WavBitDepth bitDepth = WavBitDepth::Float32; // Sanitized by capture_paths. uniqueTag (disambiguator) is supplied by the // backend caller so the pure naming logic stays testable. std::string baseName = "capture"; std::string uniqueTag; // The label the bank shows, which may legitimately differ from the file stem: the // stem must survive sanitizeStem, the label carries the source name verbatim. Empty // means "the stem base is also the label" — what a caller that names nothing else gets. std::string displayName; // The one home for that fallback rule; both backends populate Sample::displayName // from here rather than each spelling the condition out. std::string label() const { return displayName.empty() ? baseName : displayName; } // Default Bank: every existing entry point renders into the bank untouched. CaptureDestination destination = CaptureDestination::Bank; }; // Every failure is an explicit code, never a thrown exception across the REAPER boundary. enum class CaptureStatus { Ok, NoProject, // no active project to render / resolve a bank folder EmptyRange, // start >= end: nothing to render UnsupportedMode, // backend does not implement this source mode UnsupportedFormat, // requested bit depth has no known REAPER blob (Float32 only) RenderFailed, // the render action ran but produced no output file TransportBusy, // realtime backend: transport already playing/recording — refused MultiTrackSelection, // a selected-tracks render over >1 track — would render N files BoundsMismatch, // the rendered file's frames are not the requested window's — or // could not be measured to say (render_bounds_gate) }; struct CaptureResult { CaptureStatus status = CaptureStatus::RenderFailed; Sample sample; // valid only when status == Ok std::string message; // human-readable detail for the console log // The file the render actually landed, absolute — the only handle a caller that // banks nothing has on its own output (sample.relativePath is empty on the // ProjectMedia destination). Set on the Ok path only. std::string absolutePath; }; // Deterministic offline-render backend: master mix / time selection / selected // tracks / selected items / razor area, all wet-only, optional tail. Source // selection + range are resolved by the caller and handed in via CaptureRequest — // the backend drives RENDER_* and never reads the DAW selection itself. // SourceMode::Realtime returns UnsupportedMode. Non-destructive: restores every // RENDER_* setting it touches on every path. Plain concrete class — see the // no-shared-interface note in capture_realtime_shell.h before adding one back. class OfflineRenderBackend { public: CaptureResult capture(const CaptureRequest& request); }; // Mints the filesystem-safe disambiguating tag for one capture's file stem + // Sample id: "-", a per-session monotonic // counter. Wall-clock seconds alone collide when batch capture drives short // renders back-to-back, silently overwriting the first file. `prefix` is the // backend's family marker ("" offline, "rt-" realtime). std::string makeUniqueTag(const std::string& prefix); // Composes one capture's label + file-stem base (core/capture/capture_name) from the // resolved source-track names, reading the LOCAL clock for the discriminator — the one // impure step, kept here so the composition itself stays pure and tested. `ordinal` is a // batch unit's number (0 for a single capture); `fallback` is the scope literal, used // only when no source name resolved. CaptureName captureNameFor(const std::vector& sourceNames, int ordinal, const std::string& fallback); // Rewrites a just-captured WAV in place as a 1-channel file when its channels are // bit-identical (the pure `collapseToMono` decides). Every other file is left // untouched, byte for byte, so the not-collapsed path is exactly what the backend // produced. Must run BEFORE stampCaptureSample, which measures the landed file. // A Failed outcome is ALSO logged to the console here, because a successful capture's // CaptureResult::message is not printed by any caller — the return value alone would // leave a genuine I/O failure indistinguishable from a legitimately stereo capture. // `consoleLabel` matches each caller's own console-prefix convention (offline: // "ReaSampler capture"; realtime: "ReaSampler realtime capture"). MonoCollapseOutcome collapseCapturedFileToMono(const std::string& absolutePath, const char* consoleLabel = "ReaSampler capture"); // Stamps the metadata shared by both backends onto `s`: trackGuids (echoed from the // request) + channelCount (measured from the produced file's `fmt`; 0/unknown as the // fallback for a file that cannot be parsed — never the request's value, which is // always 2 and was never actually measured), resolved sampleRate (request rate, // else PROJECT_SRATE from `rateProj`), captureTempo, the capture-start time signature // (TimeMap_GetTimeSigAtTime against `timeSigProj` — offline passes nullptr for the // active project, realtime pins the record's own project), the WAV-aware // contentHash of `absolutePath` (left empty when unreadable), and createdTimestamp. // Per-backend bits (id, paths, bounds, tier, realtime's length override) stay // with each caller. void stampCaptureSample(Sample& s, const CaptureRequest& req, ReaProject* rateProj, ReaProject* timeSigProj, const std::string& absolutePath); } // namespace reasampler::capture