Stack L/R waveform lanes in stereo mode, with overlays drawn once at full band height
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user