From b3be9799e063c818cbab3647ead716ea6b2b1ace Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Thu, 23 Jul 2026 20:21:55 -0400 Subject: [PATCH] fix: apply dB compression to waveform thumbnail display Linear amplitude-to-pixel mapping made -20 dBFS content reach only 10% of cell height and -40 dBFS round to zero. Replace with a dB scale (floor kDisplayFloorDb = -60 dB, in bank_grid.h) so quiet content is visible. Pure helper compressAmplitudeForDisplay() lives in bank_grid; drawThumbnail() calls it. Five new unit tests cover full-scale, zero, mid-levels, floor clamping, and sign preservation. --- src/bank_grid.cpp | 25 +++++++++++++++++++ src/bank_grid.h | 20 +++++++++++++++ src/bank_panel.cpp | 4 +-- tests/test_bank_grid.cpp | 53 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/bank_grid.cpp b/src/bank_grid.cpp index c393d1f..1cc3a53 100644 --- a/src/bank_grid.cpp +++ b/src/bank_grid.cpp @@ -3,6 +3,7 @@ #include "bank_grid.h" #include +#include namespace reasampler { @@ -199,4 +200,28 @@ Selection navigate(const Selection& current, NavKey key, int cols, int itemCount return s; } +float compressAmplitudeForDisplay(float linear) { + const float mag = linear < 0.0f ? -linear : linear; + + // The linear magnitude at the floor threshold: 10^(kDisplayFloorDb/20). + // Any magnitude at or below this maps to display fraction 0. + // Computed once as a constant expression; std::pow is constexpr in C++20 but + // not C++17, so derive it via the floor definition directly at runtime — it is + // only called once per bin, and the branch-free math is cheap. + const float floorMag = std::pow(10.0f, kDisplayFloorDb / 20.0f); + + if (mag <= floorMag) return 0.0f; // below floor (and guards log10(0)) + + // dB in [kDisplayFloorDb, 0] for magnitude in [floorMag, 1]. + const float db = 20.0f * std::log10(mag); + + // Normalize to [0, 1]: 0 at kDisplayFloorDb, 1 at 0 dB. + const float fraction = (db - kDisplayFloorDb) / (0.0f - kDisplayFloorDb); + + // Clamp to [0, 1] so floating-point overshoot on |linear| > 1.0 stays bounded, + // then re-apply the original sign. + const float clamped = fraction < 0.0f ? 0.0f : (fraction > 1.0f ? 1.0f : fraction); + return linear < 0.0f ? -clamped : clamped; +} + } // namespace reasampler diff --git a/src/bank_grid.h b/src/bank_grid.h index a0bf2a8..3616e96 100644 --- a/src/bank_grid.h +++ b/src/bank_grid.h @@ -163,4 +163,24 @@ enum class NavKey { Left, Right, Up, Down, Home, End }; Selection navigate(const Selection& current, NavKey key, int cols, int itemCount, bool shift); +// --- Waveform display compression -------------------------------------------- +// +// Maps a raw linear amplitude magnitude to a perceptual display fraction so +// quiet and medium content remains visible in the thumbnail. +// +// The floor below which amplitude is treated as silence (display fraction 0). +// At -60 dB, 0.001 linear magnitude maps to ~0. Tune this constant in-DAW to +// taste — it is the only knob for the compression curve. +constexpr float kDisplayFloorDb = -60.0f; + +// Maps a signed linear amplitude value in [-1, 1] (a raw envelope extreme such +// as PeakBin::max or PeakBin::min) to a signed display fraction in [-1, 1]. +// +// The magnitude |linear| is converted to dB, clamped to [kDisplayFloorDb, 0], +// then normalized so kDisplayFloorDb -> 0 and 0 dB -> 1. The original sign is +// re-applied so positive max values still map positive (draw up) and negative +// min values still map negative (draw down). Exact-zero input returns 0.0f +// (stays on the midline). Full-scale (|linear| == 1.0f) returns exactly ±1.0f. +float compressAmplitudeForDisplay(float linear); + } // namespace reasampler diff --git a/src/bank_panel.cpp b/src/bank_panel.cpp index f29c26e..6e68684 100644 --- a/src/bank_panel.cpp +++ b/src/bank_panel.cpp @@ -411,8 +411,8 @@ void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env, 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(bins[i].max * halfSpan); // max -> up - int yMin = midY - static_cast(bins[i].min * halfSpan); // min -> down + int yMax = midY - static_cast(compressAmplitudeForDisplay(bins[i].max) * halfSpan); // max -> up + int yMin = midY - static_cast(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, kColWaveform, 1.0f, 0, false); diff --git a/tests/test_bank_grid.cpp b/tests/test_bank_grid.cpp index 59d8cf3..9d178b8 100644 --- a/tests/test_bank_grid.cpp +++ b/tests/test_bank_grid.cpp @@ -15,6 +15,7 @@ #include "../src/bank_grid.h" +#include #include #include #include @@ -322,6 +323,52 @@ static void testNavDegenerate() { CHECK(selEq(navigate(Selection{{0}, 0, 0}, NavKey::Down, 0, 4, false), {1}, 1, 1)); } +// --- compressAmplitudeForDisplay ---------------------------------------------- + +// Full scale: magnitude 1.0 must reach the full display fraction exactly. +static void testCompressFullScale() { + CHECK(compressAmplitudeForDisplay(1.0f) == 1.0f); + CHECK(compressAmplitudeForDisplay(-1.0f) == -1.0f); +} + +// Exact zero must stay on the midline (no log of zero; guards the singularity). +static void testCompressZeroIsMidline() { + CHECK(compressAmplitudeForDisplay(0.0f) == 0.0f); +} + +// -20 dB (0.1 linear) and -40 dB (0.01 linear) must both produce clearly visible +// (non-zero) fractions, with -20 dB > -40 dB (monotonic), and both well above +// the midline (arbitrary threshold of 0.15 chosen conservatively — at a -60 dB +// floor, -20 dB normalizes to 2/3 and -40 dB to 1/3). +static void testCompressMidValuesVisible() { + const float f20 = compressAmplitudeForDisplay(0.1f); // -20 dBFS + const float f40 = compressAmplitudeForDisplay(0.01f); // -40 dBFS + CHECK(f20 > 0.15f); // clearly non-zero + CHECK(f40 > 0.15f); // clearly non-zero + CHECK(f20 > f40); // monotonic: louder -> taller bar +} + +// At and below the floor (-60 dB = 0.001 linear) the result is ~0 (silence). +// We test at exactly the floor magnitude and well below it. +static void testCompressAtAndBelowFloor() { + // 0.001 == 10^(-60/20) is the floor ratio. Magnitude at or below it -> 0. + const float floorMag = std::pow(10.0f, kDisplayFloorDb / 20.0f); // ~0.001 + CHECK(compressAmplitudeForDisplay(floorMag) == 0.0f); + CHECK(compressAmplitudeForDisplay(floorMag * 0.5f) == 0.0f); + CHECK(compressAmplitudeForDisplay(0.0001f) == 0.0f); +} + +// Sign is preserved: negative input produces a negative fraction of the same +// magnitude as its positive counterpart. +static void testCompressSignPreserved() { + const float pos = compressAmplitudeForDisplay(0.1f); + const float neg = compressAmplitudeForDisplay(-0.1f); + CHECK(neg < 0.0f); + // Magnitudes must be equal (sign-symmetric). + const float diff = pos + neg; // pos - |neg| + CHECK(diff > -0.001f && diff < 0.001f); +} + int main() { testColumnsForWidth(); testTooNarrowClampsToOneColumn(); @@ -355,6 +402,12 @@ int main() { testNavFromEmptyFocusesFirst(); testNavDegenerate(); + testCompressFullScale(); + testCompressZeroIsMidline(); + testCompressMidValuesVisible(); + testCompressAtAndBelowFloor(); + testCompressSignPreserved(); + if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }