Merge dev (M5 bank_panel) into view-shell branch
# Conflicts: # CMakeLists.txt
This commit is contained in:
@@ -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;
|
||||
}
|
||||
+13
-13
@@ -27,8 +27,8 @@ constexpr double kPi = 3.14159265358979323846;
|
||||
|
||||
// A full-scale sine over `frames` frames, mono, `cycles` complete periods so every
|
||||
// bin sees both a near-peak and a near-trough.
|
||||
static std::vector<Sample> monoSine(std::size_t frames, double cycles, float amp) {
|
||||
std::vector<Sample> buf(frames);
|
||||
static std::vector<AudioSample> monoSine(std::size_t frames, double cycles, float amp) {
|
||||
std::vector<AudioSample> buf(frames);
|
||||
for (std::size_t i = 0; i < frames; ++i) {
|
||||
const double phase = 2.0 * kPi * cycles * (double)i / (double)frames;
|
||||
buf[i] = amp * (float)std::sin(phase);
|
||||
@@ -60,7 +60,7 @@ static void testSineEnvelope() {
|
||||
static void testRampMonotonic() {
|
||||
const std::size_t frames = 10000;
|
||||
const std::size_t bins = 50;
|
||||
std::vector<Sample> buf(frames);
|
||||
std::vector<AudioSample> buf(frames);
|
||||
for (std::size_t i = 0; i < frames; ++i) {
|
||||
buf[i] = (float)i / (float)(frames - 1); // 0.0 .. 1.0
|
||||
}
|
||||
@@ -97,14 +97,14 @@ static void testDcAndSilence() {
|
||||
const std::size_t frames = 1000;
|
||||
const std::size_t bins = 16;
|
||||
|
||||
std::vector<Sample> silence(frames, 0.0f);
|
||||
std::vector<AudioSample> silence(frames, 0.0f);
|
||||
Envelope se = computeEnvelope(silence, 1, frames, bins);
|
||||
for (const MinMax& mm : se[0]) {
|
||||
CHECK(mm.min == 0.0f);
|
||||
CHECK(mm.max == 0.0f);
|
||||
}
|
||||
|
||||
std::vector<Sample> dc(frames, 0.5f);
|
||||
std::vector<AudioSample> dc(frames, 0.5f);
|
||||
Envelope de = computeEnvelope(dc, 1, frames, bins);
|
||||
for (const MinMax& mm : de[0]) {
|
||||
CHECK(mm.min == 0.5f);
|
||||
@@ -121,7 +121,7 @@ static void testMultiChannelNoFold() {
|
||||
|
||||
// Interleave: [ch0, ch1] per frame; ch0 = sine, ch1 = 0.
|
||||
auto sine = monoSine(frames, /*cycles=*/64.0, amp);
|
||||
std::vector<Sample> buf(frames * 2);
|
||||
std::vector<AudioSample> buf(frames * 2);
|
||||
for (std::size_t i = 0; i < frames; ++i) {
|
||||
buf[i * 2 + 0] = sine[i];
|
||||
buf[i * 2 + 1] = 0.0f;
|
||||
@@ -147,7 +147,7 @@ static void testMultiChannelNoFold() {
|
||||
static void testChannelsNotAveraged() {
|
||||
const std::size_t frames = 100;
|
||||
const std::size_t bins = 4;
|
||||
std::vector<Sample> buf(frames * 2);
|
||||
std::vector<AudioSample> buf(frames * 2);
|
||||
for (std::size_t i = 0; i < frames; ++i) {
|
||||
buf[i * 2 + 0] = 1.0f;
|
||||
buf[i * 2 + 1] = -1.0f;
|
||||
@@ -164,7 +164,7 @@ static void testChannelsNotAveraged() {
|
||||
static void testShortBuffer() {
|
||||
const std::size_t frames = 3;
|
||||
const std::size_t bins = 8;
|
||||
std::vector<Sample> buf = {0.25f, -0.5f, 0.75f};
|
||||
std::vector<AudioSample> buf = {0.25f, -0.5f, 0.75f};
|
||||
|
||||
Envelope env = computeEnvelope(buf, 1, frames, bins);
|
||||
CHECK(env[0].size() == bins);
|
||||
@@ -190,7 +190,7 @@ static void testShortBuffer() {
|
||||
static void testNonDivisibleRemainderBin() {
|
||||
const std::size_t frames = 10;
|
||||
const std::size_t bins = 3;
|
||||
std::vector<Sample> buf(frames);
|
||||
std::vector<AudioSample> buf(frames);
|
||||
for (std::size_t i = 0; i < frames; ++i) buf[i] = (float)i; // 0..9
|
||||
|
||||
Envelope env = computeEnvelope(buf, 1, frames, bins);
|
||||
@@ -205,7 +205,7 @@ static void testNonDivisibleRemainderBin() {
|
||||
|
||||
// binCount == 1: the whole buffer collapses to a single min/max.
|
||||
static void testSingleBinWholeBuffer() {
|
||||
std::vector<Sample> buf = {-0.3f, 0.8f, -0.9f, 0.1f, 0.4f};
|
||||
std::vector<AudioSample> buf = {-0.3f, 0.8f, -0.9f, 0.1f, 0.4f};
|
||||
Envelope env = computeEnvelope(buf, 1, buf.size(), 1);
|
||||
CHECK(env[0].size() == 1);
|
||||
CHECK(env[0][0].min == -0.9f);
|
||||
@@ -214,7 +214,7 @@ static void testSingleBinWholeBuffer() {
|
||||
|
||||
// Degenerate inputs: defined behavior, no UB, no throw.
|
||||
static void testDegenerateInputs() {
|
||||
std::vector<Sample> buf = {0.1f, 0.2f, 0.3f, 0.4f};
|
||||
std::vector<AudioSample> buf = {0.1f, 0.2f, 0.3f, 0.4f};
|
||||
|
||||
// Zero frames -> binCount bins, all {0,0}.
|
||||
Envelope zf = computeEnvelope(buf, 1, 0, 4);
|
||||
@@ -238,7 +238,7 @@ static void testDegenerateInputs() {
|
||||
CHECK(over[0][1].min == 0.3f && over[0][1].max == 0.4f);
|
||||
|
||||
// Empty buffer, non-zero request -> all-zero bins, no crash.
|
||||
std::vector<Sample> empty;
|
||||
std::vector<AudioSample> empty;
|
||||
Envelope eb = computeEnvelope(empty, 2, 10, 3);
|
||||
CHECK(eb.size() == 2);
|
||||
for (const auto& chenv : eb) {
|
||||
@@ -257,7 +257,7 @@ static void testLargeBinCountOverflowGuard() {
|
||||
// same loop iteration path that would UB for pathological binCount near SIZE_MAX.
|
||||
const std::size_t frames = 4;
|
||||
const std::size_t binCount = 9;
|
||||
std::vector<Sample> buf = {0.1f, 0.2f, 0.3f, 0.4f};
|
||||
std::vector<AudioSample> buf = {0.1f, 0.2f, 0.3f, 0.4f};
|
||||
|
||||
Envelope env = computeEnvelope(buf, 1, frames, binCount);
|
||||
CHECK(env.size() == 1);
|
||||
|
||||
Reference in New Issue
Block a user