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:
2026-07-26 20:36:36 -04:00
parent 991c190bb8
commit 7151e19432
16 changed files with 1906 additions and 325 deletions
+203
View File
@@ -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;
}
+201
View File
@@ -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
View File
@@ -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;