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.
719 lines
36 KiB
C++
719 lines
36 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, the predicate
|
|
// that decides whether REAPER's selected-items render source can express a
|
|
// requested window at all, and the two short-render diagnostics.
|
|
|
|
#include "../src/core/capture/render_window.h"
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <string>
|
|
|
|
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)
|
|
|
|
static bool contains(const std::string& haystack, const std::string& needle) {
|
|
return haystack.find(needle) != std::string::npos;
|
|
}
|
|
|
|
// --- 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));
|
|
}
|
|
|
|
// --- msFlooredEndFrameCount: the shape both live short renders had ------------
|
|
|
|
static void testMillisecondFlooredEndReproducesBothShortRenders() {
|
|
// Both DAW observations, as arithmetic. 48 kHz, TailMode::None, start at 0: the
|
|
// requested window's count, and the count its end floored to the millisecond
|
|
// holds — which is what each render actually printed.
|
|
CHECK(frameCountFor(0.0, 4.067797, 48000) == 195254);
|
|
CHECK(msFlooredEndFrameCount(0.0, 4.067797, 48000) == 195216);
|
|
CHECK(frameCountFor(0.0, 4.067797, 48000) -
|
|
msFlooredEndFrameCount(0.0, 4.067797, 48000) == 38);
|
|
|
|
CHECK(frameCountFor(0.0, 1.655172, 48000) == 79448);
|
|
CHECK(msFlooredEndFrameCount(0.0, 1.655172, 48000) == 79440);
|
|
CHECK(frameCountFor(0.0, 1.655172, 48000) -
|
|
msFlooredEndFrameCount(0.0, 1.655172, 48000) == 8);
|
|
}
|
|
|
|
static void testTheSixDecimalDisplayDidNotCreateTheEffect() {
|
|
// Both reported ends were printed to six decimals by the refusal. Each is one 4/4
|
|
// bar — at 59 BPM and at 145 BPM — so the full-precision doubles behind them are
|
|
// 240/59 and 240/145. Same counts either way: the display rounding is not what
|
|
// produces the shortfall.
|
|
CHECK(frameCountFor(0.0, 240.0 / 59.0, 48000) == 195254);
|
|
CHECK(msFlooredEndFrameCount(0.0, 240.0 / 59.0, 48000) == 195216);
|
|
CHECK(frameCountFor(0.0, 240.0 / 145.0, 48000) == 79448);
|
|
CHECK(msFlooredEndFrameCount(0.0, 240.0 / 145.0, 48000) == 79440);
|
|
}
|
|
|
|
static void testWindowAlreadyOnTheMillisecondGridLosesNothing() {
|
|
// The "sometimes it works" case: a bar at 120 BPM is exactly 2 s.
|
|
CHECK(msFlooredEndFrameCount(0.0, 2.0, 48000) == frameCountFor(0.0, 2.0, 48000));
|
|
|
|
// The binary-representation trap a bare floor would fall into. The premise, not an
|
|
// outcome: 1.007 s is a whole millisecond that really does land BELOW 1007 ms in
|
|
// double, so flooring it without a tolerance drops a millisecond from a window
|
|
// already on the grid.
|
|
CHECK(1.007 * 1000.0 < 1007.0);
|
|
CHECK(frameCountFor(0.0, 1.007, 48000) == 48336);
|
|
CHECK(msFlooredEndFrameCount(0.0, 1.007, 48000) == 48336);
|
|
// Same end reached from a non-zero start, so nothing here rests on the window
|
|
// beginning at 0.
|
|
CHECK(msFlooredEndFrameCount(0.5, 1.007, 48000) ==
|
|
frameCountFor(0.5, 1.007, 48000));
|
|
}
|
|
|
|
static void testOneFrameOfRemainderStillFloors() {
|
|
// The whole-millisecond tolerance must sit far below a frame, or it would swallow
|
|
// the very remainder this diagnostic exists to find. A remainder JUST BELOW a
|
|
// millisecond boundary is the discriminating case: one frame short of 1.0 s is
|
|
// 999.979166 ms, only ~0.0208 ms off the next whole millisecond. The shipped
|
|
// nanosecond tolerance still floors it down; a tolerance any wider than ~0.021 ms
|
|
// would snap it up to the millisecond instead and this test would then see 48000,
|
|
// not 47952 — which is what would fail if the tolerance regressed to something
|
|
// that wide.
|
|
const double oneFrame = 1.0 / 48000.0;
|
|
CHECK(frameCountFor(0.0, 1.0 - oneFrame, 48000) == 47999);
|
|
CHECK(msFlooredEndFrameCount(0.0, 1.0 - oneFrame, 48000) == 47952);
|
|
}
|
|
|
|
static void testMillisecondFloorAt44100WhereAMillisecondIsNotWholeFrames() {
|
|
// 44.1 kHz: a millisecond is 44.1 frames, so a floored end cannot be described as
|
|
// dropping a whole number of frames — the count still resolves exactly.
|
|
CHECK(frameCountFor(0.0, 0.0105, 44100) == 463);
|
|
CHECK(msFlooredEndFrameCount(0.0, 0.0105, 44100) == 441);
|
|
// And a window that IS on the millisecond grid there is untouched, even though its
|
|
// edge is not on a frame boundary.
|
|
CHECK(frameCountFor(0.0, 0.010, 44100) == 441);
|
|
CHECK(msFlooredEndFrameCount(0.0, 0.010, 44100) == 441);
|
|
}
|
|
|
|
static void testASubMillisecondStartWouldNotHideItself() {
|
|
// Both observations started at 0.000000s, the one value that hides a start-side
|
|
// truncation. A window whose START carries a sub-millisecond remainder counts from
|
|
// that exact start...
|
|
const double start = 1.0001724, end = 2.0001724;
|
|
CHECK(frameCountFor(start, end, 48000) == 48000);
|
|
// ...so a start floored to the millisecond would print a DIFFERENT count — 8 frames
|
|
// more, the same remainder the second observation lost off its end. A start-side
|
|
// truncation is therefore visible to the same frame-count gate, not silent.
|
|
CHECK(frameCountFor(1.000, end, 48000) == 48008);
|
|
CHECK(!renderHonoredBounds(frameCountFor(start, end, 48000),
|
|
frameCountFor(1.000, end, 48000)));
|
|
}
|
|
|
|
static void testTheTwoLiveShortRendersPinnedAtFullPrecision() {
|
|
// 1.6551724137931001 is the console's own %.17g read-back. 4.0677966101694913 is
|
|
// the double nearest the six-decimal value (4.067797) the earlier refusal actually
|
|
// printed -- that refusal predates the %.17g printer (git history has no commit
|
|
// introducing this literal as a console value), so it is a reconstruction, not a
|
|
// captured one. 240/145 and 240/59 (testTheSixDecimalDisplayDidNotCreateTheEffect)
|
|
// produce the SAME counts as the literals here, so this test cannot distinguish the
|
|
// real value from the reconstruction either -- it pins the count regression (full
|
|
// precision or six-decimal input, the frame counts agree), not which double REAPER
|
|
// was really handed.
|
|
CHECK(frameCountFor(0.0, 1.6551724137931001, 48000) == 79448);
|
|
CHECK(msFlooredEndFrameCount(0.0, 1.6551724137931001, 48000) == 79440);
|
|
|
|
CHECK(frameCountFor(0.0, 4.0677966101694913, 48000) == 195254);
|
|
CHECK(msFlooredEndFrameCount(0.0, 4.0677966101694913, 48000) == 195216);
|
|
|
|
// And the counts REAPER produced are outside the gate's tolerance in both cases —
|
|
// the refusals were correct, not an artifact of the one-frame slack.
|
|
CHECK(!renderHonoredBounds(79448, 79440));
|
|
CHECK(!renderHonoredBounds(195254, 195216));
|
|
}
|
|
|
|
// --- isOnMillisecondGrid: whether an observation can speak to an edge ----------
|
|
|
|
static void testOnGridRecognizesWholeMillisecondsIncludingTheBinaryTrap() {
|
|
CHECK(isOnMillisecondGrid(0.0));
|
|
CHECK(isOnMillisecondGrid(2.0));
|
|
CHECK(isOnMillisecondGrid(0.001));
|
|
// 1.007 s does not multiply to exactly 1007.0 in double (pinned as the premise in
|
|
// testWindowAlreadyOnTheMillisecondGridLosesNothing) and must still read as on-grid.
|
|
CHECK(isOnMillisecondGrid(1.007));
|
|
// A whole millisecond at 44.1 kHz is 44.1 frames — off the frame grid, on this one.
|
|
CHECK(isOnMillisecondGrid(0.010));
|
|
}
|
|
|
|
static void testOffGridRecognizesASubMillisecondRemainder() {
|
|
CHECK(!isOnMillisecondGrid(1.6551724137931001));
|
|
CHECK(!isOnMillisecondGrid(1.0001724));
|
|
// One frame short of a whole second at 48 kHz is ~0.0208 ms off the grid — the
|
|
// tightest remainder this predicate has to keep seeing.
|
|
CHECK(!isOnMillisecondGrid(1.0 - 1.0 / 48000.0));
|
|
}
|
|
|
|
// --- describeBoundsExperiment: the console verdict on a bounds channel --------
|
|
|
|
static void testAnExactRenderReadsExactAndNamesItsChannel() {
|
|
const std::string s =
|
|
describeBoundsExperiment("time selection (RENDER_BOUNDSFLAG=2)",
|
|
0.0, 1.6551724137931001, 79448, 48000);
|
|
CHECK(contains(s, "EXACT"));
|
|
CHECK(!contains(s, "SHORT"));
|
|
CHECK(contains(s, "time selection (RENDER_BOUNDSFLAG=2)"));
|
|
CHECK(contains(s, "79448"));
|
|
CHECK(contains(s, "48000 Hz"));
|
|
// The END here carries a sub-millisecond remainder, so this run DID test it --
|
|
// the END-untested caveat must not fire on a window it didn't apply to.
|
|
CHECK(!contains(s, "END edge is UNTESTED"));
|
|
}
|
|
|
|
static void testTheLiveShortfallReadsShortAndNamesTheMillisecondShape() {
|
|
// The observation, replayed through the verdict: 79440 produced against 79448.
|
|
const std::string s =
|
|
describeBoundsExperiment("custom time bounds (RENDER_BOUNDSFLAG=0)",
|
|
0.0, 1.6551724137931001, 79440, 48000);
|
|
CHECK(contains(s, "SHORT"));
|
|
CHECK(!contains(s, "EXACT"));
|
|
CHECK(contains(s, "79440"));
|
|
CHECK(contains(s, "79448"));
|
|
// 79440 IS the ms-floored count, so the verdict has to say the floor did not move.
|
|
CHECK(contains(s, "floored to the millisecond"));
|
|
}
|
|
|
|
static void testAShortfallThatIsNotTheMillisecondShapeClaimsNothingAboutIt() {
|
|
// A render 3 frames short is short, but 79445 is not the floored count — the
|
|
// millisecond sentence must not appear, or it would assert a shape that is absent.
|
|
CHECK(msFlooredEndFrameCount(0.0, 1.6551724137931001, 48000) != 79445);
|
|
const std::string s =
|
|
describeBoundsExperiment("custom time bounds", 0.0, 1.6551724137931001,
|
|
79445, 48000);
|
|
CHECK(contains(s, "SHORT"));
|
|
CHECK(!contains(s, "floored to the millisecond"));
|
|
}
|
|
|
|
static void testARenderPastTheWindowReadsLong() {
|
|
// The whole-item widening, through the verdict: 30 s printed for a 1 s window.
|
|
const std::string s =
|
|
describeBoundsExperiment("custom time bounds", 5.0, 6.0, 30 * 48000, 48000);
|
|
CHECK(contains(s, "LONG"));
|
|
CHECK(contains(s, "1440000 frames"));
|
|
CHECK(contains(s, "the 48000 the window asks for"));
|
|
}
|
|
|
|
static void testAWindowAlreadyOnTheGridIsUnaffectedByTheChannelSwitch() {
|
|
// A window whose end is a whole millisecond has nothing for a floor to take: the
|
|
// exact count and the floored count are the same number, so an exact render reads
|
|
// EXACT and the millisecond sentence never fires.
|
|
CHECK(frameCountFor(0.0, 2.0, 48000) == msFlooredEndFrameCount(0.0, 2.0, 48000));
|
|
const std::string s =
|
|
describeBoundsExperiment("time selection", 0.0, 2.0, 96000, 48000);
|
|
CHECK(contains(s, "EXACT"));
|
|
CHECK(contains(s, "96000"));
|
|
CHECK(!contains(s, "floored to the millisecond"));
|
|
// The false positive this window is the shape of: a render that floored either edge
|
|
// alone, or both together, would have printed this identical EXACT count (every
|
|
// edge here is on the grid) -- the line has to say this run cannot rule any of them
|
|
// out rather than reading EXACT as settled.
|
|
CHECK(contains(s, "EXACT here is not proof"));
|
|
CHECK(contains(s, "floors the START edge alone"));
|
|
CHECK(contains(s, "floors the END edge alone"));
|
|
CHECK(contains(s, "floors START and END together"));
|
|
}
|
|
|
|
static void testEqualRemaindersCancelUnderAFullFloorEvenOffGrid() {
|
|
// C1: a dragged, fixed-length time selection reproduces this. Neither edge sits on
|
|
// the millisecond grid (isOnMillisecondGrid is false for both), but the START and
|
|
// END frame-rounding remainders are EQUAL (rs == re == 8 frames), so a render that
|
|
// floors both edges together lands on the identical count -- the grid predicate on
|
|
// either edge alone would have missed this collision entirely.
|
|
const double start = 1.0001724, end = 2.0001724;
|
|
CHECK(!isOnMillisecondGrid(start));
|
|
CHECK(!isOnMillisecondGrid(end));
|
|
const long long expected = frameCountFor(start, end, 48000);
|
|
CHECK(expected == 48000);
|
|
// The both-edges-floored render lands on the SAME count as the exact one.
|
|
CHECK(frameCountFor(1.000, 2.000, 48000) == expected);
|
|
// Neither edge floored ALONE reproduces it -- only the combined floor does.
|
|
CHECK(frameCountFor(1.000, end, 48000) != expected);
|
|
CHECK(frameCountFor(start, 2.000, 48000) != expected);
|
|
|
|
const std::string s =
|
|
describeBoundsExperiment("time selection", start, end, expected, 48000);
|
|
CHECK(contains(s, "EXACT"));
|
|
CHECK(contains(s, "EXACT here is not proof"));
|
|
CHECK(contains(s, "floors START and END together"));
|
|
CHECK(!contains(s, "floors the START edge alone"));
|
|
CHECK(!contains(s, "floors the END edge alone"));
|
|
}
|
|
|
|
static void testEndOffGridByUnderHalfAFrameStillCollidesWithAFlooredEnd() {
|
|
// C1's second live shape: isOnMillisecondGrid reads this END as off-grid, but the
|
|
// remainder is under half a frame at 48 kHz, so flooring it doesn't move its frame
|
|
// index -- a grid test on the edge alone would still miss this collision.
|
|
const double start = 0.0, end = 1.000005;
|
|
CHECK(!isOnMillisecondGrid(end));
|
|
const long long expected = frameCountFor(start, end, 48000);
|
|
CHECK(expected == 48000);
|
|
CHECK(frameCountFor(start, 1.000, 48000) == expected); // the floored-end model matches
|
|
|
|
const std::string s =
|
|
describeBoundsExperiment("time selection", start, end, expected, 48000);
|
|
CHECK(contains(s, "EXACT"));
|
|
CHECK(contains(s, "EXACT here is not proof"));
|
|
CHECK(contains(s, "floors the END edge alone"));
|
|
}
|
|
|
|
static void testALongVerdictNeverCarriesTheFloorSentence() {
|
|
// A floor only removes frames, so LONG can never be its signature -- the sentence
|
|
// must not appear even though the delta here is a "clean" one-frame LONG.
|
|
const std::string s =
|
|
describeBoundsExperiment("time selection", 5.0, 6.0, 48001, 48000);
|
|
CHECK(contains(s, "LONG"));
|
|
CHECK(!contains(s, "floored to the millisecond"));
|
|
}
|
|
|
|
static void testASubFrameWindowIsNotJudgedNotExact() {
|
|
// A window under one frame at this rate rounds to 0 expected frames. A 0-frame
|
|
// render against that is a 0-vs-0 coincidence of degenerate inputs, not a match --
|
|
// it must read NOT JUDGED, never EXACT.
|
|
const double oneTenthOfAFrame = 1.0 / (48000.0 * 10.0);
|
|
const long long expected = frameCountFor(0.0, oneTenthOfAFrame, 48000);
|
|
CHECK(expected == 0);
|
|
const std::string s =
|
|
describeBoundsExperiment("time selection", 0.0, oneTenthOfAFrame, 0, 48000);
|
|
CHECK(contains(s, "NOT JUDGED"));
|
|
CHECK(!contains(s, "EXACT"));
|
|
}
|
|
|
|
static void testAWithinToleranceDeltaIsTaggedNotFloorShaped() {
|
|
// One frame off frameCountFor is the gate's own edge-convention slack
|
|
// (render_window.h), not the millisecond floor -- the verdict must say so rather
|
|
// than reading like a genuine miss or like the floor was escaped.
|
|
const std::string shortByOne =
|
|
describeBoundsExperiment("time selection", 0.0, 4.067797, 195253, 48000);
|
|
CHECK(contains(shortByOne, "SHORT"));
|
|
CHECK(contains(shortByOne, "WITHIN TOLERANCE"));
|
|
CHECK(!contains(shortByOne, "floored to the millisecond"));
|
|
|
|
const std::string longByOne =
|
|
describeBoundsExperiment("time selection", 0.0, 4.067797, 195255, 48000);
|
|
CHECK(contains(longByOne, "LONG"));
|
|
CHECK(contains(longByOne, "WITHIN TOLERANCE"));
|
|
|
|
// A genuine miss (outside the tolerance) carries no such tag.
|
|
const std::string shortByThree =
|
|
describeBoundsExperiment("time selection", 0.0, 4.067797, 195251, 48000);
|
|
CHECK(contains(shortByThree, "SHORT"));
|
|
CHECK(!contains(shortByThree, "WITHIN TOLERANCE"));
|
|
}
|
|
|
|
static void testABypassingSourceReadsNotJudgedAndNamesTheSourceNotTheChannel() {
|
|
// SelectedItems/RazorArea derive their own bounds from content -- the channel
|
|
// named by channelLabel was never consulted, so a matching frame count here would
|
|
// be a coincidence, not evidence the channel escaped the floor.
|
|
const std::string s =
|
|
describeBoundsExperiment("time selection", 0.0, 1.6551724137931001,
|
|
79448, 48000, "selected media items");
|
|
CHECK(contains(s, "NOT JUDGED"));
|
|
CHECK(contains(s, "selected media items"));
|
|
CHECK(!contains(s, "EXACT"));
|
|
// The channel is still named at the top of the line -- only the verdict changes.
|
|
CHECK(contains(s, "time selection"));
|
|
}
|
|
|
|
static void testANullOrEmptyBypassLabelFallsBackToTheOrdinaryVerdict() {
|
|
// Off-grid, non-cancelling edges (see testEqualRemaindersCancelUnderAFullFloorEvenOffGrid
|
|
// for the window shape that WOULD trip the collision caveat, whose own text also
|
|
// contains "EXACT") so this assertion is pinned to the verdict word itself, not to a
|
|
// caveat sentence that happens to contain the same substring.
|
|
const double start = 1.0001724, end = 2.0009724;
|
|
const long long expected = frameCountFor(start, end, 48000);
|
|
const std::string withNull =
|
|
describeBoundsExperiment("time selection", start, end, expected, 48000, nullptr);
|
|
CHECK(contains(withNull, "EXACT"));
|
|
CHECK(!contains(withNull, "EXACT here is not proof"));
|
|
CHECK(contains(describeBoundsExperiment("time selection", start, end, expected, 48000,
|
|
""),
|
|
"EXACT"));
|
|
}
|
|
|
|
static void testAnOnGridStartSaysTheStartEdgeIsUntested() {
|
|
// Both live observations started at 0 s — the value that hides a start-side floor.
|
|
const std::string s =
|
|
describeBoundsExperiment("time selection", 0.0, 1.6551724137931001, 79448, 48000);
|
|
CHECK(contains(s, "UNTESTED"));
|
|
CHECK(contains(s, "millisecond grid"));
|
|
}
|
|
|
|
static void testAnOffGridStartSaysTheStartEdgeIsTested() {
|
|
// The run that would genuinely settle the start question: a start carrying its own
|
|
// remainder, paired with an end whose remainder does NOT cancel it (unlike
|
|
// testEqualRemaindersCancelUnderAFullFloorEvenOffGrid's window, where the same shape
|
|
// of start value pairs with an end that cancels it and the collision caveat fires
|
|
// instead). No floored model reproduces this count, so EXACT here is unqualified.
|
|
const double start = 1.0001724, end = 2.0009724;
|
|
const long long expected = frameCountFor(start, end, 48000);
|
|
const std::string s =
|
|
describeBoundsExperiment("time selection", start, end, expected, 48000);
|
|
CHECK(contains(s, "IS tested"));
|
|
CHECK(!contains(s, "UNTESTED"));
|
|
CHECK(contains(s, "EXACT"));
|
|
CHECK(!contains(s, "EXACT here is not proof"));
|
|
// A start-floored-alone render would have printed a DIFFERENT count here, so a
|
|
// mismatch against `expected` on a re-run is real evidence, not ambiguous.
|
|
CHECK(frameCountFor(1.000, end, 48000) != expected);
|
|
CHECK(contains(describeBoundsExperiment("time selection", start, end,
|
|
frameCountFor(1.000, end, 48000), 48000),
|
|
"LONG"));
|
|
}
|
|
|
|
static void testAt44100WhereAMillisecondIsNotAWholeNumberOfFrames() {
|
|
// 44.1 kHz: the window is 463 frames, the ms-floored one 441 (both pinned in
|
|
// testMillisecondFloorAt44100WhereAMillisecondIsNotWholeFrames). The verdict has to
|
|
// reach the same two numbers at a rate where a millisecond is 44.1 frames.
|
|
const std::string exact =
|
|
describeBoundsExperiment("time selection", 0.0, 0.0105, 463, 44100);
|
|
CHECK(contains(exact, "EXACT"));
|
|
CHECK(contains(exact, "44100 Hz"));
|
|
|
|
const std::string floored =
|
|
describeBoundsExperiment("custom time bounds", 0.0, 0.0105, 441, 44100);
|
|
CHECK(contains(floored, "SHORT"));
|
|
CHECK(contains(floored, "floored to the millisecond"));
|
|
}
|
|
|
|
static void testAnUnmeasuredRenderAnswersNothingRatherThanPassing() {
|
|
// Auto/Manual are not judged against a frame count, and an empty render has none.
|
|
// The line must still print and must not read as a pass — its silence would.
|
|
const std::string s =
|
|
describeBoundsExperiment("time selection", 0.0, 1.6551724137931001, 0, 0);
|
|
CHECK(!s.empty());
|
|
CHECK(contains(s, "NOT JUDGED"));
|
|
CHECK(!contains(s, "EXACT"));
|
|
CHECK(contains(s, "time selection"));
|
|
}
|
|
|
|
static void testAnUnnamedChannelStillProducesAReadableLine() {
|
|
CHECK(contains(describeBoundsExperiment(nullptr, 0.0, 1.0, 48000, 48000),
|
|
"unnamed"));
|
|
CHECK(contains(describeBoundsExperiment("", 0.0, 1.0, 48000, 48000), "unnamed"));
|
|
}
|
|
|
|
// --- describeBoundsDrift: the read-back's verdict ------------------------------
|
|
|
|
static void testBoundsThatReadBackUnchangedDescribeNothing() {
|
|
// The answer that proves the request crossed into REAPER intact — including for a
|
|
// window whose end is nowhere near a millisecond boundary.
|
|
CHECK(describeBoundsDrift(0.0, 4.067797, 0.0, 4.067797, 48000).empty());
|
|
CHECK(describeBoundsDrift(1.0001724, 2.0001724, 1.0001724, 2.0001724, 48000).empty());
|
|
}
|
|
|
|
static void testADriftedEndNamesBothWindowsAndBothCounts() {
|
|
const std::string s =
|
|
describeBoundsDrift(0.0, 4.067797, 0.0, 4.067, 48000);
|
|
CHECK(!s.empty());
|
|
// Both counts as literals from the DAW observation, not re-derived from the same
|
|
// functions the sentence was built with.
|
|
CHECK(contains(s, "195254")); // what the request asks for
|
|
CHECK(contains(s, "195216")); // what the drifted window would hold
|
|
CHECK(contains(s, "48000 Hz"));
|
|
}
|
|
|
|
static void testTheReportPrintsEnoughDigitsToShowTheDrift() {
|
|
// A report whose two numbers print identically is evidence of nothing. Two ends a
|
|
// single ULP apart — far under the sixth decimal a shorter rendering would stop at
|
|
// — must still read as two different numbers. Pinned as the actual %.17g literals
|
|
// (not the needle the two ends share, "s)", which occurs at every precision and so
|
|
// proves nothing): a report that regressed to a shorter format like %.6g would
|
|
// print the same six significant digits for both ends, and these two `contains`
|
|
// checks would then fail.
|
|
const double asked = 4.067797;
|
|
const double stored = std::nextafter(asked, 5.0);
|
|
char askedBuf[32], storedBuf[32];
|
|
std::snprintf(askedBuf, sizeof(askedBuf), "%.17g", asked);
|
|
std::snprintf(storedBuf, sizeof(storedBuf), "%.17g", stored);
|
|
CHECK(std::string(askedBuf) != std::string(storedBuf));
|
|
|
|
const std::string s = describeBoundsDrift(0.0, asked, 0.0, stored, 48000);
|
|
CHECK(!s.empty());
|
|
CHECK(contains(s, askedBuf));
|
|
CHECK(contains(s, storedBuf));
|
|
}
|
|
|
|
static void testADriftedStartIsCaughtToo() {
|
|
// The edge both observations could not test.
|
|
const std::string s = describeBoundsDrift(1.0001724, 2.0, 1.000, 2.0, 48000);
|
|
CHECK(!s.empty());
|
|
CHECK(contains(s, "1.0001724"));
|
|
}
|
|
|
|
static void testAnUnknownRateStillReportsTheDriftWithoutFrames() {
|
|
// A project that never pinned a rate reads 0. The drift is still worth saying; a
|
|
// frame count over an unknown rate is not.
|
|
const std::string s = describeBoundsDrift(0.0, 4.067797, 0.0, 4.067, 0);
|
|
CHECK(!s.empty());
|
|
CHECK(!contains(s, "frames"));
|
|
}
|
|
|
|
int main() {
|
|
testFrameCountIsExactNotRounded();
|
|
testFrameCountIsADifferenceOfIndicesNotADuration();
|
|
testFrameCountRefusesEmptyInvertedAndUnknownRate();
|
|
testWindowStartingAtExactlyZero();
|
|
testNonFrameAlignedWindowAcceptsItsAdjacentCounts();
|
|
testLengthDerivedAndSameConventionRenderersStayWithinOneFrame();
|
|
testMixedEdgeConventionsCanMissByTwoAndAreRefused();
|
|
testWholeItemWideningIsStillRefused();
|
|
testLargeShortfallIsStillRefused();
|
|
testEmptyRenderIsRefusedAgainstARealWindow();
|
|
testRangeInsideItemCannotBeExpressed();
|
|
testRangeWiderThanItemCannotBeExpressedEither();
|
|
testEachEdgeAloneDisqualifies();
|
|
testExtentEqualToWindowIsExpressible();
|
|
testSubFrameDriftStillPrintsTheSameFrames();
|
|
testUnknownRateFallsBackToExactEquality();
|
|
testMultiItemUnionExtent();
|
|
testMillisecondFlooredEndReproducesBothShortRenders();
|
|
testTheSixDecimalDisplayDidNotCreateTheEffect();
|
|
testWindowAlreadyOnTheMillisecondGridLosesNothing();
|
|
testOneFrameOfRemainderStillFloors();
|
|
testMillisecondFloorAt44100WhereAMillisecondIsNotWholeFrames();
|
|
testASubMillisecondStartWouldNotHideItself();
|
|
testTheTwoLiveShortRendersPinnedAtFullPrecision();
|
|
testOnGridRecognizesWholeMillisecondsIncludingTheBinaryTrap();
|
|
testOffGridRecognizesASubMillisecondRemainder();
|
|
testAnExactRenderReadsExactAndNamesItsChannel();
|
|
testTheLiveShortfallReadsShortAndNamesTheMillisecondShape();
|
|
testAShortfallThatIsNotTheMillisecondShapeClaimsNothingAboutIt();
|
|
testARenderPastTheWindowReadsLong();
|
|
testAWindowAlreadyOnTheGridIsUnaffectedByTheChannelSwitch();
|
|
testEqualRemaindersCancelUnderAFullFloorEvenOffGrid();
|
|
testEndOffGridByUnderHalfAFrameStillCollidesWithAFlooredEnd();
|
|
testALongVerdictNeverCarriesTheFloorSentence();
|
|
testASubFrameWindowIsNotJudgedNotExact();
|
|
testAWithinToleranceDeltaIsTaggedNotFloorShaped();
|
|
testABypassingSourceReadsNotJudgedAndNamesTheSourceNotTheChannel();
|
|
testANullOrEmptyBypassLabelFallsBackToTheOrdinaryVerdict();
|
|
testAnOnGridStartSaysTheStartEdgeIsUntested();
|
|
testAnOffGridStartSaysTheStartEdgeIsTested();
|
|
testAt44100WhereAMillisecondIsNotAWholeNumberOfFrames();
|
|
testAnUnmeasuredRenderAnswersNothingRatherThanPassing();
|
|
testAnUnnamedChannelStillProducesAReadableLine();
|
|
testBoundsThatReadBackUnchangedDescribeNothing();
|
|
testADriftedEndNamesBothWindowsAndBothCounts();
|
|
testTheReportPrintsEnoughDigitsToShowTheDrift();
|
|
testADriftedStartIsCaughtToo();
|
|
testAnUnknownRateStillReportsTheDriftWithoutFrames();
|
|
|
|
if (g_fail) { std::printf("%d check(s) FAILED\n", g_fail); return 1; }
|
|
std::printf("render_window: all checks passed\n");
|
|
return 0;
|
|
}
|