// 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 #include 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; }