a91df760cc
The tolerance is unchanged and now derived, not assumed: frameCountFor lands in {floor(L), ceil(L)}, so a non-frame-aligned window can never miss by more than a frame. Naming the source is what tells a self-bounding render from a short one.
197 lines
8.9 KiB
C++
197 lines
8.9 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 testNonFrameAlignedWindowAcceptsEveryEdgeConvention() {
|
|
// 4.067797 s at 48 kHz is 195254.26 frames — not a frame boundary. A correct
|
|
// render lands on 195254, and the neighbours a different edge convention would
|
|
// produce 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 testNonAlignmentCanNeverExceedOneFrame() {
|
|
// The provable content of the one-frame tolerance: frameCountFor rounds each
|
|
// edge, so it lands within a frame of the window's real length — and so does a
|
|
// renderer that floors, ceils or rounds that same length. Sweep both edges over
|
|
// every eighth of a frame; no pairing may fall outside the gate. The renderer's
|
|
// count is derived from the length here, independently of frameCountFor.
|
|
const int rate = 48000;
|
|
for (int s = 0; s < 8; ++s) {
|
|
for (int e = 0; e < 8; ++e) {
|
|
const double start = 3.0 + s / (8.0 * rate);
|
|
const double end = 7.5 + 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)));
|
|
}
|
|
}
|
|
}
|
|
|
|
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; the backend's
|
|
// own "did the file parse" guard is what keeps an unreadable render out of here.
|
|
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();
|
|
testNonFrameAlignedWindowAcceptsEveryEdgeConvention();
|
|
testNonAlignmentCanNeverExceedOneFrame();
|
|
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;
|
|
}
|