// realtime_lifecycle.cpp — the in-flight realtime-capture state machine + globals. // 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. #include "shell/capture/realtime_lifecycle.h" #include "shell/persist/session.h" // ReaSamplerSession #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_EnumProjects #define REAPERAPI_WANT_GetProjectName #define REAPERAPI_WANT_ShowConsoleMsg #include "reaper_plugin_functions.h" namespace reasampler::capture { RealtimeRecordBackend g_rtBackend; RealtimeCaptureHandle g_rtCapture; ReaProject* g_rtCaptureProject = nullptr; // Commits a finished realtime capture (a Done tick/abort with an Ok result): adds // the Sample to the active bank, persists + MarkProjectDirty. Shared by the // tick-completion and 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); session.recordCreated(res.sample, tracking::OriginKind::Capture); // A capture add changes what a live instance could play, so bump the generation // before persisting to refresh instances. session.bumpBankGeneration(); session.saveToActiveProject(); // persist book + ledger + generation + MarkProjectDirty } // Advances any in-flight realtime capture one 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 own project-switch handling // sees an already-cleaned-up project. void DriveRealtimeCapture(ReaSamplerSession& session) { if (!g_rtCapture) return; // If the active project is no longer the one the capture belongs to, a project // switch happened mid-record: abort + restore into the original project the // state is bound to, and drop it — never finalize into the new project. ReaProject* active = EnumProjects(-1, nullptr, 0); if (active != g_rtCaptureProject) { RealtimeTickResult r = g_rtBackend.abort(*g_rtCapture); // Log without persisting — we restored into the original project but must // not persist into the now-active foreign one: the ledger and bank we would // have to write belong to the project we just left. A Failed abort surfaces // abort()'s own message, distinguishing a clean tab-switch abort from the // closed-project case (nothing restored because the pointers were already // freed). // // The abort already moved the recorded WAV into the ORIGINAL project's bank // folder, so it survives here untracked — deleting a user's just-recorded // audio is the destructive direction and is not this shell's call, so the // path is NAMED instead and the operator decides (docs/TODO.md). // // RealtimeTickStatus::Done implies CaptureStatus::Ok (RealtimeRecordBackend:: // abort only sets Done on that path) — the Ok branch below is therefore the // whole Done case; anything else is Failed and falls to the last branch. if (r.status == RealtimeTickStatus::Done && r.result.status == CaptureStatus::Ok) { // g_rtCaptureProject is still valid here (reset only below) — it names the // ORIGINAL project, not whatever is active now, which is the whole point: // the user has already switched away from it. char nameBuf[512] = {0}; GetProjectName(g_rtCaptureProject, nameBuf, sizeof(nameBuf)); const std::string projName = nameBuf[0] ? nameBuf : "(unsaved project)"; ShowConsoleMsg(("ReaSampler realtime capture: project switched mid-record -- " "captured audio restored into the original project (" + projName + "); not persisted to avoid crossing projects. The " "recorded file was left in that project's bank folder as '" + r.result.sample.relativePath + "', untracked. Re-importing it there does NOT adopt this file " "-- it copies the audio in under a new name -- so after " "re-importing, delete this untracked original by hand.\n") .c_str()); } 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