Settle the render window on the time selection and delete the experiment that proved it

The millisecond floor lives in the custom-bounds field, not the engine, so
RENDER_BOUNDSFLAG=2 is now the only bounds mode: the two-position type, the
console verdict and the STARTPOS/ENDPOS drift probe all go. capture.cpp 697 -> 622.
This commit is contained in:
2026-08-02 16:55:12 -04:00
parent d85e50c143
commit 8331df2e91
14 changed files with 205 additions and 862 deletions
+7 -8
View File
@@ -51,8 +51,8 @@ Detail specific to these pure modules:
- `capture_paths` — the REAPER-free path arithmetic behind offline capture: bank-subfolder + unique-filename derivation (`deriveBankPaths`, forward-slash form, no filesystem touch), the absolute-render-dir vs. project-relative-index-path split (`BankPaths`), the persist-side inverse (`resolveBankFile`, `projectDirOfRpp`), the Save-As bank-relocation plan (`deriveRelocationPlan`), and the GUID-primary project-identity classifier (`classifyProjectTransition``NoOp`/`Load`/`SaveAsRelocate`) the persist-poll timer drives.
- `capture_name` — the REAPER-free composition of one capture's label + file-stem base from its source-track name(s), a local-calendar discriminator (`MM-DD HHMM`, from the shell's clock read), and an optional batch ordinal. The label and the stem deliberately diverge: the stem still passes through `capture_paths::sanitizeStem` (so a name that sanitizes to nothing files as `capture`), while the label keeps the source name verbatim. Stem uniqueness stays entirely `makeUniqueTag`'s — this module never disambiguates.
- `insert_plan` — the REAPER-free logic behind the `insert` shell (M6): computes the `InsertMedia` `mode` bitmask from an `InsertOptions` struct (placement target, tempo-conform ratio, preserve-pitch flag), guaranteeing the &4 stretch-to-time-selection bit is never set and that no tempo bits are set when `conform == None`.
- `render_settings` — the REAPER-free logic behind the capture action family: `SourceMode``RENDER_SETTINGS` bit mapping, `P_RAZOREDITS` string parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (`fxBypassPlanFor`), the bounds channel a capture hands its window over on (`RenderBoundsChannel`/`renderBoundsFlagFor`/`renderBoundsChannelLabel`), the tail-mode → `RENDER_TAILFLAG`/`RENDER_NORMALIZE`/`RENDER_TRIMEND` mapping (`tailRenderSettingsFor`) and its realtime-window analog (`realtimeRecordWindowEnd`), the capture-action taxonomy table (`captureActionTable`) `main.cpp` iterates to register the CAPTURE_ITEM/CAPTURE_TRACK family, and `renderSourceLabel` (the source named in the offline backend's bounds refusal).
- `render_window` — the REAPER-free frame arithmetic behind exact capture bounds: `frameCountFor` (the frame count a project-time window occupies at the project rate — the number the offline backend checks the rendered file against before landing it, so a render that printed something other than the window is refused rather than banked), `renderHonoredBounds` (the gate's verdict and the sole home of its one-frame tolerance, which is empirical rather than proven — the header states which renderer models it covers and which it does not), and `itemExtentPrintsWindow`, the predicate `render_settings::sourceModeForScope` consults to decide whether REAPER's selected-items render source can express a requested window at all. It also owns the short-render diagnostics: `msFlooredEndFrameCount` (the frames a window holds with its end floored to the millisecond — the shape two live short renders matched, quoted by the refusal as a count coincidence and nothing more), `describeBoundsDrift` (the sentence the offline backend prints when a channel's stored bounds do not read back as they were written), `isOnMillisecondGrid` (whether an observed edge can speak to a rounding question at all — an on-grid edge cannot), and `describeBoundsExperiment` (the always-printed verdict naming which bounds channel carried a capture's window and what the landed file measured).
- `render_settings` — the REAPER-free logic behind the capture action family: `SourceMode``RENDER_SETTINGS` bit mapping, `P_RAZOREDITS` string parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (`fxBypassPlanFor`), the one bounds mode a capture hands its window over on (`kRenderBoundsTimeSelection`) and the tail bit paired with it (`kTailFlagTimeSelection`), the tail-mode → `RENDER_TAILFLAG`/`RENDER_NORMALIZE`/`RENDER_TRIMEND` mapping (`tailRenderSettingsFor`) and its realtime-window analog (`realtimeRecordWindowEnd`), the capture-action taxonomy table (`captureActionTable`) `main.cpp` iterates to register the CAPTURE_ITEM/CAPTURE_TRACK family, and `renderSourceLabel` (the source named in the offline backend's bounds refusal).
- `render_window` — the REAPER-free frame arithmetic behind exact capture bounds: `frameCountFor` (the frame count a project-time window occupies at the project rate — the number the offline backend checks the rendered file against before landing it, so a render that printed something other than the window is refused rather than banked), `renderHonoredBounds` (the gate's verdict and the sole home of its one-frame tolerance, which is empirical rather than proven — the header states which renderer models it covers and which it does not), and `itemExtentPrintsWindow`, the predicate `render_settings::sourceModeForScope` consults to decide whether REAPER's selected-items render source can express a requested window at all. It also owns the one short-render diagnostic: `msFlooredEndFrameCount` (the frames a window holds with its end floored to the millisecond — the shape two live short renders matched on the retired custom-bounds mode, quoted by a refusal as a count coincidence and nothing more) and `isOnMillisecondGrid`, the whole-millisecond tolerance that count depends on.
- `track_topology` — the REAPER-free folder arithmetic over a project's flat `I_FOLDERDEPTH` delta list: `directChildIndices` names a folder parent's DIRECT children, the set `shell/capture/render_isolation` silences so a ranged item capture does not print its track's children. Grandchildren are excluded by construction — they reach the parent only through the child that owns them.
- `tail_control` — the REAPER-free logic behind the docked `bank_panel`'s tail-mode toggle: the cycle order (None → Auto → Manual → None), the Manual-length clamp/scroll-wheel fine-adjust (`clampManualMs`/`adjustManualMs`, 250 ms/notch, 2000 ms default), the toggle's label text (e.g. "Tail: Manual 2.0s"), and the `TailSetting` JSON round-trip persist stores per-project.
@@ -79,12 +79,11 @@ Detail specific to these pure modules:
that with a transient silencing (`shell/capture/render_isolation`) whose child-set
walk lives here in `track_topology`; the item-vs-track asymmetry behind it is in
`src/shell/capture/CLAUDE.md`.
- **The render window floors to the millisecond at render time.** Measured cause,
why two bounds channels exist, and the live experiment: `render_settings.h`'s
`RenderBoundsChannel` — the one narrative home; this bullet is a pointer, not a
retelling. The one fact worth keeping local: every observation to date started at
`0s`, on the grid, so **nothing is known about whether the start floors too**
assume neither.
- **The custom-time-bounds field floors the render window to the millisecond; the
time selection does not.** Both observations and why only one bounds mode is
reachable: `render_settings.h`'s `kRenderBoundsTimeSelection` — the one narrative
home; this bullet is a pointer, not a retelling. Do not reintroduce
`RENDER_BOUNDSFLAG=0`.
- `kRenderPreFaderStems` (&8192) is deliberately **not** used — REAPER offline
render has no true pre-FX "dry" bit; FX scoping is done entirely by the
FX-bypass-around-render mechanism, never by a render bit.
+3 -36
View File
@@ -14,36 +14,7 @@ double autoTrimEndRatio() {
return std::pow(10.0, kAutoTrimThresholdDb / 20.0);
}
int renderBoundsFlagFor(RenderBoundsChannel channel) {
switch (channel) {
case RenderBoundsChannel::CustomTimeBounds: return 0;
case RenderBoundsChannel::TimeSelection: return 2;
}
// Unreachable for a valid enum; fail closed to the channel every shipped capture
// rendered on, never to a mode that bounds itself off something else entirely.
return 0;
}
int tailFlagBitFor(RenderBoundsChannel channel) {
switch (channel) {
case RenderBoundsChannel::CustomTimeBounds: return kTailFlagCustomBounds;
case RenderBoundsChannel::TimeSelection: return kTailFlagTimeSelection;
}
return kTailFlagCustomBounds; // paired with renderBoundsFlagFor's fallback
}
const char* renderBoundsChannelLabel(RenderBoundsChannel channel) {
switch (channel) {
case RenderBoundsChannel::CustomTimeBounds:
return "custom time bounds (RENDER_BOUNDSFLAG=0, RENDER_STARTPOS/RENDER_ENDPOS)";
case RenderBoundsChannel::TimeSelection:
return "time selection (RENDER_BOUNDSFLAG=2, GetSet_LoopTimeRange)";
}
return "unnamed bounds channel";
}
TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs,
RenderBoundsChannel channel) {
TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs) {
TailRenderSettings t;
switch (mode) {
case TailMode::None:
@@ -59,7 +30,7 @@ TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs,
// postprocessing bit clear. A fixed-threshold trim scales/limits/fades
// nothing, so identical requests trim at the identical sample -> holds
// the bit-identical-repeats invariant.
t.tailFlag = tailFlagBitFor(channel);
t.tailFlag = kTailFlagTimeSelection;
t.tailMs = kMaxTailMs;
t.normalize = kNormalizeTrimEnd;
t.trimEnd = autoTrimEndRatio();
@@ -67,7 +38,7 @@ TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs,
case TailMode::Manual:
// Clamped to the cap regardless of source; negative floors to 0.
t.tailFlag = tailFlagBitFor(channel);
t.tailFlag = kTailFlagTimeSelection;
t.tailMs = std::clamp(manualTailMs, 0.0, kMaxTailMs);
t.normalize = kNormalizeDisableAll;
t.trimEnd = 0.0;
@@ -143,10 +114,6 @@ const char* renderSourceLabel(SourceMode mode) {
return "unknown"; // unreachable for a valid enum; never claim a source
}
bool sourceBypassesBoundsChannel(SourceMode mode) {
return mode == SourceMode::SelectedItems || mode == SourceMode::RazorArea;
}
SourceMode sourceModeForScope(CaptureScope scope, bool itemExtentIsWindow) {
switch (scope) {
case CaptureScope::Item:
+26 -59
View File
@@ -27,51 +27,34 @@ inline constexpr int kRenderRazorEdits = 4096; // &4096 render razor e
// render wet; the scope decides which FX remain enabled.
inline constexpr int kRenderSingleFile = (4 << 16); // items/razor -> one file
// --- Render bounds channel ----------------------------------------------------
// --- Render bounds mode -------------------------------------------------------
//
// The RENDER_BOUNDSFLAG mode a capture hands its window over on (values verbatim,
// header ~3042: 0 = custom time bounds, 2 = time selection). RENDER_STARTPOS /
// RENDER_ENDPOS apply to mode 0 ONLY (header ~3045-3046), so the TimeSelection
// channel carries the window in the project's own time selection instead — a
// different store. That difference is the whole reason two channels exist: REAPER
// resolved a custom-bounds window on a whole-millisecond grid, floored the end, wrote
// the floored value back over RENDER_ENDPOS, and rendered exactly the floored frame
// count — twice, to the frame (docs/TODO.md "An offline capture can be refused..."
// records the observations). The same read-back at store time and immediately before
// the render was silent, so the field itself holds full double precision and the floor
// happens at render time. Whether that floor sits in the custom-bounds channel or
// downstream in the render engine (where no bounds mode escapes it) cannot be settled
// from the SDK header, only in a DAW. The offline backend therefore names the channel
// it used and what the landed file measured (render_window::describeBoundsExperiment)
// so one smoke run answers it. This is the one narrative home for why two channels
// exist; other sites point here rather than retelling it.
enum class RenderBoundsChannel {
CustomTimeBounds,
TimeSelection,
};
// The RENDER_BOUNDSFLAG value for a channel.
int renderBoundsFlagFor(RenderBoundsChannel channel);
// The channel in words, for the console verdict.
const char* renderBoundsChannelLabel(RenderBoundsChannel channel);
// A capture hands its window over on RENDER_BOUNDSFLAG=2 — the project's own TIME
// SELECTION (value verbatim, header ~3042), written through GetSet_LoopTimeRange.
//
// Custom time bounds (RENDER_BOUNDSFLAG=0, RENDER_STARTPOS/RENDER_ENDPOS, header
// ~3045-3046) must NOT be reintroduced: REAPER resolved a custom-bounds window on a
// whole-millisecond grid AT RENDER TIME, floored the end, wrote the floored value back
// over RENDER_ENDPOS, and rendered exactly the floored frame count — twice, to the
// frame. Re-rendering on this mode came back exact on both edges, including a start
// carrying a sub-millisecond remainder, which is what locates the floor in the
// custom-bounds field rather than downstream in the render engine. This is the one
// narrative home for that; other sites point here.
inline constexpr int kRenderBoundsTimeSelection = 2;
// --- Tail: RENDER_NORMALIZE / RENDER_TRIMEND bits + named constants ----------
//
// RENDER_TAILFLAG's bits are keyed PER BOUNDS MODE (header ~3047), so the bit a
// tail mode has to set follows the bounds channel the window went over — a tail
// set under the other channel's bit renders no tail at all. RENDER_NORMALIZE
// (verbatim, header ~3051): &32768 = trim ending silence (Auto path);
// &(4<<16) = disable all render postprocessing (None/Manual path).
// RENDER_NORMALIZE (verbatim, header ~3051): &32768 = trim ending silence (Auto
// path); &(4<<16) = disable all render postprocessing (None/Manual path).
inline constexpr int kNormalizeTrimEnd = 32768; // &32768 trim ending silence
inline constexpr int kNormalizeDisableAll = (4 << 16); // &(4<<16) = 262144, disable all
inline constexpr int kTailFlagNone = 0;
inline constexpr int kTailFlagCustomBounds = 1; // &1, header ~3047
inline constexpr int kTailFlagTimeSelection = 4; // &4, header ~3047
inline constexpr int kTailFlagNone = 0;
// The RENDER_TAILFLAG bit that applies to a channel's bounds mode.
int tailFlagBitFor(RenderBoundsChannel channel);
// RENDER_TAILFLAG's bits are keyed PER BOUNDS MODE (header ~3047): &4 is the
// time-selection mode's bit, the pair of kRenderBoundsTimeSelection above. A tail set
// under a different mode's bit renders no tail at all, so these two move together.
inline constexpr int kTailFlagTimeSelection = 4;
// Auto-trim trailing-silence threshold; single source of truth (RENDER_TRIMEND
// ratio derives from this dB, never the reverse). Daniel-set.
@@ -99,18 +82,15 @@ enum class TailMode {
// normalize bit is set (Auto). The backend reads these straight onto
// GetSetProjectInfo.
struct TailRenderSettings {
int tailFlag = kTailFlagNone; // RENDER_TAILFLAG (0 or the channel's bit)
int tailFlag = kTailFlagNone; // RENDER_TAILFLAG (0 or the bounds mode's bit)
double tailMs = 0.0; // RENDER_TAILMS
int normalize = kNormalizeDisableAll; // RENDER_NORMALIZE
double trimEnd = 0.0; // RENDER_TRIMEND (only used when trim bit set)
};
// Maps a tail mode (+ requested manual tail ms, used only for Manual) to its
// RENDER_* values. Manual is clamped to kMaxTailMs regardless of source. `channel`
// is a parameter rather than a caller-side OR so a bounds-channel change cannot
// leave Auto/Manual setting a tail bit the render no longer reads.
TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs,
RenderBoundsChannel channel);
// RENDER_* values. Manual is clamped to kMaxTailMs regardless of source.
TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs);
// The realtime record-window end (project seconds): realtime does NOT drive
// RENDER_*, it records a generous window and trims later, so this is where the
@@ -137,26 +117,13 @@ RenderSettingsChoice renderSettingsFor(SourceMode mode, double wetDry);
// bounds refusal: the two ways a render can miss its window — a source that
// derives its own bounds (selected items, razor edits) versus a time-bounded
// render that came up short — are indistinguishable from a frame count alone,
// and naming the source is what tells them apart in a bug report. Quoted verbatim
// in docs/VERIFICATION.md, which asks for this exact line back.
// and naming the source is what tells them apart in a bug report.
//
// MasterMix and TimeSelection deliberately answer the SAME words: they map to the
// same RENDER_SETTINGS value and every capture renders custom-time-bounded, so
// naming them apart would assert a render distinction that does not exist.
// same RENDER_SETTINGS value and render identically, so naming them apart would
// assert a render distinction that does not exist.
const char* renderSourceLabel(SourceMode mode);
// True for a render source INFERRED (not SDK-confirmed) to derive its bounds from
// content rather than RENDER_BOUNDSFLAG: SelectedItems (&32), per the observed-defect
// inference in src/core/capture/CLAUDE.md §Gotchas, and RazorArea (&4096) by analogy to
// it — the SDK header (~3042) actually separates bounds (RENDER_BOUNDSFLAG, its own
// value 4 = selected media items) from source (&32), which leans the other way.
// RazorArea stays in the set on that inference even though no offline capture path
// assigns it today — razor is a RANGE source (capture_batch.cpp's razor units render
// through track scope), not a render source of its own. A capture on either never
// consults RenderBoundsChannel, so describeBoundsExperiment's verdict must not be read
// as evidence about the channel — the caller names the source instead.
bool sourceBypassesBoundsChannel(SourceMode mode);
// --- Capture scope: the FX-scope invariant ------------------------------------
//
// See src/core/capture/CLAUDE.md for the scope contract. There is NO master
-130
View File
@@ -3,7 +3,6 @@
#include "core/capture/render_window.h"
#include <cmath>
#include <cstdio>
namespace reasampler::capture {
@@ -23,14 +22,6 @@ double floorToMilliseconds(double seconds) {
return std::floor(ms) / 1000.0;
}
// Full round-trip precision: a drift report whose two numbers print identically
// would be evidence of nothing.
std::string exactly(double seconds) {
char buf[32];
std::snprintf(buf, sizeof(buf), "%.17g", seconds);
return buf;
}
} // namespace
long long frameCountFor(double startSeconds, double endSeconds, int sampleRate) {
@@ -67,125 +58,4 @@ long long msFlooredEndFrameCount(double startSeconds, double endSeconds,
return frameCountFor(startSeconds, floorToMilliseconds(endSeconds), sampleRate);
}
std::string describeBoundsExperiment(const char* channelLabel,
double reqStart, double reqEnd,
long long actualFrames, int sampleRate,
const char* bypassingSourceLabel) {
std::string s = "bounds channel: ";
s += (channelLabel && channelLabel[0]) ? channelLabel : "unnamed bounds channel";
s += ". ";
if (bypassingSourceLabel && bypassingSourceLabel[0]) {
s += "NOT JUDGED -- this capture's render source is " +
std::string(bypassingSourceLabel) +
", INFERRED (not SDK-confirmed) to derive its bounds from content rather"
" than consult this channel; this capture is not evidence either way about it.";
return s;
}
if (sampleRate <= 0) {
s += "NOT JUDGED -- this capture's frames were never counted against the "
"window, so this capture is not evidence either way about the channel.";
return s;
}
const long long expected = frameCountFor(reqStart, reqEnd, sampleRate);
const long long delta = actualFrames - expected;
// A window under a frame at this rate has nothing to compare: a 0-frame render
// against a 0-frame window is a coincidence of degenerate inputs, not a match.
if (expected == 0 && actualFrames == 0) {
s += "NOT JUDGED -- the requested window rounds to 0 frames at this rate, so a "
"0-frame render is not evidence either way about the channel.";
return s;
}
// Within the gate's own edge-convention slack (render_window.h): its normal
// tolerance, not evidence the millisecond floor was escaped or hit.
const bool withinTolerance =
delta != 0 && renderHonoredBounds(expected, actualFrames);
s += (delta == 0) ? "EXACT" : (delta < 0 ? "SHORT" : "LONG");
if (withinTolerance) s += " (WITHIN TOLERANCE)";
s += " -- the landed render holds " + std::to_string(actualFrames) +
" frames against the " + std::to_string(expected) +
" the window asks for at " + std::to_string(sampleRate) + " Hz.";
// The shape both live short renders matched to the frame. A match says this channel
// produced a floored window; it does not locate where inside REAPER the floor is. A
// floor only removes frames, so this can only ever match a SHORT, never a LONG.
if (delta < 0) {
const long long msFloored =
msFlooredEndFrameCount(reqStart, reqEnd, sampleRate);
if (msFloored > 0 && actualFrames == msFloored)
s += " That is exactly the count this window holds with its end floored to"
" the millisecond -- the shape REAPER's render was measured producing.";
}
s += isOnMillisecondGrid(reqStart)
? " The START edge is UNTESTED here: " + exactly(reqStart) +
"s is already on the millisecond grid, which floor, ceil and round all leave"
" alone. Re-run over a range starting off the grid to test it."
: " The START edge IS tested here: " + exactly(reqStart) +
"s carries a sub-millisecond remainder.";
// EXACT is proof only when no millisecond-floored model of this window could have
// produced the same count. Grid membership on an edge is a PROXY for that collision,
// not the test itself: sub-millisecond remainders on the two edges can cancel under
// a full floor even when neither edge is on the grid (a dragged, fixed-length time
// selection reproduces this), and a remainder under half a frame collides with a
// floored edge without ever registering as off-grid. Enumerate every floored model
// directly rather than inferring from grid membership.
if (delta == 0) {
const double flooredStart = floorToMilliseconds(reqStart);
const double flooredEnd = floorToMilliseconds(reqEnd);
const bool startAlone =
actualFrames == frameCountFor(flooredStart, reqEnd, sampleRate);
const bool endAlone =
actualFrames == frameCountFor(reqStart, flooredEnd, sampleRate);
const bool bothTogether =
actualFrames == frameCountFor(flooredStart, flooredEnd, sampleRate);
if (startAlone || endAlone || bothTogether) {
std::string models;
auto addModel = [&](const char* label) {
if (!models.empty()) models += ", or ";
models += label;
};
if (startAlone) addModel("floors the START edge alone");
if (endAlone) addModel("floors the END edge alone");
if (bothTogether) addModel("floors START and END together");
s += " EXACT here is not proof: a render that " + models +
" to the millisecond would print this identical count -- re-run over a"
" window where a floored edge would show a different count before"
" reading EXACT as the fix.";
}
}
return s;
}
std::string describeBoundsDrift(double reqStart, double reqEnd,
double storedStart, double storedEnd,
int sampleRate) {
// Bit equality, deliberately: the caller wrote these exact doubles and read them
// straight back, so anything but the same bits is a value REAPER changed.
if (storedStart == reqStart && storedEnd == reqEnd) return {};
// Says only that the two differ, not why -- a legitimate clamp (negative start,
// end past project end) reads back differently for the same reason a precision
// defect would, and this sentence cannot tell those apart.
std::string s = "REAPER read back different render bounds than it was handed -- "
"asked for [" +
exactly(reqStart) + "s, " + exactly(reqEnd) + "s), read back [" +
exactly(storedStart) + "s, " + exactly(storedEnd) + "s).";
if (sampleRate > 0) {
s += " The stored window is " +
std::to_string(frameCountFor(storedStart, storedEnd, sampleRate)) +
" frames against the " +
std::to_string(frameCountFor(reqStart, reqEnd, sampleRate)) +
" the request asks for, at " + std::to_string(sampleRate) + " Hz.";
}
return s;
}
} // namespace reasampler::capture
+15 -73
View File
@@ -1,14 +1,10 @@
#pragma once
// render_window — pure frame arithmetic for a capture's requested window: the
// frame count a project-time range occupies, whether a render whose bounds come
// from the selected items' own extent already prints that window, and the
// diagnostics that bound a short render: whether the stored bounds round-tripped,
// whether the shortfall matches a millisecond-floor coincidence, and the verdict on
// which bounds channel a capture used and what it produced.
// render_window — pure frame arithmetic for a capture's requested window: the frame
// count a project-time range occupies, whether a render whose bounds come from the
// selected items' own extent already prints that window, and the one diagnostic a
// refused render quotes — whether its shortfall matches a millisecond-floor coincidence.
// NO REAPER types; unit-tested by tests/test_render_window.cpp.
#include <string>
namespace reasampler::capture {
// Frames the [startSeconds, endSeconds) window occupies at `sampleRate`. Both
@@ -30,8 +26,7 @@ long long frameCountFor(double startSeconds, double endSeconds, int sampleRate);
// end edge by DIFFERENT conventions can legitimately sit TWO frames from this answer
// (tests/test_render_window.cpp pins both facts). Which model REAPER uses is
// unverified, so a refusal one or two frames wide may be this gate's fault rather than
// the render's — the open DAW question in docs/VERIFICATION.md §Capture range and
// bounds. Widening past one frame retires the exact-bounds invariant rather than
// the render's. Widening past one frame retires the exact-bounds invariant rather than
// relaxing it, and is not a fix to reach for before that question is answered.
bool renderHonoredBounds(long long expectedFrames, long long actualFrames);
@@ -50,9 +45,11 @@ bool itemExtentPrintsWindow(double reqStart, double reqEnd,
// --- Diagnostics: where a short render lost its frames ------------------------
// The frames this window would hold if its END were resolved on a whole-millisecond
// grid, floored, instead of exactly. That is what REAPER's offline render does: two
// live short renders (48 kHz, TailMode::None) printed this count to the frame, and the
// after-render read-back showed REAPER's own resolved end floored to the same value.
// grid, floored, instead of exactly. That is what REAPER's offline render did on the
// retired custom-time-bounds mode (render_settings.h's kRenderBoundsTimeSelection states
// the whole observation): two live short renders (48 kHz, TailMode::None) printed this
// count to the frame. Kept as the refusal's shape check — a refused render matching it
// says the floor is back, on a mode that was measured escaping it.
//
// Still a DESCRIPTION, never a request: nothing renders from this number and no capture
// path asks for it — a refusal quotes it to say the shortfall has the known shape, which
@@ -64,70 +61,15 @@ bool itemExtentPrintsWindow(double reqStart, double reqEnd,
//
// The tolerance is ours, not REAPER's: on a `1.007`-class grid point, a REAPER floor
// that does NOT carry the same epsilon would miss this shape entirely, and a real
// floored render would then read as an unmatched SHORT rather than the known one
// silence here is not proof the floor didn't happen (docs/TODO.md records why this
// floored render would then read as an unmatched short render rather than the known one
// silence here is not proof the floor didn't happen (docs/TODO.md records why this
// premise needs a DAW measurement before anything is built on it).
long long msFlooredEndFrameCount(double startSeconds, double endSeconds,
int sampleRate);
// True when `seconds` sits on a whole-millisecond boundary, under the same nanosecond
// tolerance msFlooredEndFrameCount uses and for the same reason (stated there).
//
// Load-bearing for reading a bounds observation: an on-grid edge is left alone by
// floor, ceil and round alike, so a window whose START is on the grid can say nothing
// about whether REAPER resolves the start edge the way it resolves the end.
// True when `seconds` sits on a whole-millisecond boundary, under the nanosecond
// tolerance msFlooredEndFrameCount depends on and for the reason stated there. Public so
// that premise is testable directly rather than only through the count it feeds.
bool isOnMillisecondGrid(double seconds);
// The one-line verdict on what a capture's bounds channel did with its window: which
// channel carried it (or, when the render source defines the window itself, which
// source bypassed the channel entirely), the frames the landed file holds against the
// frames the window asks for, and whether this run could test the START and END edges
// at all. Always non-empty — a capture that answered nothing has to say so, or its
// silence reads as a pass.
//
// EXACT never stands alone as proof: the observed count is checked against every
// millisecond-floored model of the same window (start floored alone, end floored alone,
// both together), and any model that reproduces it is named in the sentence. Grid
// membership on an edge is a PROXY for that collision, not the test itself — remainders
// on the two edges can cancel under a full floor even when NEITHER edge sits on the
// grid, and a remainder under half a frame collides with a floored edge without ever
// registering as off-grid at all. Checking the models directly is what a grid test on
// either edge alone cannot do.
//
// A non-zero delta that still falls inside the gate's own tolerance (renderHonoredBounds)
// is tagged "(WITHIN TOLERANCE)" — that is the gate's ordinary edge-convention slack, not
// evidence of the millisecond floor. The floor's signature is ONLY the "floored to the
// millisecond" sentence (a delta matching msFlooredEndFrameCount exactly); a bare
// SHORT with no such sentence means the shortfall's cause is unestablished, and LONG can
// never be the floor's signature — a floor only removes frames, never adds them. A
// window whose start and end sit in the same millisecond bucket makes msFlooredEndFrameCount
// equal the exact count, so a real one-frame floor there reads as a bare SHORT (WITHIN
// TOLERANCE) with no floor sentence at all — that combination is not evidence the floor
// didn't happen, just a case this diagnostic can't see into.
//
// `sampleRate <= 0` means the landed file was never measured: a tail mode adds frames by
// design and is not judged, an empty render has none, and a render whose layout failed to
// parse or declared no sample rate is refused before it can be judged either — the
// sentence then says the run answered nothing rather than inventing a comparison. A
// window that rounds to 0 frames at this rate reads NOT JUDGED the same way: a 0-frame
// render against a 0-frame window is not a comparison either.
//
// `bypassingSourceLabel`, when non-null and non-empty, means the render source itself
// defined the window (render_settings::sourceBypassesBoundsChannel) — `channelLabel` was
// never consulted, so the verdict names the source instead and reads NOT JUDGED
// regardless of how the frame counts compare.
std::string describeBoundsExperiment(const char* channelLabel,
double reqStart, double reqEnd,
long long actualFrames, int sampleRate,
const char* bypassingSourceLabel = nullptr);
// The sentence a capture prints when the render bounds it handed REAPER did not read
// back unchanged — the requested window, what came back, and both frame counts at
// `sampleRate` (omitted when the rate is unknown). EMPTY when both edges read back
// bit-identical, which is the only answer proving the request crossed into REAPER
// intact; a caller prints this only when it is non-empty.
std::string describeBoundsDrift(double reqStart, double reqEnd,
double storedStart, double storedEnd,
int sampleRate);
} // namespace reasampler::capture