// 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 (linear map + inverse, edge clamps, degenerate frameCount/width); // markerAtPoint (grab band, first-match on overlap, off-area + null-array rejection); // resolveDragFrame (round-to-nearest-frame, clamp to [0,frameCount], zero-delta/zero-width // no-ops); nearestZeroCrossing (nearest sign-change, sample-on-zero, equidistant-tie-to-lower, // no-crossing keeps target, target clamp, degenerate buffers); waveformSurface (two stacked // lanes L-over-R in stereo, one lane in mono AND for a mono source, overlay always the full // stacked height, grabs reaching the lower lane); laneEnvelope (per-lane channel split). #include "../src/core/instrument/ui/waveform_view.h" #include #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) // 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 ------------------------------------------------------ static void testFrameToXEndpoints() { const Rect a = wideArea(); CHECK(frameToX(a, 1000, 0) == a.x); // frame 0 -> left edge CHECK(frameToX(a, 1000, 1000) == a.right()); // frameCount -> right edge CHECK(frameToX(a, 1000, 500) == a.x + 500); // midpoint (1:1 here) } static void testFrameToXClampsOutOfRange() { const Rect a = wideArea(); CHECK(frameToX(a, 1000, -50) == a.x); // below 0 pins left CHECK(frameToX(a, 1000, 5000) == a.right()); // above count pins right } static void testFrameToXDegenerate() { const Rect a = wideArea(); CHECK(frameToX(a, 0, 100) == a.x); // no frames -> left const Rect z = Rect::ltrb(5, 5, 5, 45); // zero width CHECK(frameToX(z, 1000, 500) == z.x); } static void testXToFrameInverse() { const Rect a = wideArea(); CHECK(xToFrame(a, 1000, a.x) == 0); CHECK(xToFrame(a, 1000, a.right()) == 1000); CHECK(xToFrame(a, 1000, a.x + 250) == 250); // 1:1 map here } static void testXToFrameClampsOutside() { const Rect a = wideArea(); CHECK(xToFrame(a, 1000, a.x - 100) == 0); // left of area -> 0 CHECK(xToFrame(a, 1000, a.right() + 100) == 1000); // right of area -> frameCount CHECK(xToFrame(a, 0, a.x + 10) == 0); // no frames -> 0 } static void testFrameToXRoundTrip() { // Round-trip at a non-1:1 scale: 800px area over 2000 frames (2.5 frames/px). frameToX then // xToFrame should land within a couple frames (rounding both directions). const Rect a = Rect::ltrb(0, 0, 800, 60); for (std::int64_t f = 0; f <= 2000; f += 137) { const int x = frameToX(a, 2000, f); const std::int64_t back = xToFrame(a, 2000, x); CHECK(back >= f - 3 && back <= f + 3); } } // --- markerAtPoint ------------------------------------------------------------ static void testMarkerAtPointGrabsWithinBand() { const Rect a = wideArea(); // 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(a, 1000, frames, 3, a.x + 100, midY) == 0); CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500, midY) == 1); CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 900, midY) == 2); // Within the grab band on either side of the line. CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500 + kMarkerGrabWidth, midY) == 1); CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500 - kMarkerGrabWidth, midY) == 1); } static void testMarkerAtPointMissesBetween() { const Rect a = wideArea(); 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(a, 1000, frames, 3, a.x + 300, midY) == -1); // Off the area vertically. CHECK(markerAtPoint(a, 1000, frames, 3, a.x + 500, a.y - 5) == -1); } static void testMarkerAtPointFirstMatchOnOverlap() { const Rect a = wideArea(); // 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(a, 1000, frames, 2, a.x + 400, midY) == 0); } static void testMarkerAtPointRejectsNullEmpty() { const Rect a = wideArea(); const int midY = a.y + a.height / 2; CHECK(markerAtPoint(a, 1000, nullptr, 3, a.x + 100, midY) == -1); const std::int64_t frames[1] = {100}; CHECK(markerAtPoint(a, 1000, frames, 0, a.x + 100, midY) == -1); } // --- resolveDragFrame --------------------------------------------------------- static void testResolveDragFrameShift() { const Rect a = wideArea(); // 1:1 (1000px / 1000 frames) CHECK(resolveDragFrame(a, 1000, 300, 0) == 300); // zero delta -> unchanged CHECK(resolveDragFrame(a, 1000, 300, 100) == 400); // +100px -> +100 frames CHECK(resolveDragFrame(a, 1000, 300, -50) == 250); // -50px -> -50 frames } static void testResolveDragFrameClamps() { const Rect a = wideArea(); CHECK(resolveDragFrame(a, 1000, 50, -500) == 0); // clamp low CHECK(resolveDragFrame(a, 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 Rect a = Rect::ltrb(0, 0, 300, 60); // 1000 frames / 300px = 3.33 frames/px // +3px -> 3*1000/300 = 10.0 -> 10 frames. CHECK(resolveDragFrame(a, 1000, 100, 3) == 110); // +1px -> 1000/300 = 3.33 -> rounds to 3. CHECK(resolveDragFrame(a, 1000, 100, 1) == 103); } static void testResolveDragFrameDegenerate() { const Rect z = Rect::ltrb(0, 0, 0, 60); // zero width CHECK(resolveDragFrame(z, 1000, 300, 100) == 300); // pinned to start const Rect a = wideArea(); CHECK(resolveDragFrame(a, 0, 300, 100) == 0); // no frames -> clamp(start)=0 // startFrame out of range is clamped first. CHECK(resolveDragFrame(a, 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); } 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. CHECK(st.overlay == b); CHECK(st.overlay.height == st.upper.height + kLaneGap + st.lower.height); CHECK(st.overlay != st.upper && st.overlay != st.lower); // Mono: the same rect, which is also the single lane. CHECK(mo.overlay == b); CHECK(mo.overlay == 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); } 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.empty()); CHECK(waveformOverlayArea(Rect{10, 10, 0, 0}).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. CHECK(markerAtPoint(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); } // --- 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(); testFrameToXRoundTrip(); testMarkerAtPointGrabsWithinBand(); testMarkerAtPointMissesBetween(); testMarkerAtPointFirstMatchOnOverlap(); testMarkerAtPointRejectsNullEmpty(); testResolveDragFrameShift(); testResolveDragFrameClamps(); testResolveDragFrameRounds(); testResolveDragFrameDegenerate(); testZeroCrossingNearest(); testZeroCrossingSampleOnZero(); testZeroCrossingEquidistantTieToLower(); testZeroCrossingNoneKeepsTarget(); testZeroCrossingClampsTarget(); testZeroCrossingDegenerate(); testSurfaceStereoStacksTwoLanes(); testSurfaceMonoIsOneLane(); testSurfaceMonoSourceInStereoModeStaysOneLane(); testSurfaceOverlayIsFullStackedHeightInBothModes(); testSurfaceDegenerateBandDrawsNothing(); testMarkerGrabReachesTheLowerStereoLane(); testMarkerGrabInMonoSpansTheBand(); 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; }