Cut core/capture and core/version comment bloat ~45% (comments only, zero code change)
This commit is contained in:
@@ -1,27 +1,11 @@
|
||||
#pragma once
|
||||
// capture_realtime — the REAPER-free logic behind the realtime-record backend (M8).
|
||||
// (Renamed from realtime_record in Q-W3 — the Q-9 naming rider: the PURE module
|
||||
// takes the stem, the shell takes the suffix — capture_realtime_shell.cpp /
|
||||
// capture_realtime_finalize.cpp — matching the drag_out ↔ drag_out_win model.)
|
||||
//
|
||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
||||
// vendor/ includes. Standard library only. The realtime shell drives the
|
||||
// transport, the temp track, the send routing, and the file move —
|
||||
// all REAPER-bound and DAW-verified. The genuinely pure, easy-to-get-wrong
|
||||
// pieces are split out here and unit-tested outside the DAW:
|
||||
//
|
||||
// 1. the record-mode/recipe bookkeeping: given a capture scope + a desired
|
||||
// FX-tap point (post-fader / pre-FX / post-FX-pre-fader), the I_RECMODE and
|
||||
// I_RECMODE_FLAGS integer values the temp track must carry.
|
||||
// 2. the recorded-file -> Sample mapping: given a finished capture (the
|
||||
// recorded file's project-relative path + the request's own bounds/format),
|
||||
// the populated Sample handed to bank_model. Mirrors the inline Sample
|
||||
// population OfflineRenderBackend does — factored out so it is tested once,
|
||||
// without a DAW, and shared shape with the offline path is guaranteed.
|
||||
//
|
||||
// The I_RECMODE / I_RECMODE_FLAGS bit MEANINGS are transcribed verbatim from
|
||||
// reaper_plugin_functions.h line ~2197-2198 (see kRecMode* constants); the CHOICE
|
||||
// of which values each scope needs is this module's logic and is tested.
|
||||
// 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>
|
||||
@@ -35,26 +19,17 @@ using model::Sample;
|
||||
using model::Tier;
|
||||
using model::SourceMode;
|
||||
|
||||
// --- I_RECMODE values (verbatim from SDK header ~2197) -----------------------
|
||||
//
|
||||
// I_RECMODE : int * : record mode, 0=input, 1=stereo out, 2=none,
|
||||
// 3=stereo out w/latency compensation, 4=midi output, 5=mono out,
|
||||
// 6=mono out w/ latency compensation, 7=midi overdub, 8=midi replace.
|
||||
//
|
||||
// We record a track's OUTPUT (the scoped signal routed into the temp track),
|
||||
// latency-compensated, so the recorded file lines up sample-accurately with the
|
||||
// source. Stereo vs mono is chosen by the request's channel count.
|
||||
// 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 values (verbatim from SDK header ~2198) ------------------
|
||||
//
|
||||
// I_RECMODE_FLAGS : int * : record mode flags, &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 whole SDK — offline render has no
|
||||
// pre-FX bit (see render_settings.h note + the M10 null-test note in PLAN.md).
|
||||
// The realtime backend is therefore the true pre-FX "dry" path.
|
||||
// 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
|
||||
@@ -68,41 +43,34 @@ enum class OutputTap {
|
||||
};
|
||||
|
||||
// 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 (higher bits are left at their default 0
|
||||
// here — we only own the tap-point bits).
|
||||
// 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; otherwise stereo-out latency-comp.
|
||||
// tap -> the &3 output-recording bits.
|
||||
// Pure so the "which I_RECMODE for N channels + this tap" rule is unit-tested
|
||||
// without a DAW; the shell reads the request and applies these via
|
||||
// SetMediaTrackInfo_Value(I_RECMODE / I_RECMODE_FLAGS).
|
||||
// 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; any
|
||||
// value < 1.0 -> PreFx (true dry — the realtime backend's distinguishing
|
||||
// capability). Kept pure + separate from recordModePlanFor so the wet/dry ->
|
||||
// tap decision is tested on its own; PostFxPreFader is not selected by wetDry
|
||||
// (it is an explicit future option, not on the wet/dry axis).
|
||||
// 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 the Sample population is a single tested transform (mirror of
|
||||
// the inline population in OfflineRenderBackend::capture).
|
||||
// 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 (relative-paths-only invariant;
|
||||
// the shell resolves REAPER's recorded absolute path back to project-relative).
|
||||
// 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, so id and
|
||||
// file name stay consistent — same discipline as the offline path).
|
||||
// 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).
|
||||
@@ -115,60 +83,41 @@ struct RecordedCapture {
|
||||
|
||||
int channelCount = 0;
|
||||
|
||||
// TEST-ONLY / dead in production (Q-W3 review follow-up): the shell no longer
|
||||
// populates these five fields before calling sampleFromRecordedCapture — the
|
||||
// finalize path (capture_realtime_finalize.cpp) leaves them at their defaults
|
||||
// and instead calls the shared stampCaptureSample(result.sample, ...) right
|
||||
// after, which writes Sample::sampleRate/captureTempo/captureTimeSigNum/
|
||||
// captureTimeSigDenom/createdTimestamp directly, overwriting whatever
|
||||
// sampleFromRecordedCapture set from these. Kept (not deleted) because the pure
|
||||
// unit tests still construct/assert them directly; removing the fields is a
|
||||
// struct-shape decision out of scope here.
|
||||
// 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 (shell reads Master_GetTempo)
|
||||
// Time signature at capture start (L7 F1; shell reads TimeMap_GetTimeSigAtTime).
|
||||
// 0/0 = unstamped (matches the Sample default; formatter renders a blank read-out).
|
||||
int captureTimeSigNum = 0;
|
||||
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 (shell reads the clock)
|
||||
std::int64_t createdTimestamp = 0; // unix epoch seconds
|
||||
};
|
||||
|
||||
// Builds the Sample for a finished realtime capture. Deliberately identical in
|
||||
// shape to OfflineRenderBackend's population: exact request bounds (no rounding),
|
||||
// scratch tier, empty content hash (does not dedup), lengthSeconds = end - start.
|
||||
// PPQ/beats are left 0 (a musical-placement concern deferred exactly as offline).
|
||||
// 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 (M8 rework) ----------------------------
|
||||
// --- 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 — "given where the transport is now, should
|
||||
// the tick keep waiting, stop-and-flush, finalize, or give up?" — is pure and
|
||||
// exactly the kind of off-by-one/edge logic a unit test locks without a DAW. It is
|
||||
// factored out here; the REAPER shell only reads the transport/clock/file and applies
|
||||
// the verdict (stop, wait for the file to flush, then finalize/abort + restore).
|
||||
// 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.
|
||||
//
|
||||
// The lifecycle has TWO waits, not one:
|
||||
// 1. the RECORD wait (Recording): the transport is running; we 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, §3 of review).
|
||||
// 2. the FLUSH wait (Finalizing): the transport is stopped but REAPER closes/flushes
|
||||
// the recorded take on the AUDIO thread — the file may not be fully written/closed
|
||||
// for a tick or two. We defer the file move until the file exists AND is stable
|
||||
// (§2 of review), bounded by a flush ceiling so a file that never appears fails
|
||||
// cleanly rather than hanging.
|
||||
// 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, shell keeps ticking.
|
||||
// Finalizing — live-but-stopped: transport halted, shell stops the transport once
|
||||
// then ticks waiting for the recorded file to flush/stabilize.
|
||||
// Done — terminal: the file is flushed + stable, finalize (move + Sample) now.
|
||||
// Failed — terminal: the flush ceiling tripped without a stable file — give up
|
||||
// (RenderFailed) + restore. (A record that produced NO file at all also
|
||||
// lands here via the shell's finalize returning RenderFailed.)
|
||||
// Only Recording and Finalizing are live phases the shell advances per tick; Done and
|
||||
// Failed are the shell's verdict to act on (finalize-or-fail, then restore).
|
||||
// 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,
|
||||
@@ -176,63 +125,34 @@ enum class RecordPhase {
|
||||
Failed
|
||||
};
|
||||
|
||||
// A distilled transport reading for the pure transition, so the state machine never
|
||||
// touches a REAPER type. `recording` is (GetPlayStateEx & 4) != 0; `playPosition`
|
||||
// is GetPlayPositionEx (latency-compensated what-you-hear position).
|
||||
// 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 so the machine stays REAPER-free AND owns every timing/ceiling
|
||||
// decision (the shell only reads and reports; it never decides a transition itself).
|
||||
// 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;
|
||||
|
||||
// Wall-clock seconds since begin() (the shell reads a steady clock). Drives the
|
||||
// record safety ceiling: a transport that starts but never advances to the range
|
||||
// end (stuck / looping) would otherwise keep the machine in Recording forever.
|
||||
double elapsedSeconds = 0.0;
|
||||
|
||||
// Wall-clock seconds spent in the Finalizing phase (since the transport stop).
|
||||
// Drives the flush ceiling: bound the deferred-finalize wait so a file that never
|
||||
// stabilizes fails cleanly instead of hanging.
|
||||
double finalizingSeconds = 0.0;
|
||||
|
||||
// Whether the recorded take's file exists AND is stable/closed this tick (the
|
||||
// shell resolves the take source path and checks size-stable-across-a-tick).
|
||||
// Only consulted in Finalizing.
|
||||
bool fileReady = false;
|
||||
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)
|
||||
};
|
||||
|
||||
// --- Safety ceilings (named constants, review §2/§3) -------------------------
|
||||
//
|
||||
// kRecordMarginSeconds: added to the record's nominal duration (end - start) to form
|
||||
// the record wall-clock ceiling. Generous so a normal record (with pre-roll, count-in,
|
||||
// or transport latency) never trips it; tight enough that a stuck transport is force-
|
||||
// terminated within a few seconds of overrun.
|
||||
// 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;
|
||||
|
||||
// kFinalizeFlushCeilingSeconds: the max wall-clock the Finalizing phase waits for the
|
||||
// recorded file to flush/stabilize before giving up (RenderFailed). REAPER closes the
|
||||
// take on the audio thread within a tick or two in practice; this is a generous bound.
|
||||
// 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: given the current phase, this tick's inputs, and the record
|
||||
// range end, return the next phase. Total + deterministic.
|
||||
//
|
||||
// From Recording:
|
||||
// * recording AND cursor < end AND under the record ceiling -> Recording (wait)
|
||||
// * recording AND cursor >= end -> Finalizing (reached end)
|
||||
// * NOT recording -> Finalizing (stopped early)
|
||||
// * recording BUT over the record ceiling (end-start+margin)-> Finalizing (stuck: forced)
|
||||
// From Finalizing:
|
||||
// * fileReady -> Done (flushed + stable)
|
||||
// * over the flush ceiling without a stable file -> Failed (give up)
|
||||
// * otherwise -> Finalizing (keep flushing)
|
||||
// Done and Failed are sticky: feeding a terminal phase back returns it unchanged, so a
|
||||
// late tick before teardown finishes cannot flip the verdict (the idempotence the
|
||||
// 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,
|
||||
|
||||
Reference in New Issue
Block a user