a80eb76c1f
Division and OffsetAmount get single normalizing doors and private constructors; fromBpm validates by running the conversions rather than their reciprocal. Readers drop their re-clamps and default labels.
181 lines
7.1 KiB
C++
181 lines
7.1 KiB
C++
// 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) including the tempos whose reciprocal is finite but whose conversions overflow;
|
|
// 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 <cmath>
|
|
#include <cstdio>
|
|
#include <limits>
|
|
|
|
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<Tempo> 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<Tempo> 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<double>::quiet_NaN()).has_value());
|
|
CHECK(!Tempo::fromBpm(std::numeric_limits<double>::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());
|
|
}
|
|
|
|
static void testBpmWhoseReciprocalIsFineButWhoseConversionsOverflowIsRejected() {
|
|
// The gap a guard on 60/bpm alone leaves open: the reciprocal is an ordinary finite
|
|
// double, and the multiply that follows it is what blows up.
|
|
CHECK(std::isfinite(60.0 / 1e-306));
|
|
CHECK(!Tempo::fromBpm(1e-306).has_value());
|
|
// The fast end fails in the other direction — the divide, not the multiply.
|
|
CHECK(!Tempo::fromBpm(1e308).has_value());
|
|
}
|
|
|
|
static void testTheGuardAdmitsEveryRealTempoAndFarBeyond() {
|
|
// The guard is structural, not musical, so it must not have narrowed onto the range of
|
|
// tempos anyone would type. The extremes here are orders of magnitude past that.
|
|
for (double bpm : {1e-200, 1e-6, 0.001, 1.0, 20.0, 120.0, 240.0, 960.0, 1e6, 1e100}) {
|
|
CHECK(Tempo::fromBpm(bpm).has_value());
|
|
}
|
|
}
|
|
|
|
static void testEveryAcceptedTempoConvertsTheWholeDomainFinitely() {
|
|
// What the guard is FOR: past it, no conversion of a magnitude the module admits can
|
|
// reach inf or NaN, in either unit or either direction.
|
|
int accepted = 0, rejected = 0;
|
|
for (double bpm : {1e-320, 1e-306, 1e-300, 1e-100, 1e-6, 0.5, 120.0, 1e6, 1e100, 1e250,
|
|
1e308}) {
|
|
const std::optional<Tempo> t = Tempo::fromBpm(bpm);
|
|
if (!t) { ++rejected; continue; }
|
|
++accepted;
|
|
for (double m : {-kMaxConvertibleMagnitude, -1.0, 0.0, 1.0, kMaxConvertibleMagnitude}) {
|
|
CHECK(std::isfinite(t->beatsToSeconds(m)));
|
|
CHECK(std::isfinite(t->beatsToMs(m)));
|
|
CHECK(std::isfinite(t->msToBeats(m)));
|
|
CHECK(std::isfinite(t->secondsToBeats(msToSeconds(m))));
|
|
}
|
|
}
|
|
// Neither half of the sweep may be empty, or the loop above proves nothing.
|
|
CHECK(accepted > 0);
|
|
CHECK(rejected > 0);
|
|
}
|
|
|
|
// --- 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();
|
|
testBpmWhoseReciprocalIsFineButWhoseConversionsOverflowIsRejected();
|
|
testTheGuardAdmitsEveryRealTempoAndFarBeyond();
|
|
testEveryAcceptedTempoConvertsTheWholeDomainFinitely();
|
|
|
|
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;
|
|
}
|