diff --git a/src/bank_panel.cpp b/src/bank_panel.cpp index e43ed14..ef5227b 100644 --- a/src/bank_panel.cpp +++ b/src/bank_panel.cpp @@ -13,8 +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, oversampled -// (kWaveformOversample bins per drawn pixel column) for the FA3 gap-free draw. +// * 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 @@ -1350,10 +1350,10 @@ 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); - // FA3 anti-alias: thumbnails are computed OVERSAMPLED — kWaveformOversample bins per - // drawn pixel column — and drawWaveform collapses them per column (peaks::columnMinMax) - // so steep transients render as true full-height spans. computeThumbnail clamps the - // request to the frame count. + // 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) { diff --git a/src/component_geometry.cpp b/src/component_geometry.cpp index 1951b91..65c1d0e 100644 --- a/src/component_geometry.cpp +++ b/src/component_geometry.cpp @@ -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 diff --git a/src/component_geometry.h b/src/component_geometry.h index 875d5c6..d536932 100644 --- a/src/component_geometry.h +++ b/src/component_geometry.h @@ -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 diff --git a/src/draw_kit.cpp b/src/draw_kit.cpp index 76f5d3b..4c935fa 100644 --- a/src/draw_kit.cpp +++ b/src/draw_kit.cpp @@ -277,11 +277,6 @@ void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label, } } -int waveformColumnCount(const KitBox& box) { - const int w = box.width - 4; // fixed 2px inset each side (matches drawWaveform below) - return w > 0 ? w : 0; -} - void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) { if (!bmp || box.empty()) return; @@ -311,11 +306,12 @@ void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) { if (bins.empty() || innerW <= 0) continue; // Render one filled vertical span per pixel column. peaks::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 — callers oversample (kWaveformOversample bins per - // column) so each span shows true extremes. Same dB display compression - // everywhere (bank_grid, pure). + // 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; diff --git a/src/draw_kit.h b/src/draw_kit.h index b32cb63..ba69dae 100644 --- a/src/draw_kit.h +++ b/src/draw_kit.h @@ -115,29 +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); -// The number of pixel columns drawWaveform renders inside `box` (its fixed 2px side -// insets), never negative. Callers size the envelope they compute from this: request -// `kWaveformOversample * waveformColumnCount(box)` bins (clamped to the frame count) and -// drawWaveform collapses them per column — the anti-aliasing lever. Requesting fewer bins -// than columns still renders gap-free (the enclosing bin fills each column) but at the -// envelope's coarser resolution. -int waveformColumnCount(const KitBox& box); +// 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. -// The house oversampling factor for waveform envelopes: bins requested per drawn pixel -// column. Each display column then shows the true min/max of ~4 bins (via -// peaks::columnMinMax), so steep transients render as accurate full-height spans instead -// of aliased single-bin dots. -inline constexpr int kWaveformOversample = 4; +// 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). 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, ideally -// oversampled (see waveformColumnCount / kWaveformOversample above). 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. +// 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 diff --git a/src/vst/reasampler_editor.cpp b/src/vst/reasampler_editor.cpp index a0fb7d6..b70ac3a 100644 --- a/src/vst/reasampler_editor.cpp +++ b/src/vst/reasampler_editor.cpp @@ -121,9 +121,9 @@ std::string sampleLabel(const std::vector& samples, const std::str return "?"; } -// 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. +// 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, kWaveformOversample * waveformColumnCount(toKitBox(cardThumbnailRect(layout, 0)))); @@ -1011,9 +1011,9 @@ 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) { - // 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. + // 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::max)(1, waveformColumnCount(toKitBox(waveArea)))) * kWaveformOversample; diff --git a/tests/test_component_geometry.cpp b/tests/test_component_geometry.cpp index f39f319..3d97095 100644 --- a/tests/test_component_geometry.cpp +++ b/tests/test_component_geometry.cpp @@ -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);