diff --git a/src/core/instrument/ui/waveform_view.cpp b/src/core/instrument/ui/waveform_view.cpp index 3bd9c88..2fbf2d7 100644 --- a/src/core/instrument/ui/waveform_view.cpp +++ b/src/core/instrument/ui/waveform_view.cpp @@ -33,8 +33,10 @@ WaveformSurface waveformSurface(const Rect& band, bool stereoMode, int sourceCha waveformLanes(band, twoLanes ? LaneSplit::Stereo : LaneSplit::Single); s.upper = lanes.upper; s.lower = lanes.lower; - // Derived from the resolved lanes, not `twoLanes`, so it can never contradict them (a - // band barely over the two-lane floor can still yield an empty lower lane). + // Derived from the resolved lanes, not `twoLanes` — a stereo split's integer division + // rounds the lower lane to empty for a band this thin (height <= 3), far below the + // allocator's kWaveformMinHeight floor but reachable if this is called directly with an + // arbitrary rect (as tests do). s.laneCount = lanes.lower.empty() ? 1 : 2; return s; } @@ -53,12 +55,13 @@ int frameToX(const OverlayArea& area, std::int64_t frameCount, std::int64_t fram return area.rect.x + static_cast(num / frameCount); } -std::int64_t xToFrame(const Rect& area, std::int64_t frameCount, int x) { - const int w = std::max(0, area.width); +std::int64_t xToFrame(const OverlayArea& area, std::int64_t frameCount, int x) { + const Rect& r = area.rect; + const int w = std::max(0, r.width); if (frameCount <= 0 || w <= 0) return 0; - if (x <= area.x) return 0; - if (x >= area.right()) return frameCount; - const std::int64_t dx = static_cast(x - area.x); + if (x <= r.x) return 0; + if (x >= r.right()) return frameCount; + const std::int64_t dx = static_cast(x - r.x); // Inverse of frameToX: frame = round(dx * frameCount / w). const std::int64_t num = dx * frameCount + static_cast(w) / 2; return clampFrame(num / static_cast(w), frameCount); diff --git a/src/core/instrument/ui/waveform_view.h b/src/core/instrument/ui/waveform_view.h index b1d76c6..b3b8efc 100644 --- a/src/core/instrument/ui/waveform_view.h +++ b/src/core/instrument/ui/waveform_view.h @@ -28,10 +28,11 @@ using audio::AudioSample; struct WaveformSurface { Rect upper; // lane 0 -> channel 0 (LEFT); the whole band when single-lane Rect lower; // lane 1 -> channel 1 (RIGHT); empty() when single-lane - OverlayArea overlay; // the full band, both modes — a distinct type (not Rect) so an - // overlay-only API can't accept a lane rect by mistake - int laneCount = 0; // 0 on a degenerate band, else 1 or 2 — derived from the resolved - // lanes (never contradicts upper/lower) + OverlayArea overlay; // the full band, both modes + int laneCount = 0; // 0 on a degenerate band, else 1 or 2 — matches `lower`'s emptiness + // (2 iff lower non-empty). For a non-empty band <= 2px tall, `upper` + // can be empty too while this still reports 1 — unreachable through + // the band-stack allocator's kWaveformMinHeight floor. }; // Resolves the surface for a waveform band. Two lanes need BOTH stereo mode and a source @@ -62,7 +63,7 @@ int frameToX(const OverlayArea& area, std::int64_t frameCount, std::int64_t fram // Inverse of frameToX: the frame a point x maps to, clamped to [0, frameCount]. A point left of // area.x yields 0; right of area.right() yields frameCount. -std::int64_t xToFrame(const Rect& area, std::int64_t frameCount, int x); +std::int64_t xToFrame(const OverlayArea& area, std::int64_t frameCount, int x); // Which marker (index into the caller's parallel `frames` array, in draw order) a grab at // (x, y) lands on, or -1 for a miss. A marker is grabbed when x is within kMarkerGrabWidth of diff --git a/tests/test_envelope_edit.cpp b/tests/test_envelope_edit.cpp index fb9a402..f4a77e2 100644 --- a/tests/test_envelope_edit.cpp +++ b/tests/test_envelope_edit.cpp @@ -45,8 +45,6 @@ static bool findNode(const std::vector& poly, EnvNode node, EnvVertex static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 110); } static constexpr double kTotal = 2.0; -// nodeAtPoint/resolveNodeDrag take the overlay type, not a bare Rect (the distinct-type -// enforcement in editor_geometry.h) — this wraps a plain test Rect for them. static OverlayArea overlayOf(const Rect& r) { return OverlayArea{r}; } static const double kGateSecPerPx = 1.0 / gatePxPerSecond(wideArea()); diff --git a/tests/test_envelope_overlay.cpp b/tests/test_envelope_overlay.cpp index 3e87dfa..e32e018 100644 --- a/tests/test_envelope_overlay.cpp +++ b/tests/test_envelope_overlay.cpp @@ -26,8 +26,6 @@ static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) -// buildEnvelopePolyline takes the overlay type, not a bare Rect (the distinct-type -// enforcement in editor_geometry.h) — this wraps a plain test Rect for it. static OverlayArea overlayOf(const Rect& r) { return OverlayArea{r}; } // A comfortable overlay area: 1000px wide, 100px tall, offset so left/top != 0 (catches origin diff --git a/tests/test_waveform_view.cpp b/tests/test_waveform_view.cpp index 8d4ca76..e4cb1c6 100644 --- a/tests/test_waveform_view.cpp +++ b/tests/test_waveform_view.cpp @@ -26,8 +26,6 @@ static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) -// frameToX/markerAtPoint/resolveDragFrame take the overlay type, not a bare Rect (the -// distinct-type enforcement in editor_geometry.h) — this wraps a plain test Rect for them. static OverlayArea overlayOf(const Rect& r) { return OverlayArea{r}; } // A comfortable waveform area: 1000px wide, offset so left != 0 (catches origin bugs). @@ -57,16 +55,16 @@ static void testFrameToXDegenerate() { 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 + 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(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 + 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 } static void testFrameToXRoundTrip() { @@ -75,7 +73,7 @@ static void testFrameToXRoundTrip() { const Rect a = Rect::ltrb(0, 0, 800, 60); for (std::int64_t f = 0; f <= 2000; f += 137) { const int x = frameToX(overlayOf(a), 2000, f); - const std::int64_t back = xToFrame(a, 2000, x); + const std::int64_t back = xToFrame(overlayOf(a), 2000, x); CHECK(back >= f - 3 && back <= f + 3); } } @@ -269,6 +267,15 @@ static void testSurfaceDegenerateBandDrawsNothing() { CHECK(waveformOverlayArea(Rect{10, 10, 0, 0}).rect.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() { @@ -367,6 +374,7 @@ int main() { testSurfaceMonoSourceInStereoModeStaysOneLane(); testSurfaceOverlayIsFullStackedHeightInBothModes(); testSurfaceDegenerateBandDrawsNothing(); + testSurfaceThinBandRoundsLowerLaneEmpty(); testMarkerGrabReachesTheLowerStereoLane(); testMarkerGrabInMonoSpansTheBand();