bcdf97d6c4
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.
134 lines
8.5 KiB
C++
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
|