Stack L/R waveform lanes in stereo mode, with overlays drawn once at full band height

This commit is contained in:
2026-07-30 09:04:57 -04:00
parent 81f5861ba4
commit 2a0d10fab5
9 changed files with 341 additions and 77 deletions
+146 -4
View File
@@ -1,16 +1,19 @@
// Standalone tests for reasampler::instrument::ui::waveform_view — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests. Assert the S11 waveform surface's
// frame<->pixel mapping, marker grab regions, drag-delta frame resolver (with clamps), and
// the zero-crossing snap — the geometry + snap that back the draggable start/loop markers.
// Same fast assert loop as the sibling pure tests. Assert the waveform band's drawn surface
// (lane split + the full-height overlay contract) and its frame<->pixel mapping, marker grab
// regions, drag-delta frame resolver (with clamps), and zero-crossing snap.
//
// Covers: frameToX / xToFrame (linear map + inverse, edge clamps, degenerate frameCount/width);
// 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); waveformSurface (two stacked
// lanes L-over-R in stereo, one lane in mono AND for a mono source, overlay always the full
// stacked height, grabs reaching the lower lane); laneEnvelope (per-lane channel split).
#include "../src/core/instrument/ui/waveform_view.h"
#include <cstddef>
#include <cstdio>
#include <vector>
@@ -193,6 +196,133 @@ static void testZeroCrossingDegenerate() {
CHECK(nearestZeroCrossing(one.data(), 1, 0) == 0); // <2 frames -> clamped target
}
// --- waveformSurface: the lane split + the overlay contract --------------------
// A realistic waveform band: full-width, taller than the two-lane floor.
static Rect band() { return Rect::ltrb(8, 90, 832, 90 + kWaveformMinHeight); }
static void testSurfaceStereoStacksTwoLanes() {
const Rect b = band();
const WaveformSurface s = waveformSurface(b, /*stereoMode=*/true, /*sourceChannels=*/2);
CHECK(s.laneCount == 2);
CHECK(!s.upper.empty() && !s.lower.empty());
CHECK(s.upper.y == b.y); // L on top
CHECK(s.lower.y > s.upper.bottom()); // R below, seam between them
CHECK(s.lower.bottom() == b.bottom()); // together they reach the band's floor
CHECK(s.upper.x == b.x && s.upper.width == b.width);
CHECK(s.lower.x == b.x && s.lower.width == b.width);
// Non-overlapping, and the band is exactly lanes + the one seam gap.
CHECK(s.lower.y - s.upper.bottom() == kLaneGap);
CHECK(s.upper.height + kLaneGap + s.lower.height == b.height);
}
static void testSurfaceMonoIsOneLane() {
const Rect b = band();
const WaveformSurface s = waveformSurface(b, /*stereoMode=*/false, /*sourceChannels=*/2);
CHECK(s.laneCount == 1);
CHECK(s.upper == b); // the single lane spans the whole band
CHECK(s.lower.empty()); // no second lane to draw
}
static void testSurfaceMonoSourceInStereoModeStaysOneLane() {
// Dual-mono: a mono source under stereo mode has no second channel, so a second lane
// would be a redundant duplicate.
const Rect b = band();
const WaveformSurface s = waveformSurface(b, /*stereoMode=*/true, /*sourceChannels=*/1);
CHECK(s.laneCount == 1);
CHECK(s.upper == b);
CHECK(s.lower.empty());
}
static void testSurfaceOverlayIsFullStackedHeightInBothModes() {
const Rect b = band();
const WaveformSurface st = waveformSurface(b, /*stereoMode=*/true, 2);
const WaveformSurface mo = waveformSurface(b, /*stereoMode=*/false, 2);
// Stereo: ONE overlay rect spanning both lanes, not either lane.
CHECK(st.overlay == b);
CHECK(st.overlay.height == st.upper.height + kLaneGap + st.lower.height);
CHECK(st.overlay != st.upper && st.overlay != st.lower);
// Mono: the same rect, which is also the single lane.
CHECK(mo.overlay == b);
CHECK(mo.overlay == mo.upper);
// The standalone accessor the hit-test paths use agrees with the resolved surface.
CHECK(waveformOverlayArea(b) == st.overlay);
CHECK(waveformOverlayArea(b) == mo.overlay);
}
static void testSurfaceDegenerateBandDrawsNothing() {
const WaveformSurface s = waveformSurface(Rect{10, 10, 0, 0}, true, 2);
CHECK(s.laneCount == 0);
CHECK(s.upper.empty() && s.lower.empty() && s.overlay.empty());
CHECK(waveformOverlayArea(Rect{10, 10, 0, 0}).empty());
}
// --- Hit-testing across the stacked lanes -------------------------------------
static void testMarkerGrabReachesTheLowerStereoLane() {
const Rect b = band();
const WaveformSurface s = waveformSurface(b, /*stereoMode=*/true, 2);
const std::int64_t frames = 1000;
const std::int64_t markers[1] = {500};
const int mx = frameToX(s.overlay, frames, 500);
// The same marker answers a grab in either lane — overlays span the full stack.
const int upperY = s.upper.y + s.upper.height / 2;
const int lowerY = s.lower.y + s.lower.height / 2;
CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, upperY) == 0);
CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, lowerY) == 0);
// A lower-lane grab hit-tested against the UPPER LANE would be lost — the miss this
// contract exists to prevent.
CHECK(markerAtPoint(s.upper, frames, markers, 1, mx, lowerY) == -1);
// Off the marker's x is still a miss at either height.
CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx + 40, lowerY) == -1);
}
static void testMarkerGrabInMonoSpansTheBand() {
const Rect b = band();
const WaveformSurface s = waveformSurface(b, /*stereoMode=*/false, 2);
const std::int64_t frames = 1000;
const std::int64_t markers[1] = {250};
const int mx = frameToX(s.overlay, frames, 250);
CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, b.y) == 0);
CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, b.bottom() - 1) == 0);
CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, b.bottom() + 5) == -1);
}
// --- Per-lane envelope content -------------------------------------------------
static void testAsymmetricStereoLanesCarryDifferentContent() {
// Left is full-scale, right is a tenth of it — the lanes must look materially different.
const std::size_t frames = 400;
std::vector<AudioSample> interleaved(frames * 2);
for (std::size_t f = 0; f < frames; ++f) {
const AudioSample v = (f % 2 == 0) ? 1.0f : -1.0f;
interleaved[f * 2 + 0] = v;
interleaved[f * 2 + 1] = v * 0.1f;
}
// ONE pass over the interleaved source, split per lane — what the painter does.
const reasampler::audio::Envelope env =
reasampler::audio::computeEnvelope(interleaved, 2, frames, 20);
const reasampler::audio::Envelope upper = laneEnvelope(env, 0);
const reasampler::audio::Envelope lower = laneEnvelope(env, 1);
CHECK(upper.size() == 1 && lower.size() == 1);
CHECK(upper[0].size() == 20 && lower[0].size() == 20);
for (std::size_t i = 0; i < 20; ++i) {
CHECK(upper[0][i].max > 0.9f); // left near full scale
CHECK(lower[0][i].max < 0.2f); // right an order of magnitude down
CHECK(!(upper[0][i] == lower[0][i])); // and materially different, bin for bin
}
}
static void testLaneEnvelopeRejectsOutOfRangeLane() {
const std::size_t frames = 16;
std::vector<AudioSample> mono(frames, 0.5f);
const reasampler::audio::Envelope env =
reasampler::audio::computeEnvelope(mono, 1, frames, 4);
CHECK(laneEnvelope(env, 0).size() == 1);
CHECK(laneEnvelope(env, 1).empty()); // a mono source has no lower lane
CHECK(laneEnvelope(env, -1).empty());
}
int main() {
testFrameToXEndpoints();
testFrameToXClampsOutOfRange();
@@ -218,6 +348,18 @@ int main() {
testZeroCrossingClampsTarget();
testZeroCrossingDegenerate();
testSurfaceStereoStacksTwoLanes();
testSurfaceMonoIsOneLane();
testSurfaceMonoSourceInStereoModeStaysOneLane();
testSurfaceOverlayIsFullStackedHeightInBothModes();
testSurfaceDegenerateBandDrawsNothing();
testMarkerGrabReachesTheLowerStereoLane();
testMarkerGrabInMonoSpansTheBand();
testAsymmetricStereoLanesCarryDifferentContent();
testLaneEnvelopeRejectsOutOfRangeLane();
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;