feat(capture): M11 batch capture - one bank sample per selected item / razor area

Pure batch_capture module (plan + mixed-result aggregation, unit-tested);
CAPTURE_BATCH_ITEMS/RAZOR actions with transient per-unit selection restored
on all paths, per-unit invariants + provenance, single persist.
Conform-on-insert already shipped.
This commit is contained in:
2026-07-26 19:00:00 -04:00
parent 83cd040f26
commit d23a3f9bb8
5 changed files with 740 additions and 50 deletions
+76
View File
@@ -0,0 +1,76 @@
// batch_capture.cpp — pure logic for M11 batch capture. See header.
// NO REAPER types; unit-tested by tests/test_batch_capture.cpp.
#include "batch_capture.h"
#include <algorithm>
namespace reasampler {
std::vector<CaptureUnit> planCaptureUnits(const std::vector<BatchRange>& ranges) {
std::vector<CaptureUnit> units;
units.reserve(ranges.size());
int ordinal = 0;
for (const BatchRange& r : ranges) {
// Drop empty/inverted ranges — the offline backend refuses end<=start too, so
// planning one would only manufacture a guaranteed per-unit failure. Ordinals
// count kept units so the reported numbering is contiguous.
if (!(r.endSeconds > r.startSeconds)) continue;
++ordinal;
units.push_back({ordinal, r.startSeconds, r.endSeconds});
}
return units;
}
void BatchOutcome::record(int ordinal, bool ok, std::string detail) {
results_.push_back({ordinal, ok, std::move(detail)});
}
std::size_t BatchOutcome::succeeded() const {
return static_cast<std::size_t>(
std::count_if(results_.begin(), results_.end(),
[](const BatchUnitResult& r) { return r.ok; }));
}
std::size_t BatchOutcome::failed() const {
return results_.size() - succeeded();
}
std::vector<BatchUnitResult> BatchOutcome::failures() const {
std::vector<BatchUnitResult> out;
for (const BatchUnitResult& r : results_)
if (!r.ok) out.push_back(r);
return out;
}
std::string BatchOutcome::summaryLine(const std::string& noun) const {
const std::size_t n = total();
const std::size_t ok = succeeded();
if (n == 0)
return "ReaSampler batch capture: nothing to capture.";
const std::string plural = (n == 1) ? noun : noun + "s";
if (ok == n)
return "ReaSampler batch capture: " + std::to_string(ok) + " " +
plural + " captured.";
// Mixed / all-failed: report the ratio and enumerate the failed ordinals so the
// user knows exactly which units to retry. No partial corruption is implied —
// each captured unit is a complete, independent bank sample.
std::string line = "ReaSampler batch capture: " + std::to_string(ok) + " of " +
std::to_string(n) + " " + plural + " captured (" +
std::to_string(n - ok) + " failed: ";
bool first = true;
for (const BatchUnitResult& f : results_) {
if (f.ok) continue;
if (!first) line += ", ";
line += "#" + std::to_string(f.ordinal);
first = false;
}
line += ").";
return line;
}
} // namespace reasampler
+100
View File
@@ -0,0 +1,100 @@
#pragma once
// batch_capture — the REAPER-free logic behind M11 batch capture (one action fires
// N captures: one bank sample per selected item / per razor area).
//
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
// vendor/ includes. Standard library only. The batch shell (main.cpp) reads the DAW
// state (selected items -> their exact bounds; every track's P_RAZOREDITS -> areas)
// and hands the raw ranges here so the genuinely-pure, easy-to-get-wrong pieces are
// unit-tested outside the DAW:
//
// 1. planCaptureUnits: an ordered list of (start,end) source ranges -> an ordered
// list of CaptureUnit, each carrying its 1-based ordinal and validated bounds.
// Empty/inverted ranges are DROPPED (mirrors the offline backend's own
// end>start guard) so a zero-length item/area never produces a stray render.
// Order is preserved: unit ordinals count only the KEPT units, so a batch of
// three valid items yields ordinals 1,2,3 regardless of dropped neighbors.
// 2. BatchOutcome: order-preserving aggregation of per-unit results into a summary
// (succeeded / failed counts + the ordered list of failures) so the shell can
// report a mixed result with one console line and no partial-corruption
// ambiguity. The AGGREGATION is pure; the render loop that feeds it is shell.
//
// Range is the ONLY thing that varies per unit here. FX scope (item vs track) is a
// per-ACTION constant the shell already owns (fxBypassPlanFor); it is not a
// per-unit field. Item-batch uses item scope; razor-batch uses track scope — the
// shell passes the scope straight through to each render, unchanged from the
// single-capture path.
#include <cstddef>
#include <string>
#include <vector>
namespace reasampler {
// One capture in a batch: an exact source range plus its 1-based ordinal within the
// KEPT set. The ordinal disambiguates per-unit file stems (the offline backend's
// unique tag is 1-second-granular, so a fast batch could otherwise collide N files
// onto one name) and labels a failure in the summary.
struct CaptureUnit {
int ordinal = 0; // 1-based, counts kept units only
double startSeconds = 0.0; // exact — no rounding
double endSeconds = 0.0;
};
// A source range handed in by the shell (a selected item's [pos, pos+len] or one
// razor area's [start, end]). Kept as a distinct type from CaptureUnit so the input
// (raw, possibly-invalid) and the output (validated, ordinal-assigned) do not share
// a shape by accident. Named BatchRange (not SourceRange) to avoid collision with
// bank_model's SourceRange, which carries PPQ fields this planner does not need.
struct BatchRange {
double startSeconds = 0.0;
double endSeconds = 0.0;
};
// Validates + orders a batch's source ranges into capture units. Preserves input
// order; DROPS every range with end <= start (empty/inverted) so no stray render is
// planned; assigns 1-based ordinals over the KEPT units. An empty input (no selected
// item / no razor area) yields an empty plan — the shell reports "nothing to batch"
// and writes nothing (the same no-op posture the single-capture path takes).
std::vector<CaptureUnit> planCaptureUnits(const std::vector<BatchRange>& ranges);
// The per-unit verdict the shell records after each render attempt, in unit order.
struct BatchUnitResult {
int ordinal = 0; // the CaptureUnit's ordinal this result is for
bool ok = false; // true iff the render + bank-add succeeded
std::string detail; // failure reason (empty on success) — for the summary
};
// Order-preserving aggregation of a batch's per-unit results. Built incrementally by
// the shell (record() after each unit) so a mid-batch failure is captured without
// aborting the remaining units (no partial corruption: each unit is independent, and
// the selection is restored on every exit path by the shell's RAII guard).
class BatchOutcome {
public:
// Records one unit's verdict. Order of calls IS the reported order.
void record(int ordinal, bool ok, std::string detail = {});
std::size_t total() const { return results_.size(); }
std::size_t succeeded() const;
std::size_t failed() const;
const std::vector<BatchUnitResult>& results() const { return results_; }
// The ordered subset of results that failed (ok == false). For the summary line.
std::vector<BatchUnitResult> failures() const;
// A single human summary line for the console (explicit-action response — allowed
// by the console policy; a batch-completion summary with failure counts qualifies,
// per-unit success spam does not). `noun` is the unit word ("item" / "razor area").
// Examples:
// all-success, 3 items : "ReaSampler batch capture: 3 items captured."
// partial, 3 of 5 : "ReaSampler batch capture: 3 of 5 items captured "
// "(2 failed: #2, #4)."
// empty plan : "ReaSampler batch capture: nothing to capture."
std::string summaryLine(const std::string& noun) const;
private:
std::vector<BatchUnitResult> results_;
};
} // namespace reasampler
+385 -49
View File
@@ -18,9 +18,11 @@
#include "reaper_plugin.h"
#include "reaper_plugin_functions.h"
#include <cstddef>
#include <deque>
#include <memory>
#include <string>
#include <utility>
#include <vector>
@@ -28,6 +30,7 @@
#include "app_version.h"
#include "bank_model.h"
#include "bank_panel.h"
#include "batch_capture.h"
#include "capture.h"
#include "insert.h"
#include "persist.h"
@@ -124,6 +127,14 @@ static int g_cmdToggleBankPanel = 0;
static int g_cmdInsertSelected = 0;
static int g_cmdInsertSelectedConform = 0;
// Command ids for the M11 batch-capture actions. NEW FOREVER-STABLE strings. One action
// fires N captures: CAPTURE_BATCH_ITEMS -> one bank sample per selected item (item scope);
// CAPTURE_BATCH_RAZOR -> one bank sample per razor area (track scope, each area's range).
// Each unit honors every precision invariant; the original selection is restored on every
// exit path. Bank-only, never places on the timeline (load-bearing principle).
static int g_cmdCaptureBatchItems = 0;
static int g_cmdCaptureBatchRazor = 0;
// Command id for the "capture selected track (realtime)" action. NEW FOREVER-STABLE
// string. Records the selected track's OWN output in realtime (transport-driven) into
// a hidden temp track via RealtimeRecordBackend, then moves the recorded file into the
@@ -714,13 +725,75 @@ static reasampler::CaptureResult renderOffline(
return backend.capture(req);
}
// Runs one capture-action-table row: resolve its scope source + range, snapshot &
// clear the out-of-scope FX AND neutralize their fader gain + pan chain (RAII),
// render via the offline backend, add the Sample to the bank, persist + mark dirty.
// The load-bearing principle holds structurally — this path writes a file + a bank
// index entry ONLY; it never calls InsertMedia or touches the arrange/timeline.
// Non-destructive: FX-enable + fader gain + pan/width/law/mode are fully restored
// on every path by FxBypassGuard, and the backend restores every RENDER_* setting.
// Renders ONE capture request under the scope's FX-bypass guard, stamps provenance,
// and adds the resulting Sample to the ACTIVE bank + records the created file in the
// owned-file manifest — WITHOUT persisting. The caller persists once (single-capture:
// right after; batch: once at the end) so a batch does not write ext state N times.
//
// Provenance is read from the LIVE selection here, so a batch that transiently
// selects exactly one item per unit gets per-unit-correct provenance. `src` supplies
// the source tracks (FX bypass + Sample GUIDs); `scope` drives the bypass plan and
// provenance scope. Returns the backend's CaptureResult (status + message) so the
// caller can report success/failure. Load-bearing principle holds: writes a file +
// a bank index entry ONLY; never touches the arrange/timeline. Non-destructive: the
// out-of-scope FX/fader/pan chain is fully restored on every path (FxBypassGuard),
// and the backend restores every RENDER_* setting.
static reasampler::CaptureResult captureAndIndexOne(
reasampler::CaptureScope scope,
const ResolvedSource& src,
const std::string& baseName,
double startSeconds,
double endSeconds)
{
// The tail mode is a PANEL SETTING (docked bank panel's toggle), not a per-action
// variant: the capture actions apply whatever the panel is set to. Default is None
// (exact bounds / byte-identical to today) until the user opts in via the toggle.
const reasampler::TailSetting tail = reasampler::bankPanelTailSetting();
reasampler::CaptureRequest req;
req.sourceMode = reasampler::sourceModeForScope(scope);
req.startSeconds = startSeconds; // exact bounds — no rounding
req.endSeconds = endSeconds;
req.wetDry = 1.0; // wet post the FX left enabled by the scope
req.tailMode = tail.mode; // None / Auto / Manual, from the panel toggle
req.tailMs = tail.manualMs; // Manual-only (clamped); ignored for None/Auto
req.sampleRate = 0; // follow project rate
req.channelCount = 2;
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
req.baseName = baseName;
req.trackGuids = src.trackGuids; // recorded on the Sample (provenance)
// M10: compute provenance BEFORE the FxBypassGuard neutralizes the in-scope chain —
// the source FX-chain identity must be read from the LIVE (un-bypassed) chain, and
// the source selection is still live here. Returns nullopt unless this capture
// genuinely resamples from a bank sample (detectParent). Read-only.
const std::optional<reasampler::Provenance> prov =
buildCaptureProvenance(req, scope, src);
// Render under the scope's FX-bypass guard (out-of-scope FX / fader / pan chain
// neutralized for the render, fully restored on every path). Writes a file only.
reasampler::CaptureResult res = renderOffline(scope, src.sourceTracks, req);
if (res.status != reasampler::CaptureStatus::Ok)
return res;
// Stamp provenance onto the captured Sample (only set when this was a genuine
// resample-from-sample; otherwise the optional stays empty, per M1's contract).
res.sample.provenance = prov;
// Add to the ACTIVE bank: g_session.bank() resolves to book.activeIndex() (B2).
g_session.bank().add(res.sample);
// B-cap: record the created file in the owned-file manifest, at the same point the
// Sample is added. 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 later).
g_session.owned().add(res.sample.relativePath);
return res;
}
// Runs one capture-action-table row: resolve its scope source + range, render + add +
// record via captureAndIndexOne, then persist + mark dirty. The load-bearing principle
// holds structurally — this path writes a file + a bank index entry ONLY; it never
// calls InsertMedia or touches the arrange/timeline.
static void RunCapture(const reasampler::CaptureActionDef& def)
{
ResolvedSource src;
@@ -731,54 +804,14 @@ static void RunCapture(const reasampler::CaptureActionDef& def)
return;
}
// The tail mode is a PANEL SETTING (docked bank panel's toggle), not a per-action
// variant: the plain capture actions apply whatever the panel is set to. Default
// is None (exact bounds / byte-identical to today) until the user opts in via the
// toggle. tailMs is meaningful only for Manual and is pre-clamped by the panel.
const reasampler::TailSetting tail = reasampler::bankPanelTailSetting();
reasampler::CaptureRequest req;
req.sourceMode = reasampler::sourceModeForScope(def.scope);
req.startSeconds = src.startSeconds; // exact bounds — no rounding
req.endSeconds = src.endSeconds;
req.wetDry = 1.0; // wet post the FX left enabled by the scope
req.tailMode = tail.mode; // None / Auto / Manual, from the panel toggle
req.tailMs = tail.manualMs; // Manual-only (clamped); ignored for None/Auto
req.sampleRate = 0; // follow project rate
req.channelCount = 2;
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
req.baseName = def.baseName;
req.trackGuids = src.trackGuids; // recorded on the Sample (provenance)
// M10: compute provenance BEFORE the FxBypassGuard neutralizes the in-scope chain —
// the source FX-chain identity must be read from the LIVE (un-bypassed) chain, and
// the source selection is still live here. Returns nullopt unless this capture
// genuinely resamples from a bank sample (detectParent). Read-only.
const std::optional<reasampler::Provenance> prov =
buildCaptureProvenance(req, def.scope, src);
// Render under the scope's FX-bypass guard (out-of-scope FX / fader / pan chain
// neutralized for the render, fully restored on every path — see renderOffline
// and the FxBypassGuard header comment). Non-destructive; writes a file only.
reasampler::CaptureResult res = renderOffline(def.scope, src.sourceTracks, req);
reasampler::CaptureResult res =
captureAndIndexOne(def.scope, src, def.baseName, src.startSeconds, src.endSeconds);
if (res.status != reasampler::CaptureStatus::Ok)
{
ShowConsoleMsg(("ReaSampler capture failed: " + res.message + "\n").c_str());
return;
}
// Stamp provenance onto the captured Sample (only set when this was a genuine
// resample-from-sample; otherwise the optional stays empty, per M1's contract).
res.sample.provenance = prov;
// Add to the ACTIVE bank: g_session.bank() resolves to book.activeIndex() (B2).
g_session.bank().add(res.sample);
// B-cap: record the created file 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 later).
g_session.owned().add(res.sample.relativePath);
// Persist the updated book AND manifest into the active project's ext state (the
// `banks` + `owned_files` keys) so the capture survives Save / close+reopen (M4) and
// travels with the .rpp. saveToActiveProject also clears the retired legacy key and
@@ -786,6 +819,271 @@ static void RunCapture(const reasampler::CaptureActionDef& def)
g_session.saveToActiveProject();
}
// --- M11: batch capture (per selected item / per razor area) ----------------
//
// One action fires N captures — one bank sample per selected item (item scope) or per
// razor area (track scope, each area's own range). Each individual capture honors every
// precision invariant via captureAndIndexOne (exact bounds, non-destructive FX/fader/pan
// neutralize, relative paths, channel preservation) and M10 provenance stamping applies
// per capture where its detection rule matches. The load-bearing principle holds: each
// unit writes a file + a bank index entry ONLY; nothing lands in the arrange.
//
// Per-unit FILE NAMING: the offline backend's unique tag is 1-second-granular. The
// per-item render already takes real wall-clock time (REAPER's offline-render dialog per
// unit), so consecutive units naturally land in distinct seconds; belt-and-braces, each
// unit's baseName also carries its ordinal ("item-1", "item-2", ...) so two units are
// never asked to write the same stem within one batch. (Residual, DAW-verify: two BATCHES
// fired within the same wall-clock second with identical ordinals could still collide —
// unreachable in practice given the per-unit render latency, noted for completeness.)
// RAII snapshot/restore of the project's media-item selection. Batch item capture must
// transiently select exactly one item per render (RENDER_SETTINGS &32 renders whatever is
// selected); the user's ORIGINAL selection must be restored on EVERY exit path — including
// a mid-batch failure or early return — because selection restoration is part of the
// non-destructive invariant. Snapshot on construct (the currently-selected item set),
// restore on destruct (deselect everything, then re-select exactly the snapshot).
class ItemSelectionGuard
{
public:
ItemSelectionGuard()
{
const int n = CountSelectedMediaItems(nullptr);
for (int i = 0; i < n; ++i)
if (MediaItem* it = GetSelectedMediaItem(nullptr, i))
selected_.push_back(it);
}
~ItemSelectionGuard()
{
// Deselect every item in the project, then re-select the snapshot — restoring the
// exact original set regardless of what the batch selected in between. Iterate ALL
// items (not just the currently-selected) so any transient selection is cleared.
const int total = CountMediaItems(nullptr);
for (int i = 0; i < total; ++i)
if (MediaItem* it = GetMediaItem(nullptr, i))
SetMediaItemSelected(it, false);
for (MediaItem* it : selected_)
SetMediaItemSelected(it, true);
UpdateArrange(); // reflect the restored selection in the arrange view
}
ItemSelectionGuard(const ItemSelectionGuard&) = delete;
ItemSelectionGuard& operator=(const ItemSelectionGuard&) = delete;
private:
std::vector<MediaItem*> selected_;
};
// Selects exactly `item` (deselect-all then select-one) so the offline render's
// selected-items bit (&32) captures a single item. Used inside the batch loop under the
// ItemSelectionGuard, which restores the user's original selection afterward.
static void selectOnlyItem(MediaItem* item)
{
const int total = CountMediaItems(nullptr);
for (int i = 0; i < total; ++i)
if (MediaItem* it = GetMediaItem(nullptr, i))
SetMediaItemSelected(it, it == item);
}
// Batch item capture: one bank sample per SELECTED item, item scope. Snapshots the
// selection (RAII restore on every path), then for each selected item transiently selects
// only it, renders its exact [pos, pos+len] range under item-scope FX neutralize, adds the
// Sample, and records a per-unit verdict. Persists ONCE at the end (one ext-state write for
// the whole batch). Reports a mixed-result summary (explicit-action response — allowed).
static void RunBatchCaptureItems()
{
// Read the selected items up front (pointers stay valid — batch mutates only selection
// flags, never adds/removes items). Also capture each item's exact bounds and owning
// track NOW, while the full selection is live, before any transient re-selection.
struct ItemUnit { MediaItem* item; MediaTrack* track; double start; double end; };
std::vector<ItemUnit> itemUnits;
{
const int n = CountSelectedMediaItems(nullptr);
for (int i = 0; i < n; ++i)
{
MediaItem* it = GetSelectedMediaItem(nullptr, i);
if (!it) continue;
MediaTrack* tr = GetMediaItem_Track(it);
if (!tr) continue;
const double pos = GetMediaItemInfo_Value(it, "D_POSITION");
const double len = GetMediaItemInfo_Value(it, "D_LENGTH");
itemUnits.push_back({it, tr, pos, pos + len});
}
}
if (itemUnits.empty())
{
ShowConsoleMsg("ReaSampler batch capture: select at least one media item.\n");
return;
}
// Plan the exact source ranges -> validated, ordinal-assigned units (pure). Empty/
// inverted item ranges (a zero-length item) are dropped here so no stray render runs.
std::vector<reasampler::BatchRange> ranges;
ranges.reserve(itemUnits.size());
for (const ItemUnit& u : itemUnits)
ranges.push_back({u.start, u.end});
const std::vector<reasampler::CaptureUnit> plan = reasampler::planCaptureUnits(ranges);
reasampler::BatchOutcome outcome;
bool anyAdded = false;
{
// Restore the user's ORIGINAL item selection on every exit path (incl. early
// return / mid-batch failure) — non-destructive invariant.
ItemSelectionGuard selGuard;
// The plan and itemUnits are parallel over the KEPT units. Walk itemUnits, but only
// for those whose range survived planning (same drop rule), matching by ordinal.
std::size_t planIdx = 0;
for (const ItemUnit& u : itemUnits)
{
if (!(u.end > u.start)) continue; // dropped by planCaptureUnits — skip in lockstep
const reasampler::CaptureUnit& unit = plan[planIdx++];
// Transiently select ONLY this item so the item-scope render captures exactly it.
selectOnlyItem(u.item);
ResolvedSource src;
src.startSeconds = unit.startSeconds;
src.endSeconds = unit.endSeconds;
src.sourceTracks.push_back(u.track);
if (std::string g = reasampler::guidString(u.track); !g.empty())
src.trackGuids.push_back(std::move(g));
const std::string baseName = "item-" + std::to_string(unit.ordinal);
reasampler::CaptureResult res = captureAndIndexOne(
reasampler::CaptureScope::Item, src, baseName,
unit.startSeconds, unit.endSeconds);
const bool ok = (res.status == reasampler::CaptureStatus::Ok);
outcome.record(unit.ordinal, ok, ok ? std::string{} : res.message);
if (ok) anyAdded = true;
}
} // selGuard restores the original selection here, on every path
// Persist ONCE for the whole batch (one ext-state write) — only if something landed.
if (anyAdded)
g_session.saveToActiveProject();
ShowConsoleMsg((outcome.summaryLine("item") + "\n").c_str());
}
// Collects every track's razor AUDIO areas as (owning track, range) pairs, preserving
// track order then area order — the batch analog of resolveRazorRange, which unions them.
// Read-only (never clears the razor selection). Reuses the pure parseRazorEdits parser.
static std::vector<std::pair<MediaTrack*, reasampler::RazorRange>> collectRazorAreas()
{
std::vector<std::pair<MediaTrack*, reasampler::RazorRange>> areas;
const int n = CountTracks(nullptr);
for (int i = 0; i < n; ++i)
{
MediaTrack* tr = GetTrack(nullptr, i);
if (!tr) continue;
std::vector<char> buf(8192, '\0');
if (!GetSetMediaTrackInfo_String(tr, "P_RAZOREDITS", buf.data(), false))
continue;
for (const reasampler::RazorRange& r :
reasampler::parseRazorEdits(std::string(buf.data())))
areas.push_back({tr, r});
}
return areas;
}
// RAII snapshot/restore of the project's TRACK selection. Batch razor capture must
// transiently select exactly the area's owning track per render (track scope's &128 bit
// renders whatever TRACKS are selected); the user's original track selection is restored
// on EVERY exit path (part of the non-destructive invariant). Mirror of ItemSelectionGuard.
class TrackSelectionGuard
{
public:
TrackSelectionGuard()
{
const int n = CountSelectedTracks(nullptr);
for (int i = 0; i < n; ++i)
if (MediaTrack* tr = GetSelectedTrack(nullptr, i))
selected_.push_back(tr);
}
~TrackSelectionGuard()
{
// Deselect every track, then re-select the snapshot — the exact original set.
const int total = CountTracks(nullptr);
for (int i = 0; i < total; ++i)
if (MediaTrack* tr = GetTrack(nullptr, i))
SetTrackSelected(tr, false);
for (MediaTrack* tr : selected_)
SetTrackSelected(tr, true);
}
TrackSelectionGuard(const TrackSelectionGuard&) = delete;
TrackSelectionGuard& operator=(const TrackSelectionGuard&) = delete;
private:
std::vector<MediaTrack*> selected_;
};
// Batch razor capture: one bank sample per razor AREA, track scope over that area's own
// range (the area's owning track is the source track). Track scope renders the selected
// TRACKS via master (&128), so each unit transiently selects ONLY its owning track
// (SetOnlyTrackSelected) under the TrackSelectionGuard, which restores the user's original
// track selection on every path. The razor selection itself is read-only and left intact.
// Persists ONCE at the end. Reports a mixed-result summary.
static void RunBatchCaptureRazor()
{
const std::vector<std::pair<MediaTrack*, reasampler::RazorRange>> areas =
collectRazorAreas();
if (areas.empty())
{
ShowConsoleMsg("ReaSampler batch capture: make at least one razor area first.\n");
return;
}
std::vector<reasampler::BatchRange> ranges;
ranges.reserve(areas.size());
for (const auto& a : areas)
ranges.push_back({a.second.startSeconds, a.second.endSeconds});
const std::vector<reasampler::CaptureUnit> plan = reasampler::planCaptureUnits(ranges);
reasampler::BatchOutcome outcome;
bool anyAdded = false;
{
// Restore the user's ORIGINAL track selection on every exit path.
TrackSelectionGuard selGuard;
std::size_t planIdx = 0;
for (const auto& a : areas)
{
if (!(a.second.endSeconds > a.second.startSeconds)) continue; // dropped — lockstep
const reasampler::CaptureUnit& unit = plan[planIdx++];
MediaTrack* tr = a.first;
// Transiently select ONLY this track so the track-scope render (&128) captures
// exactly it via master (over the custom time bounds we set per unit).
SetOnlyTrackSelected(tr);
ResolvedSource src;
src.startSeconds = unit.startSeconds;
src.endSeconds = unit.endSeconds;
src.sourceTracks.push_back(tr);
if (std::string g = reasampler::guidString(tr); !g.empty())
src.trackGuids.push_back(std::move(g));
const std::string baseName = "razor-" + std::to_string(unit.ordinal);
reasampler::CaptureResult res = captureAndIndexOne(
reasampler::CaptureScope::Track, src, baseName,
unit.startSeconds, unit.endSeconds);
const bool ok = (res.status == reasampler::CaptureStatus::Ok);
outcome.record(unit.ordinal, ok, ok ? std::string{} : res.message);
if (ok) anyAdded = true;
}
} // selGuard restores the original track selection here, on every path
if (anyAdded)
g_session.saveToActiveProject();
ShowConsoleMsg((outcome.summaryLine("razor area") + "\n").c_str());
}
// --- M10: re-capture from source --------------------------------------------
//
// Regenerates a PROVENANCED bank sample's file from its recorded source's CURRENT
@@ -1124,6 +1422,8 @@ static bool OnHookCommand(int command, int /*flag*/)
if (command == g_cmdToggleBankPanel) { reasampler::bankPanelToggle(); return true; }
if (command == g_cmdInsertSelected) { RunInsertSelected(false); return true; }
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); return true; }
if (command == g_cmdCaptureBatchItems) { RunBatchCaptureItems(); return true; }
if (command == g_cmdCaptureBatchRazor) { RunBatchCaptureRazor(); return true; }
if (command == g_cmdCaptureTrackRealtime) { RunCaptureRealtimeTrack(); return true; }
if (command == g_cmdCancelRealtime) { RunCancelRealtime(); return true; }
if (command == g_cmdRecaptureFromSource) { RunRecaptureFromSource(); return true; }
@@ -1155,6 +1455,8 @@ static int OnToggleAction(int command)
static gaccel_register_t g_accelToggleBankPanel{};
static gaccel_register_t g_accelInsertSelected{};
static gaccel_register_t g_accelInsertSelectedConform{};
static gaccel_register_t g_accelCaptureBatchItems{};
static gaccel_register_t g_accelCaptureBatchRazor{};
static gaccel_register_t g_accelCaptureTrackRealtime{};
static gaccel_register_t g_accelCancelRealtime{};
static gaccel_register_t g_accelRecaptureFromSource{};
@@ -1166,6 +1468,8 @@ static gaccel_register_t g_accelShowVersion{};
static std::string g_descToggleBankPanel;
static std::string g_descInsertSelected;
static std::string g_descInsertSelectedConform;
static std::string g_descCaptureBatchItems;
static std::string g_descCaptureBatchRazor;
static std::string g_descCaptureTrackRealtime;
static std::string g_descCancelRealtime;
static std::string g_descRecaptureFromSource;
@@ -1176,6 +1480,8 @@ static std::string g_descShowVersion;
static const char* g_idToggleBankPanel = nullptr;
static const char* g_idInsertSelected = nullptr;
static const char* g_idInsertSelectedConform = nullptr;
static const char* g_idCaptureBatchItems = nullptr;
static const char* g_idCaptureBatchRazor = nullptr;
static const char* g_idCaptureTrackRealtime = nullptr;
static const char* g_idCancelRealtime = nullptr;
static const char* g_idRecaptureFromSource = nullptr;
@@ -1221,6 +1527,10 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
g_rec->Register("-command_id", (void*)g_idCancelRealtime);
g_rec->Register("-gaccel", (void*)&g_accelCaptureTrackRealtime);
g_rec->Register("-command_id", (void*)g_idCaptureTrackRealtime);
g_rec->Register("-gaccel", (void*)&g_accelCaptureBatchRazor);
g_rec->Register("-command_id", (void*)g_idCaptureBatchRazor);
g_rec->Register("-gaccel", (void*)&g_accelCaptureBatchItems);
g_rec->Register("-command_id", (void*)g_idCaptureBatchItems);
g_rec->Register("-gaccel", (void*)&g_accelInsertSelectedConform);
g_rec->Register("-command_id", (void*)g_idInsertSelectedConform);
g_rec->Register("-gaccel", (void*)&g_accelInsertSelected);
@@ -1340,6 +1650,32 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
rec->Register("gaccel", (void*)&g_accelInsertSelectedConform);
}
// Register the M11 batch-capture actions (command_id -> gaccel -> hookcommand). Each
// fires N captures (one bank sample per selected item / per razor area), honoring every
// precision invariant per unit and restoring the original selection on every path.
// Channel-qualified FOREVER-STABLE ids.
g_idCaptureBatchItems = internCmdId("CAPTURE_BATCH_ITEMS");
g_cmdCaptureBatchItems = rec->Register("command_id", (void*)g_idCaptureBatchItems);
if (g_cmdCaptureBatchItems)
{
g_descCaptureBatchItems =
reasampler::channelActionName("batch capture selected items (one per item)");
g_accelCaptureBatchItems.accel.cmd = g_cmdCaptureBatchItems;
g_accelCaptureBatchItems.desc = g_descCaptureBatchItems.c_str();
rec->Register("gaccel", (void*)&g_accelCaptureBatchItems);
}
g_idCaptureBatchRazor = internCmdId("CAPTURE_BATCH_RAZOR");
g_cmdCaptureBatchRazor = rec->Register("command_id", (void*)g_idCaptureBatchRazor);
if (g_cmdCaptureBatchRazor)
{
g_descCaptureBatchRazor =
reasampler::channelActionName("batch capture razor areas (one per area)");
g_accelCaptureBatchRazor.accel.cmd = g_cmdCaptureBatchRazor;
g_accelCaptureBatchRazor.desc = g_descCaptureBatchRazor.c_str();
rec->Register("gaccel", (void*)&g_accelCaptureBatchRazor);
}
// Register the "capture selected track (realtime)" action (command_id -> gaccel ->
// hookcommand). Realtime sibling of the offline CAPTURE_TRACK scope: records the
// selected track's own output in realtime into a hidden temp track, moves it into