fix(waveform): per-column min/max envelope fill eliminates gaps in steep segments
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
// markerAtPoint (grab band, first-match on overlap, off-area + null-array rejection);
|
||||
// resolveDragFrame (round-to-nearest-frame, clamp to [0,frameCount], zero-delta/zero-width
|
||||
// no-ops); nearestZeroCrossing (nearest sign-change, sample-on-zero, equidistant-tie-to-lower,
|
||||
// no-crossing keeps target, target clamp, degenerate buffers).
|
||||
// no-crossing keeps target, target clamp, degenerate buffers);
|
||||
// columnMinMax (per-pixel-column bin merge: 1:1, upsample, downsample, degenerate).
|
||||
|
||||
#include "../src/vst/waveform_view.h"
|
||||
|
||||
@@ -16,6 +17,8 @@
|
||||
|
||||
using namespace reasampler::vst;
|
||||
using reasampler::AudioSample;
|
||||
using reasampler::MinMax;
|
||||
using reasampler::ChannelEnvelope;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
@@ -192,6 +195,86 @@ static void testZeroCrossingDegenerate() {
|
||||
CHECK(nearestZeroCrossing(one.data(), 1, 0) == 0); // <2 frames -> clamped target
|
||||
}
|
||||
|
||||
// --- columnMinMax -------------------------------------------------------------
|
||||
// Helpers: 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 testColumnMinMaxOneToOne() {
|
||||
// 4 bins, 4 pixel columns: each column maps exactly one bin.
|
||||
ChannelEnvelope env = makeEnvelope({-1.0f, -0.5f, 0.0f, 0.5f},
|
||||
{ 0.5f, 0.0f, 0.5f, 1.0f});
|
||||
// col 0 → bin 0, col 1 → bin 1, etc.
|
||||
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 testColumnMinMaxUpsample() {
|
||||
// 2 bins, 4 pixel columns: columns 0,1 map to bin 0; columns 2,3 map to bin 1.
|
||||
// Verifies that upsampling (more columns than bins) returns the enclosing bin
|
||||
// and does not leave any column empty.
|
||||
ChannelEnvelope env = makeEnvelope({-1.0f, 0.5f}, {0.0f, 1.0f});
|
||||
// col 0: (0*2)/4=0, (1*2)/4=0 -> empty range -> fallback bin 0.
|
||||
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 testColumnMinMaxDownsample() {
|
||||
// 4 bins, 2 pixel columns: each column merges 2 bins.
|
||||
// col 0: bins [0,2) → min(-1,-0.5)=-1, max(0.5,0.0)=0.5.
|
||||
// col 1: bins [2,4) → min(0.0,0.5)=0.0, max(0.5,1.0)=1.0.
|
||||
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 testColumnMinMaxColClamp() {
|
||||
// col out of [0, innerW-1] is clamped: negative clamps to 0, >= innerW clamps to last.
|
||||
ChannelEnvelope env = makeEnvelope({-0.5f, 0.5f}, {-0.1f, 0.9f});
|
||||
CHECK(columnMinMax(env, 2, -5).min == -0.5f); // clamps to col 0
|
||||
CHECK(columnMinMax(env, 2, 999).max == 0.9f); // clamps to col 1
|
||||
}
|
||||
|
||||
static void testColumnMinMaxDegenerate() {
|
||||
ChannelEnvelope empty;
|
||||
// Empty envelope → {0, 0}.
|
||||
MinMax z = columnMinMax(empty, 4, 0);
|
||||
CHECK(z.min == 0.0f && z.max == 0.0f);
|
||||
|
||||
// innerW <= 0 → {0, 0}.
|
||||
ChannelEnvelope env = makeEnvelope({0.3f}, {0.7f});
|
||||
MinMax z2 = columnMinMax(env, 0, 0);
|
||||
CHECK(z2.min == 0.0f && z2.max == 0.0f);
|
||||
MinMax z3 = columnMinMax(env, -1, 0);
|
||||
CHECK(z3.min == 0.0f && z3.max == 0.0f);
|
||||
}
|
||||
|
||||
static void testColumnMinMaxFullCoverageNoBlanks() {
|
||||
// The critical gap-free property: for any bins/innerW ratio, every pixel column
|
||||
// in [0, innerW) returns a non-zero-width or valid result (no column is skipped).
|
||||
// Use 6 bins over 10 pixel columns (non-integer ratio). Every column must return
|
||||
// the min/max of at least one bin (not the default {0,0} that would indicate a gap).
|
||||
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) {
|
||||
MinMax mm = columnMinMax(env, 10, col);
|
||||
// Every column must have a real bin value, not zero (all bins have positive values).
|
||||
CHECK(mm.min >= 0.1f && mm.max <= 0.7f);
|
||||
CHECK(mm.min <= mm.max);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
testFrameToXEndpoints();
|
||||
testFrameToXClampsOutOfRange();
|
||||
@@ -217,6 +300,13 @@ int main() {
|
||||
testZeroCrossingClampsTarget();
|
||||
testZeroCrossingDegenerate();
|
||||
|
||||
testColumnMinMaxOneToOne();
|
||||
testColumnMinMaxUpsample();
|
||||
testColumnMinMaxDownsample();
|
||||
testColumnMinMaxColClamp();
|
||||
testColumnMinMaxDegenerate();
|
||||
testColumnMinMaxFullCoverageNoBlanks();
|
||||
|
||||
if (g_fail == 0) std::printf("waveform_view: all tests passed\n");
|
||||
else std::printf("waveform_view: %d FAILED\n", g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
|
||||
Reference in New Issue
Block a user