0ab4673887
Corrects a false comment example, fixes two tests that couldn't detect their own regressions, adds two more read-back checkpoints around Main_OnCommand so a drift report self-locates, guards a spurious zero-vs-zero coincidence match, and softens two sentences that overclaimed cause or defect.
500 lines
22 KiB
C++
500 lines
22 KiB
C++
// REAPER-facing realtime-record backend (RealtimeRecordBackend): the async record
|
|
// lifecycle — state snapshot/restore + begin/tick/abort. The file-side half
|
|
// (recorded-file discovery, move-into-bank, Auto-tail trim, Sample population)
|
|
// lives in capture_realtime_finalize.cpp.
|
|
//
|
|
// Includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is
|
|
// the one TU that defines the API pointers; here they are extern.
|
|
//
|
|
// Captures the requested scope by recording in realtime into a hidden temp
|
|
// track, then moves the recorded file into the bank as a Sample —
|
|
// non-destructively. TRACK scope only this increment (the selected track's own
|
|
// output); item realtime is deferred (UnsupportedMode) rather than half-built.
|
|
//
|
|
// ASYNC: a realtime record takes (end - start) wall-clock seconds; blocking the
|
|
// main thread for that long freezes REAPER's UI. So it's driven across timer
|
|
// ticks: begin() validates, snapshots all state to restore, creates the temp
|
|
// track, routes the source-track tap, arms, CSurf_OnRecord, and returns
|
|
// immediately; tick() (from OnTimer, same tick as session.poll()) reads the
|
|
// transport and on a terminal verdict stops + finalizes/aborts + restores
|
|
// everything; abort() force-terminates (shutdown/project switch) + restores.
|
|
//
|
|
// The snapshot + restore live on RealtimeCaptureState, not a function-scope RAII
|
|
// guard, because the record spans ticks — no single stack frame outlives it.
|
|
// restore() is idempotent: every terminal path (completion, user stop, error,
|
|
// project switch, unload) funnels through the same restore. The pure record-mode
|
|
// bookkeeping, recorded-file->Sample mapping, and completion state machine
|
|
// (advanceRecordPhase) live in core/capture/capture_realtime (unit-tested
|
|
// outside the DAW); this TU owns only the REAPER-bound lifecycle recipe.
|
|
//
|
|
// TAP: the hidden temp track receives a send FROM each selected source track
|
|
// (CreateTrackSend(source, temp)) and records its own output (B_MAINSEND=0, so
|
|
// it never sums back into the master — no feedback, no monitoring double).
|
|
// Multiple selected tracks sum in the one temp track — a real mix, which is why
|
|
// realtime accepts a multi-track selection where the offline track scope refuses
|
|
// it (the offline render source is read as emitting one file per track, DAW-
|
|
// unverified — see src/shell/capture/CLAUDE.md §Gotchas).
|
|
//
|
|
// Why this needs no FxBypassGuard: CreateTrackSend defaults to I_SENDMODE=0
|
|
// (post-fader), which taps the source track after its own FX/fader/pan — its
|
|
// own output — but before the parent/folder/master sums it. The tap is
|
|
// chain-independent by construction: there's nothing downstream of the branch
|
|
// point to neutralize. (An earlier spike sent FROM the master, which REAPER
|
|
// refuses as a feedback loop and silently recorded nothing — a regular
|
|
// track->track send has no such loop.)
|
|
//
|
|
// Non-destructive: deleting the temp track on teardown removes every send
|
|
// created into it (REAPER cannot leave a send dangling to a deleted
|
|
// destination), so no source track retains any routing change.
|
|
|
|
#include "shell/capture/capture_realtime_shell.h"
|
|
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/capture/capture_paths.h" // deriveBankPaths
|
|
#include "core/capture/capture_realtime.h" // RecordPhase machine, record-mode plan (pure)
|
|
#include "core/capture/render_settings.h" // realtimeRecordWindowEnd
|
|
#include "shell/capture/capture_realtime_finalize.h" // recordedFilePath, finalizeRecording
|
|
|
|
#define REAPERAPI_MINIMAL
|
|
#define REAPERAPI_WANT_EnumProjects
|
|
#define REAPERAPI_WANT_Main_SaveProject
|
|
#define REAPERAPI_WANT_InsertTrackAtIndex
|
|
#define REAPERAPI_WANT_DeleteTrack
|
|
#define REAPERAPI_WANT_CountTracks
|
|
#define REAPERAPI_WANT_GetTrack
|
|
#define REAPERAPI_WANT_CreateTrackSend
|
|
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
|
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
|
#define REAPERAPI_WANT_CSurf_OnRecord
|
|
#define REAPERAPI_WANT_OnStopButtonEx
|
|
#define REAPERAPI_WANT_GetPlayStateEx
|
|
#define REAPERAPI_WANT_GetPlayPositionEx
|
|
#define REAPERAPI_WANT_GetSet_LoopTimeRange
|
|
#define REAPERAPI_WANT_GetCursorPosition
|
|
#define REAPERAPI_WANT_SetEditCurPos
|
|
#define REAPERAPI_WANT_ValidatePtr2
|
|
#include "reaper_plugin_functions.h"
|
|
|
|
namespace reasampler::capture {
|
|
|
|
namespace {
|
|
|
|
std::string normSlashes(std::string s) {
|
|
for (char& c : s) if (c == '\\') c = '/';
|
|
if (s.size() > 1 && s.back() == '/') s.pop_back();
|
|
return s;
|
|
}
|
|
|
|
// Reads the ACTIVE project's .rpp path (empty if unsaved). Only needed at begin()
|
|
// time, when the record's project IS the active project.
|
|
std::string readRppPath() {
|
|
std::vector<char> buf(4096, '\0');
|
|
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
|
return std::string(buf.data());
|
|
}
|
|
|
|
// -1 if unresolved yet. Used by the flush wait to detect stability (size
|
|
// unchanged across a tick) before moving the file — a take REAPER is still
|
|
// flushing grows tick over tick.
|
|
std::int64_t recordedFileSize(MediaTrack* temp) {
|
|
const std::string path = recordedFilePath(temp);
|
|
if (path.empty()) return -1;
|
|
std::error_code ec;
|
|
const auto sz = std::filesystem::file_size(path, ec);
|
|
if (ec) return -1;
|
|
return static_cast<std::int64_t>(sz);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// Holds everything to restore across the many ticks the record spans (temp
|
|
// track + its sends, other tracks' I_RECARM, transport, edit cursor, time
|
|
// selection), plus the request echo needed to finalize the Sample. restore()
|
|
// is idempotent (restored_ latch) — the single teardown every terminal path calls.
|
|
class RealtimeCaptureState {
|
|
public:
|
|
// Transport reads use *Ex(proj_) so a project switch mid-record can't read
|
|
// the wrong transport.
|
|
ReaProject* proj_ = nullptr;
|
|
CaptureRequest request_;
|
|
BankPaths paths_;
|
|
std::string uniqueTag_;
|
|
|
|
// Project seconds, >= request_.endSeconds. A tail mode runs the transport
|
|
// past the range end (Auto: +8s cap; Manual: +set length) — this, not
|
|
// request_.endSeconds, is what the completion machine waits for.
|
|
double recordWindowEnd_ = 0.0;
|
|
|
|
// Sends created into temp_ are removed automatically when temp_ is
|
|
// deleted — no separate send handle to track.
|
|
MediaTrack* temp_ = nullptr;
|
|
|
|
RecordPhase phase_ = RecordPhase::Recording;
|
|
|
|
// Steady clock (not the play cursor) so a stuck/looping transport is still
|
|
// caught. begunAt_ set at begin(); finalizingAt_ set on the Recording->
|
|
// Finalizing edge so the flush wait is bounded from the stop, not begin.
|
|
std::chrono::steady_clock::time_point begunAt_{};
|
|
std::chrono::steady_clock::time_point finalizingAt_{};
|
|
|
|
// Deferred-finalize (review §2) flush tracking: the recorded file's size the
|
|
// previous tick, so "size unchanged across a tick" signals REAPER finished
|
|
// flushing/closing the take. -1 = not yet seen.
|
|
std::int64_t lastFileSize_ = -1;
|
|
|
|
void markElapsedStart() { begunAt_ = std::chrono::steady_clock::now(); }
|
|
double elapsedSeconds() const {
|
|
return std::chrono::duration<double>(
|
|
std::chrono::steady_clock::now() - begunAt_).count();
|
|
}
|
|
// Set the flush-wait anchor once, on the first Finalizing tick.
|
|
void markFinalizingStartOnce() {
|
|
if (finalizingAt_.time_since_epoch().count() == 0)
|
|
finalizingAt_ = std::chrono::steady_clock::now();
|
|
}
|
|
double finalizingSeconds() const {
|
|
if (finalizingAt_.time_since_epoch().count() == 0) return 0.0;
|
|
return std::chrono::duration<double>(
|
|
std::chrono::steady_clock::now() - finalizingAt_).count();
|
|
}
|
|
|
|
// Snapshot of state to restore. Filled at begin(), replayed once by restore().
|
|
double curPos_ = 0.0;
|
|
double tsStart_ = 0.0;
|
|
double tsEnd_ = 0.0;
|
|
struct ArmSnap { MediaTrack* track; double recarm; };
|
|
std::vector<ArmSnap> armSnaps_;
|
|
|
|
// Snapshot the transport-adjacent state (cursor + time selection) and every
|
|
// OTHER track's arm, disarming them so only our sink records. Call ONCE, before
|
|
// the temp track exists (so the temp track is never in the arm snapshot).
|
|
void snapshotAndDisarmOthers() {
|
|
curPos_ = GetCursorPosition();
|
|
GetSet_LoopTimeRange(false, false, &tsStart_, &tsEnd_, false);
|
|
const int n = CountTracks(proj_);
|
|
for (int i = 0; i < n; ++i) {
|
|
MediaTrack* tr = GetTrack(proj_, i);
|
|
if (!tr) continue;
|
|
const double armed = GetMediaTrackInfo_Value(tr, "I_RECARM");
|
|
if (armed != 0.0) {
|
|
armSnaps_.push_back({tr, armed});
|
|
SetMediaTrackInfo_Value(tr, "I_RECARM", 0.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Idempotent teardown called on every terminal path: stop transport if
|
|
// still running, delete temp track (drops its sends + recorded item),
|
|
// restore other tracks' arm, restore time selection + edit cursor.
|
|
// OnStopButtonEx(proj_) is project-scoped, not the global CSurf_OnStop, so
|
|
// a project switch mid-record (proj_ no longer active) still stops OUR
|
|
// project's transport, never the foreign now-active one.
|
|
void stopOwnTransport() {
|
|
if (GetPlayStateEx(proj_) & (1 | 4)) OnStopButtonEx(proj_);
|
|
}
|
|
|
|
// If the captured project was closed mid-record, proj_/temp_ point at freed
|
|
// memory; touching them is a use-after-free. ValidatePtr2 with a null
|
|
// project validates the ReaProject* itself. Every teardown that
|
|
// dereferences a captured REAPER object must gate on this first.
|
|
bool captureProjectStillOpen() const {
|
|
return proj_ && ValidatePtr2(nullptr, proj_, "ReaProject*");
|
|
}
|
|
|
|
// For the closed-project case: a closed project already reclaimed its temp
|
|
// track, arms, and transport, so drop the handle without touching REAPER state.
|
|
void dropWithoutRestore() {
|
|
restored_ = true;
|
|
temp_ = nullptr;
|
|
armSnaps_.clear();
|
|
}
|
|
|
|
void restore() {
|
|
if (restored_) return;
|
|
restored_ = true;
|
|
|
|
stopOwnTransport();
|
|
|
|
// Deleting the temp track drops the source-track sends (REAPER removes
|
|
// every send whose destination is deleted) and the recorded item in one move.
|
|
if (temp_) { DeleteTrack(temp_); temp_ = nullptr; }
|
|
|
|
for (const ArmSnap& s : armSnaps_)
|
|
SetMediaTrackInfo_Value(s.track, "I_RECARM", s.recarm);
|
|
armSnaps_.clear();
|
|
|
|
GetSet_LoopTimeRange(true, false, &tsStart_, &tsEnd_, false);
|
|
SetEditCurPos(curPos_, false, false);
|
|
}
|
|
|
|
bool restored() const { return restored_; }
|
|
bool finalized() const { return finalized_; }
|
|
void markFinalized() { finalized_ = true; }
|
|
|
|
private:
|
|
bool restored_ = false;
|
|
bool finalized_ = false;
|
|
};
|
|
|
|
void RealtimeCaptureStateDeleter::operator()(RealtimeCaptureState* p) const noexcept {
|
|
delete p; // full type is visible here — keeps capture.h REAPER-free
|
|
}
|
|
|
|
RealtimeCaptureHandle
|
|
RealtimeRecordBackend::begin(const CaptureRequest& request,
|
|
const std::vector<MediaTrack*>& sourceTracks,
|
|
CaptureResult& outFailure) {
|
|
// Item realtime is deferred — needs per-item take isolation on top of the
|
|
// track-output tap.
|
|
if (request.sourceMode != SourceMode::SelectedTracks) {
|
|
outFailure.status = CaptureStatus::UnsupportedMode;
|
|
outFailure.message = "RealtimeRecordBackend implements TRACK scope only this "
|
|
"increment (item realtime is deferred).";
|
|
return nullptr;
|
|
}
|
|
|
|
// Track scope needs at least one source track to tap. No selection -> refuse
|
|
// (matching offline track scope's no-op on an empty selection).
|
|
if (sourceTracks.empty()) {
|
|
outFailure.status = CaptureStatus::UnsupportedMode;
|
|
outFailure.message = "No track selected — realtime track capture needs at least "
|
|
"one selected track to tap.";
|
|
return nullptr;
|
|
}
|
|
|
|
// Exact bounds: refuse an empty/inverted range rather than record silence.
|
|
if (!(request.endSeconds > request.startSeconds)) {
|
|
outFailure.status = CaptureStatus::EmptyRange;
|
|
outFailure.message = "Capture range is empty (end <= start).";
|
|
return nullptr;
|
|
}
|
|
|
|
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
|
if (!proj) {
|
|
outFailure.status = CaptureStatus::NoProject;
|
|
outFailure.message = "No active project.";
|
|
return nullptr;
|
|
}
|
|
|
|
// Refuse if the transport is already playing/recording — we own the transport for
|
|
// the capture window and must not hijack a user's live take.
|
|
if (GetPlayStateEx(proj) & (1 | 4)) {
|
|
outFailure.status = CaptureStatus::TransportBusy;
|
|
outFailure.message = "Transport is already playing/recording — realtime capture "
|
|
"refused. Stop the transport first.";
|
|
return nullptr;
|
|
}
|
|
|
|
// Saved-project gate (same as offline): the bank folder resolves against the
|
|
// .rpp parent. Prompt Save-As once when unsaved; refuse if still unsaved.
|
|
std::string rppPath = readRppPath();
|
|
if (rppPath.empty()) {
|
|
Main_SaveProject(proj, true);
|
|
rppPath = readRppPath();
|
|
}
|
|
if (rppPath.empty()) {
|
|
outFailure.status = CaptureStatus::NoProject;
|
|
outFailure.message = "Project must be saved before capture — nothing captured.";
|
|
return nullptr;
|
|
}
|
|
const std::string projectDir =
|
|
normSlashes(std::filesystem::path(rppPath).parent_path().string());
|
|
|
|
RealtimeCaptureHandle st(new RealtimeCaptureState());
|
|
st->proj_ = proj;
|
|
st->request_ = request;
|
|
st->uniqueTag_ = makeUniqueTag("rt-");
|
|
st->paths_ = deriveBankPaths(projectDir, request.baseName, st->uniqueTag_);
|
|
|
|
// Extended past the range end for a tail mode (Auto/Manual), exact for
|
|
// None; the extra window is trimmed later (Auto) or kept (Manual).
|
|
st->recordWindowEnd_ = realtimeRecordWindowEnd(request.tailMode,
|
|
request.endSeconds,
|
|
request.tailMs);
|
|
|
|
// Deliberately NOT wrapped in an undo block — this backend fully restores
|
|
// its own state across every terminal path, so an undo point would surface
|
|
// an internal, fully-reversed scaffold for no user-meaningful action.
|
|
// Disarm every other track BEFORE the temp track exists so it's never in
|
|
// the arm snapshot.
|
|
st->snapshotAndDisarmOthers();
|
|
|
|
// Hidden temp track: no default FX/envelopes, hidden from both panels,
|
|
// B_MAINSEND=0 so it doesn't sum back into the master (would otherwise
|
|
// double the tapped tracks in the user's monitoring).
|
|
const int idx = CountTracks(proj);
|
|
InsertTrackAtIndex(idx, false);
|
|
st->temp_ = GetTrack(proj, idx);
|
|
if (!st->temp_) {
|
|
outFailure.status = CaptureStatus::RenderFailed;
|
|
outFailure.message = "Could not create the hidden temp record track.";
|
|
st->restore();
|
|
return nullptr;
|
|
}
|
|
SetMediaTrackInfo_Value(st->temp_, "B_SHOWINTCP", 0.0);
|
|
SetMediaTrackInfo_Value(st->temp_, "B_SHOWINMIXER", 0.0);
|
|
SetMediaTrackInfo_Value(st->temp_, "B_MAINSEND", 0.0);
|
|
|
|
// A send FROM each selected source track INTO the temp track; the temp
|
|
// records its own output, so sends sum in it — matching offline track
|
|
// scope's multi-track handling. Sends default to post-fader/full-stereo,
|
|
// left at defaults deliberately — that IS the track-scope tap point.
|
|
int sendsMade = 0;
|
|
for (MediaTrack* src : sourceTracks) {
|
|
if (!src || src == st->temp_) continue;
|
|
if (CreateTrackSend(src, st->temp_) >= 0) ++sendsMade;
|
|
}
|
|
if (sendsMade == 0) {
|
|
outFailure.status = CaptureStatus::RenderFailed;
|
|
outFailure.message = "Could not route any selected track into the record tap — "
|
|
"nothing to capture.";
|
|
st->restore(); // deleting the temp track drops any partial sends too
|
|
return nullptr;
|
|
}
|
|
|
|
// The temp track has no FX and unity fader, so its post-fader output
|
|
// equals the summed sends; track scope is fully wet -> PostFader.
|
|
const OutputTap tap = outputTapForWetDry(request.wetDry);
|
|
const RecordModePlan rec = recordModePlanFor(request.channelCount, tap);
|
|
SetMediaTrackInfo_Value(st->temp_, "I_RECMODE", static_cast<double>(rec.recMode));
|
|
SetMediaTrackInfo_Value(st->temp_, "I_RECMODE_FLAGS",
|
|
static_cast<double>(rec.recModeFlags));
|
|
SetMediaTrackInfo_Value(st->temp_, "I_RECARM", 1.0); // arm ONLY the sink
|
|
SetMediaTrackInfo_Value(st->temp_, "I_RECMON", 0.0); // no input monitoring
|
|
|
|
// recordWindowEnd extends past the range end for a tail mode so the
|
|
// transport captures the decay; cursor + time selection are restored by restore().
|
|
// `[verify — DAW]` whether rs/re come back changed on this isSet=true call: the SDK
|
|
// header names both `double*` but documents no read-back semantics for either
|
|
// direction, and nothing here reads rs/re again after the call to notice. Lower
|
|
// stakes than the offline RENDER_* store: completion is driven by the play cursor
|
|
// reaching the range end (tick(), below), not by re-reading this pair.
|
|
double rs = request.startSeconds, re = st->recordWindowEnd_;
|
|
GetSet_LoopTimeRange(true, false, &rs, &re, false);
|
|
SetEditCurPos(request.startSeconds, false, false);
|
|
|
|
// tick() detects completion via the play cursor reaching the range end
|
|
// (the pure state machine), independent of REAPER's auto-punch settings.
|
|
CSurf_OnRecord();
|
|
|
|
// Steady clock, independent of the play cursor, so a transport that starts
|
|
// but never advances is still bounded.
|
|
st->markElapsedStart();
|
|
|
|
return st;
|
|
}
|
|
|
|
RealtimeTickResult RealtimeRecordBackend::tick(RealtimeCaptureState& state) {
|
|
RealtimeTickResult out;
|
|
|
|
// A prior terminal path (e.g. abort()) already tore this down — spent.
|
|
if (state.restored()) { out.status = RealtimeTickStatus::Failed; return out; }
|
|
|
|
const RecordPhase prevPhase = state.phase_;
|
|
|
|
// *Ex(state.proj_) so a project switch can't point these reads at the
|
|
// wrong transport.
|
|
RecordTickInputs inputs;
|
|
inputs.transport.recording = (GetPlayStateEx(state.proj_) & 4) != 0;
|
|
inputs.transport.playPosition = GetPlayPositionEx(state.proj_);
|
|
inputs.elapsedSeconds = state.elapsedSeconds();
|
|
|
|
// File is ready when its size is positive and unchanged from the previous
|
|
// tick — REAPER finished flushing the take. Comparing across a tick avoids
|
|
// moving a file mid-write.
|
|
if (prevPhase == RecordPhase::Finalizing) {
|
|
state.markFinalizingStartOnce();
|
|
inputs.finalizingSeconds = state.finalizingSeconds();
|
|
const std::int64_t sz = recordedFileSize(state.temp_);
|
|
inputs.fileReady = (sz > 0 && sz == state.lastFileSize_);
|
|
state.lastFileSize_ = sz;
|
|
}
|
|
|
|
// Waits for the transport to reach the recorded window end (extended for a
|
|
// tail mode), not the request's range end — the extra tail window is part
|
|
// of the record.
|
|
state.phase_ = advanceRecordPhase(state.phase_, inputs,
|
|
state.request_.startSeconds,
|
|
state.recordWindowEnd_);
|
|
|
|
// On Recording -> Finalizing, stop OUR project's transport once so REAPER
|
|
// begins flushing the take; project-scoped so a project switch can't stop
|
|
// the wrong (foreign active) project.
|
|
if (prevPhase == RecordPhase::Recording &&
|
|
isStopRequested(state.phase_)) {
|
|
state.stopOwnTransport();
|
|
state.markFinalizingStartOnce();
|
|
}
|
|
|
|
if (!isTerminalPhase(state.phase_)) {
|
|
out.status = RealtimeTickStatus::InProgress;
|
|
return out;
|
|
}
|
|
|
|
// Done: file flushed + stable, finalize moves it into the bank. Failed:
|
|
// flush ceiling tripped, nothing usable.
|
|
CaptureResult res;
|
|
if (state.phase_ == RecordPhase::Done) {
|
|
res = finalizeRecording(state.proj_, state.temp_, state.request_,
|
|
state.paths_, state.uniqueTag_,
|
|
state.recordWindowEnd_);
|
|
} else {
|
|
res.status = CaptureStatus::RenderFailed;
|
|
res.message = "Realtime record timed out waiting for the recorded file to "
|
|
"flush/close (nothing captured).";
|
|
}
|
|
state.markFinalized();
|
|
state.restore();
|
|
|
|
out.result = res;
|
|
out.status = (res.status == CaptureStatus::Ok)
|
|
? RealtimeTickStatus::Done
|
|
: RealtimeTickStatus::Failed;
|
|
return out;
|
|
}
|
|
|
|
RealtimeTickResult RealtimeRecordBackend::abort(RealtimeCaptureState& state) {
|
|
RealtimeTickResult out;
|
|
|
|
if (state.restored()) { out.status = RealtimeTickStatus::Failed; return out; }
|
|
|
|
// If the captured project was closed mid-record, proj_/temp_ point at
|
|
// freed memory — drop the handle without touching REAPER state. This is
|
|
// the one terminal path that can run against a possibly-closed project
|
|
// (tick() only runs while proj_ is still the active project).
|
|
if (!state.captureProjectStillOpen()) {
|
|
state.dropWithoutRestore();
|
|
out.result.status = CaptureStatus::RenderFailed;
|
|
out.result.message = "Realtime capture dropped — the captured project was closed "
|
|
"mid-record (nothing to restore; no capture persisted).";
|
|
out.status = RealtimeTickStatus::Failed;
|
|
return out;
|
|
}
|
|
|
|
// Project still open: stop the transport, then try to finalize whatever
|
|
// was captured so a near-complete record keeps its audio; if nothing
|
|
// usable was recorded, finalize returns RenderFailed and we abort clean.
|
|
// abort() is the force-terminate path — unlike tick() it can't span ticks
|
|
// to wait for the flush, so it still races REAPER's audio-thread take close.
|
|
state.stopOwnTransport();
|
|
|
|
CaptureResult res = finalizeRecording(state.proj_, state.temp_, state.request_,
|
|
state.paths_, state.uniqueTag_,
|
|
state.recordWindowEnd_);
|
|
state.markFinalized();
|
|
state.restore();
|
|
|
|
out.result = res;
|
|
out.status = (res.status == CaptureStatus::Ok)
|
|
? RealtimeTickStatus::Done
|
|
: RealtimeTickStatus::Failed;
|
|
return out;
|
|
}
|
|
|
|
} // namespace reasampler::capture
|