172 lines
8.1 KiB
C++
172 lines
8.1 KiB
C++
#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 <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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<std::string> 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
|