|
|
|
@@ -6,8 +6,9 @@
|
|
|
|
|
// the one TU that defines the API pointers; here they are extern.
|
|
|
|
|
//
|
|
|
|
|
// Drives the RENDER_* project settings via GetSetProjectInfo/_String (source-
|
|
|
|
|
// selection bits come from the pure render_settings mapping), snapshots and
|
|
|
|
|
// restores every setting it changes, triggers a render, then populates a Sample.
|
|
|
|
|
// selection bits come from the pure render_settings mapping) plus, on the
|
|
|
|
|
// time-selection bounds channel, the project time selection; snapshots and restores
|
|
|
|
|
// every one of them, triggers a render, then populates a Sample.
|
|
|
|
|
// Source-agnostic: never reads the DAW selection itself, only the CaptureRequest
|
|
|
|
|
// the caller resolved. RENDER_ADDTOPROJ&1 is cleared on every path — never
|
|
|
|
|
// inserts into the arrange.
|
|
|
|
@@ -62,9 +63,13 @@ namespace {
|
|
|
|
|
// project — why we set them all explicitly first.
|
|
|
|
|
constexpr int kActionRenderUsingMostRecentSettings = 42230;
|
|
|
|
|
|
|
|
|
|
// RENDER_BOUNDSFLAG 0 = custom time bounds (we set STARTPOS/ENDPOS ourselves
|
|
|
|
|
// for exact, unrounded bounds). SDK header ~3042.
|
|
|
|
|
constexpr double kBoundsCustom = 0.0;
|
|
|
|
|
// The bounds channel this build hands the render window over on. Why there are two,
|
|
|
|
|
// and the open DAW question this selection exists to answer, are stated once on
|
|
|
|
|
// RenderBoundsChannel (core/capture/render_settings.h) — flipping this constant back
|
|
|
|
|
// to CustomTimeBounds is the whole revert.
|
|
|
|
|
constexpr RenderBoundsChannel kBoundsChannel = RenderBoundsChannel::TimeSelection;
|
|
|
|
|
constexpr bool kUsesTimeSelectionBounds =
|
|
|
|
|
(kBoundsChannel == RenderBoundsChannel::TimeSelection);
|
|
|
|
|
|
|
|
|
|
// RENDER_TAILFLAG/TAILMS/NORMALIZE/TRIMEND are driven from the pure
|
|
|
|
|
// tailRenderSettingsFor mapping (render_settings.h) in the tail-driving block below.
|
|
|
|
@@ -177,6 +182,30 @@ void restoreRenderSettings(const RenderSettingsSnapshot& s) {
|
|
|
|
|
GetSetProjectInfo(s.proj, "RENDER_TRIMEND", s.trimEnd, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The project time selection, snapshotted and restored around a render that uses it
|
|
|
|
|
// as its bounds channel. Separate from ScopedRenderSettings because it is project
|
|
|
|
|
// state rather than a RENDER_* setting, and only one channel touches it.
|
|
|
|
|
// GetSet_LoopTimeRange has no project parameter (SDK header ~2670) — it acts on the
|
|
|
|
|
// active project, which is the one capture() already resolved and renders into.
|
|
|
|
|
struct ScopedTimeSelection {
|
|
|
|
|
bool engaged;
|
|
|
|
|
double start = 0.0;
|
|
|
|
|
double end = 0.0;
|
|
|
|
|
|
|
|
|
|
explicit ScopedTimeSelection(bool engage) : engaged(engage) {
|
|
|
|
|
if (engaged) GetSet_LoopTimeRange(false, false, &start, &end, false);
|
|
|
|
|
}
|
|
|
|
|
~ScopedTimeSelection() {
|
|
|
|
|
if (!engaged) return;
|
|
|
|
|
// Copies: the setter takes non-const pointers, so the snapshot must not be
|
|
|
|
|
// what it writes through.
|
|
|
|
|
double s = start, e = end;
|
|
|
|
|
GetSet_LoopTimeRange(true, false, &s, &e, false);
|
|
|
|
|
}
|
|
|
|
|
ScopedTimeSelection(const ScopedTimeSelection&) = delete;
|
|
|
|
|
ScopedTimeSelection& operator=(const ScopedTimeSelection&) = delete;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// RAII wrapper: guarantees restore on every return path from capture().
|
|
|
|
|
struct ScopedRenderSettings {
|
|
|
|
|
RenderSettingsSnapshot snap;
|
|
|
|
@@ -440,18 +469,38 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ScopedRenderSettings guard(proj);
|
|
|
|
|
ScopedTimeSelection tsGuard(kUsesTimeSelectionBounds);
|
|
|
|
|
|
|
|
|
|
// Custom time bounds so the rendered length equals the requested range with
|
|
|
|
|
// NO rounding and NO added silence (unless a tail was explicitly requested).
|
|
|
|
|
GetSetProjectInfo(proj, "RENDER_BOUNDSFLAG", kBoundsCustom, true);
|
|
|
|
|
// The window goes over the selected channel's own store so the rendered length
|
|
|
|
|
// equals the requested range with NO rounding and NO added silence (unless a tail
|
|
|
|
|
// was explicitly requested). RENDER_STARTPOS/ENDPOS are written on BOTH channels:
|
|
|
|
|
// they are documented as applying to mode 0 only (SDK header ~3045-3046), so under
|
|
|
|
|
// the time-selection channel they are inert, and their read-back below then reports
|
|
|
|
|
// that field independently of the one actually carrying the window.
|
|
|
|
|
GetSetProjectInfo(proj, "RENDER_BOUNDSFLAG",
|
|
|
|
|
static_cast<double>(renderBoundsFlagFor(kBoundsChannel)), true);
|
|
|
|
|
GetSetProjectInfo(proj, "RENDER_STARTPOS", request.startSeconds, true);
|
|
|
|
|
GetSetProjectInfo(proj, "RENDER_ENDPOS", request.endSeconds, true);
|
|
|
|
|
if (kUsesTimeSelectionBounds) {
|
|
|
|
|
double s = request.startSeconds, e = request.endSeconds;
|
|
|
|
|
GetSet_LoopTimeRange(true, false, &s, &e, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The time-selection channel's read-back, so each checkpoint below reads the store
|
|
|
|
|
// that actually carried the window rather than the inert RENDER_* pair.
|
|
|
|
|
auto readTimeSelection = [](double& s, double& e) {
|
|
|
|
|
s = 0.0;
|
|
|
|
|
e = 0.0;
|
|
|
|
|
GetSet_LoopTimeRange(false, false, &s, &e, false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// The requested window crosses out of this process HERE and nowhere else, so the
|
|
|
|
|
// read-back is the only evidence available on this side of that boundary for
|
|
|
|
|
// whether REAPER kept it. Reported below, once the project rate is known.
|
|
|
|
|
const double storedStart = GetSetProjectInfo(proj, "RENDER_STARTPOS", 0.0, false);
|
|
|
|
|
const double storedEnd = GetSetProjectInfo(proj, "RENDER_ENDPOS", 0.0, false);
|
|
|
|
|
double storedTsStart = 0.0, storedTsEnd = 0.0;
|
|
|
|
|
if (kUsesTimeSelectionBounds) readTimeSelection(storedTsStart, storedTsEnd);
|
|
|
|
|
|
|
|
|
|
// TAILFLAG/TAILMS/NORMALIZE/TRIMEND from the pure mapping: None -> exact
|
|
|
|
|
// bounds + disable-all normalize; Auto -> 8s tail + surgical trim-end
|
|
|
|
@@ -459,7 +508,7 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|
|
|
|
// trim. NORMALIZE is driven here (not the determinism block below) so the
|
|
|
|
|
// Auto surgical value isn't clobbered.
|
|
|
|
|
const TailRenderSettings tail =
|
|
|
|
|
tailRenderSettingsFor(request.tailMode, request.tailMs);
|
|
|
|
|
tailRenderSettingsFor(request.tailMode, request.tailMs, kBoundsChannel);
|
|
|
|
|
GetSetProjectInfo(proj, "RENDER_TAILFLAG",
|
|
|
|
|
static_cast<double>(tail.tailFlag), true);
|
|
|
|
|
GetSetProjectInfo(proj, "RENDER_TAILMS", tail.tailMs, true);
|
|
|
|
@@ -496,7 +545,19 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|
|
|
|
ShowConsoleMsg(("ReaSampler capture (" + std::string(checkpoint) + "): " +
|
|
|
|
|
drift + "\n").c_str());
|
|
|
|
|
};
|
|
|
|
|
reportDrift("at store", storedStart, storedEnd);
|
|
|
|
|
|
|
|
|
|
// The same checkpoint on the other channel. No-op unless that channel is the one
|
|
|
|
|
// carrying the window, so the console gains nothing on the custom-bounds build.
|
|
|
|
|
auto reportTimeSelectionDrift = [&](const char* checkpoint) {
|
|
|
|
|
if (!kUsesTimeSelectionBounds) return;
|
|
|
|
|
double s = 0.0, e = 0.0;
|
|
|
|
|
readTimeSelection(s, e);
|
|
|
|
|
reportDrift(checkpoint, s, e);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
reportDrift("at store, custom-bounds fields", storedStart, storedEnd);
|
|
|
|
|
if (kUsesTimeSelectionBounds)
|
|
|
|
|
reportDrift("at store, time selection", storedTsStart, storedTsEnd);
|
|
|
|
|
|
|
|
|
|
GetSetProjectInfo(proj, "RENDER_CHANNELS",
|
|
|
|
|
static_cast<double>(request.channelCount), true);
|
|
|
|
@@ -519,10 +580,37 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|
|
|
|
setProjString(proj, "RENDER_FILE", paths.absoluteDir);
|
|
|
|
|
setProjString(proj, "RENDER_PATTERN", paths.fileStem);
|
|
|
|
|
|
|
|
|
|
// Which bounds channel carried this window and what the render did with it — printed
|
|
|
|
|
// on EVERY tail mode and on EVERY return past this point (refusals included),
|
|
|
|
|
// because a verdict that appeared only on some outcomes would read its own absence
|
|
|
|
|
// on the rest as a pass. Auto/Manual are not judged against a frame count (they add
|
|
|
|
|
// frames by design) and the sentence says so rather than comparing anyway.
|
|
|
|
|
// SelectedItems/RazorArea are INFERRED (not SDK-confirmed) to derive their bounds
|
|
|
|
|
// from content and never consult the channel at all
|
|
|
|
|
// (render_settings::sourceBypassesBoundsChannel) — the batch-item path renders
|
|
|
|
|
// through SelectedItems on every capture, so without this the verdict would print an
|
|
|
|
|
// EXACT/SHORT/LONG claim about a channel that was never in play.
|
|
|
|
|
const char* boundsBypassLabel = sourceBypassesBoundsChannel(request.sourceMode)
|
|
|
|
|
? renderSourceLabel(request.sourceMode)
|
|
|
|
|
: nullptr;
|
|
|
|
|
auto printBoundsVerdict = [&](long long frames, int rate) {
|
|
|
|
|
ShowConsoleMsg(("ReaSampler capture -- " +
|
|
|
|
|
describeBoundsExperiment(renderBoundsChannelLabel(kBoundsChannel),
|
|
|
|
|
request.startSeconds, request.endSeconds,
|
|
|
|
|
frames, rate, boundsBypassLabel) +
|
|
|
|
|
"\n").c_str());
|
|
|
|
|
};
|
|
|
|
|
auto reportExperiment = [&](const BoundsVerdict& v) {
|
|
|
|
|
printBoundsVerdict(v.measuredFrames, v.measuredRate);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Int16/Int24 have no captured ground-truth blob — fail explicitly rather
|
|
|
|
|
// than silently mis-render at the wrong bit depth.
|
|
|
|
|
const char* fmtBase64 = wavSinkConfigBase64(request.bitDepth);
|
|
|
|
|
if (!fmtBase64) {
|
|
|
|
|
// Nothing was rendered yet — frames/rate 0 reads as NOT JUDGED, same as any
|
|
|
|
|
// other capture that answered nothing.
|
|
|
|
|
printBoundsVerdict(0, 0);
|
|
|
|
|
result.status = CaptureStatus::UnsupportedFormat;
|
|
|
|
|
result.message = "Requested bit depth has no verified RENDER_FORMAT blob "
|
|
|
|
|
"(Float32 only; Int16/Int24 not yet supported).";
|
|
|
|
@@ -532,24 +620,30 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|
|
|
|
|
|
|
|
|
// Read again right here: a mismatch against the store-time read-back above
|
|
|
|
|
// means something between the two writes and this line moved the bounds,
|
|
|
|
|
// before the render ever ran.
|
|
|
|
|
reportDrift("before render",
|
|
|
|
|
// before the render ever ran. Both channels get the same three checkpoints, or a
|
|
|
|
|
// store-versus-render distinction would only be available on one of them.
|
|
|
|
|
reportDrift("before render, custom-bounds fields",
|
|
|
|
|
GetSetProjectInfo(proj, "RENDER_STARTPOS", 0.0, false),
|
|
|
|
|
GetSetProjectInfo(proj, "RENDER_ENDPOS", 0.0, false));
|
|
|
|
|
reportTimeSelectionDrift("before render, time selection");
|
|
|
|
|
|
|
|
|
|
Main_OnCommand(kActionRenderUsingMostRecentSettings, 0);
|
|
|
|
|
|
|
|
|
|
// And once more here, while the guard above is still live and before it restores
|
|
|
|
|
// anything: only the gap between this read and the one immediately above can be
|
|
|
|
|
// the render itself.
|
|
|
|
|
reportDrift("after render",
|
|
|
|
|
reportDrift("after render, custom-bounds fields",
|
|
|
|
|
GetSetProjectInfo(proj, "RENDER_STARTPOS", 0.0, false),
|
|
|
|
|
GetSetProjectInfo(proj, "RENDER_ENDPOS", 0.0, false));
|
|
|
|
|
reportTimeSelectionDrift("after render, time selection");
|
|
|
|
|
|
|
|
|
|
// Main_OnCommand returns void, so a failed render is silent — stat the
|
|
|
|
|
// expected output path to detect it.
|
|
|
|
|
const std::string expectedPath = paths.absoluteDir + "/" + paths.fileName;
|
|
|
|
|
if (!std::filesystem::exists(expectedPath)) {
|
|
|
|
|
// Main_OnCommand ran but produced nothing measurable — NOT JUDGED, same as
|
|
|
|
|
// the format refusal above.
|
|
|
|
|
printBoundsVerdict(0, 0);
|
|
|
|
|
result.status = CaptureStatus::RenderFailed;
|
|
|
|
|
result.message = "Render produced no output file (expected: " +
|
|
|
|
|
expectedPath + "). Check the REAPER console for errors.";
|
|
|
|
@@ -563,6 +657,7 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|
|
|
|
const BoundsVerdict emptyVerdict =
|
|
|
|
|
checkRenderedFileNotEmpty(expectedPath, projectDir, request.destination);
|
|
|
|
|
if (emptyVerdict.refused) {
|
|
|
|
|
reportExperiment(emptyVerdict);
|
|
|
|
|
result.status = CaptureStatus::BoundsMismatch;
|
|
|
|
|
result.message = emptyVerdict.message;
|
|
|
|
|
return result;
|
|
|
|
@@ -576,6 +671,7 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|
|
|
|
// on an already-warm file, judged acceptable.)
|
|
|
|
|
const BoundsVerdict bounds =
|
|
|
|
|
checkRenderedBounds(expectedPath, projectDir, request);
|
|
|
|
|
reportExperiment(bounds);
|
|
|
|
|
if (bounds.refused) {
|
|
|
|
|
result.status = CaptureStatus::BoundsMismatch;
|
|
|
|
|
result.message = bounds.message;
|
|
|
|
|