// musical_division — the note-length ladder the capture signal is programmed from: 1/64 // through 64/1, each straight, dotted, or triplet. Lengths are in BEATS only; the tempo // resolution belongs to `tempo`, which keeps this ladder provable without one. #pragma once #include #include #include namespace reasampler::instrument::note { enum class DivisionModifier : std::uint8_t { Straight, Dotted, // x 3/2 Triplet, // x 2/3 }; // A rung of the ladder is the base-2 exponent of its length in quarter notes: -4 is 1/64, // 0 is 1/4, 2 is 1/1, 8 is 64/1. Holding the exponent rather than a table of literal beat // counts keeps every straight and dotted length exactly representable in double. inline constexpr int kMinQuarterExponent = -4; inline constexpr int kMaxQuarterExponent = 8; inline constexpr int kRungCount = kMaxQuarterExponent - kMinQuarterExponent + 1; inline constexpr int kModifierCount = 3; inline constexpr int kDivisionCount = kRungCount * kModifierCount; // The longest programmable note — the dotted top rung — so a caller composing this ladder // with the tempo conversions can check the two domains against each other at compile time. inline constexpr double kMaxDivisionBeats = (1 << kMaxQuarterExponent) * 1.5; class Division; // Off-ladder inputs clamp rather than reject: the only ways to reach one are a corrupt // persisted record or a picker bug, and the nearest legal length beats a nonsense duration. // An unnamed modifier byte has no nearest rung to fall to, so it takes the field's default. Division makeDivision(int quarterExponent, DivisionModifier modifier); // In-domain by construction — `makeDivision` is the only door and it clamps BOTH fields, so // every reader below trusts the stored pair instead of re-clamping it, and equality compares // the two fields raw without disagreeing with any of them. class Division { public: Division() = default; // 1/4 straight constexpr std::int8_t quarterExponent() const { return quarterExponent_; } constexpr DivisionModifier modifier() const { return modifier_; } private: Division(std::int8_t quarterExponent, DivisionModifier modifier) : quarterExponent_(quarterExponent), modifier_(modifier) {} friend Division makeDivision(int quarterExponent, DivisionModifier modifier); std::int8_t quarterExponent_ = 0; DivisionModifier modifier_ = DivisionModifier::Straight; }; static_assert(!std::is_constructible_v, "makeDivision must be the only way to give a Division a rung"); bool operator==(Division a, Division b); bool operator!=(Division a, Division b); // Length in beats (quarter notes). Always > 0, and never above kMaxDivisionBeats. double divisionBeats(Division d); // Picker order: shortest rung first, straight/dotted/triplet within each rung. Index is // presentation order only — see this directory's CLAUDE.md before persisting one. Division divisionAt(int index); int divisionIndex(Division d); // The notation divisions are named in: "1/16", "1/8.", "1/4t", "4/1". std::string divisionLabel(Division d); } // namespace reasampler::instrument::note