fix(waveform): 4x-oversampled min/max envelope, columnMinMax homed in peaks, panel routed through shared drawWaveform — one gap-free algorithm on all surfaces

This commit is contained in:
2026-07-27 19:09:50 -04:00
parent 20308c842e
commit b3c9fad9ba
11 changed files with 240 additions and 203 deletions
+20 -7
View File
@@ -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: kWaveformOversample bins per drawn
// thumbnail pixel column (FA3 anti-alias) — drawWaveform collapses them per column via
// peaks::columnMinMax. 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
@@ -570,8 +572,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;
@@ -1005,8 +1011,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 anti-alias: request kWaveformOversample bins per drawn pixel column (clamped
// to the frame count) — drawWaveform collapses them per column via
// peaks::columnMinMax into a gap-free true min/max envelope.
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);
+1 -31
View File
@@ -3,8 +3,7 @@
#include "waveform_view.h"
#include <algorithm>
#include <cstdlib> // std::abs (int overload)
#include <cstddef> // std::size_t
#include <cstdlib> // std::abs (int overload)
namespace reasampler::vst {
@@ -66,35 +65,6 @@ std::int64_t resolveDragFrame(const Rect& area, std::int64_t frameCount, std::in
return clampFrame(start + shift, frameCount);
}
MinMax columnMinMax(const ChannelEnvelope& bins, int innerW, int col) {
const int nbins = static_cast<int>(bins.size());
if (innerW <= 0 || nbins == 0) return MinMax{};
// Clamp col to [0, innerW-1].
if (col < 0) col = 0;
if (col >= innerW) col = innerW - 1;
// Half-open bin range for this column: [colBinBegin, colBinEnd).
// Mirrors computeEnvelope's exact partition (col * nbins / innerW).
const int colBinBegin = (col * nbins) / innerW;
const int colBinEnd = ((col + 1) * nbins) / innerW;
if (colBinBegin >= nbins) return MinMax{};
// When the column spans no full bins (colBinEnd == colBinBegin), use the
// enclosing bin so every pixel column has a non-empty source.
const int scanEnd = (colBinEnd > colBinBegin) ? colBinEnd : 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::int64_t nearestZeroCrossing(const AudioSample* pcm, std::int64_t frames,
std::int64_t target) {
if (pcm == nullptr || frames < 2) return clampFrame(target, frames > 0 ? frames - 1 : 0);
-13
View File
@@ -68,19 +68,6 @@ int markerAtPoint(const Rect& area, std::int64_t frameCount, const std::int64_t*
std::int64_t resolveDragFrame(const Rect& area, std::int64_t frameCount, std::int64_t startFrame,
int dxPixels);
// The merged min/max envelope for pixel column `col` (0-based, within `innerW` total columns)
// given a pre-computed per-bin ChannelEnvelope. For each pixel column the function accumulates
// all bins whose frames project to that column, returning their true min and max — so no bin is
// silently skipped when `nbins > innerW` (multiple bins per column) and no column is left empty
// when `nbins < innerW` (a column may span a fractional bin; the enclosing bin is used).
//
// The mapping mirrors computeEnvelope's exact half-open partition:
// column col owns bins [col*nbins/innerW, (col+1)*nbins/innerW).
// When that range is empty (a column maps to a bin boundary), the enclosing bin
// (col*nbins/innerW) fills the column — ensuring no pixel column is left gap-free.
// `innerW <= 0` or `bins.empty()` returns {0, 0}. `col` is clamped to [0, innerW-1]. Pure.
MinMax columnMinMax(const ChannelEnvelope& bins, int innerW, int col);
// The nearest zero-crossing frame to `target` in the mono PCM, for the loop/start snap (the
// S2 zero-crossing-aware requirement). A zero crossing is a frame index i (1 <= i < frames)
// where the sign of pcm[i-1] and pcm[i] differ (a sample exactly 0 counts as its own crossing