Files
reasampler/tests/test_render_window.cpp
T
daniel 2005f90c66 capture: state the bounds tolerance as empirical, refuse unmeasurable renders, keep refused ones for diagnosis
The one-frame bound is not provable for a per-edge renderer; the test now shows where it breaks. Refused renders move out of the bank instead of being deleted, so the DAW experiment has something to read.
2026-08-02 07:23:30 -04:00

252 lines
12 KiB
C++

// Standalone tests for reasampler::render_window — no REAPER, no framework.
// Covers the bounds-equality number (a window's exact frame count at the project
// rate), the verdict the offline backend refuses a capture on, and the predicate
// that decides whether REAPER's selected-items render source can express a
// requested window at all.
#include "../src/core/capture/render_window.h"
#include <cmath>
#include <cstdio>
using namespace reasampler::capture;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
// --- frameCountFor: the bounds equality, stated as a number ------------------
static void testFrameCountIsExactNotRounded() {
// A 1.5 s window at 48 kHz is exactly 72000 frames — the number a capture of
// that range must produce. No rounding slack in either direction.
CHECK(frameCountFor(2.0, 3.5, 48000) == 72000);
// The same duration at a different offset still counts the same frames when
// both edges are frame-aligned.
CHECK(frameCountFor(10.0, 11.5, 48000) == 72000);
// 44.1 kHz: 0.5 s = 22050 frames.
CHECK(frameCountFor(1.0, 1.5, 44100) == 22050);
}
static void testFrameCountIsADifferenceOfIndicesNotADuration() {
// Both edges land mid-frame at 100 Hz (0.005 s = half a frame). Rounding the
// DURATION would give 1 frame; rounding each EDGE gives 0.005 -> frame 1 and
// 0.015 -> frame 2, i.e. 1 frame. Shift the window so the edges round apart
// and the count changes — the property that makes this a window, not a length.
CHECK(frameCountFor(0.005, 0.015, 100) == 1);
CHECK(frameCountFor(0.004, 0.016, 100) == 2);
}
static void testFrameCountRefusesEmptyInvertedAndUnknownRate() {
CHECK(frameCountFor(3.0, 3.0, 48000) == 0); // empty
CHECK(frameCountFor(3.0, 1.0, 48000) == 0); // inverted
CHECK(frameCountFor(1.0, 2.0, 0) == 0); // rate unknown
CHECK(frameCountFor(1.0, 2.0, -1) == 0); // rate nonsensical
}
static void testWindowStartingAtExactlyZero() {
CHECK(frameCountFor(0.0, 1.0, 48000) == 48000);
// The window from the reported blocker: it starts at 0 and its end lands a
// quarter of a frame off the grid at 48 kHz.
CHECK(frameCountFor(0.0, 4.067797, 48000) == 195254);
}
// --- renderHonoredBounds: the gate's verdict ---------------------------------
static void testNonFrameAlignedWindowAcceptsItsAdjacentCounts() {
// 4.067797 s at 48 kHz is 195254.26 frames — not a frame boundary. A correct
// render lands on 195254, and both adjacent counts are inside the gate.
const long long expected = frameCountFor(0.0, 4.067797, 48000);
CHECK(expected == 195254);
CHECK(renderHonoredBounds(expected, 195254));
CHECK(renderHonoredBounds(expected, 195255));
CHECK(renderHonoredBounds(expected, 195253));
// The shortfall actually reported from the DAW is 38 frames — far outside any
// alignment slack, so it is a render that missed the window, and is refused.
CHECK(!renderHonoredBounds(expected, 195216));
}
static void testLengthDerivedAndSameConventionRenderersStayWithinOneFrame() {
// What the one-frame tolerance is actually good for. Two families of renderer are
// inside it at every offset swept here: one that derives its count from the
// window's LENGTH (floor/ceil/round of (end-start)*rate), and one that resolves
// each EDGE to a frame using the SAME convention on both edges. Every count below
// is computed from the window, never from frameCountFor, so this compares two
// derivations rather than restating one. Round-both-edges is omitted deliberately:
// that IS frameCountFor's own convention, so asserting it would be tautological.
//
// 8192 is a power of two, so an eighth of a frame is exact in double there and the
// .5 rounding ties are really hit; at 48000/44100 (the shipping rates) they are
// only approached, which is why all three are swept.
struct Window { double start; double end; };
const int rates[] = {48000, 44100, 8192};
const Window windows[] = {{3.0, 7.5}, {0.0, 4.067797}, {10.25, 10.75}};
for (int rate : rates) {
for (const Window& w : windows) {
for (int s = 0; s < 8; ++s) {
for (int e = 0; e < 8; ++e) {
const double start = w.start + s / (8.0 * rate);
const double end = w.end + e / (8.0 * rate);
const long long expected = frameCountFor(start, end, rate);
const double length = (end - start) * rate;
CHECK(renderHonoredBounds(expected,
static_cast<long long>(std::floor(length))));
CHECK(renderHonoredBounds(expected,
static_cast<long long>(std::ceil(length))));
CHECK(renderHonoredBounds(expected, std::llround(length)));
const double startFrames = start * rate;
const double endFrames = end * rate;
CHECK(renderHonoredBounds(
expected, static_cast<long long>(std::floor(endFrames) -
std::floor(startFrames))));
CHECK(renderHonoredBounds(
expected, static_cast<long long>(std::ceil(endFrames) -
std::ceil(startFrames))));
}
}
}
}
}
static void testMixedEdgeConventionsCanMissByTwoAndAreRefused() {
// The hole in that bound, stated rather than hidden. A renderer that resolves the
// two edges by DIFFERENT conventions lands two frames from frameCountFor's answer
// whenever the start sits past mid-frame and the end before it (resolved outward),
// or the mirror image (resolved inward). The gate refuses both — correctly if
// REAPER derives its count from the window's length, wrongly if it resolves edges
// this way. No unit test can settle which; see render_window.h.
const int rate = 8192; // power of two: the eighth-frame offsets below are exact
// Outward: start .625 into a frame, end .375 into one.
const double start = 10.25 + 5.0 / (8.0 * rate);
const double end = 10.75 + 3.0 / (8.0 * rate);
CHECK(start * rate == 83968.625); // the premise, not an outcome — pinned so a
CHECK(end * rate == 88064.375); // representability slip can't fake the result
const long long expected = frameCountFor(start, end, rate);
CHECK(expected == 4095);
const long long outward = static_cast<long long>(std::ceil(end * rate) -
std::floor(start * rate));
CHECK(outward == 4097);
CHECK(!renderHonoredBounds(expected, outward));
// Inward, mirrored fractions.
const double start2 = 10.25 + 3.0 / (8.0 * rate);
const double end2 = 10.75 + 5.0 / (8.0 * rate);
const long long expected2 = frameCountFor(start2, end2, rate);
CHECK(expected2 == 4097);
const long long inward = static_cast<long long>(std::floor(end2 * rate) -
std::ceil(start2 * rate));
CHECK(inward == 4095);
CHECK(!renderHonoredBounds(expected2, inward));
}
static void testWholeItemWideningIsStillRefused() {
// The defect the gate was built for: a 1 s window inside a 30 s item printing
// the whole item.
const long long expected = frameCountFor(5.0, 6.0, 48000);
CHECK(expected == 48000);
CHECK(!renderHonoredBounds(expected, 30 * 48000));
}
static void testLargeShortfallIsStillRefused() {
const long long expected = frameCountFor(0.0, 4.067797, 48000);
CHECK(!renderHonoredBounds(expected, 190000));
// Two frames is the smallest miss outside the tolerance, in both directions —
// the tolerance is one frame and stays one frame.
CHECK(!renderHonoredBounds(expected, expected - 2));
CHECK(!renderHonoredBounds(expected, expected + 2));
}
static void testEmptyRenderIsRefusedAgainstARealWindow() {
// A render that produced nothing is a bounds miss like any other. A render whose
// frames could not be MEASURED never reaches this predicate — shell/capture/
// render_bounds_gate refuses it before the comparison.
CHECK(!renderHonoredBounds(48000, 0));
}
// --- itemExtentPrintsWindow: can the selected-items source express this? -----
static void testRangeInsideItemCannotBeExpressed() {
// The defect this whole module exists for: a 1 s selection inside a 30 s item.
// The selected-items source would print the item's 30 s, not the 1 s asked for,
// so the capture must NOT take that path.
CHECK(!itemExtentPrintsWindow(5.0, 6.0, /*item*/ 0.0, 30.0, 48000));
}
static void testRangeWiderThanItemCannotBeExpressedEither() {
// The same violation in the other direction: a 10 s selection over a 6 s item
// would print 6 s. Under-printing is a bounds violation exactly as much as
// over-printing is.
CHECK(!itemExtentPrintsWindow(0.0, 10.0, /*item*/ 2.0, 8.0, 48000));
}
static void testEachEdgeAloneDisqualifies() {
// Matching start, drifting end.
CHECK(!itemExtentPrintsWindow(2.0, 8.0, 2.0, 9.0, 48000));
// Matching end, drifting start.
CHECK(!itemExtentPrintsWindow(2.0, 8.0, 1.0, 8.0, 48000));
}
static void testExtentEqualToWindowIsExpressible() {
// The regression floor: a capture whose range IS the item's extent keeps the
// selected-items render, byte-identical to what it produces today.
CHECK(itemExtentPrintsWindow(2.0, 8.0, 2.0, 8.0, 48000));
}
static void testSubFrameDriftStillPrintsTheSameFrames() {
// A time selection snapped a fraction of a sample off the item edge prints the
// identical frames, so it must NOT be pushed onto the time-bounded path — that
// would swap the render mechanism under a capture that was already exact.
const double eighthOfAFrameAt48k = 1.0 / (48000.0 * 8.0);
CHECK(itemExtentPrintsWindow(2.0 + eighthOfAFrameAt48k, 8.0 - eighthOfAFrameAt48k,
2.0, 8.0, 48000));
// A full frame of drift is a real difference and must disqualify.
const double oneFrameAt48k = 1.0 / 48000.0;
CHECK(!itemExtentPrintsWindow(2.0 + oneFrameAt48k, 8.0, 2.0, 8.0, 48000));
}
static void testUnknownRateFallsBackToExactEquality() {
// With no project rate there is no frame grid to compare on. Exact equality
// still recognizes the regression floor...
CHECK(itemExtentPrintsWindow(2.0, 8.0, 2.0, 8.0, 0));
// ...and anything else takes the time-bounded render, which honors the request
// whatever the rate turns out to be.
const double eighthOfAFrameAt48k = 1.0 / (48000.0 * 8.0);
CHECK(!itemExtentPrintsWindow(2.0 + eighthOfAFrameAt48k, 8.0, 2.0, 8.0, 0));
CHECK(!itemExtentPrintsWindow(5.0, 6.0, 0.0, 30.0, 0));
}
static void testMultiItemUnionExtent() {
// Two items spanning 1..4 and 6..9 present a 1..9 union extent to the render.
// A selection over the whole union is expressible; one over only the first
// item's half is not.
CHECK(itemExtentPrintsWindow(1.0, 9.0, 1.0, 9.0, 48000));
CHECK(!itemExtentPrintsWindow(1.0, 4.0, 1.0, 9.0, 48000));
}
int main() {
testFrameCountIsExactNotRounded();
testFrameCountIsADifferenceOfIndicesNotADuration();
testFrameCountRefusesEmptyInvertedAndUnknownRate();
testWindowStartingAtExactlyZero();
testNonFrameAlignedWindowAcceptsItsAdjacentCounts();
testLengthDerivedAndSameConventionRenderersStayWithinOneFrame();
testMixedEdgeConventionsCanMissByTwoAndAreRefused();
testWholeItemWideningIsStillRefused();
testLargeShortfallIsStillRefused();
testEmptyRenderIsRefusedAgainstARealWindow();
testRangeInsideItemCannotBeExpressed();
testRangeWiderThanItemCannotBeExpressedEither();
testEachEdgeAloneDisqualifies();
testExtentEqualToWindowIsExpressible();
testSubFrameDriftStillPrintsTheSameFrames();
testUnknownRateFallsBackToExactEquality();
testMultiItemUnionExtent();
if (g_fail) { std::printf("%d check(s) FAILED\n", g_fail); return 1; }
std::printf("render_window: all checks passed\n");
return 0;
}