// Standalone tests for reasampler::instrument::ui::keyboard_strip — no VST3, no REAPER, no // framework. Same fast assert loop as the sibling pure tests. // // Covers: layoutStrip (normal, degenerate, sub-key-width); same-class key-width uniformity // swept across editor client widths (including multiples of them, standing in for larger // client sizes — see the client-pixel-only note below); the tiled key area staying centred // inside a band that spans the full width it was handed; whiteIndexOf / isNaturalKey across // octave boundaries and the 0..127 extremes; keyRect tiling and black-over-white overlap; // keyAtPoint resolving black-over-white by zone and missing off-band; the root affordance's // hit-to-marker round trip; resolveDragNote clamping a wandering pointer; noteName under the // C4 (MIDI 60) DAW convention; the gutter at the sawtooth's maximum residue; and the gutter // pinned at the shipped default window size (derived from kEditorMinWidth/kEditorMinHeight). // // Client-pixel-only guarantee: every width swept below is a CLIENT-pixel width. Nothing in // the instrument implements IPlugViewContentScaleSupport, so if a host scales the plugin // window itself, uniform integer key widths get resampled at the physical-pixel level — // unverified by this suite (see src/core/instrument/CLAUDE.md). #include "../src/core/instrument/ui/keyboard_strip.h" #include "../src/core/instrument/ui/sample_bands.h" // computeSampleBands, to derive the CHROME // band the shipped default window (840x620, // editor_session.cpp) hands the strip #include "../src/core/instrument/ui/sample_chrome.h" // chromeRects, to derive rootStrip's width // the same way the shell does #include #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: wide enough that every class is several pixels across. static StripLayout wideStrip() { return layoutStrip(1280, 30); } // The widths a real editor hands the strip — the 560px minimum client up to a wide window. static const int kBaseWidths[] = {544, 600, 640, 700, 749, 750, 751, 824, 900, 1000, 1024, 1103, 1264, 1600, 1920, 2400}; // Multiplies each base width to widen client-pixel coverage (e.g. a maximized/larger client // area) — NOT a host DPI/content-scale factor; see the client-pixel-only note above. static const double kWidthMultipliers[] = {1.0, 1.25, 1.5, 1.75, 2.0}; // --- layoutStrip -------------------------------------------------------------- static void testLayoutFillsTheBandAndCentresTheKeys() { const StripLayout L = layoutStrip(824, 30); CHECK(L.band == Rect::ltrb(0, 0, 824, 30)); CHECK(L.whiteWidth == 824 / kStripWhiteKeyCount); CHECK(L.keys.width == L.whiteWidth * kStripWhiteKeyCount); CHECK(L.keys.y == 0 && L.keys.height == 30); // The residue an indivisible width leaves splits evenly between the two end margins. const int leftMargin = L.keys.x - L.band.x; const int rightMargin = L.band.right() - L.keys.right(); CHECK(leftMargin >= 0 && rightMargin >= 0); CHECK(rightMargin - leftMargin >= 0 && rightMargin - leftMargin <= 1); } static void testDegenerateSizesYieldNoKeys() { const StripLayout zero = layoutStrip(0, 0); CHECK(zero.band.empty()); CHECK(zero.keys.empty()); CHECK(keyAtPoint(zero, 0, 0) == -1); CHECK(keyRect(zero, 60).empty()); CHECK(resolveDragNote(zero, 5, 5) == -1); // Narrower than one pixel per white key: no keys at all, but the band still reports its // size so the caller can draw the empty surface. const StripLayout narrow = layoutStrip(kStripWhiteKeyCount - 1, 30); CHECK(narrow.band.width == kStripWhiteKeyCount - 1); CHECK(narrow.keys.empty()); CHECK(keyAtPoint(narrow, 10, 10) == -1); } // --- the sharp one: same-class widths are uniform at every client width tested --- static void testSameClassKeysAreEqualWidthAcrossClientWidths() { for (const int base : kBaseWidths) { for (const double scale : kWidthMultipliers) { const int w = static_cast(base * scale); const int h = static_cast(30 * scale); const StripLayout L = layoutStrip(w, h); if (L.keys.empty()) continue; // covered by the degenerate test int whiteW = -1; int blackW = -1; int blackH = -1; for (int n = 0; n < kStripKeyCount; ++n) { const Rect k = keyRect(L, n); CHECK(!k.empty()); if (isNaturalKey(n)) { if (whiteW < 0) whiteW = k.width; CHECK(k.width == whiteW); CHECK(k.height == L.keys.height); // whites run the full band height } else { if (blackW < 0) { blackW = k.width; blackH = k.height; } CHECK(k.width == blackW); CHECK(k.height == blackH); } } CHECK(whiteW == L.whiteWidth); CHECK(blackW == L.blackWidth); CHECK(blackH == L.blackHeight); CHECK(blackW < whiteW); // the two classes stay visually distinct CHECK(blackH < L.keys.height); } } } static void testKeyAreaSpansTheBandWithinOneKeyAtEveryTestedWidth() { for (const int base : kBaseWidths) { for (const double scale : kWidthMultipliers) { const int w = static_cast(base * scale); const StripLayout L = layoutStrip(w, 30); CHECK(L.band.width == w); // the strip always spans the width it was handed if (L.keys.empty()) continue; // The keys cover all but w % 75 — the price of uniform integer key widths, and // the reason the residue is a margin rather than a per-key rounding wobble. const int margins = L.band.width - L.keys.width; CHECK(margins == w % kStripWhiteKeyCount); CHECK(margins >= 0 && margins < kStripWhiteKeyCount); const int leftMargin = L.keys.x - L.band.x; CHECK(leftMargin == margins / 2); // split evenly, odd pixel to the right CHECK(L.keys.x >= L.band.x && L.keys.right() <= L.band.right()); } } } // --- key classification + white ordinals -------------------------------------- static void testIsNaturalKeyAcrossAnOctaveAndTheExtremes() { // C4..B4 (MIDI 60..71). 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]); CHECK(isNaturalKey(0) == true); // C-1 CHECK(isNaturalKey(1) == false); // C#-1 CHECK(isNaturalKey(127) == true); // G9 CHECK(isNaturalKey(126) == false); // F#9 // Out-of-range clamps rather than indexing off the table. CHECK(isNaturalKey(-100) == true); CHECK(isNaturalKey(200) == true); } static void testWhiteIndexCountsNaturalsBelowTheNote() { CHECK(whiteIndexOf(0) == 0); // C-1 is the first white key CHECK(whiteIndexOf(1) == 1); // C#-1 straddles the C/D boundary CHECK(whiteIndexOf(2) == 1); // D-1 is the second white key CHECK(whiteIndexOf(4) == 2); // E-1 CHECK(whiteIndexOf(5) == 3); // F-1 (no black between E and F) CHECK(whiteIndexOf(11) == 6); // B-1 CHECK(whiteIndexOf(12) == 7); // C0 opens the next octave CHECK(whiteIndexOf(60) == 35); // C4 CHECK(whiteIndexOf(127) == kStripWhiteKeyCount - 1); // G9 is the last white key } // --- keyRect ------------------------------------------------------------------ static void testWhiteKeysTileTheKeyAreaGapFree() { const StripLayout L = wideStrip(); int expectedLeft = L.keys.x; for (int n = 0; n < kStripKeyCount; ++n) { if (!isNaturalKey(n)) continue; const Rect k = keyRect(L, n); CHECK(k.x == expectedLeft); expectedLeft = k.right(); } CHECK(expectedLeft == L.keys.right()); // the last white ends exactly on the key area } static void testBlackKeysStraddleTheirWhiteBoundary() { const StripLayout L = wideStrip(); for (int n = 1; n < kStripKeyCount - 1; ++n) { if (isNaturalKey(n)) continue; const Rect black = keyRect(L, n); const Rect below = keyRect(L, n - 1); // the natural under the accidental const Rect above = keyRect(L, n + 1); CHECK(black.x > below.x && black.right() < above.right()); CHECK(black.x < below.right()); // overlaps the white on its left CHECK(black.right() > above.x); // and the white on its right } } static void testRootMarkerIsTheRootKey() { const StripLayout L = wideStrip(); CHECK(rootMarkerRect(L, 64) == keyRect(L, 64)); CHECK(rootMarkerRect(L, 61) == keyRect(L, 61)); // Out-of-range roots clamp instead of producing a stray rect. CHECK(rootMarkerRect(L, -5) == keyRect(L, 0)); CHECK(rootMarkerRect(L, 999) == keyRect(L, 127)); } // --- keyAtPoint --------------------------------------------------------------- static void testEveryKeyIsReachableAtItsOwnCentre() { const StripLayout L = wideStrip(); for (int n = 0; n < kStripKeyCount; ++n) { const Rect k = keyRect(L, n); const int cx = k.x + k.width / 2; // A white key only answers below the black zone, where the accidentals end. const int cy = isNaturalKey(n) ? L.keys.bottom() - 1 : k.y + k.height / 2; CHECK(keyAtPoint(L, cx, cy) == n); } } static void testBlackKeysWinInTheirZoneAndWhitesWinBelowIt() { const StripLayout L = wideStrip(); const Rect cSharp = keyRect(L, 61); // C#4 const int cx = cSharp.x + cSharp.width / 2; CHECK(keyAtPoint(L, cx, cSharp.y) == 61); // in the black zone const int below = keyAtPoint(L, cx, L.keys.bottom() - 1); CHECK(below != 61); // below it, a white answers CHECK(below == 60 || below == 62); // C4 or D4, whichever it overlaps // E-F and B-C have no accidental between them: the top row there is still white. const Rect e4 = keyRect(L, 64); CHECK(keyAtPoint(L, e4.right() - 1, e4.y) == 64); } static void testKeyAtPointMissesOffBandAndInTheEndMargins() { const StripLayout L = layoutStrip(824, 30); CHECK(keyAtPoint(L, -5, 5) == -1); CHECK(keyAtPoint(L, L.band.right() + 5, 5) == -1); CHECK(keyAtPoint(L, 100, L.band.bottom() + 5) == -1); CHECK(keyAtPoint(L, 100, -1) == -1); if (L.keys.x > L.band.x) CHECK(keyAtPoint(L, L.band.x, 5) == -1); // left cheek margin if (L.keys.right() < L.band.right()) CHECK(keyAtPoint(L, L.band.right() - 1, 5) == -1); // right cheek margin } // --- the root affordance ------------------------------------------------------ static void testHitTestingAKeyMarksThatSameKey() { // The pure half of "click a key, the displayed root moves there": whatever keyAtPoint // resolves, the root marker lands exactly on that key — no off-by-one between the key // the pointer hit and the key drawn lit. const StripLayout L = wideStrip(); for (int n = 0; n < kStripKeyCount; ++n) { const Rect k = keyRect(L, n); const int cx = k.x + k.width / 2; const int cy = isNaturalKey(n) ? L.keys.bottom() - 1 : k.y + k.height / 2; const int hit = keyAtPoint(L, cx, cy); CHECK(hit == n); CHECK(rootMarkerRect(L, hit) == k); } } // --- resolveDragNote ---------------------------------------------------------- static void testDragTracksThePointerAndClampsWhenItWanders() { const StripLayout L = wideStrip(); const Rect g4 = keyRect(L, 67); const int cx = g4.x + g4.width / 2; CHECK(resolveDragNote(L, cx, L.keys.bottom() - 1) == 67); // Wandering off the strip keeps tracking at the clamped edge rather than dropping out. CHECK(resolveDragNote(L, -500, L.keys.bottom() - 1) == 0); CHECK(resolveDragNote(L, L.band.right() + 500, L.keys.bottom() - 1) == 127); // Above the strip clamps into the black zone; G4's centre is clear of both flanking // accidentals, so it still answers G4 rather than F#4 or G#4. CHECK(resolveDragNote(L, cx, -400) == 67); CHECK(resolveDragNote(L, cx, 4000) == 67); } // --- noteName ----------------------------------------------------------------- static void testNoteNamesFollowTheC4Convention() { CHECK(noteName(60) == "C4"); // middle C, the convention REAPER uses CHECK(noteName(61) == "C#4"); CHECK(noteName(59) == "B3"); // the octave rolls at B->C, not at A->B CHECK(noteName(72) == "C5"); CHECK(noteName(0) == "C-1"); // the low extreme CHECK(noteName(11) == "B-1"); CHECK(noteName(12) == "C0"); CHECK(noteName(127) == "G9"); // the high extreme CHECK(noteName(126) == "F#9"); // Out-of-range clamps to the extremes rather than naming an unplayable note. CHECK(noteName(-1) == "C-1"); CHECK(noteName(500) == "G9"); } // --- the gutter at the sawtooth's maximum residue ------------------------------- // The rule-based sweep above pins `margins == w % 75` and `leftMargin == margins/2` at every // width, but pins no concrete number. The strip is a sawtooth with period kStripWhiteKeyCount // (75) px of window width; 840 is an arbitrary but independently useful sample because it // lands on residue 74 — the cycle's maximum: one pixel of resize (840->841) collapses both // gutters to zero and grows every white key from 10px to 11px. Kept as a synthetic worst-case // sample; NOT tied to any shipped window size (see testGutterAtTheShippedDefaultWindowSize // below for that). static void testGutterAtTheSawtoothMaximumResidueWidth() { constexpr int kSawtoothMaxResidueStripW = 824; // an arbitrary width landing on residue 74 const StripLayout L = layoutStrip(kSawtoothMaxResidueStripW, 30); CHECK(L.whiteWidth == 10); const int margins = L.band.width - L.keys.width; CHECK(margins == 74); const int leftMargin = L.keys.x - L.band.x; CHECK(leftMargin == 37); } // --- the gutter at the shipped default window size ----------------------------- // Daniel is making a visual call on the specific gutter at the shipped default; neither kPad // nor the floor is covered by another test firing if either ever changes. Derived from // kEditorMinWidth/kEditorMinHeight (editor_session.cpp's ViewRect default IS the floor) rather // than a hardcoded window size, so a floor change fails HERE instead of silently moving the // shipped gutter out from under it. The numbers below are today's floor (1190x680); re-derive // them by hand if the floor ever moves. static void testGutterAtTheShippedDefaultWindowSize() { // Derive rootStrip's width the same way the shell does, through the real allocator + // chrome layout, rather than re-deriving the inset formula. const SampleBands bands = computeSampleBands(kEditorMinWidth, kEditorMinHeight, 0); const ChromeRects chrome = chromeRects(bands.chrome, /*knobSize=*/24); const int stripW = chrome.rootStrip.width; CHECK(stripW == kEditorMinWidth - 2 * kPad); CHECK(stripW == 1174); const StripLayout L = layoutStrip(stripW, 30); CHECK(L.whiteWidth == 15); const int margins = L.band.width - L.keys.width; CHECK(margins == 49); const int leftMargin = L.keys.x - L.band.x; CHECK(leftMargin == 24); } int main() { testLayoutFillsTheBandAndCentresTheKeys(); testDegenerateSizesYieldNoKeys(); testSameClassKeysAreEqualWidthAcrossClientWidths(); testKeyAreaSpansTheBandWithinOneKeyAtEveryTestedWidth(); testIsNaturalKeyAcrossAnOctaveAndTheExtremes(); testWhiteIndexCountsNaturalsBelowTheNote(); testWhiteKeysTileTheKeyAreaGapFree(); testBlackKeysStraddleTheirWhiteBoundary(); testRootMarkerIsTheRootKey(); testEveryKeyIsReachableAtItsOwnCentre(); testBlackKeysWinInTheirZoneAndWhitesWinBelowIt(); testKeyAtPointMissesOffBandAndInTheEndMargins(); testHitTestingAKeyMarksThatSameKey(); testDragTracksThePointerAndClampsWhenItWanders(); testNoteNamesFollowTheC4Convention(); testGutterAtTheSawtoothMaximumResidueWidth(); testGutterAtTheShippedDefaultWindowSize(); if (g_fail == 0) { std::printf("keyboard_strip: all tests passed\n"); return 0; } std::printf("keyboard_strip: %d failure(s)\n", g_fail); return 1; }