#pragma once // capture_realtime — the REAPER-free logic behind the realtime-record backend. // The shell drives the transport, temp track, send routing, and file move; the // pure pieces split out here and unit-tested outside the DAW are: (1) record- // mode bookkeeping — scope + FX-tap point -> I_RECMODE/I_RECMODE_FLAGS values // (bit MEANINGS transcribed verbatim from reaper_plugin_functions.h ~2197-2198; // the CHOICE of value per scope is this module's tested logic) — and (2) the // recorded-file -> Sample mapping (mirrors OfflineRenderBackend's population). #include #include #include #include "core/model/bank_model.h" // Sample, SourceMode (pure) namespace reasampler::capture { using model::Sample; using model::Tier; using model::SourceMode; // I_RECMODE (verbatim from SDK header ~2197): 0=input, 1=stereo out, 2=none, // 3=stereo out w/latency comp, 4=midi output, 5=mono out, 6=mono out w/latency // comp, 7=midi overdub, 8=midi replace. We record a track's OUTPUT, latency- // compensated, so the recorded file lines up sample-accurately with the source. inline constexpr int kRecModeStereoOutLatComp = 3; // stereo out w/latency comp inline constexpr int kRecModeMonoOutLatComp = 6; // mono out w/latency comp // I_RECMODE_FLAGS (verbatim from SDK header ~2198): &3=output recording mode // (0=post fader, 1=pre-fx, 2=post-fx/pre-fader). This is the only documented // pre-FX tap in the SDK — offline render has no pre-FX bit — so the realtime // backend is the true pre-FX "dry" path. inline constexpr int kRecOutPostFader = 0; // &3==0: post-fader (fully wet) inline constexpr int kRecOutPreFx = 1; // &3==1: pre-FX (true dry) inline constexpr int kRecOutPostFxPreFader = 2; // &3==2: post-FX, pre-fader // The tap point on the source track's output the temp track records from. // Orthogonal to the record mode (stereo/mono); this only sets the &3 flags bits. enum class OutputTap { PostFader, // fully wet, after this track's fader (kRecOutPostFader) PreFx, // true dry, before this track's FX (kRecOutPreFx) PostFxPreFader, // wet FX, before the fader (kRecOutPostFxPreFader) }; // The concrete record-mode values a temp track must carry to capture the scoped // output. `recMode` sets I_RECMODE (stereo/mono, latency-compensated); // `recModeFlags` sets the &3 output-recording tap bits (we only own those bits). struct RecordModePlan { int recMode = kRecModeStereoOutLatComp; int recModeFlags = kRecOutPostFader; }; // Maps (channelCount, tap) to the record-mode values: channelCount <= 1 -> // mono-out latency-comp, else stereo-out; tap -> the &3 bits. The shell applies // these via SetMediaTrackInfo_Value(I_RECMODE / I_RECMODE_FLAGS). RecordModePlan recordModePlanFor(int channelCount, OutputTap tap); // Maps a wetDry value to the output tap point: 1.0 (fully wet) -> PostFader, // anything less -> PreFx (true dry — the realtime backend's distinguishing // capability over offline render). PostFxPreFader is not reachable from wetDry. OutputTap outputTapForWetDry(double wetDry); // --- Recorded-file -> Sample mapping ---------------------------------------- // The inputs a finished realtime capture yields, gathered by the shell into a // pure struct so Sample population is a single tested transform (mirrors the // inline population in OfflineRenderBackend::capture). struct RecordedCapture { // Project-relative path of the recorded file (the shell resolves REAPER's // absolute path back to project-relative). std::string relativePath; // The disambiguating tag that named the file (feeds the Sample id). std::string uniqueTag; // Echoed from the request (exact bounds — no re-measuring the file). SourceMode sourceMode = SourceMode::Realtime; double startSeconds = 0.0; double endSeconds = 0.0; double wetDry = 1.0; std::string displayName; std::vector trackGuids; int channelCount = 0; // Left at defaults here — capture_realtime_finalize.cpp calls // stampCaptureSample(result.sample, ...) afterward, overwriting these five // from the live project. Kept because the pure unit tests still assert them. int sampleRate = 0; // 0 when the project rate was unknown (as offline) double captureTempo = 0.0; // BPM at capture time int captureTimeSigNum = 0; // 0/0 = unstamped int captureTimeSigDenom = 0; std::int64_t createdTimestamp = 0; // unix epoch seconds }; // Builds the Sample for a finished realtime capture: exact request bounds, // scratch tier, empty content hash, lengthSeconds = end - start. PPQ/beats // left 0 (deferred, as offline). Sample sampleFromRecordedCapture(const RecordedCapture& cap); // --- Async record-phase state machine ---------------------------------------- // // A realtime record spans many timer ticks (CSurf_OnRecord starts the transport // on REAPER's audio thread and returns immediately — it does not block until the // range completes). The completion decision — keep waiting, stop-and-flush, // finalize, or give up — is pure and unit-tested without a DAW; the shell only // reads the transport/clock/file and applies the verdict. // // Two waits, not one: // 1. RECORD wait (Recording): transport running; wait for the play cursor to // reach the range end, OR the user stops early, OR a wall-clock safety // ceiling trips (a started-but-never-advancing transport). // 2. FLUSH wait (Finalizing): transport stopped but REAPER closes/flushes the // recorded take on the audio thread — the file may lag a tick or two. // Defer the move until the file exists AND is stable, bounded by a flush // ceiling so a file that never appears fails cleanly instead of hanging. // Where an in-progress capture is in its lifecycle: Recording (live, transport // running) and Finalizing (live-but-stopped, waiting for flush) are the two // waits above; Done/Failed are terminal — the shell's verdict to act on. enum class RecordPhase { Recording, Finalizing, Done, Failed }; // A distilled transport reading so the state machine never touches a REAPER // type. `recording` is (GetPlayStateEx & 4) != 0; `playPosition` is // GetPlayPositionEx (latency-compensated). struct TransportReading { bool recording = false; double playPosition = 0.0; }; // Everything the pure transition needs beyond the current phase, gathered by // the shell each tick (the shell only reads and reports; never decides). struct RecordTickInputs { TransportReading transport; double elapsedSeconds = 0.0; // wall-clock since begin() — record ceiling double finalizingSeconds = 0.0; // wall-clock in Finalizing — flush ceiling bool fileReady = false; // recorded file exists+stable (Finalizing only) }; // Record ceiling margin added to nominal duration: generous enough that // pre-roll/count-in/latency never trips it, tight enough a stuck transport is // force-terminated within seconds. inline constexpr double kRecordMarginSeconds = 5.0; // Max wall-clock Finalizing waits for the file to flush/stabilize before // giving up (REAPER closes the take within a tick or two in practice). inline constexpr double kFinalizeFlushCeilingSeconds = 5.0; // The pure transition (total + deterministic). Done/Failed are sticky — a late // tick before teardown finishes cannot flip the verdict (the idempotence the // shell's single-restore relies on). RecordPhase advanceRecordPhase(RecordPhase current, const RecordTickInputs& inputs, double rangeStartSeconds, double rangeEndSeconds); // True once the shell must STOP the transport and begin the flush wait — i.e. the // phase has left Recording (Finalizing/Done/Failed). Used by the shell to fire the // (idempotent) transport stop exactly on the Recording -> Finalizing edge. bool isStopRequested(RecordPhase phase); // True for the phases the shell must ACT on to conclude (finalize-or-fail + restore). // Only Done and Failed are terminal; Recording and Finalizing are live. bool isTerminalPhase(RecordPhase phase); } // namespace reasampler::capture