feat(bank_panel): docked bank grid with LICE waveform thumbnails (M5 Wave A)

Docked SWELL window toggled by a new action, drawing the current bank as
per-sample min/max thumbnails (PCM_source + peaks) with an in-memory cache.
Pure grid-layout/cache-key math in bank_grid (tested). Renames peaks::Sample
-> AudioSample to avoid colliding with bank_model::Sample.
This commit is contained in:
2026-07-22 21:02:22 -04:00
parent ec211a5620
commit 71a3b26a47
12 changed files with 936 additions and 33 deletions
+144
View File
@@ -0,0 +1,144 @@
// Standalone tests for reasampler::bank_grid — no REAPER, no test framework.
// Same fast loop as the sibling pure tests: assert the grid-layout math and the
// thumbnail cache-key stringification directly.
//
// Covers (M5 Wave A brief §Test cases): column count for a given panel width;
// cell rects for a full grid (row/column wrapping); the partial-last-row case;
// itemCount == 0; a single item; a panel too narrow for even one cell (clamp to
// one column); content-height for exact and partial rows; cache-key stability,
// width/generation/id sensitivity, and length-prefix collision resistance.
#include "../src/bank_grid.h"
#include <cstdio>
#include <string>
#include <vector>
using namespace reasampler;
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 spec with round numbers so expected rects are trivial to hand-compute:
// cell 100x50, gap 10. Column pitch = 110, row pitch = 60, margin = 10.
static GridSpec spec() {
GridSpec s;
s.cellWidth = 100;
s.cellHeight = 50;
s.gap = 10;
return s;
}
// gap(10) + n*(100+10): 1 col needs 120, 2 need 230, 3 need 340.
static void testColumnsForWidth() {
const GridSpec s = spec();
CHECK(columnsForWidth(120, s) == 1); // exactly one cell + margins
CHECK(columnsForWidth(229, s) == 1); // one pixel short of two columns
CHECK(columnsForWidth(230, s) == 2); // exactly two
CHECK(columnsForWidth(345, s) == 3); // three plus slack
CHECK(columnsForWidth(1000, s) == 9); // many
}
// A panel narrower than a single cell still yields one column (clipped by the
// window, never zero — that would drop every sample).
static void testTooNarrowClampsToOneColumn() {
const GridSpec s = spec();
CHECK(columnsForWidth(50, s) == 1);
CHECK(columnsForWidth(0, s) == 1);
CHECK(columnsForWidth(-20, s) == 1);
}
// Zero items -> no rects (empty state is the panel's concern, not the layout's).
static void testZeroItems() {
const GridSpec s = spec();
auto rects = computeCellRects(0, 500, s);
CHECK(rects.empty());
CHECK(contentHeight(0, 500, s) == 0);
}
// One item sits at the top-left margin.
static void testSingleItem() {
const GridSpec s = spec();
auto rects = computeCellRects(1, 500, s);
CHECK(rects.size() == 1);
CHECK(rects[0] == (CellRect{10, 10, 100, 50}));
}
// A full 2x2: width forces exactly two columns, four items fill two rows.
static void testFullGridWrapping() {
const GridSpec s = spec();
// Width 230 -> exactly 2 columns.
auto rects = computeCellRects(4, 230, s);
CHECK(rects.size() == 4);
// Row 0: x = 10, 120 ; y = 10.
CHECK(rects[0] == (CellRect{10, 10, 100, 50}));
CHECK(rects[1] == (CellRect{120, 10, 100, 50}));
// Row 1: y = 70 (10 + 60).
CHECK(rects[2] == (CellRect{10, 70, 100, 50}));
CHECK(rects[3] == (CellRect{120, 70, 100, 50}));
}
// Partial last row: 5 items in a 2-column grid -> rows of 2,2,1. The lone last
// cell is left-aligned in its row (no centering), same x as column 0.
static void testPartialLastRow() {
const GridSpec s = spec();
auto rects = computeCellRects(5, 230, s); // 2 columns
CHECK(rects.size() == 5);
CHECK(rects[4] == (CellRect{10, 130, 100, 50})); // row 2, col 0: y = 10 + 2*60
// content height spans 3 rows: 10 + 3*(50+10) = 190.
CHECK(contentHeight(5, 230, s) == 190);
}
// Content height for an exact-fill grid: 4 items / 2 cols = 2 rows.
static void testContentHeightExactRows() {
const GridSpec s = spec();
CHECK(contentHeight(4, 230, s) == 130); // 10 + 2*60
CHECK(contentHeight(2, 230, s) == 70); // 10 + 1*60
}
// The cache key is stable and sensitive to every field.
static void testCacheKeyStabilityAndSensitivity() {
ThumbnailKey a{"sample-1", 120, 7};
ThumbnailKey aSame{"sample-1", 120, 7};
CHECK(thumbnailKeyString(a) == thumbnailKeyString(aSame)); // deterministic
ThumbnailKey diffWidth{"sample-1", 121, 7};
ThumbnailKey diffGen{"sample-1", 120, 8};
ThumbnailKey diffId{"sample-2", 120, 7};
CHECK(thumbnailKeyString(a) != thumbnailKeyString(diffWidth));
CHECK(thumbnailKeyString(a) != thumbnailKeyString(diffGen));
CHECK(thumbnailKeyString(a) != thumbnailKeyString(diffId));
}
// A sampleId containing the delimiter byte must not forge a collision with a
// different key. Without the length prefix, id "x|9" with width 0 and id "x" with
// width 9 would both tail-concatenate through the '|' delimiter and could match;
// the length prefix on the id disambiguates them.
static void testCacheKeyDelimiterCollisionResistance() {
// The canonical would-be collision: moving a "|9" fragment from the id into
// the width field. Length-prefixing the id makes the two forms distinct.
ThumbnailKey a{"x|9", 0, 0};
ThumbnailKey b{"x", 9, 0};
CHECK(thumbnailKeyString(a) != thumbnailKeyString(b));
// A second pair in the same spirit, delimiters in both id and adjacent fields.
ThumbnailKey k1{"1|2", 3, 0};
ThumbnailKey k2{"1", 23, 0};
CHECK(thumbnailKeyString(k1) != thumbnailKeyString(k2));
}
int main() {
testColumnsForWidth();
testTooNarrowClampsToOneColumn();
testZeroItems();
testSingleItem();
testFullGridWrapping();
testPartialLastRow();
testContentHeightExactRows();
testCacheKeyStabilityAndSensitivity();
testCacheKeyDelimiterCollisionResistance();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}