// Standalone tests for reasampler::instrument::note::musical_division — no VST3, no REAPER, // no framework. Same fast assert loop as the sibling pure tests. // // 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 of BOTH persisted // fields, measured through the readers rather than by comparing two clamped values. #include "../src/core/instrument/note/musical_division.h" #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) { const double d = a - b; return (d < 0 ? -d : d) < 1e-12; } // Length in beats (quarter notes) of each straight rung, written out rather than computed, // so a broken exponent formula cannot agree with its own mistake. static const double kStraightBeats[kRungCount] = { 0.0625, // 1/64 0.125, // 1/32 0.25, // 1/16 0.5, // 1/8 1.0, // 1/4 2.0, // 1/2 4.0, // 1/1 8.0, // 2/1 16.0, // 4/1 32.0, // 8/1 64.0, // 16/1 128.0, // 32/1 256.0, // 64/1 }; static const char* const kStraightLabels[kRungCount] = { "1/64", "1/32", "1/16", "1/8", "1/4", "1/2", "1/1", "2/1", "4/1", "8/1", "16/1", "32/1", "64/1", }; // --- The ladder --------------------------------------------------------------- static void testLadderSpansSixtyfourthToSixtyFourWhole() { CHECK(kRungCount == 13); CHECK(kDivisionCount == 39); CHECK(divisionLabel(divisionAt(0)) == "1/64"); CHECK(divisionLabel(divisionAt(kDivisionCount - 1)) == "64/1t"); } static void testEveryStraightRungHasItsWrittenBeatLength() { for (int rung = 0; rung < kRungCount; ++rung) { const Division d = makeDivision(kMinQuarterExponent + rung, DivisionModifier::Straight); CHECK(almostEqual(divisionBeats(d), kStraightBeats[rung])); CHECK(divisionLabel(d) == kStraightLabels[rung]); } } static void testDottedIsHalfAgainAndTripletIsTwoThirds() { for (int rung = 0; rung < kRungCount; ++rung) { const int e = kMinQuarterExponent + rung; CHECK(almostEqual(divisionBeats(makeDivision(e, DivisionModifier::Dotted)), kStraightBeats[rung] * 1.5)); CHECK(almostEqual(divisionBeats(makeDivision(e, DivisionModifier::Triplet)), kStraightBeats[rung] * 2.0 / 3.0)); } } static void testExtremes() { // 1/64 straight is the shortest rung; 64/1 straight is the longest. CHECK(almostEqual(divisionBeats(makeDivision(kMinQuarterExponent, DivisionModifier::Straight)), 0.0625)); CHECK(almostEqual(divisionBeats(makeDivision(kMaxQuarterExponent, DivisionModifier::Straight)), 256.0)); // 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)); } // --- The four named examples -------------------------------------------------- static void testNamedExamples() { // 1/8. — an eighth is half a beat, dotted is three quarters of one. const Division dottedEighth = makeDivision(-1, DivisionModifier::Dotted); CHECK(almostEqual(divisionBeats(dottedEighth), 0.75)); CHECK(divisionLabel(dottedEighth) == "1/8."); // 1/4t — a quarter is one beat, the triplet is two thirds of one. const Division quarterTriplet = makeDivision(0, DivisionModifier::Triplet); CHECK(almostEqual(divisionBeats(quarterTriplet), 2.0 / 3.0)); CHECK(divisionLabel(quarterTriplet) == "1/4t"); // 1/16 — a quarter of a beat. const Division sixteenth = makeDivision(-2, DivisionModifier::Straight); CHECK(almostEqual(divisionBeats(sixteenth), 0.25)); CHECK(divisionLabel(sixteenth) == "1/16"); // 4/1 — four whole notes, sixteen beats. const Division fourWhole = makeDivision(4, DivisionModifier::Straight); CHECK(almostEqual(divisionBeats(fourWhole), 16.0)); CHECK(divisionLabel(fourWhole) == "4/1"); } // --- Picker order ------------------------------------------------------------- static void testPickerOrderIsShortestFirst() { // Straight lengths ascend across rungs; within EVERY rung (not just rung 0) the order is // straight, dotted, triplet — so the index is not itself sorted by duration. for (int rung = 1; rung < kRungCount; ++rung) { const double prev = divisionBeats(divisionAt((rung - 1) * kModifierCount)); const double here = divisionBeats(divisionAt(rung * kModifierCount)); CHECK(here > prev); } for (int rung = 0; rung < kRungCount; ++rung) { const int e = kMinQuarterExponent + rung; CHECK(divisionAt(rung * kModifierCount + 0) == makeDivision(e, DivisionModifier::Straight)); CHECK(divisionAt(rung * kModifierCount + 1) == makeDivision(e, DivisionModifier::Dotted)); CHECK(divisionAt(rung * kModifierCount + 2) == makeDivision(e, DivisionModifier::Triplet)); } } static void testIndexRoundTripsOverTheWholeSet() { for (int i = 0; i < kDivisionCount; ++i) { CHECK(divisionIndex(divisionAt(i)) == i); } } static void testEverySetMemberIsDistinct() { // No two indices name the same division, so the picker offers 39 real choices. for (int i = 0; i < kDivisionCount; ++i) { for (int j = i + 1; j < kDivisionCount; ++j) { CHECK(divisionAt(i) != divisionAt(j)); } } } // --- Clamping ----------------------------------------------------------------- static void testOffLadderExponentClampsToTheNearestRung() { // 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 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(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)); // Measured (clamp removed from clampModifier): still passes. junk(7) != Dotted's stored // modifier either way, clamped or raw — this discriminates a degenerate operator== that // ignores the modifier field, not the clamp itself. 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) { // 260, not 256: m=256..259 wrap modulo uint8_t back to 0..3, re-covering the four // lowest bytes rather than reaching any byte 256 alone couldn't already reach. for (int m = 0; m < 260; ++m) { const Division d = makeDivision(e, static_cast(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() { CHECK(divisionAt(-1) == divisionAt(0)); CHECK(divisionAt(kDivisionCount) == divisionAt(kDivisionCount - 1)); } int main() { testLadderSpansSixtyfourthToSixtyFourWhole(); testEveryStraightRungHasItsWrittenBeatLength(); testDottedIsHalfAgainAndTripletIsTwoThirds(); testExtremes(); testNamedExamples(); testPickerOrderIsShortestFirst(); testIndexRoundTripsOverTheWholeSet(); testEverySetMemberIsDistinct(); testOffLadderExponentClampsToTheNearestRung(); testUnnamedModifierClampsToStraight(); testEveryConstructibleDivisionIndexesIntoThePickerSet(); testOutOfRangeIndexClampsIntoTheSet(); if (g_fail == 0) std::printf("musical_division: all tests passed\n"); else std::printf("musical_division: %d FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }