Merge pS-fa3-waveform: gap-free per-column min/max waveform render (columnMinMax in peaks, shared across panel + editor)
This commit is contained in:
+20
-44
@@ -13,7 +13,8 @@
|
||||
// LICE-drawn named-banks tab-page region below (one tab per named bank, an
|
||||
// overflow/scroll strip), and two full-height toggles that collapse the split.
|
||||
// Each region reuses the M5 grid render loop (waveform thumbnails / empty state).
|
||||
// * per-sample PCM read via PCM_source fed to peaks::computeEnvelope at cell width.
|
||||
// * per-sample PCM read via PCM_source fed to peaks::computeEnvelope, one bin per
|
||||
// drawn pixel column; drawWaveform's gap-free render comes from peaks::columnMinMax.
|
||||
// * an in-memory thumbnail cache keyed by (sample id, draw width, bank generation).
|
||||
// * id-keyed bank management (create / rename / delete / evacuate / activate) and
|
||||
// sample move/copy — driven from a tab context menu and a drag — against the B1
|
||||
@@ -416,7 +417,7 @@ std::vector<const Bank*> namedBanks() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- Thumbnail computation (unchanged from M5) --------------------------------
|
||||
// --- Thumbnail computation (M5; `width` is a BIN count since FA3 oversampling) --
|
||||
|
||||
Envelope computeThumbnail(const std::string& absPath, int width) {
|
||||
if (width <= 0 || absPath.empty()) return {};
|
||||
@@ -458,9 +459,13 @@ Envelope computeThumbnail(const std::string& absPath, int width) {
|
||||
for (std::size_t i = 0; i < sampleCount; ++i)
|
||||
pcm[i] = static_cast<float>(buf[i]);
|
||||
|
||||
// Clamp bins to the frame count: computeEnvelope pads binCount > frameCount with
|
||||
// trailing empty {0,0} bins, which would render a very short sample as a comb of
|
||||
// spikes over flat gaps.
|
||||
const int binCount = width < got ? width : got;
|
||||
return computeEnvelope(pcm, static_cast<std::size_t>(nch),
|
||||
static_cast<std::size_t>(got),
|
||||
static_cast<std::size_t>(width));
|
||||
static_cast<std::size_t>(binCount));
|
||||
}
|
||||
|
||||
const Envelope& thumbnailFor(const Sample& sample, int width,
|
||||
@@ -479,7 +484,7 @@ const Envelope& thumbnailFor(const Sample& sample, int width,
|
||||
return ins.first->second.envelope;
|
||||
}
|
||||
|
||||
// --- Drawing: thumbnails (unchanged from M5) ----------------------------------
|
||||
// --- Drawing: thumbnails (via the kit's shared drawWaveform since FA3) ---------
|
||||
|
||||
// Draws the L7 decorative metadata overlay on a card: bars.beats.subdivisions bottom-LEFT
|
||||
// (musical, from the capture-time tempo + meter stamp) and seconds.milliseconds bottom-RIGHT
|
||||
@@ -529,45 +534,11 @@ void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env,
|
||||
LICE_DrawRect(bmp, rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2, ring, 1.0f, 0);
|
||||
}
|
||||
|
||||
// Waveform plot (peaks invariant: min<=max). The wave keeps its NORMAL accent color in
|
||||
// every state (L7 dropped the inverted bg/base wave on the selected cell — the cell fill
|
||||
// is no longer inverted, so no contrast swap is needed).
|
||||
const LICE_pixel midCol = toLice(roleColor(Role::LineHairline));
|
||||
const LICE_pixel waveCol = toLice(roleColor(Role::AccentPrimary));
|
||||
|
||||
if (env.empty()) {
|
||||
const int midY = rect.y + rect.height / 2;
|
||||
LICE_Line(bmp, rect.x + 2, midY, rect.x + rect.width - 2, midY, midCol, 1.0f, 0, false);
|
||||
if (sample) drawCardMeta(bmp, rect, *sample); // L7 overlay even on an empty envelope
|
||||
return;
|
||||
}
|
||||
|
||||
const int channels = static_cast<int>(env.size());
|
||||
const int bandH = rect.height / channels;
|
||||
|
||||
for (int ch = 0; ch < channels; ++ch) {
|
||||
const ChannelEnvelope& bins = env[ch];
|
||||
const int bandTop = rect.y + ch * bandH;
|
||||
const int midY = bandTop + bandH / 2;
|
||||
const double halfSpan = (bandH / 2) - 2;
|
||||
|
||||
LICE_Line(bmp, rect.x + 2, midY, rect.x + rect.width - 2, midY, midCol, 1.0f, 0, false);
|
||||
|
||||
const int nbins = static_cast<int>(bins.size());
|
||||
if (nbins <= 0) continue;
|
||||
|
||||
const int innerW = rect.width - 4;
|
||||
for (int i = 0; i < nbins; ++i) {
|
||||
const int x = rect.x + 2 + (nbins > 1 ? (i * (innerW - 1)) / (nbins - 1) : 0);
|
||||
// min<=max always (peaks invariant). Draw a vertical line from the
|
||||
// min sample to the max sample, clamped to the band.
|
||||
int yMax = midY - static_cast<int>(compressAmplitudeForDisplay(bins[i].max) * halfSpan); // max -> up
|
||||
int yMin = midY - static_cast<int>(compressAmplitudeForDisplay(bins[i].min) * halfSpan); // min -> down
|
||||
if (yMax < bandTop) yMax = bandTop;
|
||||
if (yMin > bandTop + bandH - 1) yMin = bandTop + bandH - 1;
|
||||
LICE_Line(bmp, x, yMin, x, yMax, waveCol, 1.0f, 0, false);
|
||||
}
|
||||
}
|
||||
// Waveform plot through the kit's shared primitive (FA3): the SAME per-pixel-column
|
||||
// min/max envelope draw the VST editor hero + browser cards use — one algorithm, one
|
||||
// look, everywhere. The oversampled env (see drawRegionGrid's binWidth) collapses per
|
||||
// column via peaks::columnMinMax inside the kit; an empty env draws just the midline.
|
||||
drawWaveform(bmp, cell, env);
|
||||
|
||||
// L7 decorative metadata overlay, drawn last so it sits over the waveform.
|
||||
if (sample) drawCardMeta(bmp, rect, *sample);
|
||||
@@ -1379,7 +1350,12 @@ void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks,
|
||||
// BankIndex insertion order. Selection/focus are keyed by the occupied-ordinal (selection
|
||||
// space); a slot maps back to its ordinal via selectionForSlot.
|
||||
const RegionDisplay disp = regionDisplay(region, isBanks, reg);
|
||||
const int binWidth = kGrid.cellWidth - 4;
|
||||
// FA3 gap-free: request one bin per drawn pixel column; drawWaveform's
|
||||
// peaks::columnMinMax exact partition makes every column gap-free — overbinning
|
||||
// produces byte-identical pixels at higher memory/CPU cost. computeThumbnail clamps
|
||||
// the request to the frame count.
|
||||
const int binWidth = kWaveformOversample *
|
||||
waveformColumnCount(KitBox{0, 0, kGrid.cellWidth, kGrid.cellHeight});
|
||||
for (const SlotCellRect& r : disp.slotRects) {
|
||||
if (r.y >= grid.bottom) continue; // below the viewport: skip (no scroll)
|
||||
const CellRect rect{r.x, r.y, r.width, r.height};
|
||||
|
||||
@@ -100,4 +100,9 @@ int hitTestListRow(int px, int py, const KitBox& list, int rowHeight, int rowCou
|
||||
return row;
|
||||
}
|
||||
|
||||
int waveformColumnCount(const KitBox& box) {
|
||||
const int w = box.width - 4; // fixed 2px inset each side (matches drawWaveform)
|
||||
return w > 0 ? w : 0;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -126,4 +126,14 @@ ListRowBox computeListRow(const KitBox& list, int index, int rowHeight);
|
||||
// a phantom row. Half-open bounds match computeListRow so the hit maps to the drawn row.
|
||||
int hitTestListRow(int px, int py, const KitBox& list, int rowHeight, int rowCount);
|
||||
|
||||
// --- Waveform column count ---------------------------------------------------
|
||||
//
|
||||
// The number of pixel columns drawWaveform renders inside `box` (its fixed 2px side
|
||||
// insets), never negative. Callers pass this count directly as the `binCount` argument to
|
||||
// peaks::computeEnvelope — one bin per column is the correct resolution, and
|
||||
// peaks::columnMinMax's exact partition makes the render gap-free at any bins-to-pixels
|
||||
// ratio. Overbinning does NOT improve render quality (columnMinMax's frame union is
|
||||
// identical whether bins == columns or bins == k*columns) and wastes memory and CPU.
|
||||
int waveformColumnCount(const KitBox& box);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
+15
-10
@@ -292,6 +292,8 @@ void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) {
|
||||
|
||||
const int channels = static_cast<int>(env.size());
|
||||
const int bandH = box.height / channels;
|
||||
const int innerW = waveformColumnCount(box); // columns: box.x+2 .. box.x+2+innerW-1
|
||||
|
||||
for (int ch = 0; ch < channels; ++ch) {
|
||||
const ChannelEnvelope& bins = env[static_cast<std::size_t>(ch)];
|
||||
const int bandTop = box.y + ch * bandH;
|
||||
@@ -301,19 +303,22 @@ void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) {
|
||||
LICE_Line(bmp, box.x + 2, midY, box.x + box.width - 2, midY,
|
||||
midCol, 1.0f, 0, false);
|
||||
|
||||
const int nbins = static_cast<int>(bins.size());
|
||||
if (nbins <= 0) continue;
|
||||
if (bins.empty() || innerW <= 0) continue;
|
||||
|
||||
const int innerW = box.width - 4;
|
||||
for (int i = 0; i < nbins; ++i) {
|
||||
const int x = box.x + 2 +
|
||||
(nbins > 1 ? (i * (innerW - 1)) / (nbins - 1) : 0);
|
||||
// Same dB display compression as the panel thumbnail (bank_grid, pure) so a
|
||||
// waveform reads identically wherever the kit draws it.
|
||||
// Render one filled vertical span per pixel column. peaks::columnMinMax merges
|
||||
// all bins that project to column `col` under the exact same partition as
|
||||
// computeEnvelope used to build the envelope, so every pixel column is covered
|
||||
// with no gaps regardless of the bins-to-pixels ratio. With one bin per column
|
||||
// (kWaveformOversample == 1) each span covers the true min/max of exactly the
|
||||
// frames that fall in that column. Same dB display compression everywhere
|
||||
// (bank_grid, pure).
|
||||
for (int col = 0; col < innerW; ++col) {
|
||||
const MinMax mm = columnMinMax(bins, innerW, col);
|
||||
const int x = box.x + 2 + col;
|
||||
int yMax = midY - static_cast<int>(
|
||||
compressAmplitudeForDisplay(bins[static_cast<std::size_t>(i)].max) * halfSpan);
|
||||
compressAmplitudeForDisplay(mm.max) * halfSpan);
|
||||
int yMin = midY - static_cast<int>(
|
||||
compressAmplitudeForDisplay(bins[static_cast<std::size_t>(i)].min) * halfSpan);
|
||||
compressAmplitudeForDisplay(mm.min) * halfSpan);
|
||||
if (yMax < bandTop) yMax = bandTop;
|
||||
if (yMin > bandTop + bandH - 1) yMin = bandTop + bandH - 1;
|
||||
LICE_Line(bmp, x, yMin, x, yMax, waveCol, 1.0f, 0, false);
|
||||
|
||||
+22
-6
@@ -115,12 +115,28 @@ void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState
|
||||
void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label,
|
||||
int thumbWidth, InteractionState state);
|
||||
|
||||
// A waveform envelope drawn as a min/max column plot over the bg/panel surface: a midline
|
||||
// per channel and an accent vertical line per bin (the same shape the panel thumbnail
|
||||
// draws, lifted into the kit so the panel and the L3 editor waveform share it). `box` is
|
||||
// the draw region; `env` is the per-channel min/max envelope from peaks::computeEnvelope.
|
||||
// An empty env draws just the midline. The caller fills the surface first (or passes a box
|
||||
// already filled); this draws only the wave + midline.
|
||||
// waveformColumnCount — declared in component_geometry.h (already included above). Returns
|
||||
// the drawable column count inside `box` (box.width minus the fixed 2px insets each side).
|
||||
// Callers pass this value directly as the `binCount` argument to peaks::computeEnvelope;
|
||||
// overbinning (more bins than columns) costs memory and CPU without changing a rendered
|
||||
// pixel — peaks::columnMinMax's exact partition already makes the draw gap-free.
|
||||
|
||||
// Multiplier kept at 1 (no oversampling). kWaveformOversample is present only so existing
|
||||
// call sites `kWaveformOversample * waveformColumnCount(box)` compile unchanged; a value of
|
||||
// 1 means they request exactly one bin per column, which is correct. The gap-free render
|
||||
// comes from peaks::columnMinMax's exact partition, NOT from extra bins.
|
||||
inline constexpr int kWaveformOversample = 1;
|
||||
|
||||
// A waveform envelope drawn as a min/max plot over the bg/panel surface: a midline per
|
||||
// channel and one accent vertical span PER PIXEL COLUMN, each column covering the true
|
||||
// extremes of every bin that projects to it (peaks::columnMinMax — gap-free at any
|
||||
// bins-to-pixels ratio because columnMinMax partitions bins exactly as computeEnvelope
|
||||
// does, so every pixel column is always covered). The ONE waveform shape in the system:
|
||||
// the dock-panel thumbnail, the browser cards, and the editor hero all render through
|
||||
// this. `box` is the draw region; `env` is the per-channel min/max envelope from
|
||||
// peaks::computeEnvelope, sized to waveformColumnCount(box) bins (clamped to frame count).
|
||||
// An empty env draws just the midline. The caller fills the surface first (or passes a
|
||||
// box already filled); this draws only the wave + midline.
|
||||
void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
// peaks implementation.
|
||||
//
|
||||
@@ -64,6 +65,37 @@ Envelope computeEnvelope(const std::vector<AudioSample>& interleaved,
|
||||
return envelope;
|
||||
}
|
||||
|
||||
MinMax columnMinMax(const ChannelEnvelope& bins, int columnCount, int col) {
|
||||
const int nbins = static_cast<int>(bins.size());
|
||||
if (columnCount <= 0 || nbins == 0) return MinMax{};
|
||||
|
||||
// Clamp col to [0, columnCount-1].
|
||||
if (col < 0) col = 0;
|
||||
if (col >= columnCount) col = columnCount - 1;
|
||||
|
||||
// Half-open bin range for this column, mirroring computeEnvelope's exact partition.
|
||||
// 64-bit products: col*nbins can exceed int range for a large oversampled envelope
|
||||
// (same overflow discipline as computeEnvelope's frame-span arithmetic above).
|
||||
const std::int64_t begin64 = (static_cast<std::int64_t>(col) * nbins) / columnCount;
|
||||
const std::int64_t end64 =
|
||||
(static_cast<std::int64_t>(col) + 1) * nbins / columnCount;
|
||||
// col <= columnCount-1 guarantees begin64 <= (columnCount-1)*nbins/columnCount < nbins.
|
||||
const int colBinBegin = static_cast<int>(begin64);
|
||||
|
||||
// When the column spans no full bin (more columns than bins), use the enclosing bin
|
||||
// so no column is left empty.
|
||||
const int scanEnd = (end64 > begin64) ? static_cast<int>(end64) : colBinBegin + 1;
|
||||
const int clampedEnd = (scanEnd <= nbins) ? scanEnd : nbins;
|
||||
|
||||
MinMax result = bins[static_cast<std::size_t>(colBinBegin)];
|
||||
for (int b = colBinBegin + 1; b < clampedEnd; ++b) {
|
||||
const MinMax& mm = bins[static_cast<std::size_t>(b)];
|
||||
if (mm.min < result.min) result.min = mm.min;
|
||||
if (mm.max > result.max) result.max = mm.max;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::size_t lastFrameAboveThreshold(const std::vector<AudioSample>& interleaved,
|
||||
std::size_t channelCount,
|
||||
std::size_t frameCount,
|
||||
|
||||
+15
@@ -66,6 +66,21 @@ Envelope computeEnvelope(const std::vector<AudioSample>& interleaved,
|
||||
std::size_t frameCount,
|
||||
std::size_t binCount);
|
||||
|
||||
// The merged min/max for display column `col` (0-based, of `columnCount` total columns)
|
||||
// of a pre-computed per-bin ChannelEnvelope: the true extremes of every bin that projects
|
||||
// to that column. This is the display-side collapse of an envelope computed at HIGHER
|
||||
// resolution than the drawn width (oversampled bins -> per-pixel-column min/max), so a
|
||||
// steep transient whose adjacent bins hold disjoint spans (e.g. {0.9,1.0} then
|
||||
// {-1.0,-0.9}) renders as one gap-free vertical span instead of two separated dots.
|
||||
//
|
||||
// Bin->column mapping mirrors computeEnvelope's half-open partition:
|
||||
// column col owns bins [col*nbins/columnCount, (col+1)*nbins/columnCount).
|
||||
// When that range is empty (more columns than bins), the enclosing bin
|
||||
// (col*nbins/columnCount) fills the column — so no column is left empty and no bin is
|
||||
// ever dropped. columnCount <= 0 or bins.empty() returns {0, 0}; `col` is clamped to
|
||||
// [0, columnCount-1]. Pure.
|
||||
MinMax columnMinMax(const ChannelEnvelope& bins, int columnCount, int col);
|
||||
|
||||
// Sentinel returned by lastFrameAboveThreshold when NO frame in the scanned range
|
||||
// peaks above the threshold (pure silence at that level). SIZE_MAX is unambiguous:
|
||||
// no valid frame index can equal it (a real index is < frameCount <= SIZE_MAX for
|
||||
|
||||
@@ -121,10 +121,12 @@ std::string sampleLabel(const std::vector<SampleChoice>& samples, const std::str
|
||||
return "?";
|
||||
}
|
||||
|
||||
// The bin count a card's thumbnail is computed at: the card thumbnail width, so one bin
|
||||
// per horizontal pixel.
|
||||
// The bin count a card's thumbnail is computed at: one bin per drawn pixel column — the
|
||||
// gap-free render comes from peaks::columnMinMax's exact partition, not from extra bins.
|
||||
// thumbnailFor clamps the request to the decoded frame count.
|
||||
int thumbBins(const BrowserLayout& layout) {
|
||||
return (std::max)(1, cardThumbnailRect(layout, 0).width());
|
||||
return (std::max)(1, kWaveformOversample *
|
||||
waveformColumnCount(toKitBox(cardThumbnailRect(layout, 0))));
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
@@ -582,8 +584,12 @@ const Envelope& ReaSamplerEditor::thumbnailFor(const std::string& sampleId, int
|
||||
const std::vector<AudioSample>& mono = monoPcmFor(sampleId);
|
||||
Envelope env;
|
||||
if (!mono.empty()) {
|
||||
env = computeEnvelope(mono, 1, mono.size(),
|
||||
static_cast<std::size_t>((std::max)(1, binCount)));
|
||||
// Clamp bins to the frame count: computeEnvelope pads binCount > frameCount with
|
||||
// trailing empty {0,0} bins, which would render a very short sample as a comb of
|
||||
// spikes over flat gaps.
|
||||
const std::size_t bins =
|
||||
(std::min)(static_cast<std::size_t>((std::max)(1, binCount)), mono.size());
|
||||
env = computeEnvelope(mono, 1, mono.size(), bins);
|
||||
}
|
||||
auto ins = thumbCache_.emplace(key, std::move(env));
|
||||
return ins.first->second;
|
||||
@@ -1017,8 +1023,15 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) {
|
||||
const Rect waveArea = bands.hero;
|
||||
fillSurface(bmp, toKitBox(waveArea), Role::BgBase, InteractionState::Rest);
|
||||
if (frames > 0 && waveArea.width() > 0) {
|
||||
const int bins = (std::max)(1, waveArea.width());
|
||||
const Envelope env = computeEnvelope(pcm, 1, pcm.size(), static_cast<std::size_t>(bins));
|
||||
// FA3 gap-free: one bin per drawn pixel column (kWaveformOversample == 1, so this
|
||||
// multiplies by 1). The gap-free draw comes from peaks::columnMinMax's exact
|
||||
// partition — extra bins produce no visible change. Clamped to frame count below.
|
||||
const std::int64_t wantBins =
|
||||
static_cast<std::int64_t>((std::max)(1, waveformColumnCount(toKitBox(waveArea)))) *
|
||||
kWaveformOversample;
|
||||
const std::size_t bins =
|
||||
static_cast<std::size_t>(wantBins < frames ? wantBins : frames);
|
||||
const Envelope env = computeEnvelope(pcm, 1, pcm.size(), bins);
|
||||
drawEnvelope(bmp, waveArea, env);
|
||||
|
||||
const SetupMarkers m = pickedMarkers(frames);
|
||||
|
||||
@@ -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