Files
reasampler/src/core/capture/render_window.h
T
daniel bcdf97d6c4 Fix render-bounds EXACT verdict: enumerate floored models instead of trusting grid membership
Grid-ness of an edge was a proxy for "no floor could explain this count," not
the test itself — equal remainders on both edges cancel under a full floor. Now
checks all three floored models directly and corrects the SHORT/LONG
floor-signature docs.
2026-08-02 16:21:46 -04:00

134 lines
8.5 KiB
C++

#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.
// 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
// edges are resolved to the NEAREST frame boundary and subtracted, so the answer
// is a difference of frame indices rather than a rounded duration — two windows
// of equal length at different offsets can legitimately differ by one frame.
// Returns 0 for a non-positive rate or an empty/inverted window.
//
// The offline backend compares this against the rendered file's own frame count, so
// exact-bounds failures surface as a refused capture rather than a wrong file.
long long frameCountFor(double startSeconds, double endSeconds, int sampleRate);
// True when a landed render's frame count is consistent with `frameCountFor`'s
// answer for the same window. Tolerates a one-frame difference, and exactly one.
//
// That bound is EMPIRICAL. It is provable only for renderer models that derive the
// count from the window's LENGTH (floor/ceil/round of (end-start)*rate) or resolve
// both edges by the SAME convention; a renderer that resolves the start edge and the
// 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
// relaxing it, and is not a fix to reach for before that question is answered.
bool renderHonoredBounds(long long expectedFrames, long long actualFrames);
// True when a render bounded by the selected items' own extent
// [itemStart, itemEnd) already prints exactly the requested
// [reqStart, reqEnd) window — the one case where REAPER's selected-items render
// source is believed to need no correction (the bounds-override inference behind
// that is unverified; src/core/capture/CLAUDE.md §Gotchas states what it rests on).
// Compared at frame resolution, because a sub-frame difference prints the same
// frames. An unknown rate (<= 0) falls back to exact equality, which can only send
// a window to the time-bounded render, never widen one.
bool itemExtentPrintsWindow(double reqStart, double reqEnd,
double itemStart, double itemEnd,
int sampleRate);
// --- 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.
//
// 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
// is not the same as proving that this particular render took it. Whole-millisecond values
// are recognized within a nanosecond, because a decimal millisecond is not always one
// in binary (1.007 * 1000 lands just below 1007) and a bare floor would drop a
// millisecond from a window already on the grid. A nanosecond is far under one frame
// at any rate we render, so a real sub-millisecond remainder still floors.
//
// 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
// 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.
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