Merge m11-w1-t1-capture-polish: batch capture per item / razor area
This commit is contained in:
+17
-1
@@ -178,6 +178,18 @@ add_library(render_settings STATIC src/render_settings.cpp)
|
|||||||
target_include_directories(render_settings PUBLIC src)
|
target_include_directories(render_settings PUBLIC src)
|
||||||
target_link_libraries(render_settings PUBLIC bank_model)
|
target_link_libraries(render_settings PUBLIC bank_model)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2f'') Pure batch_capture library — NO REAPER, NO SWELL. The M11 batch-capture
|
||||||
|
# logic: an ordered list of source ranges (one per selected item / per razor
|
||||||
|
# area) -> validated, ordinal-assigned capture units (empty/inverted dropped),
|
||||||
|
# plus order-preserving per-unit result aggregation into a mixed-result summary
|
||||||
|
# line. Split out so the plan/aggregate/summary is unit-tested outside the DAW;
|
||||||
|
# the selection read, transient re-selection, and render loop stay in main.cpp.
|
||||||
|
# No dependency on bank_model — it takes plain ranges/values at its boundary.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(batch_capture STATIC src/batch_capture.cpp)
|
||||||
|
target_include_directories(batch_capture PUBLIC src)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2f') Pure tail_control library — NO REAPER, NO SWELL. The docked bank_panel's
|
# 2f') Pure tail_control library — NO REAPER, NO SWELL. The docked bank_panel's
|
||||||
# tail-mode toggle logic (T1 exposure): TailSetting state, cycle order
|
# tail-mode toggle logic (T1 exposure): TailSetting state, cycle order
|
||||||
@@ -341,6 +353,10 @@ add_executable(render_settings_tests tests/test_render_settings.cpp)
|
|||||||
target_link_libraries(render_settings_tests PRIVATE render_settings)
|
target_link_libraries(render_settings_tests PRIVATE render_settings)
|
||||||
add_test(NAME render_settings_tests COMMAND render_settings_tests)
|
add_test(NAME render_settings_tests COMMAND render_settings_tests)
|
||||||
|
|
||||||
|
add_executable(batch_capture_tests tests/test_batch_capture.cpp)
|
||||||
|
target_link_libraries(batch_capture_tests PRIVATE batch_capture)
|
||||||
|
add_test(NAME batch_capture_tests COMMAND batch_capture_tests)
|
||||||
|
|
||||||
add_executable(tail_control_tests tests/test_tail_control.cpp)
|
add_executable(tail_control_tests tests/test_tail_control.cpp)
|
||||||
target_link_libraries(tail_control_tests PRIVATE tail_control)
|
target_link_libraries(tail_control_tests PRIVATE tail_control)
|
||||||
add_test(NAME tail_control_tests COMMAND tail_control_tests)
|
add_test(NAME tail_control_tests COMMAND tail_control_tests)
|
||||||
@@ -417,7 +433,7 @@ add_library(reaper_reasampler MODULE
|
|||||||
src/bank_book.cpp
|
src/bank_book.cpp
|
||||||
src/owned_manifest.cpp
|
src/owned_manifest.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings tail_control realtime_record bank_book wav_trim owned_manifest prune_reconcile prune_button app_version provenance)
|
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings batch_capture tail_control realtime_record bank_book wav_trim owned_manifest prune_reconcile prune_button app_version provenance)
|
||||||
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
||||||
# OUTPUT_NAME is channel-derived (Phase V, V4): "reaper_reasampler" (stable, default) or
|
# OUTPUT_NAME is channel-derived (Phase V, V4): "reaper_reasampler" (stable, default) or
|
||||||
# "reaper_reasampler_beta" (beta). REAPER dlopen's any reaper_* module, so both channels'
|
# "reaper_reasampler_beta" (beta). REAPER dlopen's any reaper_* module, so both channels'
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -18,9 +18,11 @@
|
|||||||
#include "reaper_plugin.h"
|
#include "reaper_plugin.h"
|
||||||
#include "reaper_plugin_functions.h"
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
#include "app_version.h"
|
#include "app_version.h"
|
||||||
#include "bank_model.h"
|
#include "bank_model.h"
|
||||||
#include "bank_panel.h"
|
#include "bank_panel.h"
|
||||||
|
#include "batch_capture.h"
|
||||||
#include "capture.h"
|
#include "capture.h"
|
||||||
#include "insert.h"
|
#include "insert.h"
|
||||||
#include "persist.h"
|
#include "persist.h"
|
||||||
@@ -124,6 +127,14 @@ static int g_cmdToggleBankPanel = 0;
|
|||||||
static int g_cmdInsertSelected = 0;
|
static int g_cmdInsertSelected = 0;
|
||||||
static int g_cmdInsertSelectedConform = 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
|
// 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
|
// 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
|
// 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);
|
return backend.capture(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs one capture-action-table row: resolve its scope source + range, snapshot &
|
// Renders ONE capture request under the scope's FX-bypass guard, stamps provenance,
|
||||||
// clear the out-of-scope FX AND neutralize their fader gain + pan chain (RAII),
|
// and adds the resulting Sample to the ACTIVE bank + records the created file in the
|
||||||
// render via the offline backend, add the Sample to the bank, persist + mark dirty.
|
// owned-file manifest — WITHOUT persisting. The caller persists once (single-capture:
|
||||||
// The load-bearing principle holds structurally — this path writes a file + a bank
|
// right after; batch: once at the end) so a batch does not write ext state N times.
|
||||||
// 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
|
// Provenance is read from the LIVE selection here, so a batch that transiently
|
||||||
// on every path by FxBypassGuard, and the backend restores every RENDER_* setting.
|
// 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)
|
static void RunCapture(const reasampler::CaptureActionDef& def)
|
||||||
{
|
{
|
||||||
ResolvedSource src;
|
ResolvedSource src;
|
||||||
@@ -731,54 +804,14 @@ static void RunCapture(const reasampler::CaptureActionDef& def)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The tail mode is a PANEL SETTING (docked bank panel's toggle), not a per-action
|
reasampler::CaptureResult res =
|
||||||
// variant: the plain capture actions apply whatever the panel is set to. Default
|
captureAndIndexOne(def.scope, src, def.baseName, src.startSeconds, src.endSeconds);
|
||||||
// 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);
|
|
||||||
|
|
||||||
if (res.status != reasampler::CaptureStatus::Ok)
|
if (res.status != reasampler::CaptureStatus::Ok)
|
||||||
{
|
{
|
||||||
ShowConsoleMsg(("ReaSampler capture failed: " + res.message + "\n").c_str());
|
ShowConsoleMsg(("ReaSampler capture failed: " + res.message + "\n").c_str());
|
||||||
return;
|
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
|
// 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
|
// `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
|
// 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();
|
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 --------------------------------------------
|
// --- M10: re-capture from source --------------------------------------------
|
||||||
//
|
//
|
||||||
// Regenerates a PROVENANCED bank sample's file from its recorded source's CURRENT
|
// 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_cmdToggleBankPanel) { reasampler::bankPanelToggle(); return true; }
|
||||||
if (command == g_cmdInsertSelected) { RunInsertSelected(false); return true; }
|
if (command == g_cmdInsertSelected) { RunInsertSelected(false); return true; }
|
||||||
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); 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_cmdCaptureTrackRealtime) { RunCaptureRealtimeTrack(); return true; }
|
||||||
if (command == g_cmdCancelRealtime) { RunCancelRealtime(); return true; }
|
if (command == g_cmdCancelRealtime) { RunCancelRealtime(); return true; }
|
||||||
if (command == g_cmdRecaptureFromSource) { RunRecaptureFromSource(); 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_accelToggleBankPanel{};
|
||||||
static gaccel_register_t g_accelInsertSelected{};
|
static gaccel_register_t g_accelInsertSelected{};
|
||||||
static gaccel_register_t g_accelInsertSelectedConform{};
|
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_accelCaptureTrackRealtime{};
|
||||||
static gaccel_register_t g_accelCancelRealtime{};
|
static gaccel_register_t g_accelCancelRealtime{};
|
||||||
static gaccel_register_t g_accelRecaptureFromSource{};
|
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_descToggleBankPanel;
|
||||||
static std::string g_descInsertSelected;
|
static std::string g_descInsertSelected;
|
||||||
static std::string g_descInsertSelectedConform;
|
static std::string g_descInsertSelectedConform;
|
||||||
|
static std::string g_descCaptureBatchItems;
|
||||||
|
static std::string g_descCaptureBatchRazor;
|
||||||
static std::string g_descCaptureTrackRealtime;
|
static std::string g_descCaptureTrackRealtime;
|
||||||
static std::string g_descCancelRealtime;
|
static std::string g_descCancelRealtime;
|
||||||
static std::string g_descRecaptureFromSource;
|
static std::string g_descRecaptureFromSource;
|
||||||
@@ -1176,6 +1480,8 @@ static std::string g_descShowVersion;
|
|||||||
static const char* g_idToggleBankPanel = nullptr;
|
static const char* g_idToggleBankPanel = nullptr;
|
||||||
static const char* g_idInsertSelected = nullptr;
|
static const char* g_idInsertSelected = nullptr;
|
||||||
static const char* g_idInsertSelectedConform = 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_idCaptureTrackRealtime = nullptr;
|
||||||
static const char* g_idCancelRealtime = nullptr;
|
static const char* g_idCancelRealtime = nullptr;
|
||||||
static const char* g_idRecaptureFromSource = 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("-command_id", (void*)g_idCancelRealtime);
|
||||||
g_rec->Register("-gaccel", (void*)&g_accelCaptureTrackRealtime);
|
g_rec->Register("-gaccel", (void*)&g_accelCaptureTrackRealtime);
|
||||||
g_rec->Register("-command_id", (void*)g_idCaptureTrackRealtime);
|
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("-gaccel", (void*)&g_accelInsertSelectedConform);
|
||||||
g_rec->Register("-command_id", (void*)g_idInsertSelectedConform);
|
g_rec->Register("-command_id", (void*)g_idInsertSelectedConform);
|
||||||
g_rec->Register("-gaccel", (void*)&g_accelInsertSelected);
|
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);
|
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 ->
|
// Register the "capture selected track (realtime)" action (command_id -> gaccel ->
|
||||||
// hookcommand). Realtime sibling of the offline CAPTURE_TRACK scope: records the
|
// 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
|
// selected track's own output in realtime into a hidden temp track, moves it into
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
// Standalone tests for reasampler::batch_capture — no REAPER, no test framework.
|
||||||
|
// Same fast loop as the sibling pure tests (render_settings et al.): assert the batch
|
||||||
|
// planning (selected-set -> ordered capture units) and result aggregation directly.
|
||||||
|
//
|
||||||
|
// Covers (M11 brief §test cases):
|
||||||
|
// * selected-set -> capture-request-list planning: items; razor areas; empty set;
|
||||||
|
// multiple razor areas across tracks (order preserved, ordinals contiguous);
|
||||||
|
// invalid (empty/inverted) ranges dropped.
|
||||||
|
// * batch result aggregation: all-success; partial-failure ORDERING (failed
|
||||||
|
// ordinals reported in unit order); all-failed; single-unit noun singular.
|
||||||
|
|
||||||
|
#include "../src/batch_capture.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace reasampler;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
// --- planCaptureUnits: items -------------------------------------------------
|
||||||
|
|
||||||
|
// Three selected items -> three units, order preserved, ordinals 1,2,3, bounds exact.
|
||||||
|
static void testPlanItemsPreservesOrderAndBounds() {
|
||||||
|
std::vector<BatchRange> in = {{0.0, 1.5}, {2.0, 2.25}, {10.0, 12.0}};
|
||||||
|
auto units = planCaptureUnits(in);
|
||||||
|
CHECK(units.size() == 3);
|
||||||
|
CHECK(units[0].ordinal == 1 && units[0].startSeconds == 0.0 && units[0].endSeconds == 1.5);
|
||||||
|
CHECK(units[1].ordinal == 2 && units[1].startSeconds == 2.0 && units[1].endSeconds == 2.25);
|
||||||
|
CHECK(units[2].ordinal == 3 && units[2].startSeconds == 10.0 && units[2].endSeconds == 12.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- planCaptureUnits: empty set ---------------------------------------------
|
||||||
|
|
||||||
|
// No selected item / no razor area -> empty plan (shell reports nothing-to-batch).
|
||||||
|
static void testPlanEmptySet() {
|
||||||
|
CHECK(planCaptureUnits({}).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- planCaptureUnits: invalid ranges dropped, ordinals stay contiguous ------
|
||||||
|
|
||||||
|
// An empty (end==start) and an inverted (end<start) range are DROPPED; the kept
|
||||||
|
// units are renumbered 1..k with no gap, so a dropped neighbor never leaves a hole
|
||||||
|
// in the reported ordinals.
|
||||||
|
static void testPlanDropsInvalidAndRenumbers() {
|
||||||
|
std::vector<BatchRange> in = {
|
||||||
|
{0.0, 1.0}, // kept -> #1
|
||||||
|
{5.0, 5.0}, // empty -> dropped
|
||||||
|
{3.0, 2.0}, // inverted -> dropped
|
||||||
|
{8.0, 9.0}, // kept -> #2
|
||||||
|
};
|
||||||
|
auto units = planCaptureUnits(in);
|
||||||
|
CHECK(units.size() == 2);
|
||||||
|
CHECK(units[0].ordinal == 1 && units[0].startSeconds == 0.0 && units[0].endSeconds == 1.0);
|
||||||
|
CHECK(units[1].ordinal == 2 && units[1].startSeconds == 8.0 && units[1].endSeconds == 9.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- planCaptureUnits: multiple razor areas across tracks --------------------
|
||||||
|
|
||||||
|
// The shell concatenates every track's parsed razor areas in track order; planning
|
||||||
|
// preserves that concatenation order and assigns contiguous ordinals. Two areas on
|
||||||
|
// track A followed by one on track B -> ordinals 1,2,3 in that exact order.
|
||||||
|
static void testPlanRazorAreasAcrossTracksOrdered() {
|
||||||
|
std::vector<BatchRange> in = {
|
||||||
|
{0.0, 0.5}, // track A, area 1
|
||||||
|
{1.0, 1.5}, // track A, area 2
|
||||||
|
{4.0, 6.0}, // track B, area 1
|
||||||
|
};
|
||||||
|
auto units = planCaptureUnits(in);
|
||||||
|
CHECK(units.size() == 3);
|
||||||
|
CHECK(units[0].startSeconds == 0.0 && units[0].endSeconds == 0.5);
|
||||||
|
CHECK(units[1].startSeconds == 1.0 && units[1].endSeconds == 1.5);
|
||||||
|
CHECK(units[2].startSeconds == 4.0 && units[2].endSeconds == 6.0);
|
||||||
|
CHECK(units[0].ordinal == 1 && units[1].ordinal == 2 && units[2].ordinal == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- BatchOutcome: all-success -----------------------------------------------
|
||||||
|
|
||||||
|
static void testOutcomeAllSuccess() {
|
||||||
|
BatchOutcome o;
|
||||||
|
o.record(1, true);
|
||||||
|
o.record(2, true);
|
||||||
|
o.record(3, true);
|
||||||
|
CHECK(o.total() == 3);
|
||||||
|
CHECK(o.succeeded() == 3);
|
||||||
|
CHECK(o.failed() == 0);
|
||||||
|
CHECK(o.failures().empty());
|
||||||
|
CHECK(o.summaryLine("item") == "ReaSampler batch capture: 3 items captured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- BatchOutcome: partial-failure ordering ----------------------------------
|
||||||
|
|
||||||
|
// Failures are reported in UNIT ORDER (#2, #4), not record-count order or sorted —
|
||||||
|
// the summary must let the user retry exactly the right units. This assertion fails
|
||||||
|
// if the aggregation reordered, deduped, or dropped a failure.
|
||||||
|
static void testOutcomePartialFailureOrdering() {
|
||||||
|
BatchOutcome o;
|
||||||
|
o.record(1, true);
|
||||||
|
o.record(2, false, "render produced no output file");
|
||||||
|
o.record(3, true);
|
||||||
|
o.record(4, false, "capture range is empty");
|
||||||
|
o.record(5, true);
|
||||||
|
CHECK(o.total() == 5);
|
||||||
|
CHECK(o.succeeded() == 3);
|
||||||
|
CHECK(o.failed() == 2);
|
||||||
|
|
||||||
|
auto fails = o.failures();
|
||||||
|
CHECK(fails.size() == 2);
|
||||||
|
CHECK(fails[0].ordinal == 2 && !fails[0].ok);
|
||||||
|
CHECK(fails[0].detail == "render produced no output file");
|
||||||
|
CHECK(fails[1].ordinal == 4);
|
||||||
|
|
||||||
|
CHECK(o.summaryLine("item") ==
|
||||||
|
"ReaSampler batch capture: 3 of 5 items captured (2 failed: #2, #4).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- BatchOutcome: all-failed ------------------------------------------------
|
||||||
|
|
||||||
|
static void testOutcomeAllFailed() {
|
||||||
|
BatchOutcome o;
|
||||||
|
o.record(1, false, "x");
|
||||||
|
o.record(2, false, "y");
|
||||||
|
CHECK(o.succeeded() == 0);
|
||||||
|
CHECK(o.failed() == 2);
|
||||||
|
CHECK(o.summaryLine("razor area") ==
|
||||||
|
"ReaSampler batch capture: 0 of 2 razor areas captured (2 failed: #1, #2).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- BatchOutcome: single unit uses singular noun ----------------------------
|
||||||
|
|
||||||
|
static void testOutcomeSingularNoun() {
|
||||||
|
BatchOutcome o;
|
||||||
|
o.record(1, true);
|
||||||
|
CHECK(o.summaryLine("item") == "ReaSampler batch capture: 1 item captured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- BatchOutcome: empty (no units planned) ----------------------------------
|
||||||
|
|
||||||
|
static void testOutcomeEmpty() {
|
||||||
|
BatchOutcome o;
|
||||||
|
CHECK(o.total() == 0);
|
||||||
|
CHECK(o.summaryLine("item") == "ReaSampler batch capture: nothing to capture.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testPlanItemsPreservesOrderAndBounds();
|
||||||
|
testPlanEmptySet();
|
||||||
|
testPlanDropsInvalidAndRenumbers();
|
||||||
|
testPlanRazorAreasAcrossTracksOrdered();
|
||||||
|
|
||||||
|
testOutcomeAllSuccess();
|
||||||
|
testOutcomePartialFailureOrdering();
|
||||||
|
testOutcomeAllFailed();
|
||||||
|
testOutcomeSingularNoun();
|
||||||
|
testOutcomeEmpty();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user