// Standalone tests for reasampler::instrument::ui::waveform_view — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure tests. Assert the waveform band's drawn surface // (lane split + the full-height overlay contract) and its frame<->pixel mapping, marker grab // regions, drag-delta frame resolver (with clamps), and zero-crossing snap. // // Covers: frameToX / xToFrame — the ONE map, asserted against the REAL draw chain // (computeEnvelope + columnMinMax) rather than a restatement of it, at frame 0 / the last frame // / an interior frame and then exhaustively, in both the frames>columns and frames #include #include using namespace reasampler; using namespace reasampler::instrument::ui; using reasampler::audio::AudioSample; 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 OverlayArea overlayOf(const Rect& r) { return OverlayArea{r}; } // A comfortable waveform area: 1000px wide, offset so left != 0 (catches origin bugs). static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 90); } // width 1000 // --- frameToX / xToFrame ------------------------------------------------------ // 1000 frames over 1000 columns: each frame owns exactly one column, so the map is the // identity and every endpoint is exact. static void testFrameToXEndpoints() { const Rect a = wideArea(); CHECK(frameToX(overlayOf(a), 1000, 0) == a.x); // frame 0 -> first column CHECK(frameToX(overlayOf(a), 1000, 999) == a.right() - 1); // last FRAME -> last column CHECK(frameToX(overlayOf(a), 1000, 1000) == a.right()); // the exclusive span end -> past it CHECK(frameToX(overlayOf(a), 1000, 500) == a.x + 500); // midpoint (1:1 here) } static void testFrameToXClampsOutOfRange() { const Rect a = wideArea(); CHECK(frameToX(overlayOf(a), 1000, -50) == a.x); // below 0 pins left CHECK(frameToX(overlayOf(a), 1000, 5000) == a.right()); // above count pins right } static void testFrameToXDegenerate() { const Rect a = wideArea(); CHECK(frameToX(overlayOf(a), 0, 100) == a.x); // no frames -> left const Rect z = Rect::ltrb(5, 5, 5, 45); // zero width CHECK(frameToX(overlayOf(z), 1000, 500) == z.x); } static void testXToFrameInverse() { const Rect a = wideArea(); CHECK(xToFrame(overlayOf(a), 1000, a.x) == 0); CHECK(xToFrame(overlayOf(a), 1000, a.right()) == 1000); CHECK(xToFrame(overlayOf(a), 1000, a.x + 250) == 250); // 1:1 map here } static void testXToFrameClampsOutside() { const Rect a = wideArea(); CHECK(xToFrame(overlayOf(a), 1000, a.x - 100) == 0); // left of area -> 0 CHECK(xToFrame(overlayOf(a), 1000, a.right() + 100) == 1000); // right of area -> frameCount CHECK(xToFrame(overlayOf(a), 0, a.x + 10) == 0); // no frames -> 0 } // --- The frame<->pixel mapping against the draw chain it must agree with ------- // // The whole Ω.6 contract: the overlay reads the SAME frame->column partition the waveform is // binned and drawn through, so these fixtures run the REAL chain (computeEnvelope + // columnMinMax) rather than restating the partition, which would only prove the test agrees // with itself. // Which columns the draw chain actually paints frame `f` into: a spike at f over silence, binned // exactly as paintWaveform bins it, read back per column. Inclusive run, or lo < 0 for none. struct ColumnRun { int lo = -1; int hi = -1; }; static ColumnRun drawnColumnsForFrame(int columns, std::int64_t frameCount, std::int64_t f) { std::vector pcm(static_cast(frameCount), 0.0f); pcm[static_cast(f)] = 1.0f; // paintWaveform's own bin count: one per drawn column, capped at the frames available. const std::int64_t wantBins = static_cast(columns); const std::size_t bins = static_cast(wantBins < frameCount ? wantBins : frameCount); const reasampler::audio::Envelope env = reasampler::audio::computeEnvelope(pcm, 1, static_cast(frameCount), bins); ColumnRun run; for (int c = 0; c < columns; ++c) { if (reasampler::audio::columnMinMax(env[0], columns, c).max < 1.0f) continue; if (run.lo < 0) run.lo = c; run.hi = c; } return run; } static void checkMarkLandsOnItsOwnWaveformColumn(const Rect& band, std::int64_t frameCount, std::int64_t f) { const OverlayArea ov = waveformOverlayArea(band); const ColumnRun run = drawnColumnsForFrame(ov.rect.width, frameCount, f); CHECK(run.lo >= 0); // the draw chain paints every frame somewhere const int col = frameToX(ov, frameCount, f) - ov.rect.x; CHECK(col >= run.lo && col <= run.hi); } static void testAMarkLandsOnTheWaveformColumnForItsOwnFrame() { const Rect b = Rect{8, 90, 404, 60}; // 400 drawn columns // frames > columns: many frames share one column, and the mark must pick that column. const std::int64_t many = 9973; // prime, so no boundary falls anywhere convenient checkMarkLandsOnItsOwnWaveformColumn(b, many, 0); checkMarkLandsOnItsOwnWaveformColumn(b, many, many - 1); checkMarkLandsOnItsOwnWaveformColumn(b, many, 4001); // frames < columns: one frame spans many columns, and the mark must land inside its own run. const std::int64_t few = 37; checkMarkLandsOnItsOwnWaveformColumn(b, few, 0); checkMarkLandsOnItsOwnWaveformColumn(b, few, few - 1); checkMarkLandsOnItsOwnWaveformColumn(b, few, 19); } // Not just the three probe frames: EVERY frame, across both regimes and the 1:1 boundary. static void testTheMappingAgreesWithTheDrawChainAtEveryFrame() { const int widths[] = {21, 64, 104}; // 17 / 60 / 100 drawn columns const std::int64_t counts[] = {7, 60, 100, 251}; // below, equal to and above each for (int w : widths) { for (std::int64_t n : counts) { const Rect b = Rect{3, 0, w, 40}; for (std::int64_t f = 0; f < n; ++f) checkMarkLandsOnItsOwnWaveformColumn(b, n, f); } } } // The closed domain is a SPAN's exclusive end, not a frame: it is what the loop fill and the // crossfade wedge stop at, so it belongs one past the last column and nowhere else. static void testTheExclusiveSpanEndLandsOnTheRightEdge() { const Rect b = Rect{8, 90, 404, 60}; const OverlayArea ov = waveformOverlayArea(b); const std::int64_t counts[] = {7, 400, 9973}; for (std::int64_t n : counts) { CHECK(frameToX(ov, n, n) == ov.rect.right()); CHECK(frameToX(ov, n, n + 5000) == ov.rect.right()); // and clamps there // The last real FRAME is the last real column — one inside that edge. CHECK(frameToX(ov, n, n - 1) == ov.rect.right() - 1); } } static void testXToFrameRoundTripsEveryFrameWhileAFrameOwnsAColumn() { // frames <= columns is exactly where a frame spans several columns and the choice of which // one to mark is observable, so it is where the inverse has to be exact. const Rect b = Rect{8, 90, 404, 60}; const OverlayArea ov = waveformOverlayArea(b); const std::int64_t counts[] = {1, 37, 399, 400}; for (std::int64_t n : counts) { for (std::int64_t f = 0; f < n; ++f) CHECK(xToFrame(ov, n, frameToX(ov, n, f)) == f); } } static void testColumnsRoundTripWhereFramesShareThem() { // Above the column count a per-frame round trip cannot exist — several frames share one // column. What must still hold is the COLUMN round trip: every column answers a frame that // maps straight back to that same column, so no column is unreachable or ambiguous. const Rect b = Rect{8, 90, 404, 60}; const OverlayArea ov = waveformOverlayArea(b); const std::int64_t n = 9973; for (int c = 0; c < ov.rect.width; ++c) { const int x = ov.rect.x + c; CHECK(frameToX(ov, n, xToFrame(ov, n, x)) == x); } } // --- markerAtPoint ------------------------------------------------------------ static void testMarkerAtPointGrabsWithinBand() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); // Markers at frames 100, 500, 900 -> x = left+100, left+500, left+900. const std::int64_t frames[3] = {100, 500, 900}; const int midY = a.y + a.height / 2; CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 100, midY) == 0); CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 500, midY) == 1); CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 900, midY) == 2); // Within the grab band on either side of the line. CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 500 + kMarkerGrabWidth, midY) == 1); CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 500 - kMarkerGrabWidth, midY) == 1); } static void testMarkerAtPointMissesBetween() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); const std::int64_t frames[3] = {100, 500, 900}; const int midY = a.y + a.height / 2; // Well away from any marker line. CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 300, midY) == -1); // Off the area vertically. CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 500, a.y - 5) == -1); } static void testMarkerAtPointFirstMatchOnOverlap() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); // Two markers at the same frame -> first in order wins. const std::int64_t frames[2] = {400, 400}; const int midY = a.y + a.height / 2; CHECK(markerAtPoint(ov, 1000, frames, 2, a.x + 400, midY) == 0); } static void testMarkerAtPointRejectsNullEmpty() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); const int midY = a.y + a.height / 2; CHECK(markerAtPoint(ov, 1000, nullptr, 3, a.x + 100, midY) == -1); const std::int64_t frames[1] = {100}; CHECK(markerAtPoint(ov, 1000, frames, 0, a.x + 100, midY) == -1); } // --- resolveDragFrame --------------------------------------------------------- static void testResolveDragFrameShift() { const Rect a = wideArea(); // 1:1 (1000px / 1000 frames) const OverlayArea ov = overlayOf(a); CHECK(resolveDragFrame(ov, 1000, 300, 0) == 300); // zero delta -> unchanged CHECK(resolveDragFrame(ov, 1000, 300, 100) == 400); // +100px -> +100 frames CHECK(resolveDragFrame(ov, 1000, 300, -50) == 250); // -50px -> -50 frames } static void testResolveDragFrameClamps() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); CHECK(resolveDragFrame(ov, 1000, 50, -500) == 0); // clamp low CHECK(resolveDragFrame(ov, 1000, 950, 500) == 1000); // clamp high (== frameCount) } static void testResolveDragFrameRounds() { // 500px area over 1000 frames -> 2 frames/px. A +3px drag -> round(6.0)=6; the rounding is // at the frame centre. Use a scale where a fractional result appears. const OverlayArea ov = overlayOf(Rect::ltrb(0, 0, 300, 60)); // 1000 frames / 300px = 3.33 frames/px // +3px -> 3*1000/300 = 10.0 -> 10 frames. CHECK(resolveDragFrame(ov, 1000, 100, 3) == 110); // +1px -> 1000/300 = 3.33 -> rounds to 3. CHECK(resolveDragFrame(ov, 1000, 100, 1) == 103); } static void testResolveDragFrameDegenerate() { const Rect z = Rect::ltrb(0, 0, 0, 60); // zero width CHECK(resolveDragFrame(overlayOf(z), 1000, 300, 100) == 300); // pinned to start const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); CHECK(resolveDragFrame(ov, 0, 300, 100) == 0); // no frames -> clamp(start)=0 // startFrame out of range is clamped first. CHECK(resolveDragFrame(ov, 1000, 5000, 0) == 1000); } // --- nearestZeroCrossing ------------------------------------------------------ static void testZeroCrossingNearest() { // Crossings (sign change from i-1 to i): i=4 (1->-1), i=5 (-1->1), i=10 (1->-1). std::vector pcm = {1, 1, 1, 1, -1, 1, 1, 1, 1, 1, -1, -1}; // Target 4 is itself a crossing -> 4. CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 4) == 4); // Nearest to 6: crossing 5 (dist 1) beats 4 (dist 2) and 10 (dist 4) -> 5. CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 6) == 5); // Nearest to 9: crossing 10 (dist 1) beats 5 (dist 4) -> 10. CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 9) == 10); } static void testZeroCrossingSampleOnZero() { // A sample exactly 0 is its own crossing (frame index of the zero sample). std::vector pcm = {1, 1, 0, 1, 1}; CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 2) == 2); CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 3) == 2); } static void testZeroCrossingEquidistantTieToLower() { // Crossings at i=2 (1->-1) and i=6 (-1->1). Target 4 is equidistant (dist 2) -> lower (2). std::vector pcm = {1, 1, -1, -1, -1, -1, 1, 1}; CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 4) == 2); } // The snap has to survive the mapping change BEHAVIOUR-IDENTICAL, ties included, so the tie // rule is pinned at every distance rather than at one: the fan-out probes t-d before t+d, so an // equidistant pair always resolves to the LOWER frame. A single spike to 0 is its own isolated // crossing (the sample-on-zero rule), which is what keeps each side's crossing count at one. static void testZeroCrossingTiesAlwaysResolveToTheLowerFrame() { const std::int64_t n = 200, t = 100; for (std::int64_t d = 1; d <= 40; ++d) { std::vector pcm(static_cast(n), 1.0f); pcm[static_cast(t - d)] = 0.0f; pcm[static_cast(t + d)] = 0.0f; CHECK(nearestZeroCrossing(pcm.data(), n, t) == t - d); } } // ...and the tie rule is the ONLY asymmetry: wherever one side is strictly nearer, that side // wins, from either direction. Without this, "lower wins" could hide a left-biased search. static void testZeroCrossingTakesTheNearerSideFromEitherDirection() { const std::int64_t n = 200, t = 100; for (std::int64_t d = 2; d <= 40; ++d) { { std::vector pcm(static_cast(n), 1.0f); pcm[static_cast(t - d)] = 0.0f; pcm[static_cast(t + d - 1)] = 0.0f; // right nearer by one CHECK(nearestZeroCrossing(pcm.data(), n, t) == t + d - 1); } { std::vector pcm(static_cast(n), 1.0f); pcm[static_cast(t - d + 1)] = 0.0f; // left nearer by one pcm[static_cast(t + d)] = 0.0f; CHECK(nearestZeroCrossing(pcm.data(), n, t) == t - d + 1); } } } static void testZeroCrossingNoneKeepsTarget() { // All one sign -> no crossing -> the (clamped) target comes back unchanged. std::vector pcm = {0.5f, 0.6f, 0.7f, 0.8f}; CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 2) == 2); } static void testZeroCrossingClampsTarget() { std::vector pcm = {1, -1, 1, -1}; // crossings at 1,2,3 // Target beyond the end clamps to frames-1 (3) then finds crossing at 3. CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 999) == 3); // Negative target clamps to 0; nearest crossing is 1. CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), -999) == 1); } static void testZeroCrossingDegenerate() { CHECK(nearestZeroCrossing(nullptr, 0, 5) == 0); std::vector one = {1}; CHECK(nearestZeroCrossing(one.data(), 1, 0) == 0); // <2 frames -> clamped target } // --- waveformSurface: the lane split + the overlay contract -------------------- // A realistic waveform band: full-width, taller than the two-lane floor. static Rect band() { return Rect::ltrb(8, 90, 832, 90 + kWaveformMinHeight); } static void testSurfaceStereoStacksTwoLanes() { const Rect b = band(); const WaveformSurface s = waveformSurface(b, /*stereoMode=*/true, /*sourceChannels=*/2); CHECK(s.laneCount == 2); CHECK(!s.upper.empty() && !s.lower.empty()); CHECK(s.upper.y == b.y); // L on top CHECK(s.lower.y > s.upper.bottom()); // R below, seam between them CHECK(s.lower.bottom() == b.bottom()); // together they reach the band's floor CHECK(s.upper.x == b.x && s.upper.width == b.width); CHECK(s.lower.x == b.x && s.lower.width == b.width); // Non-overlapping, and the band is exactly lanes + the one seam gap. CHECK(s.lower.y - s.upper.bottom() == kLaneGap); CHECK(s.upper.height + kLaneGap + s.lower.height == b.height); } static void testSurfaceMonoIsOneLane() { const Rect b = band(); const WaveformSurface s = waveformSurface(b, /*stereoMode=*/false, /*sourceChannels=*/2); CHECK(s.laneCount == 1); CHECK(s.upper == b); // the single lane spans the whole band CHECK(s.lower.empty()); // no second lane to draw } static void testSurfaceMonoSourceInStereoModeStaysOneLane() { // Dual-mono: a mono source under stereo mode has no second channel, so a second lane // would be a redundant duplicate. const Rect b = band(); const WaveformSurface s = waveformSurface(b, /*stereoMode=*/true, /*sourceChannels=*/1); CHECK(s.laneCount == 1); CHECK(s.upper == b); CHECK(s.lower.empty()); } static void testSurfaceOverlayIsFullStackedHeightInBothModes() { const Rect b = band(); const WaveformSurface st = waveformSurface(b, /*stereoMode=*/true, 2); const WaveformSurface mo = waveformSurface(b, /*stereoMode=*/false, 2); // Stereo: ONE overlay rect spanning both lanes, not either lane. The HEIGHT is what the // overlay contract is about, and it is the whole stack in both modes. CHECK(st.overlay.rect.y == b.y && st.overlay.rect.height == b.height); CHECK(st.overlay.rect.height == st.upper.height + kLaneGap + st.lower.height); CHECK(st.overlay.rect != st.upper && st.overlay.rect != st.lower); // Mono: the same rect. It is NOT the single lane any more — the lane is the whole band, // the overlay is the band's drawn column span inside it. CHECK(mo.overlay.rect.y == b.y && mo.overlay.rect.height == b.height); CHECK(mo.overlay.rect == st.overlay.rect); CHECK(mo.overlay.rect != mo.upper); // The standalone accessor the hit-test paths use agrees with the resolved surface. CHECK(waveformOverlayArea(b) == st.overlay); CHECK(waveformOverlayArea(b) == mo.overlay); } // THE Ω.6 contract at the construction site: the overlay is the band's drawn column span, so // an overlay pixel and a waveform column are the same pixel. Read from the draw chain's own // column count — a hardcoded 2/4 here would be the second copy that let the two drift. static void testTheOverlayIsExactlyTheDrawnColumnBand() { const Rect b = band(); const OverlayArea ov = waveformOverlayArea(b); const int columns = reasampler::ui::waveformColumnCount(b); CHECK(columns > 0); CHECK(ov.rect.width == columns); CHECK(ov.rect.x == b.x + (b.width - columns) / 2); // Inset on BOTH sides, and the same amount on each — the halving above is only legitimate // because the draw chain's inset is symmetric. CHECK(ov.rect.x - b.x == b.right() - ov.rect.right()); CHECK(ov.rect.x > b.x && ov.rect.right() < b.right()); // Held across widths, not just this one. for (int w = 5; w <= 300; ++w) { const Rect band2 = Rect{7, 40, w, 60}; const OverlayArea o2 = waveformOverlayArea(band2); CHECK(o2.rect.width == reasampler::ui::waveformColumnCount(band2)); CHECK(o2.rect.x - band2.x == band2.right() - o2.rect.right()); } } static void testSurfaceDegenerateBandDrawsNothing() { const WaveformSurface s = waveformSurface(Rect{10, 10, 0, 0}, true, 2); CHECK(s.laneCount == 0); CHECK(s.upper.empty() && s.lower.empty() && s.overlay.rect.empty()); CHECK(waveformOverlayArea(Rect{10, 10, 0, 0}).rect.empty()); // A band too narrow to hold a single column has no overlay to draw into, even though the // band itself is not degenerate and still gets a lane. CHECK(reasampler::ui::waveformColumnCount(Rect{0, 0, 4, 40}) == 0); CHECK(waveformOverlayArea(Rect{0, 0, 4, 40}).rect.empty()); CHECK(!waveformSurface(Rect{0, 0, 4, 40}, false, 1).upper.empty()); } static void testSurfaceThinBandRoundsLowerLaneEmpty() { // Height 3 is the edge where the stereo split's integer division rounds the lower lane to // empty even though the band itself isn't degenerate — pins the laneCount derivation. const WaveformSurface s = waveformSurface(Rect{0, 0, 100, 3}, true, 2); CHECK(s.laneCount == 1); CHECK(!s.upper.empty()); CHECK(s.lower.empty()); } // --- Hit-testing across the stacked lanes ------------------------------------- static void testMarkerGrabReachesTheLowerStereoLane() { const Rect b = band(); const WaveformSurface s = waveformSurface(b, /*stereoMode=*/true, 2); const std::int64_t frames = 1000; const std::int64_t markers[1] = {500}; const int mx = frameToX(s.overlay, frames, 500); // The same marker answers a grab in either lane — overlays span the full stack. const int upperY = s.upper.y + s.upper.height / 2; const int lowerY = s.lower.y + s.lower.height / 2; CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, upperY) == 0); CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, lowerY) == 0); // A lower-lane grab hit-tested against the UPPER LANE would be lost — the miss this // contract exists to prevent. (Explicit OverlayArea{} wrap: production code can't do // this by accident — markerAtPoint won't accept a bare lane Rect — but the geometry // claim still needs proving.) CHECK(markerAtPoint(overlayOf(s.upper), frames, markers, 1, mx, lowerY) == -1); // Off the marker's x is still a miss at either height. CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx + 40, lowerY) == -1); } static void testMarkerGrabInMonoSpansTheBand() { const Rect b = band(); const WaveformSurface s = waveformSurface(b, /*stereoMode=*/false, 2); const std::int64_t frames = 1000; const std::int64_t markers[1] = {250}; const int mx = frameToX(s.overlay, frames, 250); CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, b.y) == 0); CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, b.bottom() - 1) == 0); CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, b.bottom() + 5) == -1); } // --- The marker grab handle ---------------------------------------------------- static void testMarkerHandleIsATopStripCentredOnTheMarker() { const Rect a = wideArea(); const int mx = frameToX(overlayOf(a), 1000, 250); const Rect h = markerHandleRect(overlayOf(a), 1000, 250); CHECK(h.x == mx - kMarkerHandleHalfWidth); CHECK(h.right() == mx + kMarkerHandleHalfWidth + 1); CHECK(h.y == a.y); CHECK(h.height == kMarkerHandleHeight); CHECK(contains(h, mx, a.y)); CHECK(contains(h, mx, a.y + kMarkerHandleHeight - 1)); CHECK(!contains(h, mx, a.y + kMarkerHandleHeight)); // below the strip is the column's } // The whole reason the handle exists: two markers that share a frame both stay reachable — // markerAtPoint gives its full-height column to the first in draw order, and the handle owns // the strip above. Without the split, the loser could never be dragged apart again. static void testCoincidentMarkersStayIndependentlyGrabbable() { const Rect a = wideArea(); const std::int64_t markers[2] = {250, 250}; const int mx = frameToX(overlayOf(a), 1000, 250); // The column resolves to the first marker at every height, including the top strip. CHECK(markerAtPoint(overlayOf(a), 1000, markers, 2, mx, a.y) == 0); CHECK(markerAtPoint(overlayOf(a), 1000, markers, 2, mx, a.bottom() - 1) == 0); // The handle, asked first, resolves the second one in that same top strip. CHECK(contains(markerHandleRect(overlayOf(a), 1000, 250), mx, a.y)); CHECK(!contains(markerHandleRect(overlayOf(a), 1000, 250), mx, a.bottom() - 1)); } static void testMarkerHandleClipsIntoTheArea() { const Rect a = wideArea(); // At the last frame the marker maps to right(); an unclipped tab would claim pixels // outside the band the caller already hit-tested. const Rect hi = markerHandleRect(overlayOf(a), 1000, 1000); CHECK(hi.right() == a.right()); CHECK(!contains(hi, a.right(), a.y)); CHECK(contains(hi, a.right() - 1, a.y)); // And at frame 0 it cannot reach left of the band. const Rect lo = markerHandleRect(overlayOf(a), 1000, 0); CHECK(lo.x == a.x); CHECK(!contains(lo, a.x - 1, a.y)); } static void testMarkerHandleOnDegenerateAreas() { CHECK(markerHandleRect(overlayOf(Rect{}), 1000, 0).empty()); // A band shorter than the strip yields a handle the height of the band, never taller. const Rect thin = Rect{0, 0, 100, 4}; CHECK(markerHandleRect(overlayOf(thin), 1000, 500).height == 4); } // The shell (editor_input_waveform.cpp) resolves a mark's CAP before it iterates the ordinary // marker array, because a zero-length fade puts the crossfade cap exactly on the loop-end // marker's frame. The same coincidence recurs whenever ANY marker shares that frame, most // plausibly the START marker dragged up against the fade edge: this module can't exercise the // shell's check-order itself, but it can prove the geometric ambiguity that makes the ordering // load-bearing — the array's own first-match rule would otherwise resolve the top strip to the // START marker, not the fade handle. static void testStartMarkerSharesTheHandleStripWhenItSitsAtTheFadeEdge() { const Rect a = wideArea(); const std::int64_t loopEnd = 400, crossfade = 30; const std::int64_t fadeEdge = loopEnd - crossfade; // where the crossfade cap sits const std::int64_t markers[3] = {fadeEdge, 200, loopEnd}; // start dialled onto the fade edge const int mx = frameToX(overlayOf(a), 1000, fadeEdge); const int topY = a.y; // inside the handle's top strip // Without the shell's priority check, the array's own first-match rule already resolves the // column to the start marker (index 0) at this x/y... CHECK(markerAtPoint(overlayOf(a), 1000, markers, 3, mx, topY) == 0); // ...and the fade handle's rect claims the exact same pixel — the ambiguity the shell // resolves by smallest-target-first (the handle's clipped tab is always the narrower // target), same as it does for the zero-fade/loop-end case. CHECK(contains(markerHandleRect(overlayOf(a), 1000, fadeEdge), mx, topY)); } // --- The four marks: cap resolve, labels, suppression, crossfade wedge ---------- static WaveMarks marksAt(std::int64_t start, std::int64_t loopStart, std::int64_t loopEnd, std::int64_t xfade, bool loopPresent) { WaveMarks m; m.frame[0] = start; m.frame[1] = loopStart; m.frame[2] = loopEnd; m.frame[3] = xfade; m.present[0] = true; m.present[1] = m.present[2] = m.present[3] = loopPresent; return m; } static void testEveryMarkAnswersItsOwnCap() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); const WaveMarks m = marksAt(50, 300, 700, 620, true); for (int i = 0; i < kWaveMarkCount; ++i) { const int mx = frameToX(ov, 1000, m.frame[i]); CHECK(capAtPoint(ov, 1000, m, mx, a.y) == i); CHECK(capAtPoint(ov, 1000, m, mx, a.y + kMarkerHandleHeight - 1) == i); // Below the cap strip is the column's, never the cap's. CHECK(capAtPoint(ov, 1000, m, mx, a.y + kMarkerHandleHeight) == -1); } } static void testAMarkThatIsNotPresentAnswersNoCap() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); const WaveMarks m = marksAt(50, 300, 700, 620, /*loopPresent=*/false); CHECK(capAtPoint(ov, 1000, m, frameToX(ov, 1000, 300), a.y) == -1); CHECK(capAtPoint(ov, 1000, m, frameToX(ov, 1000, 620), a.y) == -1); CHECK(capAtPoint(ov, 1000, m, frameToX(ov, 1000, 50), a.y) == 0); // START stays live } // The separability argument the reverse cap order exists for: for a coincident PAIR, one mark // answers the cap and the OTHER answers the full-height column, so neither is ever stranded. static void testACoincidentPairStaysSeparableAcrossCapAndColumn() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); const int midY = a.y + a.height / 2; // Zero-length fade: the crossfade mark sits at loopEnd - 0, i.e. exactly on the END marker. // This is the live case — the crossfade is anchored to the seam it closes. { const WaveMarks m = marksAt(50, 300, 700, 700, true); const int mx = frameToX(ov, 1000, 700); CHECK(capAtPoint(ov, 1000, m, mx, a.y) == static_cast(WaveMark::kCrossfade)); const std::int64_t cols[3] = {m.frame[0], m.frame[1], m.frame[2]}; CHECK(markerAtPoint(ov, 1000, cols, 3, mx, midY) == static_cast(WaveMark::kLoopEnd)); } // START dragged onto the loop start: the cap goes to LOOP, the column to START. { const WaveMarks m = marksAt(300, 300, 700, 100, true); const int mx = frameToX(ov, 1000, 300); CHECK(capAtPoint(ov, 1000, m, mx, a.y) == static_cast(WaveMark::kLoopStart)); const std::int64_t cols[3] = {m.frame[0], m.frame[1], m.frame[2]}; CHECK(markerAtPoint(ov, 1000, cols, 3, mx, midY) == static_cast(WaveMark::kStart)); } // START dragged onto the loop end: the cap goes to END, the column to START. { const WaveMarks m = marksAt(700, 300, 700, 100, true); const int mx = frameToX(ov, 1000, 700); CHECK(capAtPoint(ov, 1000, m, mx, a.y) == static_cast(WaveMark::kLoopEnd)); const std::int64_t cols[3] = {m.frame[0], m.frame[1], m.frame[2]}; CHECK(markerAtPoint(ov, 1000, cols, 3, mx, midY) == static_cast(WaveMark::kStart)); } } // The crossfade is the one mark with NO full-height column, so it must never lose a cap tie. static void testTheCrossfadeCapOutranksEveryOtherMark() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); const WaveMarks m = marksAt(400, 400, 400, 400, true); // every mark on one frame CHECK(capAtPoint(ov, 1000, m, frameToX(ov, 1000, 400), a.y) == static_cast(WaveMark::kCrossfade)); } static void testLabelSidesKeepEachLabelOutOfTheSpanItBounds() { CHECK(!markLabelLeftOfLine(WaveMark::kStart)); CHECK(!markLabelLeftOfLine(WaveMark::kLoopStart)); CHECK(markLabelLeftOfLine(WaveMark::kLoopEnd)); CHECK(markLabelLeftOfLine(WaveMark::kCrossfade)); const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); const int mx = frameToX(ov, 1000, 500); const Rect right = markLabelRect(ov, 1000, 500, /*leftOfLine=*/false, 30); const Rect left = markLabelRect(ov, 1000, 500, /*leftOfLine=*/true, 30); CHECK(right.x == mx + kMarkLabelGap && right.width == 30); CHECK(left.right() == mx - kMarkLabelGap && left.width == 30); // Directly under the cap strip, so caps and labels never fight for the same pixels. CHECK(right.y == a.y + kMarkerHandleHeight && right.height == kMarkLabelHeight); CHECK(left.y == right.y); } static void testALabelIsNudgedInsideTheAreaRatherThanClipped() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); // At frame 0 a right-side label would still fit; at the last frame it would overhang. const Rect atEnd = markLabelRect(ov, 1000, 1000, /*leftOfLine=*/false, 40); CHECK(atEnd.width == 40); CHECK(atEnd.right() == a.right()); const Rect atStart = markLabelRect(ov, 1000, 0, /*leftOfLine=*/true, 40); CHECK(atStart.width == 40); CHECK(atStart.x == a.x); // Wider than the whole band, or no band to draw in: nothing placed. CHECK(markLabelRect(ov, 1000, 500, false, a.width + 1).empty()); CHECK(markLabelRect(overlayOf(Rect{0, 0, 200, kMarkerHandleHeight}), 1000, 500, false, 20) .empty()); } static void testOverlappingLabelsAreSuppressedInPlacementOrder() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); // LOOP labels right of its line at 500, XFADE left of its line at 520: the two boxes point // at each other and cannot both fit. (LOOP and END never collide however close they get — // their labels point away from the span they bound.) const WaveMarks m = marksAt(50, 500, 900, 520, true); const int w[kWaveMarkCount] = {36, 32, 26, 40}; const WaveMarkLabels lab = layoutMarkLabels(ov, 1000, m, w, /*promoted=*/-1); CHECK(!lab.box[0].empty()); // START, far away, always placed CHECK(!lab.box[1].empty()); // LOOP placed before XFADE, so LOOP wins CHECK(!lab.box[2].empty()); // END, far away, always placed CHECK(lab.box[3].empty()); // XFADE suppressed // Every placed box is disjoint from every other. for (int i = 0; i < kWaveMarkCount; ++i) { for (int j = i + 1; j < kWaveMarkCount; ++j) { if (lab.box[i].empty() || lab.box[j].empty()) continue; CHECK(lab.box[i].x >= lab.box[j].right() || lab.box[j].x >= lab.box[i].right()); } } } // The promoted mark is placed FIRST, so grabbing or hovering a mark always shows its label — // even the one the resting layout suppresses. static void testThePromotedMarkIsNeverTheSuppressedOne() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); const WaveMarks m = marksAt(50, 500, 900, 520, true); const int w[kWaveMarkCount] = {36, 32, 26, 40}; CHECK(layoutMarkLabels(ov, 1000, m, w, -1).box[3].empty()); // XFADE suppressed at rest const WaveMarkLabels grabbed = layoutMarkLabels(ov, 1000, m, w, static_cast(WaveMark::kCrossfade)); CHECK(!grabbed.box[3].empty()); // and placed when it is the one being grabbed CHECK(grabbed.box[1].empty()); // LOOP yields to it instead } static void testAbsentMarksTakeNoLabel() { const Rect a = wideArea(); const OverlayArea ov = overlayOf(a); const WaveMarks m = marksAt(50, 300, 700, 620, /*loopPresent=*/false); const int w[kWaveMarkCount] = {36, 32, 26, 40}; const WaveMarkLabels lab = layoutMarkLabels(ov, 1000, m, w, -1); CHECK(!lab.box[0].empty()); CHECK(lab.box[1].empty() && lab.box[2].empty() && lab.box[3].empty()); } static void testTheCrossfadeWedgeRampsToItsPeakAtTheSeam() { // Zero at the fade's start, the peak at its last column, monotone in between. CHECK(crossfadeWedgeHeight(100, 200, 100) == 0); CHECK(crossfadeWedgeHeight(100, 200, 199) == kCrossfadeWedgePx); int prev = -1; for (int x = 100; x < 200; ++x) { const int h = crossfadeWedgeHeight(100, 200, x); CHECK(h >= prev); CHECK(h >= 0 && h <= kCrossfadeWedgePx); prev = h; } // Outside the span it contributes nothing, so a caller can sweep a wider range safely. CHECK(crossfadeWedgeHeight(100, 200, 99) == 0); CHECK(crossfadeWedgeHeight(100, 200, 200) == 0); // Degenerate spans: an empty one draws nothing, a one-column one is all peak. CHECK(crossfadeWedgeHeight(100, 100, 100) == 0); CHECK(crossfadeWedgeHeight(100, 99, 100) == 0); CHECK(crossfadeWedgeHeight(100, 101, 100) == kCrossfadeWedgePx); } // --- Per-lane envelope content ------------------------------------------------- static void testAsymmetricStereoLanesCarryDifferentContent() { // Left is full-scale, right is a tenth of it — the lanes must look materially different. const std::size_t frames = 400; std::vector interleaved(frames * 2); for (std::size_t f = 0; f < frames; ++f) { const AudioSample v = (f % 2 == 0) ? 1.0f : -1.0f; interleaved[f * 2 + 0] = v; interleaved[f * 2 + 1] = v * 0.1f; } // ONE pass over the interleaved source, split per lane — what the painter does. const reasampler::audio::Envelope env = reasampler::audio::computeEnvelope(interleaved, 2, frames, 20); const reasampler::audio::Envelope upper = laneEnvelope(env, 0); const reasampler::audio::Envelope lower = laneEnvelope(env, 1); CHECK(upper.size() == 1 && lower.size() == 1); CHECK(upper[0].size() == 20 && lower[0].size() == 20); for (std::size_t i = 0; i < 20; ++i) { CHECK(upper[0][i].max > 0.9f); // left near full scale CHECK(lower[0][i].max < 0.2f); // right an order of magnitude down CHECK(!(upper[0][i] == lower[0][i])); // and materially different, bin for bin } } static void testLaneEnvelopeRejectsOutOfRangeLane() { const std::size_t frames = 16; std::vector mono(frames, 0.5f); const reasampler::audio::Envelope env = reasampler::audio::computeEnvelope(mono, 1, frames, 4); CHECK(laneEnvelope(env, 0).size() == 1); CHECK(laneEnvelope(env, 1).empty()); // a mono source has no lower lane CHECK(laneEnvelope(env, -1).empty()); } int main() { testFrameToXEndpoints(); testFrameToXClampsOutOfRange(); testFrameToXDegenerate(); testXToFrameInverse(); testXToFrameClampsOutside(); testAMarkLandsOnTheWaveformColumnForItsOwnFrame(); testTheMappingAgreesWithTheDrawChainAtEveryFrame(); testTheExclusiveSpanEndLandsOnTheRightEdge(); testXToFrameRoundTripsEveryFrameWhileAFrameOwnsAColumn(); testColumnsRoundTripWhereFramesShareThem(); testMarkerAtPointGrabsWithinBand(); testMarkerAtPointMissesBetween(); testMarkerAtPointFirstMatchOnOverlap(); testMarkerAtPointRejectsNullEmpty(); testResolveDragFrameShift(); testResolveDragFrameClamps(); testResolveDragFrameRounds(); testResolveDragFrameDegenerate(); testZeroCrossingNearest(); testZeroCrossingSampleOnZero(); testZeroCrossingEquidistantTieToLower(); testZeroCrossingTiesAlwaysResolveToTheLowerFrame(); testZeroCrossingTakesTheNearerSideFromEitherDirection(); testZeroCrossingNoneKeepsTarget(); testZeroCrossingClampsTarget(); testZeroCrossingDegenerate(); testSurfaceStereoStacksTwoLanes(); testSurfaceMonoIsOneLane(); testSurfaceMonoSourceInStereoModeStaysOneLane(); testSurfaceOverlayIsFullStackedHeightInBothModes(); testTheOverlayIsExactlyTheDrawnColumnBand(); testSurfaceDegenerateBandDrawsNothing(); testSurfaceThinBandRoundsLowerLaneEmpty(); testMarkerGrabReachesTheLowerStereoLane(); testMarkerGrabInMonoSpansTheBand(); testMarkerHandleIsATopStripCentredOnTheMarker(); testCoincidentMarkersStayIndependentlyGrabbable(); testMarkerHandleClipsIntoTheArea(); testMarkerHandleOnDegenerateAreas(); testStartMarkerSharesTheHandleStripWhenItSitsAtTheFadeEdge(); testEveryMarkAnswersItsOwnCap(); testAMarkThatIsNotPresentAnswersNoCap(); testACoincidentPairStaysSeparableAcrossCapAndColumn(); testTheCrossfadeCapOutranksEveryOtherMark(); testLabelSidesKeepEachLabelOutOfTheSpanItBounds(); testALabelIsNudgedInsideTheAreaRatherThanClipped(); testOverlappingLabelsAreSuppressedInPlacementOrder(); testThePromotedMarkIsNeverTheSuppressedOne(); testAbsentMarksTakeNoLabel(); testTheCrossfadeWedgeRampsToItsPeakAtTheSeam(); testAsymmetricStereoLanesCarryDifferentContent(); testLaneEnvelopeRejectsOutOfRangeLane(); if (g_fail == 0) std::printf("waveform_view: all tests passed\n"); else std::printf("waveform_view: %d FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }