192 lines
7.9 KiB
C++
192 lines
7.9 KiB
C++
// 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 editor's keyboard-strip
|
|
// layout, root marker, key mapping, and drag-delta note resolver directly — the geometry
|
|
// that backs the root display and root-set.
|
|
//
|
|
// 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; 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 <cstdio>
|
|
|
|
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
|
|
}
|
|
|
|
// --- 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();
|
|
testResolveDragRoundsToNearestKey();
|
|
testResolveDragClampsAndNoOps();
|
|
testResolveDragProportionalNonDivisibleWidth();
|
|
testIsNaturalKeyFullOctave();
|
|
testIsNaturalKeyBoundaryNotes();
|
|
testIsNaturalKeyOutOfRangeClamped();
|
|
|
|
if (g_fail == 0) std::printf("keyboard_strip: all tests passed\n");
|
|
return g_fail != 0;
|
|
}
|