capture: render a ranged item capture time-bounded — the selected-items source can't narrow a window, only a full-extent one uses it
sourceModeForScope now takes the item extent vs. the requested window. Full-extent item captures and the batch keep the old path unchanged.
This commit is contained in:
@@ -20,6 +20,11 @@ relative-paths-only) and the load-bearing capture/placement separation — see r
|
||||
`CLAUDE.md` §Precision invariants and §The load-bearing principle. Shell-specific
|
||||
detail not covered there:
|
||||
|
||||
- **The item scope's render source is window-dependent.** `ResolveScopeSource` is
|
||||
where that is decided — it measures the selected items' extent against the
|
||||
resolved range (`core/capture/render_window`) and hands the answer to
|
||||
`sourceModeForScope` on `ResolvedSource`. Why, in
|
||||
`src/core/capture/CLAUDE.md`.
|
||||
- **FX-bypass guard ordering.** `scope_resolve` reads the M10 provenance-assembly
|
||||
inputs (track/item selection, FX-chain identity) BEFORE the FX-bypass guard
|
||||
neutralizes the in-scope chain — provenance must see the chain as it really is,
|
||||
@@ -36,6 +41,7 @@ detail not covered there:
|
||||
|
||||
- `capture` — two CONCRETE backends with deliberately different lifecycles (no shared interface — the former `ICaptureBackend` was deleted in Q-W3, T4-26: one deriver, zero polymorphic call sites): `OfflineRenderBackend` (deterministic default, synchronous) and `RealtimeRecordBackend` (async begin/tick/abort). Input: `CaptureRequest`. Output: finished file + populated `Sample` handed to `bank_model`.
|
||||
- `scope_resolve` (`shell/capture`) — scope/source resolution shared by every capture entry point (Q-W3 hoist out of `main.cpp`): razor-else-time range inference, selected-track/selected-item-owning-track collection with canonical GUIDs, and the M10 provenance-assembly inputs (read BEFORE the FX-bypass guard neutralizes the in-scope chain).
|
||||
- `render_selection` (`shell/capture`) — the transient track selection a selected-tracks render (`&128`) requires, as a stack RAII guard: REAPER prints whatever tracks are selected, so `renderOffline` makes the request's own tracks BE the selection for the render's duration and restores the user's set on every exit path. Engaged only for that source mode; a ranged item capture and re-capture-from-source both name tracks the user has not selected.
|
||||
- `capture_orchestrator` (`shell/capture`) — single-capture orchestration + the realtime/insert action bodies (Q-W3 hoist, T4-02): `renderOffline` (one offline render under the scope's FX-bypass guard), `captureAndIndexOne` (render + provenance stamp + bank add + tracking-ledger record, unpersisted), `RunCapture`/`RunCaptureItemAssign`, `RunCaptureRealtimeTrack`/`RunCancelRealtime` (the realtime action bodies — the in-flight state lives in `realtime_lifecycle`), and `RunInsertSelected` (the ONE deliberate exception to capture-never-places).
|
||||
- `bake_land` (`shell/capture`) — the EXTENSION's half of the resample chain: scans every open project tab for pending `rsbake_*` requests, lands the ones belonging to the project this session has loaded, and refuses the rest with `WrongProject` — one undo point for the batch, each answered over its own key inside the invoking instance's synchronous action call. It RENDERS NOTHING — the instrument already did, through its own engine in its own process, which is what makes the baked audio the sound the user approved and what keeps the voice engine out of the extension's link graph. Replace-vs-add comes from `tracking::resampleLanding`; a replace keeps the entry's id and slot and never deletes the superseded file. Hash-dedup applies on the add path only, before the disk write, matching `updateSampleInPlace`'s "an in-place refresh is not an insert". A refused index withdraws the bytes this call had just written — the self-cleanup carve-out from prune's deletion authority, stated in `prune_fs.cpp`'s header.
|
||||
- `capture_batch` (`shell/capture`) — the batch-capture family + re-capture-from-source (Q-W3 hoist, T4-02): `RunBatchCaptureItems` (one sample per selected item), `RunBatchCaptureRazor` (one sample per razor area), `RunRecaptureFromSource` (regenerate a provenanced sample from its recorded source's current state, bank-only). Every unit routes through `capture_orchestrator` so every precision invariant holds; persist is batched to one ext-state write per action.
|
||||
|
||||
@@ -214,6 +214,11 @@ void RunBatchCaptureItems(ReaSamplerSession& session)
|
||||
src.startSeconds = unit.startSeconds;
|
||||
src.endSeconds = unit.endSeconds;
|
||||
src.sourceTracks.push_back(u.track);
|
||||
// The unit's range IS this item's own extent (read above from
|
||||
// D_POSITION/D_LENGTH) and it is the only selected item, so the
|
||||
// selected-items render source prints exactly it — batch keeps the
|
||||
// one-sample-per-item-at-item-extent semantics, unchanged.
|
||||
src.itemExtentIsWindow = true;
|
||||
if (std::string g = guidString(u.track); !g.empty())
|
||||
src.trackGuids.push_back(std::move(g));
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "shell/persist/session.h" // ReaSamplerSession
|
||||
#include "shell/capture/insert.h" // runInsert / InsertRequest
|
||||
#include "shell/capture/realtime_lifecycle.h" // the in-flight realtime state
|
||||
#include "shell/capture/render_selection.h" // RenderTrackSelection
|
||||
|
||||
#include "reaper_plugin.h" // UNDO_STATE_MISCCFG
|
||||
|
||||
@@ -181,6 +182,14 @@ CaptureResult renderOffline(CaptureScope scope,
|
||||
const CaptureRequest& req)
|
||||
{
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
// A selected-tracks render prints the DAW's track selection, so the request's
|
||||
// own tracks must BE that selection for the render — the plain track scope
|
||||
// already resolved them from the live selection (identity), but a ranged item
|
||||
// capture and a re-capture-from-source both name tracks the user has not
|
||||
// selected. Both guards outlive the render call and restore on every path.
|
||||
std::optional<RenderTrackSelection> selection;
|
||||
if (req.sourceMode == SourceMode::SelectedTracks)
|
||||
selection.emplace(sourceTracks);
|
||||
FxBypassGuard fxGuard(scope, sourceTracks, proj);
|
||||
OfflineRenderBackend backend;
|
||||
return backend.capture(req);
|
||||
@@ -217,7 +226,7 @@ CaptureResult captureAndIndexOne(ReaSamplerSession& session,
|
||||
const TailSetting tail = bankPanelTailSetting();
|
||||
|
||||
CaptureRequest req;
|
||||
req.sourceMode = sourceModeForScope(scope);
|
||||
req.sourceMode = sourceModeForScope(scope, src.itemExtentIsWindow);
|
||||
req.startSeconds = startSeconds; // exact bounds — no rounding
|
||||
req.endSeconds = endSeconds;
|
||||
req.wetDry = 1.0; // wet post the FX left enabled by the scope
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// render_selection.cpp — 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/render_selection.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_CountSelectedTracks
|
||||
#define REAPERAPI_WANT_GetSelectedTrack
|
||||
#define REAPERAPI_WANT_SetTrackSelected
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler::capture {
|
||||
|
||||
namespace {
|
||||
|
||||
// Deselect every track in GetTrack's index space (which excludes the master, SDK
|
||||
// ~2634), then select exactly `tracks` — so the resulting selection is the set,
|
||||
// not the set unioned with whatever was already selected.
|
||||
void selectOnly(const std::vector<MediaTrack*>& tracks)
|
||||
{
|
||||
const int total = CountTracks(nullptr);
|
||||
for (int i = 0; i < total; ++i)
|
||||
if (MediaTrack* tr = GetTrack(nullptr, i))
|
||||
SetTrackSelected(tr, false);
|
||||
for (MediaTrack* tr : tracks)
|
||||
if (tr) SetTrackSelected(tr, true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RenderTrackSelection::RenderTrackSelection(const std::vector<MediaTrack*>& tracks)
|
||||
{
|
||||
if (tracks.empty()) return;
|
||||
|
||||
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
||||
for (int i = 0; i < n; ++i)
|
||||
if (MediaTrack* tr = GetSelectedTrack(nullptr, i))
|
||||
original_.push_back(tr);
|
||||
|
||||
selectOnly(tracks);
|
||||
applied_ = true;
|
||||
}
|
||||
|
||||
RenderTrackSelection::~RenderTrackSelection()
|
||||
{
|
||||
if (applied_) selectOnly(original_);
|
||||
}
|
||||
|
||||
} // namespace reasampler::capture
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
// The transient DAW track selection a selected-tracks render requires. REAPER's
|
||||
// &128 source prints whatever tracks are selected, not the tracks the request
|
||||
// names, so the render owns the selection for its duration and hands it back.
|
||||
// The .cpp includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT.
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "shell/capture/capture.h" // MediaTrack fwd
|
||||
|
||||
namespace reasampler::capture {
|
||||
|
||||
// RAII: selects exactly `tracks`, restores the project's original track selection
|
||||
// on every exit path (non-destructive — selection flags only, no restructuring).
|
||||
// Two callers need this and neither has the right selection standing: a ranged
|
||||
// item capture renders through the items' tracks while the user's track selection
|
||||
// is unrelated, and re-capture-from-source resolves its tracks by GUID and selects
|
||||
// nothing at all. An empty `tracks` is a deliberate no-op — forcing an empty
|
||||
// selection would render silence.
|
||||
class RenderTrackSelection
|
||||
{
|
||||
public:
|
||||
explicit RenderTrackSelection(const std::vector<MediaTrack*>& tracks);
|
||||
~RenderTrackSelection();
|
||||
|
||||
RenderTrackSelection(const RenderTrackSelection&) = delete;
|
||||
RenderTrackSelection& operator=(const RenderTrackSelection&) = delete;
|
||||
|
||||
private:
|
||||
std::vector<MediaTrack*> original_;
|
||||
bool applied_ = false;
|
||||
};
|
||||
|
||||
} // namespace reasampler::capture
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <filesystem> // project-dir derivation for provenance parent resolution
|
||||
#include <utility>
|
||||
|
||||
#include "core/capture/render_window.h" // itemExtentPrintsWindow
|
||||
#include "shell/capture/provenance_shell.h" // fxChainIdentity* / *SourceFiles / bankFileRefs
|
||||
#include "shell/capture/track_guid.h" // guidString
|
||||
|
||||
@@ -19,9 +20,11 @@
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetSetProjectInfo
|
||||
#define REAPERAPI_WANT_CountSelectedMediaItems
|
||||
#define REAPERAPI_WANT_GetSelectedMediaItem
|
||||
#define REAPERAPI_WANT_GetMediaItem_Track
|
||||
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
||||
#define REAPERAPI_WANT_CountSelectedTracks
|
||||
#define REAPERAPI_WANT_GetSelectedTrack
|
||||
#include "reaper_plugin_functions.h"
|
||||
@@ -50,14 +53,26 @@ model::ProvenanceScope provenanceScopeFor(CaptureScope scope)
|
||||
// Collects the tracks that own the selected items (Item scope) into
|
||||
// out.sourceTracks (deduped) — these are the tracks whose FX must be bypassed so an
|
||||
// item capture hears take/item FX only. GUIDs recorded for provenance.
|
||||
bool collectSelectedItemTracks(ResolvedSource& out)
|
||||
// extentStart/extentEnd come back as the union of the selected items' own extents:
|
||||
// the window REAPER's selected-items render source would print (SDK ~1990:
|
||||
// D_POSITION/D_LENGTH in seconds).
|
||||
bool collectSelectedItemTracks(ResolvedSource& out,
|
||||
double& extentStart, double& extentEnd)
|
||||
{
|
||||
const int n = CountSelectedMediaItems(nullptr);
|
||||
if (n <= 0) return false;
|
||||
bool anyExtent = false;
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
||||
if (!it) continue;
|
||||
const double pos = GetMediaItemInfo_Value(it, "D_POSITION");
|
||||
const double len = GetMediaItemInfo_Value(it, "D_LENGTH");
|
||||
if (!anyExtent) { extentStart = pos; extentEnd = pos + len; anyExtent = true; }
|
||||
else {
|
||||
if (pos < extentStart) extentStart = pos;
|
||||
if (pos + len > extentEnd) extentEnd = pos + len;
|
||||
}
|
||||
MediaTrack* tr = GetMediaItem_Track(it);
|
||||
if (!tr) continue;
|
||||
// Dedup: several selected items can share a track.
|
||||
@@ -71,6 +86,16 @@ bool collectSelectedItemTracks(ResolvedSource& out)
|
||||
return !out.sourceTracks.empty();
|
||||
}
|
||||
|
||||
// 0 when the project never pinned a rate (and nominal unless PROJECT_SRATE_USE is
|
||||
// set, SDK ~3064) — passed through as "unknown", which the pure window comparison
|
||||
// handles by falling back to exact equality.
|
||||
int projectSampleRate()
|
||||
{
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
if (!proj) return 0;
|
||||
return static_cast<int>(GetSetProjectInfo(proj, "PROJECT_SRATE", 0.0, false));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Reads every track's P_RAZOREDITS (SDK header ~2899: space-separated triples of
|
||||
@@ -128,10 +153,11 @@ bool collectSelectedTracks(ResolvedSource& out)
|
||||
|
||||
bool ResolveScopeSource(CaptureScope scope, ResolvedSource& out, std::string& why)
|
||||
{
|
||||
double itemExtentStart = 0.0, itemExtentEnd = 0.0;
|
||||
switch (scope)
|
||||
{
|
||||
case CaptureScope::Item:
|
||||
if (!collectSelectedItemTracks(out)) {
|
||||
if (!collectSelectedItemTracks(out, itemExtentStart, itemExtentEnd)) {
|
||||
why = "select at least one media item"; return false;
|
||||
}
|
||||
break;
|
||||
@@ -141,7 +167,13 @@ bool ResolveScopeSource(CaptureScope scope, ResolvedSource& out, std::string& wh
|
||||
}
|
||||
break;
|
||||
}
|
||||
return resolveRange(out.startSeconds, out.endSeconds, why);
|
||||
if (!resolveRange(out.startSeconds, out.endSeconds, why)) return false;
|
||||
|
||||
if (scope == CaptureScope::Item)
|
||||
out.itemExtentIsWindow = itemExtentPrintsWindow(
|
||||
out.startSeconds, out.endSeconds, itemExtentStart, itemExtentEnd,
|
||||
projectSampleRate());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Empty for an unsaved project (EnumProjects writes an empty .rpp path), which
|
||||
|
||||
@@ -34,6 +34,12 @@ struct ResolvedSource
|
||||
double endSeconds = 0.0;
|
||||
std::vector<MediaTrack*> sourceTracks;
|
||||
std::vector<std::string> trackGuids;
|
||||
|
||||
// Item scope only: does the selected items' own extent already print
|
||||
// [startSeconds, endSeconds)? Feeds sourceModeForScope. Defaults false so a
|
||||
// hand-built source fails closed to the time-bounded render — a caller whose
|
||||
// range IS the item extent (batch item capture) says so explicitly.
|
||||
bool itemExtentIsWindow = false;
|
||||
};
|
||||
|
||||
// Reads every track's P_RAZOREDITS and returns the union of parsed track-audio
|
||||
|
||||
Reference in New Issue
Block a user