Ω-W1-T5: one overlay↔waveform mapping, and loop marks that tell the truth about their mode

This commit is contained in:
2026-08-03 12:39:41 -04:00
parent 0eb2c67875
commit a3853231a7
9 changed files with 293 additions and 67 deletions
+189 -17
View File
@@ -3,7 +3,11 @@
// (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);
// Covers: frameToX / xToFrame — the ONE map, asserted against the REAL draw chain
// (computeEnvelope + columnMinMax) rather than a restatement of it, at frame 0 / the last frame
// / an interior frame and then exhaustively, in both the frames>columns and frames<columns
// regimes, plus the exclusive span end, both round trips, edge clamps and degenerate inputs;
// waveformOverlayArea (the overlay IS the drawn column band, inset symmetrically);
// markerAtPoint (grab band, first-match on overlap, off-area + null-array rejection);
// markerHandleRect (the top-strip tab that keeps coincident markers independently grabbable);
// resolveDragFrame (round-to-nearest-frame, clamp to [0,frameCount], zero-delta/zero-width
@@ -17,6 +21,7 @@
#include "../src/core/instrument/ui/waveform_view.h"
#include "../src/core/instrument/ui/sample_bands.h" // kWaveformMinHeight, kLaneGap
#include "../src/core/ui/component_geometry.h" // waveformColumnCount (the draw chain's own)
#include <cstddef>
#include <cstdio>
@@ -37,10 +42,13 @@ static Rect wideArea() { return Rect::ltrb(20, 10, 1020, 90); } // width 1000
// --- frameToX / xToFrame ------------------------------------------------------
// 1000 frames over 1000 columns: each frame owns exactly one column, so the map is the
// identity and every endpoint is exact.
static void testFrameToXEndpoints() {
const Rect a = wideArea();
CHECK(frameToX(overlayOf(a), 1000, 0) == a.x); // frame 0 -> left edge
CHECK(frameToX(overlayOf(a), 1000, 1000) == a.right()); // frameCount -> right edge
CHECK(frameToX(overlayOf(a), 1000, 0) == a.x); // frame 0 -> first column
CHECK(frameToX(overlayOf(a), 1000, 999) == a.right() - 1); // last FRAME -> last column
CHECK(frameToX(overlayOf(a), 1000, 1000) == a.right()); // the exclusive span end -> past it
CHECK(frameToX(overlayOf(a), 1000, 500) == a.x + 500); // midpoint (1:1 here)
}
@@ -71,14 +79,105 @@ static void testXToFrameClampsOutside() {
CHECK(xToFrame(overlayOf(a), 0, a.x + 10) == 0); // no frames -> 0
}
static void testFrameToXRoundTrip() {
// Round-trip at a non-1:1 scale: 800px area over 2000 frames (2.5 frames/px). frameToX then
// xToFrame should land within a couple frames (rounding both directions).
const Rect a = Rect::ltrb(0, 0, 800, 60);
for (std::int64_t f = 0; f <= 2000; f += 137) {
const int x = frameToX(overlayOf(a), 2000, f);
const std::int64_t back = xToFrame(overlayOf(a), 2000, x);
CHECK(back >= f - 3 && back <= f + 3);
// --- The frame<->pixel mapping against the draw chain it must agree with -------
//
// The whole Ω.6 contract: the overlay reads the SAME frame->column partition the waveform is
// binned and drawn through, so these fixtures run the REAL chain (computeEnvelope +
// columnMinMax) rather than restating the partition, which would only prove the test agrees
// with itself.
// Which columns the draw chain actually paints frame `f` into: a spike at f over silence, binned
// exactly as paintWaveform bins it, read back per column. Inclusive run, or lo < 0 for none.
struct ColumnRun { int lo = -1; int hi = -1; };
static ColumnRun drawnColumnsForFrame(int columns, std::int64_t frameCount, std::int64_t f) {
std::vector<AudioSample> pcm(static_cast<std::size_t>(frameCount), 0.0f);
pcm[static_cast<std::size_t>(f)] = 1.0f;
// paintWaveform's own bin count: one per drawn column, capped at the frames available.
const std::int64_t wantBins = static_cast<std::int64_t>(columns);
const std::size_t bins =
static_cast<std::size_t>(wantBins < frameCount ? wantBins : frameCount);
const reasampler::audio::Envelope env =
reasampler::audio::computeEnvelope(pcm, 1, static_cast<std::size_t>(frameCount), bins);
ColumnRun run;
for (int c = 0; c < columns; ++c) {
if (reasampler::audio::columnMinMax(env[0], columns, c).max < 1.0f) continue;
if (run.lo < 0) run.lo = c;
run.hi = c;
}
return run;
}
static void checkMarkLandsOnItsOwnWaveformColumn(const Rect& band, std::int64_t frameCount,
std::int64_t f) {
const OverlayArea ov = waveformOverlayArea(band);
const ColumnRun run = drawnColumnsForFrame(ov.rect.width, frameCount, f);
CHECK(run.lo >= 0); // the draw chain paints every frame somewhere
const int col = frameToX(ov, frameCount, f) - ov.rect.x;
CHECK(col >= run.lo && col <= run.hi);
}
static void testAMarkLandsOnTheWaveformColumnForItsOwnFrame() {
const Rect b = Rect{8, 90, 404, 60}; // 400 drawn columns
// frames > columns: many frames share one column, and the mark must pick that column.
const std::int64_t many = 9973; // prime, so no boundary falls anywhere convenient
checkMarkLandsOnItsOwnWaveformColumn(b, many, 0);
checkMarkLandsOnItsOwnWaveformColumn(b, many, many - 1);
checkMarkLandsOnItsOwnWaveformColumn(b, many, 4001);
// frames < columns: one frame spans many columns, and the mark must land inside its own run.
const std::int64_t few = 37;
checkMarkLandsOnItsOwnWaveformColumn(b, few, 0);
checkMarkLandsOnItsOwnWaveformColumn(b, few, few - 1);
checkMarkLandsOnItsOwnWaveformColumn(b, few, 19);
}
// Not just the three probe frames: EVERY frame, across both regimes and the 1:1 boundary.
static void testTheMappingAgreesWithTheDrawChainAtEveryFrame() {
const int widths[] = {21, 64, 104}; // 17 / 60 / 100 drawn columns
const std::int64_t counts[] = {7, 60, 100, 251}; // below, equal to and above each
for (int w : widths) {
for (std::int64_t n : counts) {
const Rect b = Rect{3, 0, w, 40};
for (std::int64_t f = 0; f < n; ++f) checkMarkLandsOnItsOwnWaveformColumn(b, n, f);
}
}
}
// The closed domain is a SPAN's exclusive end, not a frame: it is what the loop fill and the
// crossfade wedge stop at, so it belongs one past the last column and nowhere else.
static void testTheExclusiveSpanEndLandsOnTheRightEdge() {
const Rect b = Rect{8, 90, 404, 60};
const OverlayArea ov = waveformOverlayArea(b);
const std::int64_t counts[] = {7, 400, 9973};
for (std::int64_t n : counts) {
CHECK(frameToX(ov, n, n) == ov.rect.right());
CHECK(frameToX(ov, n, n + 5000) == ov.rect.right()); // and clamps there
// The last real FRAME is the last real column — one inside that edge.
CHECK(frameToX(ov, n, n - 1) == ov.rect.right() - 1);
}
}
static void testXToFrameRoundTripsEveryFrameWhileAFrameOwnsAColumn() {
// frames <= columns is exactly where a frame spans several columns and the choice of which
// one to mark is observable, so it is where the inverse has to be exact.
const Rect b = Rect{8, 90, 404, 60};
const OverlayArea ov = waveformOverlayArea(b);
const std::int64_t counts[] = {1, 37, 399, 400};
for (std::int64_t n : counts) {
for (std::int64_t f = 0; f < n; ++f) CHECK(xToFrame(ov, n, frameToX(ov, n, f)) == f);
}
}
static void testColumnsRoundTripWhereFramesShareThem() {
// Above the column count a per-frame round trip cannot exist — several frames share one
// column. What must still hold is the COLUMN round trip: every column answers a frame that
// maps straight back to that same column, so no column is unreachable or ambiguous.
const Rect b = Rect{8, 90, 404, 60};
const OverlayArea ov = waveformOverlayArea(b);
const std::int64_t n = 9973;
for (int c = 0; c < ov.rect.width; ++c) {
const int x = ov.rect.x + c;
CHECK(frameToX(ov, n, xToFrame(ov, n, x)) == x);
}
}
@@ -190,6 +289,40 @@ static void testZeroCrossingEquidistantTieToLower() {
CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 4) == 2);
}
// The snap has to survive the mapping change BEHAVIOUR-IDENTICAL, ties included, so the tie
// rule is pinned at every distance rather than at one: the fan-out probes t-d before t+d, so an
// equidistant pair always resolves to the LOWER frame. A single spike to 0 is its own isolated
// crossing (the sample-on-zero rule), which is what keeps each side's crossing count at one.
static void testZeroCrossingTiesAlwaysResolveToTheLowerFrame() {
const std::int64_t n = 200, t = 100;
for (std::int64_t d = 1; d <= 40; ++d) {
std::vector<AudioSample> pcm(static_cast<std::size_t>(n), 1.0f);
pcm[static_cast<std::size_t>(t - d)] = 0.0f;
pcm[static_cast<std::size_t>(t + d)] = 0.0f;
CHECK(nearestZeroCrossing(pcm.data(), n, t) == t - d);
}
}
// ...and the tie rule is the ONLY asymmetry: wherever one side is strictly nearer, that side
// wins, from either direction. Without this, "lower wins" could hide a left-biased search.
static void testZeroCrossingTakesTheNearerSideFromEitherDirection() {
const std::int64_t n = 200, t = 100;
for (std::int64_t d = 2; d <= 40; ++d) {
{
std::vector<AudioSample> pcm(static_cast<std::size_t>(n), 1.0f);
pcm[static_cast<std::size_t>(t - d)] = 0.0f;
pcm[static_cast<std::size_t>(t + d - 1)] = 0.0f; // right nearer by one
CHECK(nearestZeroCrossing(pcm.data(), n, t) == t + d - 1);
}
{
std::vector<AudioSample> pcm(static_cast<std::size_t>(n), 1.0f);
pcm[static_cast<std::size_t>(t - d + 1)] = 0.0f; // left nearer by one
pcm[static_cast<std::size_t>(t + d)] = 0.0f;
CHECK(nearestZeroCrossing(pcm.data(), n, t) == t - d + 1);
}
}
}
static void testZeroCrossingNoneKeepsTarget() {
// All one sign -> no crossing -> the (clamped) target comes back unchanged.
std::vector<AudioSample> pcm = {0.5f, 0.6f, 0.7f, 0.8f};
@@ -252,23 +385,54 @@ 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.rect == b);
// Stereo: ONE overlay rect spanning both lanes, not either lane. The HEIGHT is what the
// overlay contract is about, and it is the whole stack in both modes.
CHECK(st.overlay.rect.y == b.y && st.overlay.rect.height == b.height);
CHECK(st.overlay.rect.height == st.upper.height + kLaneGap + st.lower.height);
CHECK(st.overlay.rect != st.upper && st.overlay.rect != st.lower);
// Mono: the same rect, which is also the single lane.
CHECK(mo.overlay.rect == b);
CHECK(mo.overlay.rect == mo.upper);
// Mono: the same rect. It is NOT the single lane any more — the lane is the whole band,
// the overlay is the band's drawn column span inside it.
CHECK(mo.overlay.rect.y == b.y && mo.overlay.rect.height == b.height);
CHECK(mo.overlay.rect == st.overlay.rect);
CHECK(mo.overlay.rect != 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);
}
// THE Ω.6 contract at the construction site: the overlay is the band's drawn column span, so
// an overlay pixel and a waveform column are the same pixel. Read from the draw chain's own
// column count — a hardcoded 2/4 here would be the second copy that let the two drift.
static void testTheOverlayIsExactlyTheDrawnColumnBand() {
const Rect b = band();
const OverlayArea ov = waveformOverlayArea(b);
const int columns = reasampler::ui::waveformColumnCount(b);
CHECK(columns > 0);
CHECK(ov.rect.width == columns);
CHECK(ov.rect.x == b.x + (b.width - columns) / 2);
// Inset on BOTH sides, and the same amount on each — the halving above is only legitimate
// because the draw chain's inset is symmetric.
CHECK(ov.rect.x - b.x == b.right() - ov.rect.right());
CHECK(ov.rect.x > b.x && ov.rect.right() < b.right());
// Held across widths, not just this one.
for (int w = 5; w <= 300; ++w) {
const Rect band2 = Rect{7, 40, w, 60};
const OverlayArea o2 = waveformOverlayArea(band2);
CHECK(o2.rect.width == reasampler::ui::waveformColumnCount(band2));
CHECK(o2.rect.x - band2.x == band2.right() - o2.rect.right());
}
}
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.rect.empty());
CHECK(waveformOverlayArea(Rect{10, 10, 0, 0}).rect.empty());
// A band too narrow to hold a single column has no overlay to draw into, even though the
// band itself is not degenerate and still gets a lane.
CHECK(reasampler::ui::waveformColumnCount(Rect{0, 0, 4, 40}) == 0);
CHECK(waveformOverlayArea(Rect{0, 0, 4, 40}).rect.empty());
CHECK(!waveformSurface(Rect{0, 0, 4, 40}, false, 1).upper.empty());
}
static void testSurfaceThinBandRoundsLowerLaneEmpty() {
@@ -606,7 +770,12 @@ int main() {
testFrameToXDegenerate();
testXToFrameInverse();
testXToFrameClampsOutside();
testFrameToXRoundTrip();
testAMarkLandsOnTheWaveformColumnForItsOwnFrame();
testTheMappingAgreesWithTheDrawChainAtEveryFrame();
testTheExclusiveSpanEndLandsOnTheRightEdge();
testXToFrameRoundTripsEveryFrameWhileAFrameOwnsAColumn();
testColumnsRoundTripWhereFramesShareThem();
testMarkerAtPointGrabsWithinBand();
testMarkerAtPointMissesBetween();
@@ -621,6 +790,8 @@ int main() {
testZeroCrossingNearest();
testZeroCrossingSampleOnZero();
testZeroCrossingEquidistantTieToLower();
testZeroCrossingTiesAlwaysResolveToTheLowerFrame();
testZeroCrossingTakesTheNearerSideFromEitherDirection();
testZeroCrossingNoneKeepsTarget();
testZeroCrossingClampsTarget();
testZeroCrossingDegenerate();
@@ -629,6 +800,7 @@ int main() {
testSurfaceMonoIsOneLane();
testSurfaceMonoSourceInStereoModeStaysOneLane();
testSurfaceOverlayIsFullStackedHeightInBothModes();
testTheOverlayIsExactlyTheDrawnColumnBand();
testSurfaceDegenerateBandDrawsNothing();
testSurfaceThinBandRoundsLowerLaneEmpty();