Bake window: derived note lengths carry exact durations, not ladder rungs — a long take is no longer cut at 384 beats

Hold keeps its picker. Also: one home for the %-fold, duration-ordered Hold travel, and a corrupt tail degrades to absent rather than fabricating one.
This commit is contained in:
2026-08-01 21:10:38 -04:00
parent 19aeb92775
commit 65f6070348
35 changed files with 772 additions and 226 deletions
+42 -1
View File
@@ -85,6 +85,47 @@ OffsetAmount redenominate(OffsetAmount amount, Denomination to, Tempo tempo);
OffsetAmount withMsView(OffsetAmount amount, double ms, Tempo tempo);
OffsetAmount withBeatsView(OffsetAmount amount, double beats, Tempo tempo);
// The largest exact note length, in seconds — the ms ceiling restated in the unit an exact
// length is entered in, so a length and an offset cap at the same instant.
inline constexpr double kMaxLengthSeconds = msToSeconds(kMaxConvertibleMagnitude);
class NoteLength;
// The two doors. A DERIVED length is exact: `lengthOfSeconds` is what every computed duration
// takes, and rounding one onto the ladder is what truncates a source longer than the ladder's
// top rung. A PICKED length is a `Division`, because a picker's rungs are the point. Both
// clamp: a negative or NaN duration names no length and becomes zero, a magnitude past
// kMaxLengthSeconds clamps to it, and `makeDivision` already holds the rung's own domain.
NoteLength lengthOfDivision(Division division);
NoteLength lengthOfSeconds(double seconds);
// One length, in whichever denomination its door established. There is no accessor per
// denomination: `noteLengthSeconds` resolves both, so no reader can pick the wrong one.
class NoteLength {
public:
NoteLength() = default; // a quarter note — Division's own default
private:
NoteLength(Division division, double seconds, bool exact)
: division_(division), seconds_(seconds), exact_(exact) {}
friend NoteLength lengthOfDivision(Division division);
friend NoteLength lengthOfSeconds(double seconds);
friend double noteLengthSeconds(NoteLength length, Tempo tempo);
friend bool operator==(NoteLength a, NoteLength b);
Division division_{};
double seconds_ = 0.0;
bool exact_ = false;
};
static_assert(!std::is_constructible_v<NoteLength, Division>,
"lengthOfDivision must be the only way to give a NoteLength a rung");
bool operator==(NoteLength a, NoteLength b);
bool operator!=(NoteLength a, NoteLength b);
double noteLengthSeconds(NoteLength length, Tempo tempo);
// Two types rather than one carrying an anchor field: the anchor is then unswappable at
// compile time. Sign is uniform — positive is later in time — so a capture that opens before
// the note is a negative start offset, and a negative end offset truncates before release.
@@ -114,7 +155,7 @@ static_assert(!std::is_convertible_v<OffsetAmount, StartOffset>,
"the anchor constructor must stay explicit");
struct NoteProgram {
Division length{};
NoteLength length{};
StartOffset start{};
EndOffset end{};
Velocity velocity{};