#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 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