Merge peaks display fix: dB-compressed waveform thumbnails
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "../src/bank_grid.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user