Merge pS-fa3-waveform: gap-free per-column min/max waveform render (columnMinMax in peaks, shared across panel + editor)
This commit is contained in:
@@ -173,6 +173,22 @@ static void testListRowLayoutHitAgreement() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- waveformColumnCount -----------------------------------------------------
|
||||
|
||||
static void testWaveformColumnCount() {
|
||||
// Normal box: width minus the two 2px side insets.
|
||||
CHECK(waveformColumnCount(KitBox{0, 0, 100, 40}) == 96);
|
||||
CHECK(waveformColumnCount(KitBox{10, 5, 50, 20}) == 46);
|
||||
// Minimum: a 5-wide box yields exactly 1 drawable column.
|
||||
CHECK(waveformColumnCount(KitBox{0, 0, 5, 10}) == 1);
|
||||
// Too narrow to have any drawable columns: 4 or fewer columns returns 0.
|
||||
CHECK(waveformColumnCount(KitBox{0, 0, 4, 10}) == 0);
|
||||
CHECK(waveformColumnCount(KitBox{0, 0, 1, 10}) == 0);
|
||||
// Degenerate (empty) box returns 0 — graceful suppression.
|
||||
CHECK(waveformColumnCount(KitBox{}) == 0);
|
||||
CHECK(waveformColumnCount(KitBox{0, 0, 0, 40}) == 0);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testHitTestBoxHalfOpen();
|
||||
testButtonBoxInset();
|
||||
@@ -190,6 +206,7 @@ int main() {
|
||||
testListRowHitTest();
|
||||
testListRowHitTestBoundedByCount();
|
||||
testListRowLayoutHitAgreement();
|
||||
testWaveformColumnCount();
|
||||
|
||||
if (g_fail == 0) std::printf("component_geometry: all tests passed\n");
|
||||
else std::printf("component_geometry: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
+112
-1
@@ -6,7 +6,10 @@
|
||||
// ramp envelope monotonic across bins; DC/silence -> min==max; multi-channel
|
||||
// independence (no fold); short buffer (fewer frames than bins) and non-divisible
|
||||
// length (remainder bin); binCount==1 whole-buffer envelope; zero frames / zero
|
||||
// channels / binCount==0 degenerate inputs.
|
||||
// channels / binCount==0 degenerate inputs. Plus columnMinMax (the display-side
|
||||
// per-pixel-column collapse, FA3): 1:1 passthrough, upsample fallback, downsample
|
||||
// merge, steep disjoint-span merge, no-bin-dropped spike sweep, no-column-empty
|
||||
// coverage, col clamp, degenerate inputs.
|
||||
|
||||
#include "../src/peaks.h"
|
||||
|
||||
@@ -348,6 +351,106 @@ static void testLastFrameDegenerate() {
|
||||
CHECK(lastFrameAboveThreshold(buf, 2, 100, 0.1f) == 1);
|
||||
}
|
||||
|
||||
// --- columnMinMax (display-side per-pixel-column collapse, FA3) ----------------
|
||||
|
||||
// Build a ChannelEnvelope from parallel min/max arrays.
|
||||
static ChannelEnvelope makeEnvelope(const std::vector<float>& mins,
|
||||
const std::vector<float>& maxs) {
|
||||
ChannelEnvelope env(mins.size());
|
||||
for (std::size_t i = 0; i < mins.size(); ++i) env[i] = MinMax{mins[i], maxs[i]};
|
||||
return env;
|
||||
}
|
||||
|
||||
static void testColumnOneToOne() {
|
||||
// 4 bins, 4 columns: each column is a pure passthrough of its one bin.
|
||||
ChannelEnvelope env = makeEnvelope({-1.0f, -0.5f, 0.0f, 0.5f},
|
||||
{ 0.5f, 0.0f, 0.5f, 1.0f});
|
||||
CHECK(columnMinMax(env, 4, 0).min == -1.0f && columnMinMax(env, 4, 0).max == 0.5f);
|
||||
CHECK(columnMinMax(env, 4, 1).min == -0.5f && columnMinMax(env, 4, 1).max == 0.0f);
|
||||
CHECK(columnMinMax(env, 4, 2).min == 0.0f && columnMinMax(env, 4, 2).max == 0.5f);
|
||||
CHECK(columnMinMax(env, 4, 3).min == 0.5f && columnMinMax(env, 4, 3).max == 1.0f);
|
||||
}
|
||||
|
||||
static void testColumnUpsampleFallback() {
|
||||
// 2 bins, 4 columns: columns 0,1 fall back to enclosing bin 0; 2,3 to bin 1 —
|
||||
// no column left empty when there are more columns than bins.
|
||||
ChannelEnvelope env = makeEnvelope({-1.0f, 0.5f}, {0.0f, 1.0f});
|
||||
CHECK(columnMinMax(env, 4, 0).min == -1.0f && columnMinMax(env, 4, 0).max == 0.0f);
|
||||
CHECK(columnMinMax(env, 4, 1).min == -1.0f && columnMinMax(env, 4, 1).max == 0.0f);
|
||||
CHECK(columnMinMax(env, 4, 2).min == 0.5f && columnMinMax(env, 4, 2).max == 1.0f);
|
||||
CHECK(columnMinMax(env, 4, 3).min == 0.5f && columnMinMax(env, 4, 3).max == 1.0f);
|
||||
}
|
||||
|
||||
static void testColumnDownsampleMerge() {
|
||||
// 4 bins, 2 columns: each column is the true min/max union of its 2 bins.
|
||||
ChannelEnvelope env = makeEnvelope({-1.0f, -0.5f, 0.0f, 0.5f},
|
||||
{ 0.5f, 0.0f, 0.5f, 1.0f});
|
||||
CHECK(columnMinMax(env, 2, 0).min == -1.0f && columnMinMax(env, 2, 0).max == 0.5f);
|
||||
CHECK(columnMinMax(env, 2, 1).min == 0.0f && columnMinMax(env, 2, 1).max == 1.0f);
|
||||
}
|
||||
|
||||
static void testColumnSteepDisjointSpans() {
|
||||
// THE anti-alias case: two adjacent bins holding DISJOINT spans (a steep edge —
|
||||
// all-positive then all-negative). Merged into one column the result must bridge
|
||||
// both extremes as one full span {-1.0, 1.0}. The old per-bin overdraw could never
|
||||
// produce a wrong value here — only the merge path exercises this.
|
||||
ChannelEnvelope env = makeEnvelope({0.9f, -1.0f}, {1.0f, -0.9f});
|
||||
const MinMax mm = columnMinMax(env, 1, 0);
|
||||
CHECK(mm.min == -1.0f && mm.max == 1.0f);
|
||||
}
|
||||
|
||||
static void testColumnNoBinDropped() {
|
||||
// Downsample coverage: EVERY bin must land in some column (union of columns ==
|
||||
// union of bins). For several non-integer ratios, plant a lone {-1,+1} spike in
|
||||
// bin j (all other bins {0,0}) and assert some column reports it — a partition
|
||||
// that skipped bin j would lose the spike entirely.
|
||||
const struct { int nbins; int cols; } cases[] = {{7, 3}, {10, 4}, {9, 2}, {6, 10}};
|
||||
for (const auto& c : cases) {
|
||||
for (int j = 0; j < c.nbins; ++j) {
|
||||
ChannelEnvelope env(static_cast<std::size_t>(c.nbins)); // all {0,0}
|
||||
env[static_cast<std::size_t>(j)] = MinMax{-1.0f, 1.0f};
|
||||
bool found = false;
|
||||
for (int col = 0; col < c.cols; ++col) {
|
||||
const MinMax mm = columnMinMax(env, c.cols, col);
|
||||
if (mm.min == -1.0f && mm.max == 1.0f) { found = true; break; }
|
||||
}
|
||||
CHECK(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void testColumnNoColumnEmpty() {
|
||||
// Gap-free coverage: 6 bins over 10 columns (non-integer ratio) — every column
|
||||
// must carry a real bin's values (all bins strictly positive, so a default {0,0}
|
||||
// would expose a skipped column).
|
||||
ChannelEnvelope env = makeEnvelope({0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f},
|
||||
{0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f});
|
||||
for (int col = 0; col < 10; ++col) {
|
||||
const MinMax mm = columnMinMax(env, 10, col);
|
||||
CHECK(mm.min >= 0.1f && mm.max <= 0.7f);
|
||||
CHECK(mm.min <= mm.max);
|
||||
}
|
||||
}
|
||||
|
||||
static void testColumnColClamp() {
|
||||
// col outside [0, columnCount-1] clamps: negative to the first, large to the last.
|
||||
ChannelEnvelope env = makeEnvelope({-0.5f, 0.5f}, {-0.1f, 0.9f});
|
||||
CHECK(columnMinMax(env, 2, -5).min == -0.5f);
|
||||
CHECK(columnMinMax(env, 2, 999).max == 0.9f);
|
||||
}
|
||||
|
||||
static void testColumnDegenerate() {
|
||||
// Empty envelope, columnCount <= 0 -> {0, 0}.
|
||||
ChannelEnvelope empty;
|
||||
const MinMax z = columnMinMax(empty, 4, 0);
|
||||
CHECK(z.min == 0.0f && z.max == 0.0f);
|
||||
ChannelEnvelope env = makeEnvelope({0.3f}, {0.7f});
|
||||
const MinMax z2 = columnMinMax(env, 0, 0);
|
||||
CHECK(z2.min == 0.0f && z2.max == 0.0f);
|
||||
const MinMax z3 = columnMinMax(env, -1, 0);
|
||||
CHECK(z3.min == 0.0f && z3.max == 0.0f);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testSineEnvelope();
|
||||
testRampMonotonic();
|
||||
@@ -364,6 +467,14 @@ int main() {
|
||||
testLastFrameAllAbove();
|
||||
testLastFramePerChannelMaxAbs();
|
||||
testLastFrameDegenerate();
|
||||
testColumnOneToOne();
|
||||
testColumnUpsampleFallback();
|
||||
testColumnDownsampleMerge();
|
||||
testColumnSteepDisjointSpans();
|
||||
testColumnNoBinDropped();
|
||||
testColumnNoColumnEmpty();
|
||||
testColumnColClamp();
|
||||
testColumnDegenerate();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
Reference in New Issue
Block a user