fix(waveform): per-column min/max envelope fill eliminates gaps in steep segments
This commit is contained in:
+14
-11
@@ -8,7 +8,8 @@
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "bank_grid.h" // compressAmplitudeForDisplay — the shared dB display curve (pure)
|
||||
#include "bank_grid.h" // compressAmplitudeForDisplay — the shared dB display curve (pure)
|
||||
#include "vst/waveform_view.h" // columnMinMax — per-pixel-column envelope merge (pure)
|
||||
|
||||
// SWELL / LICE. On Windows use native Win32 (windows.h first); on mac/linux SWELL is
|
||||
// provided by the host. Mirrors bank_panel.cpp's include discipline.
|
||||
@@ -292,6 +293,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 = box.width - 4; // drawable pixel 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 +304,19 @@ 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. columnMinMax merges all
|
||||
// bins that project to column `col` under the same partition as computeEnvelope,
|
||||
// so every pixel column is covered with no gaps regardless of the bins-to-pixels
|
||||
// ratio. Same dB display compression as the panel thumbnail (bank_grid, pure).
|
||||
for (int col = 0; col < innerW; ++col) {
|
||||
const MinMax mm = vst::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);
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#include "waveform_view.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib> // std::abs (int overload)
|
||||
#include <cstdlib> // std::abs (int overload)
|
||||
#include <cstddef> // std::size_t
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
@@ -65,6 +66,35 @@ 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);
|
||||
|
||||
@@ -68,6 +68,19 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user