// Standalone tests for reasampler::instrument::ui::keyboard_strip — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure tests. Assert the capture-first editor's // keyboard-strip layout, root marker, key mapping, zone-bar hit regions, and the drag-delta // note resolver directly — the geometry that backs the single-capture root-set and the opt-in // Zones panel. // // Covers: layoutStrip (normal + zero); keyLeftX monotonic across the 128-key span with the // boundary at 128 == band right; keyRect / rootMarkerRect (rootMarkerRect == keyRect); // keyAtPoint inverting the mapping and clamping/ missing off-band; zoneBarRect spanning // [low,high] inclusive and collapsing (not inverting) a malformed low>high; zoneGrabAt // classifying low-edge / high-edge / body and the narrow-bar midpoint split (low wins the // tie); zoneBarAtPoint first-match on overlap + null-list rejection; resolveDragNote rounding // to the nearest key at the key centre, clamping to [0,127], and the zero-delta / zero-width // no-ops; isNaturalKey across a full octave (C4..B4), at boundary notes 0 and 127, and with // out-of-range inputs that clamp to [0,127]. #include "../src/core/instrument/ui/keyboard_strip.h" #include using namespace reasampler; using namespace reasampler::instrument::ui; 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 strip: 1280px wide (10px per key) so key math is exact and easy to reason // about. static StripLayout wideStrip() { return layoutStrip(1280, 40); } // --- layoutStrip -------------------------------------------------------------- static void testLayoutNormalArea() { const StripLayout L = layoutStrip(640, 40); CHECK(L.keys.x == 0 && L.keys.y == 0); CHECK(L.keys.right() == 640 && L.keys.bottom() == 40); } static void testLayoutZeroArea() { const StripLayout L = layoutStrip(0, 0); CHECK(L.keys.width == 0 && L.keys.height == 0); } // --- keyLeftX / keyRect / rootMarkerRect -------------------------------------- static void testKeyLeftMonotonicAndBounds() { const StripLayout L = wideStrip(); // Key 0's left edge is the band left; the 128 boundary is the band right. CHECK(keyLeftX(L, 0) == L.keys.x); CHECK(keyLeftX(L, 128) == L.keys.right()); // Strictly non-decreasing across the span. int prev = keyLeftX(L, 0); for (int n = 1; n <= 128; ++n) { const int x = keyLeftX(L, n); CHECK(x >= prev); prev = x; } // At 10px/key, key 12 (one octave) starts at 120px. CHECK(keyLeftX(L, 12) == 120); } static void testKeyRectHalfOpen() { const StripLayout L = wideStrip(); const Rect k = keyRect(L, 60); CHECK(k.x == keyLeftX(L, 60)); CHECK(k.right() == keyLeftX(L, 61)); CHECK(k.y == L.keys.y && k.bottom() == L.keys.bottom()); CHECK(k.width == 10); // 10px/key } static void testRootMarkerEqualsKeyRect() { const StripLayout L = wideStrip(); const Rect m = rootMarkerRect(L, 64); const Rect k = keyRect(L, 64); CHECK(m.x == k.x && m.right() == k.right() && m.y == k.y && m.bottom() == k.bottom()); } // --- keyAtPoint --------------------------------------------------------------- static void testKeyAtPointInverts() { const StripLayout L = wideStrip(); // A point in the middle of key 60's cell resolves to 60. const Rect k = keyRect(L, 60); CHECK(keyAtPoint(L, k.x + 5, k.y + 2) == 60); // The very left of the band is key 0; just inside the right edge is key 127. CHECK(keyAtPoint(L, L.keys.x, 2) == 0); CHECK(keyAtPoint(L, L.keys.right() - 1, 2) == 127); } static void testKeyAtPointOffBand() { const StripLayout L = wideStrip(); CHECK(keyAtPoint(L, -5, 2) == -1); // left of band CHECK(keyAtPoint(L, L.keys.right() + 5, 2) == -1); // right of band CHECK(keyAtPoint(L, 100, L.keys.bottom() + 5) == -1); // below band } // --- zoneBarRect -------------------------------------------------------------- static void testZoneBarSpansInclusive() { const StripLayout L = wideStrip(); const Rect bar = zoneBarRect(L, 12, 23); // C1..B1 inclusive CHECK(bar.x == keyLeftX(L, 12)); CHECK(bar.right() == keyLeftX(L, 24)); // high+1 -> the bar covers key 23 fully CHECK(bar.width == 120); // 12 keys * 10px } static void testZoneBarMalformedCollapses() { const StripLayout L = wideStrip(); // low > high must collapse, never invert. const Rect bar = zoneBarRect(L, 80, 40); CHECK(bar.width >= 0); CHECK(bar.right() >= bar.x); } // --- zoneGrabAt --------------------------------------------------------------- static void testZoneGrabEdgesAndBody() { const StripLayout L = wideStrip(); const Rect bar = zoneBarRect(L, 20, 60); // wide bar with a clear body const int y = L.keys.y + 2; // Near the left edge -> low; near the right edge -> high; the middle -> body. CHECK(zoneGrabAt(L, 20, 60, bar.x + 1, y) == ZoneGrab::kLowEdge); CHECK(zoneGrabAt(L, 20, 60, bar.right() - 1, y) == ZoneGrab::kHighEdge); CHECK(zoneGrabAt(L, 20, 60, bar.x + bar.width / 2, y) == ZoneGrab::kBody); // Off the bar entirely -> none. CHECK(zoneGrabAt(L, 20, 60, bar.right() + 20, y) == ZoneGrab::kNone); } static void testZoneGrabNarrowBarSplitsAtMidpointLowWins() { const StripLayout L = wideStrip(); // A 1-key bar is narrower than 2*edge: no body; the low edge wins the exact midpoint. const Rect bar = zoneBarRect(L, 50, 50); const int y = L.keys.y + 2; const int mid = bar.x + bar.width / 2; CHECK(zoneGrabAt(L, 50, 50, mid, y) == ZoneGrab::kLowEdge); // tie -> low CHECK(zoneGrabAt(L, 50, 50, bar.right() - 1, y) == ZoneGrab::kHighEdge); } // --- zoneBarAtPoint ----------------------------------------------------------- static void testZoneBarAtPointFirstMatch() { const StripLayout L = wideStrip(); const int lows[2] = {20, 30}; // zone 0 and zone 1 overlap on [30,50] const int highs[2] = {50, 70}; const Rect overlap = zoneBarRect(L, 30, 50); const int y = L.keys.y + 2; const int cx = overlap.x + overlap.width / 2; // A point in the overlap resolves to the FIRST covering zone (draw order). const ZoneBarHit hit = zoneBarAtPoint(L, lows, highs, 2, cx, y); CHECK(hit.zoneIndex == 0); CHECK(hit.grab != ZoneGrab::kNone); } static void testZoneBarAtPointNullList() { const StripLayout L = wideStrip(); const ZoneBarHit hit = zoneBarAtPoint(L, nullptr, nullptr, 0, 100, 2); CHECK(hit.zoneIndex == -1 && hit.grab == ZoneGrab::kNone); } // --- resolveDragNote ---------------------------------------------------------- static void testResolveDragRoundsToNearestKey() { const StripLayout L = wideStrip(); // 10px/key // A +25px drag from key 60 = +2.5 keys -> rounds to +3 (half-key flips at the centre). CHECK(resolveDragNote(L, 60, 25) == 63); // A +24px drag = +2.4 keys -> rounds to +2. CHECK(resolveDragNote(L, 60, 24) == 62); // Symmetric for negative deltas. CHECK(resolveDragNote(L, 60, -25) == 57); CHECK(resolveDragNote(L, 60, -24) == 58); } static void testResolveDragClampsAndNoOps() { const StripLayout L = wideStrip(); CHECK(resolveDragNote(L, 60, 0) == 60); // zero delta -> unchanged CHECK(resolveDragNote(L, 2, -1000) == 0); // clamps at 0 CHECK(resolveDragNote(L, 120, 1000) == 127); // clamps at 127 // Zero-width band -> no motion (pins to startNote, clamped). const StripLayout Z = layoutStrip(0, 40); CHECK(resolveDragNote(Z, 60, 500) == 60); } // --- isNaturalKey ------------------------------------------------------------- static void testIsNaturalKeyFullOctave() { // Semitone positions 0..11 starting at C4 (MIDI 60): // C=60(nat) C#=61(acc) D=62(nat) D#=63(acc) E=64(nat) F=65(nat) // F#=66(acc) G=67(nat) G#=68(acc) A=69(nat) A#=70(acc) B=71(nat) const bool expected[12] = { true, false, true, false, true, true, false, true, false, true, false, true, }; for (int i = 0; i < 12; ++i) { CHECK(isNaturalKey(60 + i) == expected[i]); } } static void testIsNaturalKeyBoundaryNotes() { // Note 0 is C (natural); note 127 is G (natural); note 1 is C# (accidental). CHECK(isNaturalKey(0) == true); // C0 — natural CHECK(isNaturalKey(1) == false); // C#0 — accidental CHECK(isNaturalKey(127) == true); // G9 — natural (127 % 12 == 7) CHECK(isNaturalKey(126) == false); // F#9 — accidental (126 % 12 == 6) } static void testIsNaturalKeyOutOfRangeClamped() { // Values outside [0,127] clamp to [0,127]; must not crash/UB. // note -1 clamps to 0 (C, natural); note 128 clamps to 127 (G, natural). CHECK(isNaturalKey(-1) == true); CHECK(isNaturalKey(128) == true); CHECK(isNaturalKey(-100) == true); CHECK(isNaturalKey(200) == true); } static void testResolveDragProportionalNonDivisibleWidth() { // THE REVIEW FINDING: 544px / 128 = 4.25 (non-integer). Old uniform-keyW math used // keyW = 4 (floor), accumulating ~7 keys of drift at the far end. The proportional fix // must agree with keyAtPoint at every point — specifically the far-end invariant: // a drag from note 0 by (width-1) pixels must land at keyAtPoint(width-1), which is 127. const int width = 544; const StripLayout L = layoutStrip(width, 40); CHECK(keyAtPoint(L, width - 1, L.keys.y + 1) == 127); CHECK(resolveDragNote(L, 0, width - 1) == 127); // Also verify mid-strip coherence: for each key N, a drag from 0 by N's left-edge // pixel offset should land at N (or N-1 at worst — left-edge pixel is a boundary, so // rounding may round down). The critical direction is that it must NOT over-shoot by // more than 0 (it must reach at least the right key). for (int n = 1; n < kStripKeyCount; ++n) { const int leftPx = keyRect(L, n).x; const int resolved = resolveDragNote(L, 0, leftPx); // The left edge of key N is the first pixel "in" that key, so we expect resolved == N. // Allow resolved == N-1 only when the pixel is at the exact boundary (keyEdgeToX may // produce the same x for adjacent keys when keys share a pixel). Disallow over-shoot. const int expected = keyAtPoint(L, leftPx, L.keys.y + 1); CHECK(resolved >= expected - 1 && resolved <= expected + 1); } } int main() { testLayoutNormalArea(); testLayoutZeroArea(); testKeyLeftMonotonicAndBounds(); testKeyRectHalfOpen(); testRootMarkerEqualsKeyRect(); testKeyAtPointInverts(); testKeyAtPointOffBand(); testZoneBarSpansInclusive(); testZoneBarMalformedCollapses(); testZoneGrabEdgesAndBody(); testZoneGrabNarrowBarSplitsAtMidpointLowWins(); testZoneBarAtPointFirstMatch(); testZoneBarAtPointNullList(); testResolveDragRoundsToNearestKey(); testResolveDragClampsAndNoOps(); testResolveDragProportionalNonDivisibleWidth(); testIsNaturalKeyFullOctave(); testIsNaturalKeyBoundaryNotes(); testIsNaturalKeyOutOfRangeClamped(); if (g_fail == 0) std::printf("keyboard_strip: all tests passed\n"); return g_fail != 0; }