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:
@@ -13,6 +13,11 @@
|
||||
|
||||
namespace reasampler::instrument::note {
|
||||
|
||||
// The ladder and the offsets both feed the tempo conversions, so both must sit inside the
|
||||
// domain fromBpm validates — checked here because this is the one file that composes them.
|
||||
static_assert(kMaxDivisionBeats <= kMaxConvertibleMagnitude,
|
||||
"the note-length ladder must stay inside the tempo conversions' domain");
|
||||
|
||||
class Velocity {
|
||||
public:
|
||||
static constexpr int kMin = 1; // 0 is note-off in MIDI; a programmed note must sound
|
||||
@@ -21,7 +26,7 @@ public:
|
||||
Velocity() = default;
|
||||
static Velocity of(int value); // clamped into [kMin, kMax]
|
||||
|
||||
std::uint8_t value() const { return value_; }
|
||||
constexpr std::uint8_t value() const { return value_; }
|
||||
|
||||
private:
|
||||
std::uint8_t value_ = 100;
|
||||
@@ -31,20 +36,42 @@ bool operator==(Velocity a, Velocity b);
|
||||
|
||||
enum class Denomination : std::uint8_t { Milliseconds, Beats };
|
||||
|
||||
class OffsetAmount;
|
||||
|
||||
// The one door. Normalizes both fields so nothing downstream has to: a magnitude past
|
||||
// +/-kMaxConvertibleMagnitude clamps to it, a NaN magnitude — which names no value to clamp
|
||||
// toward — becomes zero, and a denomination outside the enum becomes Milliseconds, the
|
||||
// field's own default. A corrupt persisted record therefore resolves to a plausible offset
|
||||
// rather than an unrepresentable one, and no two readers can disagree about which.
|
||||
OffsetAmount offsetOf(double magnitude, Denomination denomination);
|
||||
OffsetAmount offsetFromMs(double ms);
|
||||
OffsetAmount offsetFromBeats(double beats);
|
||||
|
||||
// One magnitude, in the denomination it was entered in; the other view is derived on demand
|
||||
// and never stored. Which one was entered is itself the intent: a beats offset must follow a
|
||||
// tempo change and a ms offset must hold still, and only a stored denomination says which.
|
||||
struct OffsetAmount {
|
||||
double magnitude = 0.0;
|
||||
Denomination denomination = Denomination::Milliseconds;
|
||||
class OffsetAmount {
|
||||
public:
|
||||
OffsetAmount() = default;
|
||||
|
||||
constexpr double magnitude() const { return magnitude_; }
|
||||
constexpr Denomination denomination() const { return denomination_; }
|
||||
|
||||
private:
|
||||
OffsetAmount(double magnitude, Denomination denomination)
|
||||
: magnitude_(magnitude), denomination_(denomination) {}
|
||||
friend OffsetAmount offsetOf(double magnitude, Denomination denomination);
|
||||
|
||||
double magnitude_ = 0.0;
|
||||
Denomination denomination_ = Denomination::Milliseconds;
|
||||
};
|
||||
|
||||
static_assert(!std::is_constructible_v<OffsetAmount, double, Denomination>,
|
||||
"offsetOf must be the only way to give an OffsetAmount a value");
|
||||
|
||||
bool operator==(OffsetAmount a, OffsetAmount b);
|
||||
bool operator!=(OffsetAmount a, OffsetAmount b);
|
||||
|
||||
OffsetAmount offsetFromMs(double ms);
|
||||
OffsetAmount offsetFromBeats(double beats);
|
||||
|
||||
double offsetMs(OffsetAmount amount, Tempo tempo);
|
||||
double offsetBeats(OffsetAmount amount, Tempo tempo);
|
||||
double offsetSeconds(OffsetAmount amount, Tempo tempo);
|
||||
@@ -100,7 +127,7 @@ struct ResolvedNote {
|
||||
double noteOffSeconds = 0.0; // == the note's sounding length, note-on being 0
|
||||
double captureStartSeconds = 0.0; // negative when the capture opens before the note
|
||||
double captureEndSeconds = 0.0;
|
||||
std::uint8_t velocity = 100; // resolveNote always overwrites this; matches Velocity's own default
|
||||
std::uint8_t velocity = Velocity{}.value(); // resolveNote always overwrites this
|
||||
// True when the programmed end offset inverted the window and resolveNote collapsed it
|
||||
// to zero length instead — lets a popup explain an empty window rather than just show one.
|
||||
bool windowCollapsed = false;
|
||||
@@ -108,6 +135,8 @@ struct ResolvedNote {
|
||||
double captureLengthSeconds() const { return captureEndSeconds - captureStartSeconds; }
|
||||
};
|
||||
|
||||
// Total: every field of the result is finite for every constructible program and tempo,
|
||||
// which is why there is no failure path here. See this directory's CLAUDE.md.
|
||||
ResolvedNote resolveNote(const NoteProgram& program, Tempo tempo);
|
||||
|
||||
} // namespace reasampler::instrument::note
|
||||
|
||||
Reference in New Issue
Block a user