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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user