Files
reasampler/tests/test_meter_ballistics.cpp

141 lines
5.5 KiB
C++

// Standalone tests for reasampler::instrument::engine::meter_ballistics — no VST3, no REAPER,
// no framework. The meter's whole behaviour is asserted here without a host: instantaneous
// rise, the 20 dB/s fall, the 1.5 s peak hold and its release at the same rate, the clip
// latch and its clear, and the dB -> normalized map the bar is drawn against.
#include "../src/core/instrument/engine/meter_ballistics.h"
#include <cmath>
#include <cstdio>
using namespace reasampler::instrument::engine;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
static bool near(double a, double b, double eps = 1e-9) { return std::fabs(a - b) <= eps; }
static double linearFromDb(double db) { return std::pow(10.0, db / 20.0); }
static void testDbFromLinear() {
CHECK(near(meterDbFromLinear(1.0), 0.0));
CHECK(near(meterDbFromLinear(0.5), -6.0205999132796239, 1e-9));
CHECK(near(meterDbFromLinear(2.0), 6.0205999132796239, 1e-9));
// Silence and anything under the floor read the floor, not -inf: the ballistics subtract
// from this value, so it has to stay finite.
CHECK(meterDbFromLinear(0.0) == kMeterFloorDb);
CHECK(meterDbFromLinear(-1.0) == kMeterFloorDb);
CHECK(meterDbFromLinear(1e-9) == kMeterFloorDb);
}
static void testNormFromDbIsLinearInDbAndClamped() {
CHECK(meterNormFromDb(kMeterFloorDb) == 0.0);
CHECK(meterNormFromDb(kMeterTopDb) == 1.0);
CHECK(meterNormFromDb(-1000.0) == 0.0);
CHECK(meterNormFromDb(1000.0) == 1.0);
// Linear in dB: equal dB steps are equal normalized steps anywhere in the span.
const double a = meterNormFromDb(-48.0) - meterNormFromDb(-54.0);
const double b = meterNormFromDb(-6.0) - meterNormFromDb(-12.0);
CHECK(near(a, b, 1e-12));
CHECK(near(a, 6.0 / (kMeterTopDb - kMeterFloorDb), 1e-12));
// The 0 dB tick, which the scale's heavier rule is drawn on.
CHECK(near(meterNormFromDb(0.0), 60.0 / 66.0, 1e-12));
}
static void testRiseIsInstantaneous() {
MeterState s;
s = advanceMeter(s, linearFromDb(-12.0), 1.0 / 30.0);
CHECK(near(s.levelDb, -12.0, 1e-9));
// A louder block on the very next frame displays at once, however short the frame.
s = advanceMeter(s, linearFromDb(-3.0), 1e-6);
CHECK(near(s.levelDb, -3.0, 1e-9));
}
static void testFallIsTwentyDbPerSecond() {
MeterState s;
s = advanceMeter(s, 1.0, 0.0); // 0 dBFS
CHECK(near(s.levelDb, 0.0, 1e-9));
s = advanceMeter(s, 0.0, 0.5);
CHECK(near(s.levelDb, -10.0, 1e-9));
s = advanceMeter(s, 0.0, 0.25);
CHECK(near(s.levelDb, -15.0, 1e-9));
// The fall never takes the bar under the peak the block itself carried.
s = advanceMeter(s, linearFromDb(-14.0), 1.0);
CHECK(near(s.levelDb, -14.0, 1e-9));
// And it stops at the floor.
for (int i = 0; i < 20; ++i) s = advanceMeter(s, 0.0, 0.5);
CHECK(near(s.levelDb, kMeterFloorDb, 1e-9));
}
static void testPeakHoldLatchesForOnePointFiveSecondsThenFallsAtTheSameRate() {
MeterState s;
s = advanceMeter(s, 1.0, 0.0);
CHECK(near(s.holdDb, 0.0, 1e-9));
CHECK(near(s.holdRemainingSeconds, kMeterPeakHoldSeconds, 1e-12));
// 1.4 s of silence: the bar has long fallen away, the tick has not moved.
for (int i = 0; i < 14; ++i) s = advanceMeter(s, 0.0, 0.1);
CHECK(near(s.holdDb, 0.0, 1e-9));
CHECK(s.levelDb < -20.0);
// Past 1.5 s it releases at the bar's own rate — 0.2 s past the latch is 4 dB down.
s = advanceMeter(s, 0.0, 0.3);
CHECK(near(s.holdDb, -4.0, 1e-9));
s = advanceMeter(s, 0.0, 0.1);
CHECK(near(s.holdDb, -6.0, 1e-9));
// A new peak re-latches it and restarts the hold.
s = advanceMeter(s, linearFromDb(-2.0), 0.1);
CHECK(near(s.holdDb, -2.0, 1e-9));
CHECK(near(s.holdRemainingSeconds, kMeterPeakHoldSeconds, 1e-12));
}
static void testHoldNeverFallsBelowTheBar() {
MeterState s;
s = advanceMeter(s, 1.0, 0.0);
for (int i = 0; i < 40; ++i) s = advanceMeter(s, linearFromDb(-20.0), 0.1);
CHECK(near(s.levelDb, -20.0, 1e-9));
CHECK(near(s.holdDb, -20.0, 1e-9));
}
static void testClipLatchesAtFullScaleAndOnlyClearsOnRequest() {
MeterState s;
s = advanceMeter(s, linearFromDb(-0.01), 0.1);
CHECK(!s.clip); // under 0 dBFS does not latch
s = advanceMeter(s, 1.0, 0.1);
CHECK(s.clip); // exactly 0 dBFS does
for (int i = 0; i < 100; ++i) s = advanceMeter(s, 0.0, 0.1);
CHECK(s.clip); // and silence does not unlatch it
s = clearMeterClip(s);
CHECK(!s.clip);
s = advanceMeter(s, linearFromDb(-6.0), 0.1);
CHECK(!s.clip); // cleared stays cleared while nothing reaches full scale
CHECK(near(s.levelDb, -6.0, 1e-9)); // and clearing left the ballistics alone
}
static void testNonPositiveElapsedFreezesTheBallistics() {
MeterState s;
s = advanceMeter(s, 1.0, 0.0);
const MeterState frozen = advanceMeter(s, 0.0, -1.0);
CHECK(near(frozen.levelDb, s.levelDb, 1e-12));
CHECK(near(frozen.holdDb, s.holdDb, 1e-12));
}
int main() {
testDbFromLinear();
testNormFromDbIsLinearInDbAndClamped();
testRiseIsInstantaneous();
testFallIsTwentyDbPerSecond();
testPeakHoldLatchesForOnePointFiveSecondsThenFallsAtTheSameRate();
testHoldNeverFallsBelowTheBar();
testClipLatchesAtFullScaleAndOnlyClearsOnRequest();
testNonPositiveElapsedFreezesTheBallistics();
if (g_fail) {
std::printf("%d FAILURE(S)\n", g_fail);
return 1;
}
std::printf("meter_ballistics tests passed\n");
return 0;
}