diff --git a/src/vst/keyboard_strip.cpp b/src/vst/keyboard_strip.cpp index 9e6f4c1..98638b1 100644 --- a/src/vst/keyboard_strip.cpp +++ b/src/vst/keyboard_strip.cpp @@ -102,6 +102,29 @@ ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* return ZoneBarHit{}; // on the band but on no bar } +bool isNaturalKey(int note) { + // Clamp to the valid MIDI range before indexing. + const int n = note < 0 ? 0 : (note > kStripKeyCount - 1 ? kStripKeyCount - 1 : note); + // The 12-semitone pattern of natural (white) keys within an octave, starting at C: + // positions 0(C) 2(D) 4(E) 5(F) 7(G) 9(A) 11(B) are natural; + // positions 1(C#) 3(D#) 6(F#) 8(G#) 10(A#) are accidental. + static constexpr bool kNatural[12] = { + true, // 0 C + false, // 1 C# + true, // 2 D + false, // 3 D# + true, // 4 E + true, // 5 F + false, // 6 F# + true, // 7 G + false, // 8 G# + true, // 9 A + false, // 10 A# + true, // 11 B + }; + return kNatural[n % 12]; +} + int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels) { if (dxPixels == 0) return clampNote(startNote); const int bandWidth = std::max(0, layout.keys.width()); diff --git a/src/vst/keyboard_strip.h b/src/vst/keyboard_strip.h index 9b6a846..ac8b4c3 100644 --- a/src/vst/keyboard_strip.h +++ b/src/vst/keyboard_strip.h @@ -118,4 +118,14 @@ ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* // key centre. Returns startNote unchanged for dxPixels==0. int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels); +// Returns true when `note` (0..127) is a NATURAL (white) key in standard 12-tone equal +// temperament; false when it is an ACCIDENTAL (black) key. Notes out of the [0,127] +// range are clamped to [0,127] before classification (i.e. this never throws/UBs on a +// bad input). The 12 semitone positions within an octave: +// Natural (white): 0(C) 2(D) 4(E) 5(F) 7(G) 9(A) 11(B) +// Accidental (black): 1(C#) 3(D#) 6(F#) 8(G#) 10(A#) +// Used by the shell to overlay the two-tone bright/dark piano-key pattern over the +// pastel spectral fill (S-VIEW-7). Pure — no layout required, no host types. +bool isNaturalKey(int note); + } // namespace reasampler::vst diff --git a/tests/test_keyboard_strip.cpp b/tests/test_keyboard_strip.cpp index 65e6880..57596cf 100644 --- a/tests/test_keyboard_strip.cpp +++ b/tests/test_keyboard_strip.cpp @@ -11,7 +11,8 @@ // 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. +// 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/vst/keyboard_strip.h" @@ -179,6 +180,38 @@ static void testResolveDragClampsAndNoOps() { 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 @@ -221,6 +254,9 @@ int main() { testResolveDragRoundsToNearestKey(); testResolveDragClampsAndNoOps(); testResolveDragProportionalNonDivisibleWidth(); + testIsNaturalKeyFullOctave(); + testIsNaturalKeyBoundaryNotes(); + testIsNaturalKeyOutOfRangeClamped(); if (g_fail == 0) std::printf("keyboard_strip: all tests passed\n"); return g_fail != 0;