Q-W6: registration table (OCP) in main.cpp; bank verbs -> shell/bank_ops(Session&); persist.h + wav_trim + namespaces.h shims deleted; 61/61

capture.h realtime seam split to capture_realtime_shell.h; GetProjExtState grow-loop rehomed to core/wire/ext_state_read; stale persist.cpp/bank_panel.cpp comment refs fixed; CLAUDE.md persist/bank_book/actions bullets updated. Command-id suffixes, display phrases, and undo labels byte-identical.
This commit is contained in:
2026-07-29 13:40:09 -04:00
parent 4831e0e172
commit f3be4d8cce
81 changed files with 970 additions and 1085 deletions
+5 -111
View File
@@ -1,16 +1,16 @@
#pragma once
// capture — the REAPER-facing capture shell (CLAUDE.md §load-bearing split).
//
// This header declares the capture *seam* the later milestones fill:
// * CaptureRequest — everything a capture needs, source-mode-agnostic.
// This header declares the SHARED capture seam (Q-W6 split of the former fat
// header — the realtime backend's async begin/tick/abort surface now lives in
// capture_realtime_shell.h):
// * CaptureRequest / CaptureResult — everything a capture needs and yields,
// source-mode-agnostic; the types BOTH backends speak.
// * OfflineRenderBackend — the deterministic default; a plain CONCRETE class
// (the former ICaptureBackend interface was deleted in
// Q-W3, T4-26 — it had one deriver and zero polymorphic
// call sites; every construction site instantiates the
// concrete type).
// * RealtimeRecordBackend — the ASYNC realtime seam (begin/tick/abort), driven
// across timer ticks; a genuinely different lifecycle
// (see the SEAM CHOICE note at its declaration).
// * makeUniqueTag / stampCaptureSample — the shared file-tag mint and the shared
// finished-capture metadata stamp both backends call
// (Q-W3 riders T1-11 / T2-09).
@@ -20,7 +20,6 @@
// REAPER-free lets callers (the capture orchestration TUs) depend on the seam
// without dragging the SDK into every include site.
#include <memory>
#include <string>
#include <vector>
@@ -156,109 +155,4 @@ void stampCaptureSample(Sample& s, const CaptureRequest& req,
ReaProject* rateProj, ReaProject* timeSigProj,
const std::string& absolutePath);
// --- Realtime-record backend: the ASYNC seam ---------------------------------
//
// A realtime record is inherently asynchronous: CSurf_OnRecord starts the transport
// on REAPER's audio thread and returns immediately — it does NOT block until the
// range completes, which takes (end - start) wall-clock seconds. Blocking the main
// thread for that duration freezes REAPER's UI, so the realtime backend is DRIVEN
// ACROSS TIMER TICKS instead: begin() starts and returns at once; tick() (called
// from the same OnTimer that runs session.poll()) advances the in-flight record and
// reports when it is done.
//
// SEAM CHOICE (surfaced): the two backends deliberately share NO interface. The
// lifecycles are genuinely different (offline is headless + immediate — one
// synchronous capture() call returns a finished Sample; realtime is
// transport-driven + async — begin/tick/abort across timer ticks), so a shared
// interface would make offline fake a lifecycle it does not have (its tick()
// would always be Done on the first call — dead code / an LSP smell). Offline
// stays synchronous; the realtime backend owns this small bespoke async seam,
// driven by exactly one caller (the timer-driven realtime_lifecycle). This is the
// split-sync/async fork, chosen over a unified async interface for that reason.
// (The old synchronous ICaptureBackend interface over OfflineRenderBackend was
// deleted in Q-W3 — T4-26: one deriver, zero polymorphic call sites.)
// One tick's verdict from the in-flight record.
enum class RealtimeTickStatus {
InProgress, // still recording — call tick() again next timer tick
Done, // finished (range end reached, or the user stopped) — `result` is set
Failed, // an error tore the capture down — `result.message` explains
};
struct RealtimeTickResult {
RealtimeTickStatus status = RealtimeTickStatus::InProgress;
CaptureResult result; // meaningful only when status == Done or Failed
};
// The opaque in-flight capture state. Owns the snapshot of everything to restore
// (temp track + its receive sends from the source tracks, other tracks' I_RECARM,
// transport, edit cursor, time selection) and the record's own project handle.
// Defined in capture_realtime_shell.cpp; the header stays REAPER-free (nothing is
// dereferenced here) by holding it behind a forward-declared type + unique_ptr.
//
// restore()/teardown is idempotent and lives ON THIS OBJECT (not a function-scope
// RAII guard) because the record spans ticks — no single stack frame outlives it.
// Every terminal path (normal completion, user stop, error, project switch, unload)
// funnels through the same single restore, safe to call once from whichever fires.
class RealtimeCaptureState;
// Out-of-line deleter so callers (realtime_lifecycle) can own a unique_ptr to the
// opaque RealtimeCaptureState WITHOUT its full (REAPER-typed) definition — the
// delete is compiled in capture_realtime_shell.cpp where the type is complete,
// keeping this header REAPER-free (load-bearing split).
struct RealtimeCaptureStateDeleter {
void operator()(RealtimeCaptureState* p) const noexcept;
};
using RealtimeCaptureHandle =
std::unique_ptr<RealtimeCaptureState, RealtimeCaptureStateDeleter>;
// Realtime-record backend — captures by RECORDING in realtime (transport-driven)
// into a hidden temp track, then moves the recorded file into the bank as a Sample.
// For sources offline render cannot do (hardware, performed FX) and as the true
// pre-FX-dry path (I_RECMODE_FLAGS &3==1 — the only pre-FX tap in the SDK; offline
// render has none). Dialog-free: never invokes the offline-render progress window.
//
// Non-bit-identical by nature (it is realtime); offline stays the deterministic
// default. Non-destructive across EVERY terminal path — the review gate — which is
// harder here than offline because the record spans ticks: the snapshot + restore
// live on RealtimeCaptureState, not a function-scope RAII destructor.
//
// SCOPE (this increment): TRACK scope only — records the selected track's OWN
// output (item + that track's own FX + its own fader/pan, PRE-parent), matching
// offline's track scope. This needs NO FxBypassGuard: a send tapping a track's
// output is naturally PRE-parent (the parent has not summed it yet), so the tap is
// chain-independent by construction. Item realtime is deferred (UnsupportedMode).
class RealtimeRecordBackend {
public:
// Starts a realtime record: validates the request (track scope, non-empty range,
// at least one source track, active + saved project, transport idle), snapshots
// all state to restore, creates the hidden temp track, routes a send FROM each
// source track INTO the temp track, arms, and CSurf_OnRecord — then returns
// IMMEDIATELY (no wait, no UI block). `sourceTracks` are the selected tracks to
// tap (resolved by the action layer — the CaptureRequest itself stays REAPER-free,
// carrying only the provenance GUIDs). On success the returned unique_ptr owns the
// in-flight state; drive it with tick(). On a validation/setup failure returns
// nullptr and fills `outFailure` with the CaptureStatus + message (nothing was
// left mutated — begin() restores on its own failure paths).
RealtimeCaptureHandle begin(const CaptureRequest& request,
const std::vector<MediaTrack*>& sourceTracks,
CaptureResult& outFailure);
// Advances the in-flight record one tick. Reads the transport (bound to the
// record's OWN project handle so a project switch cannot confuse it), and on a
// terminal verdict stops the transport, finalizes the recorded file into the
// bank Sample (Done) or reports the failure (Failed), then restores ALL
// snapshotted state. Returns InProgress while the record is still running.
// After Done/Failed the state is spent — the caller drops the unique_ptr.
RealtimeTickResult tick(RealtimeCaptureState& state);
// Force-terminate an in-flight record NOW without waiting for the range end:
// stops the transport, finalizes whatever was captured (best effort) or abandons
// it, and restores ALL snapshotted state. For the shutdown / project-switch
// paths (extension unload, a new project became active) where the record must
// not leak a temp track / armed track / altered transport into the user's
// project. Idempotent — safe even if a prior tick already tore the state down.
RealtimeTickResult abort(RealtimeCaptureState& state);
};
} // namespace reasampler::capture
+1 -1
View File
@@ -19,7 +19,7 @@
#include "core/capture/batch_capture.h" // planCaptureUnits / BatchOutcome
#include "core/model/bank_book.h" // BankBook / Bank
#include "core/model/provenance.h" // recipe parse/build, fingerprint
#include "persist.h" // ReaSamplerSession
#include "shell/persist/session.h" // ReaSamplerSession
#include "shell/capture/capture_orchestrator.h" // captureAndIndexOne / renderOffline
#include "shell/capture/provenance_shell.h" // fxChainIdentity* / trackByGuid
#include "shell/capture/scope_resolve.h" // ResolvedSource
+1 -1
View File
@@ -17,7 +17,7 @@
#include "core/capture/tail_control.h" // TailSetting
#include "core/model/provenance.h" // model::Provenance
#include "ingest.h" // ingestAssignActiveInstance
#include "persist.h" // ReaSamplerSession
#include "shell/persist/session.h" // ReaSamplerSession
#include "shell/capture/insert.h" // runInsert / InsertRequest
#include "shell/capture/realtime_lifecycle.h" // the in-flight realtime state
+1 -1
View File
@@ -74,7 +74,7 @@
// Item realtime is deferred (UnsupportedMode): item scope would need per-item take
// isolation on top of the tap, which is a separate increment.
#include "shell/capture/capture.h"
#include "shell/capture/capture_realtime_shell.h"
#include <chrono>
#include <cstdint>
+121
View File
@@ -0,0 +1,121 @@
#pragma once
// capture_realtime_shell — the ASYNC realtime-record seam (Q-W6 split of the former
// fat capture.h: this header owns the realtime backend's begin/tick/abort surface;
// capture.h keeps the shared CaptureRequest/CaptureResult types, the offline
// backend, and the shared backend helpers). Implemented by
// capture_realtime_shell.cpp; driven by exactly one caller (realtime_lifecycle).
//
// A realtime record is inherently asynchronous: CSurf_OnRecord starts the transport
// on REAPER's audio thread and returns immediately — it does NOT block until the
// range completes, which takes (end - start) wall-clock seconds. Blocking the main
// thread for that duration freezes REAPER's UI, so the realtime backend is DRIVEN
// ACROSS TIMER TICKS instead: begin() starts and returns at once; tick() (called
// from the same OnTimer that runs session.poll()) advances the in-flight record and
// reports when it is done.
//
// SEAM CHOICE (surfaced): the two backends deliberately share NO interface. The
// lifecycles are genuinely different (offline is headless + immediate — one
// synchronous capture() call returns a finished Sample; realtime is
// transport-driven + async — begin/tick/abort across timer ticks), so a shared
// interface would make offline fake a lifecycle it does not have (its tick()
// would always be Done on the first call — dead code / an LSP smell). Offline
// stays synchronous; the realtime backend owns this small bespoke async seam.
// This is the split-sync/async fork, chosen over a unified async interface for
// that reason. (The old synchronous ICaptureBackend interface over
// OfflineRenderBackend was deleted in Q-W3 — T4-26: one deriver, zero polymorphic
// call sites.)
//
// REAPER-free like capture.h: MediaTrack is forward-declared there and never
// dereferenced here; the REAPER-facing TU is capture_realtime_shell.cpp.
#include <memory>
#include <vector>
#include "shell/capture/capture.h" // CaptureRequest / CaptureResult / MediaTrack fwd
namespace reasampler::capture {
// One tick's verdict from the in-flight record.
enum class RealtimeTickStatus {
InProgress, // still recording — call tick() again next timer tick
Done, // finished (range end reached, or the user stopped) — `result` is set
Failed, // an error tore the capture down — `result.message` explains
};
struct RealtimeTickResult {
RealtimeTickStatus status = RealtimeTickStatus::InProgress;
CaptureResult result; // meaningful only when status == Done or Failed
};
// The opaque in-flight capture state. Owns the snapshot of everything to restore
// (temp track + its receive sends from the source tracks, other tracks' I_RECARM,
// transport, edit cursor, time selection) and the record's own project handle.
// Defined in capture_realtime_shell.cpp; the header stays REAPER-free (nothing is
// dereferenced here) by holding it behind a forward-declared type + unique_ptr.
//
// restore()/teardown is idempotent and lives ON THIS OBJECT (not a function-scope
// RAII guard) because the record spans ticks — no single stack frame outlives it.
// Every terminal path (normal completion, user stop, error, project switch, unload)
// funnels through the same single restore, safe to call once from whichever fires.
class RealtimeCaptureState;
// Out-of-line deleter so callers (realtime_lifecycle) can own a unique_ptr to the
// opaque RealtimeCaptureState WITHOUT its full (REAPER-typed) definition — the
// delete is compiled in capture_realtime_shell.cpp where the type is complete,
// keeping this header REAPER-free (load-bearing split).
struct RealtimeCaptureStateDeleter {
void operator()(RealtimeCaptureState* p) const noexcept;
};
using RealtimeCaptureHandle =
std::unique_ptr<RealtimeCaptureState, RealtimeCaptureStateDeleter>;
// Realtime-record backend — captures by RECORDING in realtime (transport-driven)
// into a hidden temp track, then moves the recorded file into the bank as a Sample.
// For sources offline render cannot do (hardware, performed FX) and as the true
// pre-FX-dry path (I_RECMODE_FLAGS &3==1 — the only pre-FX tap in the SDK; offline
// render has none). Dialog-free: never invokes the offline-render progress window.
//
// Non-bit-identical by nature (it is realtime); offline stays the deterministic
// default. Non-destructive across EVERY terminal path — the review gate — which is
// harder here than offline because the record spans ticks: the snapshot + restore
// live on RealtimeCaptureState, not a function-scope RAII destructor.
//
// SCOPE (this increment): TRACK scope only — records the selected track's OWN
// output (item + that track's own FX + its own fader/pan, PRE-parent), matching
// offline's track scope. This needs NO FxBypassGuard: a send tapping a track's
// output is naturally PRE-parent (the parent has not summed it yet), so the tap is
// chain-independent by construction. Item realtime is deferred (UnsupportedMode).
class RealtimeRecordBackend {
public:
// Starts a realtime record: validates the request (track scope, non-empty range,
// at least one source track, active + saved project, transport idle), snapshots
// all state to restore, creates the hidden temp track, routes a send FROM each
// source track INTO the temp track, arms, and CSurf_OnRecord — then returns
// IMMEDIATELY (no wait, no UI block). `sourceTracks` are the selected tracks to
// tap (resolved by the action layer — the CaptureRequest itself stays REAPER-free,
// carrying only the provenance GUIDs). On success the returned unique_ptr owns the
// in-flight state; drive it with tick(). On a validation/setup failure returns
// nullptr and fills `outFailure` with the CaptureStatus + message (nothing was
// left mutated — begin() restores on its own failure paths).
RealtimeCaptureHandle begin(const CaptureRequest& request,
const std::vector<MediaTrack*>& sourceTracks,
CaptureResult& outFailure);
// Advances the in-flight record one tick. Reads the transport (bound to the
// record's OWN project handle so a project switch cannot confuse it), and on a
// terminal verdict stops the transport, finalizes the recorded file into the
// bank Sample (Done) or reports the failure (Failed), then restores ALL
// snapshotted state. Returns InProgress while the record is still running.
// After Done/Failed the state is spent — the caller drops the unique_ptr.
RealtimeTickResult tick(RealtimeCaptureState& state);
// Force-terminate an in-flight record NOW without waiting for the range end:
// stops the transport, finalizes whatever was captured (best effort) or abandons
// it, and restores ALL snapshotted state. For the shutdown / project-switch
// paths (extension unload, a new project became active) where the record must
// not leak a temp track / armed track / altered transport into the user's
// project. Idempotent — safe even if a prior tick already tore the state down.
RealtimeTickResult abort(RealtimeCaptureState& state);
};
} // namespace reasampler::capture
+8 -3
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// insert.cpp — REAPER-facing placement shell (M6). See insert.h.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
@@ -40,7 +39,7 @@
#include "core/model/bank_model.h"
#include "shell/panel/panel_bank_ops.h" // bankPanelSelectedSampleIds / SourceBankId
#include "core/capture/capture_paths.h"
#include "persist.h"
#include "shell/persist/session.h"
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_CountSelectedTracks
@@ -58,13 +57,19 @@
namespace reasampler {
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
using capture::computeInsertMode;
using capture::normalizeSlashes;
using capture::resolveBankFile;
using capture::TempoConform;
namespace {
namespace fs = std::filesystem;
// The current project's directory (mirrors bank_panel/capture/persist). The bank
// index stores relative paths; resolving a bank file needs the current .rpp dir.
// FOLLOW-UP (already noted in bank_panel.cpp): a shared "current project dir"
// FOLLOW-UP (already noted in panel_bank_ops.cpp): a shared "current project dir"
// REAPER helper is a clean small refactor now that a fourth consumer exists — out
// of scope for M6.
std::string currentProjectDir() {
+1 -2
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// insert — placement of bank samples into the arrange (M6). REAPER-facing shell:
// it reads the bank_panel's current selection, resolves each selected sample's
// file, and drops it into the arrange at the edit cursor via InsertMedia, wrapped
@@ -28,7 +27,7 @@ class ReaSamplerSession;
// tempo-conform choice) so the two action variants (native-length vs
// conform-to-tempo) differ only by this struct — no divergent code paths.
struct InsertRequest {
InsertOptions options; // defaults: current track, no conform, native length
capture::InsertOptions options; // defaults: current track, no conform, native length
};
// The outcome of an insert action, for the caller to log to the console.
-1
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// item_read.cpp — the single MediaItem* read seam (GUID + fixed-lane name). See
// item_read.h. Compiled into the reaper_reasampler MODULE; includes
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT (main.cpp is the one TU that
-1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// item_read — the ONE place a MediaItem* is read for its canonical GUID string and for
// the durable P_LANENAME of the fixed lane it sits on. Before this seam, view.cpp and
// bank_panel.cpp each carried a near-identical private itemGuid / itemLaneName pair
+4 -1
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// provenance_shell.cpp — the REAPER reads behind Milestone 10. See provenance_shell.h.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
@@ -52,6 +51,10 @@
namespace reasampler {
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
using capture::normalizeSlashes;
using capture::resolveBankFile;
std::string fxChainIdentityForTrack(MediaTrack* tr) {
if (!tr) return fxChainIdentity({});
std::vector<FxIdentityEntry> rows;
+3 -1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// provenance_shell — the REAPER-facing reads Milestone 10 needs, in one place.
//
// The PURE provenance module (provenance.h) owns the fingerprint encoding, the
@@ -29,6 +28,9 @@ namespace reasampler {
class BankBook;
// Real-namespace-home using-declaration (Q-W6: the namespaces.h shim is retired).
using model::BankFileRef;
// The in-scope FX-chain identity of a source track (Track scope), folded to the
// pure provenance string. Reads the track's own FX chain via TrackFX_GetCount /
// TrackFX_GetFXName / TrackFX_GetFXGUID / TrackFX_GetEnabled in chain order.
+1 -1
View File
@@ -8,7 +8,7 @@
#include "shell/capture/realtime_lifecycle.h"
#include "persist.h" // ReaSamplerSession
#include "shell/persist/session.h" // ReaSamplerSession
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_EnumProjects
+1 -1
View File
@@ -16,7 +16,7 @@
// REAPER-facing: the .cpp includes reaper_plugin_functions.h WITHOUT
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers — CLAUDE.md §contract).
#include "shell/capture/capture.h" // RealtimeRecordBackend / RealtimeCaptureHandle
#include "shell/capture/capture_realtime_shell.h" // RealtimeRecordBackend / RealtimeCaptureHandle
namespace reasampler {
class ReaSamplerSession;
-1
View File
@@ -1,4 +1,3 @@
#include "core/namespaces.h"
// track_guid.cpp — the single MediaTrack* -> canonical GUID key formatter. See
// track_guid.h. Compiled into the reaper_reasampler MODULE; includes
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT (main.cpp is the one TU
-1
View File
@@ -1,5 +1,4 @@
#pragma once
#include "core/namespaces.h"
// track_guid — the ONE place a MediaTrack* is formatted into the canonical GUID
// string used as a membership-index key. Both the Design View shell (view.cpp) and
// the actions layer (design_view_actions.cpp) key membership on this exact string, so the key