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.
This commit is contained in:
2026-07-23 20:21:55 -04:00
parent b9046b2842
commit b3be9799e0
4 changed files with 100 additions and 2 deletions
+25
View File
@@ -3,6 +3,7 @@
#include "bank_grid.h"
#include <algorithm>
#include <cmath>
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
+20
View File
@@ -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
+2 -2
View File
@@ -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<int>(bins[i].max * halfSpan); // max -> up
int yMin = midY - static_cast<int>(bins[i].min * halfSpan); // min -> down
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, kColWaveform, 1.0f, 0, false);