// realtime_lifecycle.cpp — the in-flight realtime-capture state machine + globals // (Q-W3 hoist out of main.cpp; the code moved verbatim, the session threaded as a // parameter). See the header. // // Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h // WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API // pointers; here they are extern (CLAUDE.md §contract). #include "shell/capture/realtime_lifecycle.h" #include "shell/persist/session.h" // ReaSamplerSession #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_EnumProjects #define REAPERAPI_WANT_ShowConsoleMsg #include "reaper_plugin_functions.h" namespace reasampler::capture { // --- M8 in-flight realtime capture (async, timer-driven) -------------------- RealtimeRecordBackend g_rtBackend; RealtimeCaptureHandle g_rtCapture; ReaProject* g_rtCaptureProject = nullptr; // Commit a finished realtime capture (a Done tick/abort with an Ok result): add the // Sample to the ACTIVE bank (session.bank() resolves to book.activeIndex() — B2), // persist + MarkProjectDirty. Shared by the tick-completion path and the abort // paths. On a non-Ok result, logs the failure only. void CommitRealtimeResult(ReaSamplerSession& session, const CaptureResult& res) { if (res.status != CaptureStatus::Ok) { ShowConsoleMsg(("ReaSampler realtime capture failed: " + res.message + "\n").c_str()); return; } session.bank().add(res.sample); // B-cap: record the file the capture created in the owned-file manifest, at the same // point the Sample is added and before the same persist. Recorded regardless of the // index AddResult — even a hash-collapse still WROTE a file the tool owns, and the // manifest dedups a repeat path itself (Phase R prune reconciles manifest vs index). session.owned().add(res.sample.relativePath); // S9: a capture add changes what a live instance could play (a new sample landed in the // active bank) -> bump before the persist so the stamped generation refreshes instances. session.bumpBankGeneration(); session.saveToActiveProject(); // persist book + manifest + generation + MarkProjectDirty (travels with .rpp) } // Advance any in-flight realtime capture one tick. Cheap when none is running (a // null check) and fast even mid-record (tick() only reads the transport until the // terminal tick). Detects a project switch mid-capture and aborts+restores so the // capture never leaks across projects. Called from OnTimer BEFORE session.poll() so // poll's project-switch handling sees a cleaned-up project. void DriveRealtimeCapture(ReaSamplerSession& session) { if (!g_rtCapture) return; // Project switch guard: if the active project is no longer the one the capture // belongs to, a new/other project became active mid-record — abort + restore // (into the ORIGINAL project the state is bound to) and drop it. Do NOT finalize // into the new project. ReaProject* active = EnumProjects(-1, nullptr, 0); if (active != g_rtCaptureProject) { RealtimeTickResult r = g_rtBackend.abort(*g_rtCapture); // Only commit if the ORIGINAL project is still open and active would be it — // on a switch we restored into the original but must not persist into the // now-active foreign project. Log the outcome without persisting. On a Failed // abort surface abort()'s own message — it distinguishes a clean tab-switch // abort from the closed-project DROP (the captured project was closed mid-record, // review §1: nothing restored because the pointers were already freed). if (r.status == RealtimeTickStatus::Done) ShowConsoleMsg("ReaSampler realtime capture: project switched mid-record -- " "captured audio restored into the original project; not " "persisted to avoid crossing projects.\n"); else ShowConsoleMsg(("ReaSampler realtime capture: project changed mid-record -- " + r.result.message + "\n").c_str()); g_rtCapture.reset(); g_rtCaptureProject = nullptr; return; } RealtimeTickResult r = g_rtBackend.tick(*g_rtCapture); if (r.status == RealtimeTickStatus::InProgress) return; // Terminal (Done or Failed): commit/log and drop the in-flight state. CommitRealtimeResult(session, r.result); g_rtCapture.reset(); g_rtCaptureProject = nullptr; } void AbortRealtimeCaptureForUnload(ReaSamplerSession& session) { if (!g_rtCapture) return; RealtimeTickResult r = g_rtBackend.abort(*g_rtCapture); CommitRealtimeResult(session, r.result); g_rtCapture.reset(); g_rtCaptureProject = nullptr; } } // namespace reasampler::capture