// Standalone tests for reasampler::instrument::note::tempo — no VST3, no REAPER, no // framework. Same fast assert loop as the sibling pure tests. // // Covers: BPM validation (the only rejection point, which is what makes the conversions // total); seconds-per-beat at several tempos; beats<->seconds and beats<->ms round-trips // across tempos and signs; the proportionality between two tempos, asserted as a ratio // rather than against any fixed seconds value. #include "../src/core/instrument/note/tempo.h" #include #include #include using namespace reasampler::instrument::note; 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 almostEqual(double a, double b, double eps = 1e-9) { return std::fabs(a - b) < eps; } static Tempo at(double bpm) { const std::optional t = Tempo::fromBpm(bpm); if (!t) { std::printf("FAIL: fixture tempo %f rejected\n", bpm); ++g_fail; } return t.value_or(Tempo::fromBpm(120.0).value()); } // --- Validation --------------------------------------------------------------- static void testUsableBpmIsAccepted() { const std::optional t = Tempo::fromBpm(137.5); CHECK(t.has_value()); CHECK(t && almostEqual(t->bpm(), 137.5)); } static void testUnusableBpmIsRejected() { CHECK(!Tempo::fromBpm(0.0).has_value()); CHECK(!Tempo::fromBpm(-120.0).has_value()); CHECK(!Tempo::fromBpm(std::numeric_limits::quiet_NaN()).has_value()); CHECK(!Tempo::fromBpm(std::numeric_limits::infinity()).has_value()); // Finite, positive, subnormal — but 60/bpm overflows to +inf, which turns // beatsToSeconds(0) into NaN downstream if let through. CHECK(!Tempo::fromBpm(1e-310).has_value()); } // --- Conversions --------------------------------------------------------------- static void testSecondsPerBeatFollowsBpm() { CHECK(almostEqual(at(60.0).secondsPerBeat(), 1.0)); CHECK(almostEqual(at(120.0).secondsPerBeat(), 0.5)); CHECK(almostEqual(at(240.0).secondsPerBeat(), 0.25)); } static void testBeatsToSecondsAtAKnownTempo() { // 90 BPM: one beat is 2/3 s, so four beats are 8/3 s. const Tempo t = at(90.0); CHECK(almostEqual(t.beatsToSeconds(1.0), 2.0 / 3.0)); CHECK(almostEqual(t.beatsToSeconds(4.0), 8.0 / 3.0)); CHECK(almostEqual(t.secondsToBeats(8.0 / 3.0), 4.0)); } static void testBeatsToMsAtAKnownTempo() { // 150 BPM: one beat is 400 ms. const Tempo t = at(150.0); CHECK(almostEqual(t.beatsToMs(1.0), 400.0, 1e-6)); CHECK(almostEqual(t.msToBeats(400.0), 1.0)); CHECK(almostEqual(t.msToBeats(100.0), 0.25)); } static void testMsAndBeatsRoundTripAcrossTemposAndSigns() { const double bpms[] = {33.0, 77.3, 120.0, 174.6, 300.0}; const double values[] = {-500.0, -20.0, 0.0, 0.5, 250.0, 12345.678}; for (double bpm : bpms) { const Tempo t = at(bpm); for (double ms : values) { CHECK(almostEqual(t.beatsToMs(t.msToBeats(ms)), ms, 1e-9 + 1e-9 * std::fabs(ms))); } for (double beats : values) { CHECK(almostEqual(t.msToBeats(t.beatsToMs(beats)), beats, 1e-9 + 1e-9 * std::fabs(beats))); } } } static void testSecondsRoundTrip() { const Tempo t = at(101.7); CHECK(almostEqual(t.secondsToBeats(t.beatsToSeconds(3.25)), 3.25)); CHECK(almostEqual(t.beatsToSeconds(t.secondsToBeats(-1.75)), -1.75)); } // --- Proportionality ----------------------------------------------------------- static void testHalvingTheTempoDoublesEveryBeatDuration() { // The ratio is the claim; no seconds value is asserted, so the test cannot encode a // fixed tempo of its own. const Tempo fast = at(140.0); const Tempo slow = at(70.0); for (double beats : {0.0625, 0.75, 2.0 / 3.0, 16.0, 256.0}) { CHECK(almostEqual(slow.beatsToSeconds(beats), 2.0 * fast.beatsToSeconds(beats), 1e-9)); } } static void testSecondsScaleInverselyWithBpm() { const Tempo a = at(96.0); const Tempo b = at(123.0); const double beats = 3.5; CHECK(almostEqual(a.beatsToSeconds(beats) / b.beatsToSeconds(beats), 123.0 / 96.0)); } static void testMillisecondsAreTempoFree() { // The ms<->seconds pair carries no tempo — that is what lets a ms-denominated offset // hold still while a beats-denominated one moves. CHECK(almostEqual(msToSeconds(250.0), 0.25)); CHECK(almostEqual(secondsToMs(1.5), 1500.0)); CHECK(almostEqual(msToSeconds(secondsToMs(0.037)), 0.037)); } int main() { testUsableBpmIsAccepted(); testUnusableBpmIsRejected(); testSecondsPerBeatFollowsBpm(); testBeatsToSecondsAtAKnownTempo(); testBeatsToMsAtAKnownTempo(); testMsAndBeatsRoundTripAcrossTemposAndSigns(); testSecondsRoundTrip(); testHalvingTheTempoDoublesEveryBeatDuration(); testSecondsScaleInverselyWithBpm(); testMillisecondsAreTempoFree(); if (g_fail == 0) std::printf("tempo: all tests passed\n"); else std::printf("tempo: %d FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }