note: close every value type's domain at construction, so resolveNote is finite for every constructible input
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.
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
//
|
||||
// Covers: the beat length of all 39 divisions against a literal rung table (NOT the module's
|
||||
// own exponent formula); the 1/64 and 64/1 extremes; the four named example divisions; the
|
||||
// label notation; picker order and index round-trip; off-ladder clamping.
|
||||
// label notation; picker order and index round-trip; off-ladder clamping of BOTH persisted
|
||||
// fields, measured through the readers rather than by comparing two clamped values.
|
||||
|
||||
#include "../src/core/instrument/note/musical_division.h"
|
||||
|
||||
@@ -76,9 +77,14 @@ static void testExtremes() {
|
||||
0.0625));
|
||||
CHECK(almostEqual(divisionBeats(makeDivision(kMaxQuarterExponent, DivisionModifier::Straight)),
|
||||
256.0));
|
||||
// The dotted 64/1 is the single longest programmable note.
|
||||
// The dotted 64/1 is the single longest programmable note, and kMaxDivisionBeats — which
|
||||
// note_program checks the tempo conversions' domain against — must name exactly it.
|
||||
CHECK(almostEqual(divisionBeats(makeDivision(kMaxQuarterExponent, DivisionModifier::Dotted)),
|
||||
384.0));
|
||||
CHECK(almostEqual(kMaxDivisionBeats, 384.0));
|
||||
for (int i = 0; i < kDivisionCount; ++i) {
|
||||
CHECK(divisionBeats(divisionAt(i)) <= kMaxDivisionBeats);
|
||||
}
|
||||
// The 1/64 triplet is the shortest.
|
||||
CHECK(almostEqual(divisionBeats(makeDivision(kMinQuarterExponent, DivisionModifier::Triplet)),
|
||||
0.0625 * 2.0 / 3.0));
|
||||
@@ -144,24 +150,47 @@ static void testEverySetMemberIsDistinct() {
|
||||
// --- Clamping -----------------------------------------------------------------
|
||||
|
||||
static void testOffLadderExponentClampsToTheNearestRung() {
|
||||
CHECK(makeDivision(-99, DivisionModifier::Straight)
|
||||
== makeDivision(kMinQuarterExponent, DivisionModifier::Straight));
|
||||
CHECK(makeDivision(99, DivisionModifier::Triplet)
|
||||
== makeDivision(kMaxQuarterExponent, DivisionModifier::Triplet));
|
||||
// A record carrying an off-ladder exponent still resolves to a real length.
|
||||
Division corrupt;
|
||||
corrupt.quarterExponent = 120;
|
||||
CHECK(almostEqual(divisionBeats(corrupt), 256.0));
|
||||
// Asserted through the readers, never by comparing two clamped Divisions: a clamp that
|
||||
// collapsed every exponent to one rung would make Division-to-Division comparisons agree
|
||||
// with their own mistake.
|
||||
CHECK(almostEqual(divisionBeats(makeDivision(-99, DivisionModifier::Straight)), 0.0625));
|
||||
CHECK(divisionLabel(makeDivision(-99, DivisionModifier::Straight)) == "1/64");
|
||||
CHECK(divisionIndex(makeDivision(-99, DivisionModifier::Straight)) == 0);
|
||||
|
||||
CHECK(almostEqual(divisionBeats(makeDivision(99, DivisionModifier::Triplet)),
|
||||
256.0 * 2.0 / 3.0));
|
||||
CHECK(divisionLabel(makeDivision(99, DivisionModifier::Triplet)) == "64/1t");
|
||||
CHECK(divisionIndex(makeDivision(99, DivisionModifier::Triplet)) == kDivisionCount - 1);
|
||||
|
||||
// The exponent that only a corrupt persisted record could carry still names a real rung.
|
||||
CHECK(almostEqual(divisionBeats(makeDivision(120, DivisionModifier::Straight)), 256.0));
|
||||
}
|
||||
|
||||
static void testEqualityNormalizesOffLadderExponentsLikeEveryOtherReader() {
|
||||
// divisionBeats/divisionIndex/divisionLabel all re-clamp through makeDivision; equality
|
||||
// must too, or a corrupt persisted value reads as a spurious diff on every reload.
|
||||
Division corrupt;
|
||||
corrupt.quarterExponent = 120;
|
||||
corrupt.modifier = DivisionModifier::Straight;
|
||||
CHECK(corrupt == makeDivision(kMaxQuarterExponent, DivisionModifier::Straight));
|
||||
CHECK(corrupt != makeDivision(kMaxQuarterExponent, DivisionModifier::Dotted));
|
||||
static void testUnnamedModifierClampsToStraight() {
|
||||
// The other half of the persisted pair. Neither divisionBeats nor divisionLabel can see
|
||||
// an unnamed modifier — both already fall through to the straight case — so the clamp is
|
||||
// measured where it does show: the picker index and equality.
|
||||
const DivisionModifier junk = static_cast<DivisionModifier>(7);
|
||||
CHECK(divisionIndex(makeDivision(0, junk))
|
||||
== divisionIndex(makeDivision(0, DivisionModifier::Straight)));
|
||||
CHECK(divisionLabel(makeDivision(0, junk)) == "1/4"); // and no junk reaches the readout
|
||||
CHECK(makeDivision(0, junk) == makeDivision(0, DivisionModifier::Straight));
|
||||
CHECK(makeDivision(0, junk) != makeDivision(0, DivisionModifier::Dotted));
|
||||
}
|
||||
|
||||
static void testEveryConstructibleDivisionIndexesIntoThePickerSet() {
|
||||
// divisionIndex is what a picker array is subscripted with, so an out-of-set index is an
|
||||
// overrun in the caller. Both corrupt fields at once is the worst case: 12*3+7 without a
|
||||
// modifier clamp.
|
||||
const int exponents[] = {-9000, -99, kMinQuarterExponent, 0, kMaxQuarterExponent, 120, 9000};
|
||||
for (int e : exponents) {
|
||||
for (int m = 0; m < 260; ++m) {
|
||||
const Division d = makeDivision(e, static_cast<DivisionModifier>(m));
|
||||
const int index = divisionIndex(d);
|
||||
CHECK(index >= 0 && index < kDivisionCount);
|
||||
CHECK(divisionAt(index) == d); // and the picker round-trips it back
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void testOutOfRangeIndexClampsIntoTheSet() {
|
||||
@@ -182,7 +211,8 @@ int main() {
|
||||
testEverySetMemberIsDistinct();
|
||||
|
||||
testOffLadderExponentClampsToTheNearestRung();
|
||||
testEqualityNormalizesOffLadderExponentsLikeEveryOtherReader();
|
||||
testUnnamedModifierClampsToStraight();
|
||||
testEveryConstructibleDivisionIndexesIntoThePickerSet();
|
||||
testOutOfRangeIndexClampsIntoTheSet();
|
||||
|
||||
if (g_fail == 0) std::printf("musical_division: all tests passed\n");
|
||||
|
||||
Reference in New Issue
Block a user