S10: capture-first ReaSampler 9000 editor — browser, single-capture setup, zones toggle
Reverse S4 auto-select (empty=silence+empty state), add peak-thumbnail capture browser + bank filter + keyboard-strip drag machine, v3 component state (selection + zones), and the S-NAME-1 filename/display rename (UID locked). MSVC min/max macro collisions fixed post-implementation; 26/26 tests green.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
// Standalone tests for reasampler::vst::capture_browser — no VST3, no REAPER, no framework.
|
||||
// Same fast assert loop as the sibling pure tests (embed_strip / editor_geometry): assert
|
||||
// the capture-first browser's card-grid + bank-filter-tab layout and hit-testing directly.
|
||||
//
|
||||
// Covers: layoutBrowser splitting an area into the tab strip + card grid and deriving the
|
||||
// column count; a tiny/zero area (no inversion, columns >= 1); cardCellRect / cardContentRect
|
||||
// / cardThumbnailRect / cardLabelRect tiling row-major across columns with the gutter inset
|
||||
// and the thumbnail band above the label; cardHitTest landing on the card content (and MISSING
|
||||
// in the inter-card gutter, past the last card, and on the tab strip); filterTabRect dividing
|
||||
// the strip into equal segments with the last tab absorbing the remainder; filterTabHitTest
|
||||
// hitting each tab and missing off-strip.
|
||||
|
||||
#include "../src/vst/capture_browser.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
using namespace reasampler::vst;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// --- layoutBrowser ------------------------------------------------------------
|
||||
|
||||
static void testLayoutNormalArea() {
|
||||
// Wide enough for several columns of the fixed-width card.
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
CHECK(L.tabStrip.left == 0 && L.tabStrip.top == 0 && L.tabStrip.right == 560);
|
||||
CHECK(L.tabStrip.height() == kBrowserTabHeight);
|
||||
// The grid starts right below the tab strip and fills the rest, contiguous.
|
||||
CHECK(L.grid.top == L.tabStrip.bottom);
|
||||
CHECK(L.grid.bottom == 300 && L.grid.right == 560);
|
||||
// columns = grid.width() / cardWidth (>= 1).
|
||||
CHECK(L.columns == 560 / kBrowserCardWidth);
|
||||
CHECK(L.columns >= 1);
|
||||
}
|
||||
|
||||
static void testLayoutNarrowAreaSingleColumn() {
|
||||
// Narrower than one card: still a single column, no inversion.
|
||||
const BrowserLayout L = layoutBrowser(kBrowserCardWidth - 10, 200);
|
||||
CHECK(L.columns == 1);
|
||||
CHECK(L.grid.width() >= 0);
|
||||
CHECK(L.tabStrip.height() == kBrowserTabHeight);
|
||||
}
|
||||
|
||||
static void testLayoutZeroArea() {
|
||||
const BrowserLayout L = layoutBrowser(0, 0);
|
||||
CHECK(L.tabStrip.width() == 0 && L.tabStrip.height() == 0);
|
||||
CHECK(L.grid.width() == 0);
|
||||
CHECK(L.columns == 1); // never zero (avoids a divide-by-zero in card layout)
|
||||
}
|
||||
|
||||
static void testLayoutTinyHeightClampsTabStrip() {
|
||||
// A height below the tab band: the tab strip clamps to the area, the grid is empty.
|
||||
const BrowserLayout L = layoutBrowser(560, kBrowserTabHeight - 6);
|
||||
CHECK(L.tabStrip.height() == kBrowserTabHeight - 6);
|
||||
CHECK(L.grid.height() <= 0); // no room left for cards
|
||||
}
|
||||
|
||||
// --- card rects ---------------------------------------------------------------
|
||||
|
||||
static void testCardCellsTileRowMajor() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
const int cols = L.columns;
|
||||
// Card 0 is top-left of the grid.
|
||||
const Rect c0 = cardCellRect(L, 0);
|
||||
CHECK(c0.left == L.grid.left && c0.top == L.grid.top);
|
||||
CHECK(c0.width() == kBrowserCardWidth && c0.height() == kBrowserCardHeight);
|
||||
// Card 1 is one card-width to the right, same row.
|
||||
const Rect c1 = cardCellRect(L, 1);
|
||||
CHECK(c1.left == L.grid.left + kBrowserCardWidth);
|
||||
CHECK(c1.top == c0.top);
|
||||
// The first card of the SECOND row wraps back to the left, one card-height down.
|
||||
const Rect wrap = cardCellRect(L, cols);
|
||||
CHECK(wrap.left == L.grid.left);
|
||||
CHECK(wrap.top == L.grid.top + kBrowserCardHeight);
|
||||
}
|
||||
|
||||
static void testCardCellNegativeIndex() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
const Rect r = cardCellRect(L, -1);
|
||||
CHECK(r.left == 0 && r.top == 0 && r.right == 0 && r.bottom == 0);
|
||||
}
|
||||
|
||||
static void testCardContentInsetByGutter() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
const Rect cell = cardCellRect(L, 0);
|
||||
const Rect content = cardContentRect(L, 0);
|
||||
CHECK(content.left == cell.left + kBrowserCardGutter);
|
||||
CHECK(content.top == cell.top + kBrowserCardGutter);
|
||||
CHECK(content.right == cell.right - kBrowserCardGutter);
|
||||
CHECK(content.bottom == cell.bottom - kBrowserCardGutter);
|
||||
}
|
||||
|
||||
static void testThumbnailAboveLabel() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
const Rect content = cardContentRect(L, 0);
|
||||
const Rect thumb = cardThumbnailRect(L, 0);
|
||||
const Rect label = cardLabelRect(L, 0);
|
||||
// Thumbnail is the top band of the content; the label is the remainder below it, contiguous.
|
||||
CHECK(thumb.left == content.left && thumb.right == content.right);
|
||||
CHECK(thumb.top == content.top);
|
||||
CHECK(thumb.height() == kBrowserThumbHeight);
|
||||
CHECK(label.top == thumb.bottom);
|
||||
CHECK(label.bottom == content.bottom);
|
||||
CHECK(label.left == content.left && label.right == content.right);
|
||||
}
|
||||
|
||||
// --- cardHitTest --------------------------------------------------------------
|
||||
|
||||
static void testCardHitCenterOfCard() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
const Rect content = cardContentRect(L, 3);
|
||||
const int cx = content.left + content.width() / 2;
|
||||
const int cy = content.top + content.height() / 2;
|
||||
CHECK(cardHitTest(L, 12, cx, cy) == 3);
|
||||
}
|
||||
|
||||
static void testCardHitMissesGutter() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
// A point in the gutter between the content and the cell edge (top-left corner of cell 0)
|
||||
// is a miss — only the card CONTENT counts.
|
||||
const Rect cell = cardCellRect(L, 0);
|
||||
CHECK(cardHitTest(L, 12, cell.left, cell.top) == -1);
|
||||
}
|
||||
|
||||
static void testCardHitMissesPastLastCard() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
// Only 2 cards exist; a point on where card 5 WOULD be is a miss.
|
||||
const Rect content = cardContentRect(L, 5);
|
||||
const int cx = content.left + content.width() / 2;
|
||||
const int cy = content.top + content.height() / 2;
|
||||
CHECK(cardHitTest(L, 2, cx, cy) == -1);
|
||||
}
|
||||
|
||||
static void testCardHitMissesTabStrip() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
CHECK(cardHitTest(L, 12, 10, L.tabStrip.top + 2) == -1);
|
||||
}
|
||||
|
||||
static void testCardHitZeroCards() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
CHECK(cardHitTest(L, 0, 20, 40) == -1);
|
||||
}
|
||||
|
||||
// --- filter tabs --------------------------------------------------------------
|
||||
|
||||
static void testFilterTabsTileStrip() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
const int n = 4; // "All" + 3 banks
|
||||
const Rect t0 = filterTabRect(L, n, 0);
|
||||
const Rect tLast = filterTabRect(L, n, n - 1);
|
||||
CHECK(t0.left == L.tabStrip.left);
|
||||
// Adjacent tabs share an exact edge (no gap).
|
||||
CHECK(filterTabRect(L, n, 0).right == filterTabRect(L, n, 1).left);
|
||||
CHECK(filterTabRect(L, n, 1).right == filterTabRect(L, n, 2).left);
|
||||
// The last tab reaches the strip's right edge exactly (absorbs the remainder).
|
||||
CHECK(tLast.right == L.tabStrip.right);
|
||||
// All tabs share the strip's height.
|
||||
CHECK(t0.top == L.tabStrip.top && t0.bottom == L.tabStrip.bottom);
|
||||
}
|
||||
|
||||
static void testFilterTabOutOfRange() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
CHECK(filterTabRect(L, 3, -1).width() == 0);
|
||||
CHECK(filterTabRect(L, 3, 3).width() == 0);
|
||||
CHECK(filterTabRect(L, 0, 0).width() == 0);
|
||||
}
|
||||
|
||||
static void testFilterTabHit() {
|
||||
const BrowserLayout L = layoutBrowser(560, 300);
|
||||
const int n = 3;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const Rect t = filterTabRect(L, n, i);
|
||||
const int cx = t.left + t.width() / 2;
|
||||
const int cy = t.top + t.height() / 2;
|
||||
CHECK(filterTabHitTest(L, n, cx, cy) == i);
|
||||
}
|
||||
// Below the strip (in the grid) -> no tab.
|
||||
CHECK(filterTabHitTest(L, n, 20, L.grid.top + 4) == -1);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testLayoutNormalArea();
|
||||
testLayoutNarrowAreaSingleColumn();
|
||||
testLayoutZeroArea();
|
||||
testLayoutTinyHeightClampsTabStrip();
|
||||
testCardCellsTileRowMajor();
|
||||
testCardCellNegativeIndex();
|
||||
testCardContentInsetByGutter();
|
||||
testThumbnailAboveLabel();
|
||||
testCardHitCenterOfCard();
|
||||
testCardHitMissesGutter();
|
||||
testCardHitMissesPastLastCard();
|
||||
testCardHitMissesTabStrip();
|
||||
testCardHitZeroCards();
|
||||
testFilterTabsTileStrip();
|
||||
testFilterTabOutOfRange();
|
||||
testFilterTabHit();
|
||||
|
||||
if (g_fail == 0) std::printf("capture_browser: all tests passed\n");
|
||||
return g_fail != 0;
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// Standalone tests for reasampler::vst::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.
|
||||
|
||||
#include "../src/vst/keyboard_strip.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
using namespace reasampler::vst;
|
||||
|
||||
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.left == 0 && L.keys.top == 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.left);
|
||||
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.left == keyLeftX(L, 60));
|
||||
CHECK(k.right == keyLeftX(L, 61));
|
||||
CHECK(k.top == L.keys.top && 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.left == k.left && m.right == k.right && m.top == k.top && 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.left + 5, k.top + 2) == 60);
|
||||
// The very left of the band is key 0; just inside the right edge is key 127.
|
||||
CHECK(keyAtPoint(L, L.keys.left, 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.left == 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.left);
|
||||
}
|
||||
|
||||
// --- 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.top + 2;
|
||||
// Near the left edge -> low; near the right edge -> high; the middle -> body.
|
||||
CHECK(zoneGrabAt(L, 20, 60, bar.left + 1, y) == ZoneGrab::kLowEdge);
|
||||
CHECK(zoneGrabAt(L, 20, 60, bar.right - 1, y) == ZoneGrab::kHighEdge);
|
||||
CHECK(zoneGrabAt(L, 20, 60, bar.left + 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.top + 2;
|
||||
const int mid = bar.left + 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.top + 2;
|
||||
const int cx = overlap.left + 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);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testLayoutNormalArea();
|
||||
testLayoutZeroArea();
|
||||
testKeyLeftMonotonicAndBounds();
|
||||
testKeyRectHalfOpen();
|
||||
testRootMarkerEqualsKeyRect();
|
||||
testKeyAtPointInverts();
|
||||
testKeyAtPointOffBand();
|
||||
testZoneBarSpansInclusive();
|
||||
testZoneBarMalformedCollapses();
|
||||
testZoneGrabEdgesAndBody();
|
||||
testZoneGrabNarrowBarSplitsAtMidpointLowWins();
|
||||
testZoneBarAtPointFirstMatch();
|
||||
testZoneBarAtPointNullList();
|
||||
testResolveDragRoundsToNearestKey();
|
||||
testResolveDragClampsAndNoOps();
|
||||
|
||||
if (g_fail == 0) std::printf("keyboard_strip: all tests passed\n");
|
||||
return g_fail != 0;
|
||||
}
|
||||
+147
-18
@@ -9,13 +9,15 @@
|
||||
// and the selection / downmix / keymap / state values are checked against independently
|
||||
// computed expectations.
|
||||
//
|
||||
// Covers: selectSample by-id hit (across pool + named banks), first-sample fallback for
|
||||
// an empty / unknown id, empty & malformed blob -> nullopt, zero-samples -> nullopt,
|
||||
// rootNote/loop intrinsic threading incl. the middle-C default; listSamples ordinal
|
||||
// order + empty/malformed; downmixToMono mono passthrough / stereo average / 3-ch
|
||||
// average / zero-stride / empty; buildTier0Keymap single full-keyboard zone with the
|
||||
// root + loop + rate threaded and rate defaulting; selection state round-trip + empty id
|
||||
// + wrong-version / truncated -> "".
|
||||
// Covers: selectSample by-id hit (across pool + named banks), the S10 policy reversal
|
||||
// (empty / stale id -> SILENCE nullopt, not the first sample), empty & malformed blob ->
|
||||
// nullopt, zero-samples -> nullopt, rootNote/loop intrinsic threading incl. the middle-C
|
||||
// default; listSamples ordinal order + the card metadata (rootNote/key/bankId) + empty/
|
||||
// malformed; listBanks ordinal order (pool first) + empty/malformed; downmixToMono mono
|
||||
// passthrough / stereo average / 3-ch average / zero-stride / empty; buildTier0Keymap single
|
||||
// full-keyboard zone with the root + loop + rate threaded and rate defaulting; selection
|
||||
// state round-trip + empty id + wrong-version / truncated -> ""; component state (v3)
|
||||
// round-trip + v1/v2 back-compat lift + empty/unknown -> empty.
|
||||
// wav_trim -> extractFloatFrames -> downmixToMono integration: locks the interleave-
|
||||
// stride contract across the seam (that the byte stride wav_trim reports matches the
|
||||
// channel-count stride downmixToMono divides by).
|
||||
@@ -78,24 +80,25 @@ static void testSelectByIdHit() {
|
||||
CHECK(sel && sel->rootNote == 38);
|
||||
}
|
||||
|
||||
static void testSelectFirstSampleFallbackOnEmptyId() {
|
||||
static void testSelectEmptyIdIsSilence() {
|
||||
const std::string json = bookJson(
|
||||
{makeSample("a", "Kick", "reasampler_bank/a.wav", 36)},
|
||||
{makeSample("b", "Snare", "reasampler_bank/b.wav", 38)});
|
||||
// No stored selection -> the FIRST sample in ordinal order (pool first).
|
||||
// POLICY REVERSAL (S10): no stored selection resolves to SILENCE (nullopt), NOT the
|
||||
// bank's first sample. A fresh instance plays nothing and shows the "pick a capture"
|
||||
// empty state — the deliberate reversal of the S4 first-sample auto-play.
|
||||
auto sel = selectSample(json, "");
|
||||
CHECK(sel.has_value());
|
||||
CHECK(sel && sel->relativePath == "reasampler_bank/a.wav");
|
||||
CHECK(sel && sel->rootNote == 36);
|
||||
CHECK(!sel.has_value());
|
||||
}
|
||||
|
||||
static void testSelectFirstSampleFallbackOnUnknownId() {
|
||||
static void testSelectUnknownIdIsSilence() {
|
||||
const std::string json = bookJson(
|
||||
{makeSample("a", "Kick", "reasampler_bank/a.wav", 36)}, {});
|
||||
// A stored id that no longer resolves falls back to the first sample, not silence.
|
||||
// A stale stored id (deleted/moved-out sample) resolves to SILENCE, not a substituted
|
||||
// first sample — the editor reflects the missing pick with its empty state rather than
|
||||
// masking it with a mystery sample.
|
||||
auto sel = selectSample(json, "deleted-id");
|
||||
CHECK(sel.has_value());
|
||||
CHECK(sel && sel->relativePath == "reasampler_bank/a.wav");
|
||||
CHECK(!sel.has_value());
|
||||
}
|
||||
|
||||
static void testSelectRootNoteDefault() {
|
||||
@@ -155,12 +158,53 @@ static void testListSamplesOrdinalOrder() {
|
||||
CHECK(list.size() == 3 && list[2].id == "b" && list[2].displayName == "Snare");
|
||||
}
|
||||
|
||||
static void testListSamplesCarriesCardMetadata() {
|
||||
// The browser card needs rootNote/key badge + the bank id (for the filter). A pool sample
|
||||
// reports the pool bank id; a named-bank sample reports "drums-id"; an un-rooted sample
|
||||
// reports no rootNote (the badge shows "root —", never a guessed value).
|
||||
Sample rooted = makeSample("a", "Kick", "reasampler_bank/a.wav", 36);
|
||||
rooted.key = "Cm";
|
||||
Sample unrooted = makeSample("u", "Loop", "reasampler_bank/u.wav", std::nullopt);
|
||||
const std::string json = bookJson({rooted, unrooted},
|
||||
{makeSample("b", "Snare", "reasampler_bank/b.wav", 38)});
|
||||
const std::vector<SampleChoice> list = listSamples(json);
|
||||
CHECK(list.size() == 3);
|
||||
// Pool sample "a": rooted + keyed, pool bank id.
|
||||
CHECK(list[0].id == "a" && list[0].rootNote.has_value() && *list[0].rootNote == 36);
|
||||
CHECK(list[0].key.has_value() && *list[0].key == "Cm");
|
||||
CHECK(!list[0].bankId.empty()); // the pool has an id; the filter matches on it
|
||||
// Pool sample "u": no root intrinsic -> no rootNote (badge shows "root —").
|
||||
CHECK(list[1].id == "u" && !list[1].rootNote.has_value());
|
||||
// Named-bank sample "b": its bank id distinguishes it from the pool for the filter.
|
||||
CHECK(list[2].id == "b" && list[2].bankId == "drums-id");
|
||||
CHECK(list[2].bankId != list[0].bankId); // pool vs. named bank differ (filterable apart)
|
||||
}
|
||||
|
||||
static void testListSamplesEmptyAndMalformed() {
|
||||
CHECK(listSamples("").empty());
|
||||
CHECK(listSamples("{garbage").empty());
|
||||
CHECK(listSamples(bookJson({}, {})).empty());
|
||||
}
|
||||
|
||||
static void testListBanksOrdinalOrder() {
|
||||
const std::string json = bookJson(
|
||||
{makeSample("a", "Kick", "reasampler_bank/a.wav", 36)},
|
||||
{makeSample("b", "Snare", "reasampler_bank/b.wav", 38)});
|
||||
const std::vector<BankChoice> banks = listBanks(json);
|
||||
// Pool first (bank-zero), then the named bank "Drums". Both ids are present so the filter
|
||||
// tab strip can key on them.
|
||||
CHECK(banks.size() == 2);
|
||||
CHECK(banks.size() == 2 && banks[1].id == "drums-id" && banks[1].displayName == "Drums");
|
||||
CHECK(banks.size() == 2 && !banks[0].id.empty()); // the pool bank has an id too
|
||||
}
|
||||
|
||||
static void testListBanksEmptyAndMalformed() {
|
||||
CHECK(listBanks("").empty());
|
||||
CHECK(listBanks("{garbage").empty());
|
||||
// A valid book with no samples still has the pool bank -> one entry.
|
||||
CHECK(listBanks(bookJson({}, {})).size() == 1);
|
||||
}
|
||||
|
||||
// --- downmixToMono ------------------------------------------------------------
|
||||
|
||||
static bool approx(double a, double b) { return std::fabs(a - b) < 1e-6; }
|
||||
@@ -558,10 +602,86 @@ static void testPerformanceStateNegativeNotesRoundTrip() {
|
||||
*back.zones[0].rootOverride == 0);
|
||||
}
|
||||
|
||||
// --- Combined component state (v3, S10) --------------------------------------
|
||||
|
||||
static void testComponentStateRoundTrip() {
|
||||
// The v3 state carries the single-capture selection AND the opt-in zones, distinctly.
|
||||
ComponentState s;
|
||||
s.selectionId = "picked-capture";
|
||||
s.map.zones.push_back(zone("z0", 0, 59, /*override=*/std::nullopt));
|
||||
s.map.zones.push_back(zone("z1", 60, 127, /*override=*/48));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
CHECK(back.selectionId == "picked-capture");
|
||||
CHECK(back.map.zones.size() == 2);
|
||||
CHECK(back.map.zones.size() == 2 && back.map.zones[0].sampleId == "z0" &&
|
||||
back.map.zones[0].highNote == 59 && !back.map.zones[0].rootOverride.has_value());
|
||||
CHECK(back.map.zones.size() == 2 && back.map.zones[1].sampleId == "z1" &&
|
||||
back.map.zones[1].rootOverride.has_value() && *back.map.zones[1].rootOverride == 48);
|
||||
}
|
||||
|
||||
static void testComponentStateSelectionOnlyNoZones() {
|
||||
// A single-capture instance: a pick, no zones. Must restore the pick with an empty map
|
||||
// (NOT synthesize a zone) — the default face is one capture, zones are opt-in.
|
||||
ComponentState s;
|
||||
s.selectionId = "just-a-pick";
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
CHECK(back.selectionId == "just-a-pick");
|
||||
CHECK(back.map.zones.empty());
|
||||
}
|
||||
|
||||
static void testComponentStateEmptyIsEmpty() {
|
||||
// No pick, no zones -> restores EMPTY (the S10 silent empty state), never a first sample.
|
||||
const ComponentState s; // selectionId "", empty map
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
CHECK(back.selectionId.empty());
|
||||
CHECK(back.map.zones.empty());
|
||||
}
|
||||
|
||||
static void testComponentStateV1BackCompat() {
|
||||
// A v1 S4 blob (single-selection) lifts to {id, one full-keyboard zone} so an old pick
|
||||
// survives as BOTH the selection and a one-zone map.
|
||||
const std::vector<std::uint8_t> v1 = serializeSelection("legacy-id");
|
||||
const ComponentState back = deserializeComponentState(v1);
|
||||
CHECK(back.selectionId == "legacy-id");
|
||||
CHECK(back.map.zones.size() == 1);
|
||||
CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "legacy-id" &&
|
||||
back.map.zones[0].lowNote == 0 && back.map.zones[0].highNote == 127);
|
||||
// A v1 blob with an EMPTY id -> empty state (no selection, no zone).
|
||||
const ComponentState empty = deserializeComponentState(serializeSelection(""));
|
||||
CHECK(empty.selectionId.empty() && empty.map.zones.empty());
|
||||
}
|
||||
|
||||
static void testComponentStateV2BackCompat() {
|
||||
// A v2 S5 blob (zones-only) lifts to {"", zones}: that instance had zones but no separate
|
||||
// single-capture selection.
|
||||
PerformanceMap m;
|
||||
m.zones.push_back(zone("s", 12, 24, /*override=*/std::nullopt));
|
||||
const std::vector<std::uint8_t> v2 = serializePerformance(m);
|
||||
const ComponentState back = deserializeComponentState(v2);
|
||||
CHECK(back.selectionId.empty());
|
||||
CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "s" &&
|
||||
back.map.zones[0].lowNote == 12 && back.map.zones[0].highNote == 24);
|
||||
}
|
||||
|
||||
static void testComponentStateGarbage() {
|
||||
// Empty / unknown version -> empty (never throws across the host).
|
||||
CHECK(deserializeComponentState({}).selectionId.empty());
|
||||
CHECK(deserializeComponentState({}).map.zones.empty());
|
||||
const std::vector<std::uint8_t> unknown{0xAA, 0xBB, 0xCC, 0xDD};
|
||||
CHECK(deserializeComponentState(unknown).map.zones.empty());
|
||||
CHECK(deserializeComponentState(unknown).selectionId.empty());
|
||||
// A v3 header claiming a longer id than the blob holds -> empty (bounded read).
|
||||
std::vector<std::uint8_t> t;
|
||||
t.push_back(3); t.push_back(0); t.push_back(0); t.push_back(0); // version 3
|
||||
t.push_back(200); t.push_back(0); t.push_back(0); t.push_back(0); // id length 200 (absent)
|
||||
CHECK(deserializeComponentState(t).selectionId.empty());
|
||||
CHECK(deserializeComponentState(t).map.zones.empty());
|
||||
}
|
||||
|
||||
int main() {
|
||||
testSelectByIdHit();
|
||||
testSelectFirstSampleFallbackOnEmptyId();
|
||||
testSelectFirstSampleFallbackOnUnknownId();
|
||||
testSelectEmptyIdIsSilence();
|
||||
testSelectUnknownIdIsSilence();
|
||||
testSelectRootNoteDefault();
|
||||
testSelectLoopThreaded();
|
||||
testSelectNoLoopIsAbsent();
|
||||
@@ -569,7 +689,10 @@ int main() {
|
||||
testSelectMalformedBlob();
|
||||
testSelectZeroSamples();
|
||||
testListSamplesOrdinalOrder();
|
||||
testListSamplesCarriesCardMetadata();
|
||||
testListSamplesEmptyAndMalformed();
|
||||
testListBanksOrdinalOrder();
|
||||
testListBanksEmptyAndMalformed();
|
||||
testDownmixMonoPassthrough();
|
||||
testDownmixStereoAverages();
|
||||
testDownmixThreeChannelAverages();
|
||||
@@ -597,6 +720,12 @@ int main() {
|
||||
testPerformanceStateV1BackCompat();
|
||||
testPerformanceStateGarbage();
|
||||
testPerformanceStateNegativeNotesRoundTrip();
|
||||
testComponentStateRoundTrip();
|
||||
testComponentStateSelectionOnlyNoZones();
|
||||
testComponentStateEmptyIsEmpty();
|
||||
testComponentStateV1BackCompat();
|
||||
testComponentStateV2BackCompat();
|
||||
testComponentStateGarbage();
|
||||
|
||||
if (g_fail == 0) std::printf("sample_map: all tests passed\n");
|
||||
return g_fail != 0;
|
||||
|
||||
Reference in New Issue
Block a user