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
+53 -3
View File
@@ -1,7 +1,8 @@
// Standalone tests for reasampler::instrument::note::note_program — no VST3, no REAPER, no
// framework. Same fast assert loop as the sibling pure tests.
//
// Covers: velocity clamping; the ms/beats denomination seam and its round-trip; anchoring
// Covers: velocity clamping; the picked/exact note-length seam and its bounded door; the
// ms/beats denomination seam and its round-trip; anchoring
// (start to note-on, end to note-off); the resolved window against hand-computed values and
// its windowCollapsed flag, including the zero-length window the flag exists to distinguish;
// every division resolving to its duration in seconds; proportionality across two tempos;
@@ -39,7 +40,7 @@ static const double kStraightBeats[kRungCount] = {
static NoteProgram program(Division length, OffsetAmount start, OffsetAmount end, int velocity) {
NoteProgram p;
p.length = length;
p.length = lengthOfDivision(length);
p.start = StartOffset(start);
p.end = EndOffset(end);
p.velocity = Velocity::of(velocity);
@@ -194,6 +195,52 @@ static void testNoteLengthIsProportionalToTempo() {
}
}
// --- NoteLength: the two denominations -----------------------------------------
// A picked length follows the tempo (that is what a picker means); a derived one does not,
// because it was computed against a concrete sound and a tempo change did not lengthen that
// sound. Both resolve through the one function, so no reader chooses.
static void testAnExactLengthIsTempoIndependentAndAPickedOneIsNot() {
const NoteLength picked = lengthOfDivision(makeDivision(0, DivisionModifier::Straight));
const NoteLength exact = lengthOfSeconds(0.5);
CHECK(almostEqual(noteLengthSeconds(picked, at(120.0)), 0.5));
CHECK(almostEqual(noteLengthSeconds(picked, at(60.0)), 1.0));
CHECK(almostEqual(noteLengthSeconds(exact, at(120.0)), 0.5));
CHECK(almostEqual(noteLengthSeconds(exact, at(60.0)), 0.5));
// Equal at one tempo is not equal as records — the denomination IS part of the value.
CHECK(picked != exact);
CHECK(lengthOfSeconds(0.5) == exact);
CHECK(lengthOfSeconds(0.5) != lengthOfSeconds(0.5000001));
}
// The whole point of the exact denomination: a duration past the longest ladder rung is
// representable, where quantizing onto the ladder would saturate at kMaxDivisionBeats.
static void testAnExactLengthCarriesDurationsPastTheLaddersTopRung() {
const Tempo t = at(120.0);
const double pastTheLadder = t.beatsToSeconds(kMaxDivisionBeats) * 3.0;
CHECK(almostEqual(noteLengthSeconds(lengthOfSeconds(pastTheLadder), t), pastTheLadder));
// …and the ladder really does saturate there, which is what makes the seam load-bearing.
CHECK(almostEqual(divisionBeats(shortestDivisionAtLeast(kMaxDivisionBeats * 3.0)),
kMaxDivisionBeats));
}
// The door establishes the domain, like every other value type here: a corrupt or absurd
// duration becomes a representable one rather than reaching resolveNote as a poison value.
static void testTheExactLengthDoorBoundsItsDomain() {
const Tempo t = at(120.0);
CHECK(almostEqual(noteLengthSeconds(lengthOfSeconds(-4.0), t), 0.0));
CHECK(almostEqual(noteLengthSeconds(lengthOfSeconds(std::nan("")), t), 0.0));
CHECK(almostEqual(noteLengthSeconds(lengthOfSeconds(kMaxLengthSeconds * 10.0), t),
kMaxLengthSeconds));
CHECK(std::isfinite(
noteLengthSeconds(lengthOfSeconds(std::numeric_limits<double>::infinity()), t)));
// And the whole resolve stays finite over it, which is the module's headline claim.
NoteProgram p;
p.length = lengthOfSeconds(std::numeric_limits<double>::infinity());
const ResolvedNote r = resolveNote(p, t);
CHECK(std::isfinite(r.noteOffSeconds) && std::isfinite(r.captureEndSeconds));
}
// --- The resolved window -------------------------------------------------------
static void testWindowAnchorsStartToNoteOnAndEndToNoteOff() {
@@ -279,7 +326,7 @@ static void testRecordRoundTripsAsAWhole() {
offsetFromMs(-20.0), offsetFromBeats(2.0), 96);
const NoteProgram copy = original;
CHECK(copy == original);
CHECK(copy.length == makeDivision(-1, DivisionModifier::Dotted));
CHECK(copy.length == lengthOfDivision(makeDivision(-1, DivisionModifier::Dotted)));
CHECK(copy.start.amount() == offsetFromMs(-20.0));
CHECK(copy.end.amount() == offsetFromBeats(2.0));
CHECK(copy.velocity.value() == 96);
@@ -526,6 +573,9 @@ int main() {
testEveryDivisionResolvesToItsDuration();
testExtremeAndNamedDivisionsInSeconds();
testNoteLengthIsProportionalToTempo();
testAnExactLengthIsTempoIndependentAndAPickedOneIsNot();
testAnExactLengthCarriesDurationsPastTheLaddersTopRung();
testTheExactLengthDoorBoundsItsDomain();
testWindowAnchorsStartToNoteOnAndEndToNoteOff();
testEndOffsetMovesWithTheNoteLength();