loop: fix the crossfade seam's residual discontinuity, plus six review minors
Normalizes crossfadeWeight over crossfade-1 so the last rendered frame lands at exactly the incoming tap instead of a residual step; corrects the CLAUDE.md invariant and seam test to match. Shares lerpSource/crossfadedSource/maxCrossfade, fixes stale docs/constants, and clears crossfade on the loop-OFF gesture.
This commit is contained in:
+27
-10
@@ -93,18 +93,34 @@ static void testZeroCrossfadeWeighsNothingAnywhere() {
|
||||
CHECK(crossfadeWeight(lp, 399.999) == 0.0);
|
||||
}
|
||||
|
||||
// The weight rises from exactly 0 at the region's start to exactly 1 at the loop end, which
|
||||
// is what makes the seam continuous: at `end` the incoming tap has reached `start`, and the
|
||||
// wrap puts the head there.
|
||||
// The weight rises from exactly 0 at the region's start to exactly 1 at the LAST rendered
|
||||
// frame (end - 1, d == crossfade - 1) — normalizing over crossfade - 1 rather than crossfade is
|
||||
// what lands the ceiling exactly there instead of merely approaching it, which is what makes
|
||||
// the seam continuous: at that frame the incoming tap has fully replaced the raw read, and the
|
||||
// wrap hands over exactly that value.
|
||||
static void testWeightRunsZeroToOneAcrossTheFadeRegion() {
|
||||
const ResolvedLoop lp = resolveLoop(span(100, 400), 100, 1000, true);
|
||||
CHECK(lp.crossfade == 100);
|
||||
CHECK(lp.fadeBegin == 300.0);
|
||||
// xf = 101 so xf - 1 = 100, a clean denominator (loopStart 200 keeps the clamp out of the
|
||||
// way: max is min(200, 200)).
|
||||
const ResolvedLoop lp = resolveLoop(span(200, 400), 101, 1000, true);
|
||||
CHECK(lp.crossfade == 101);
|
||||
CHECK(lp.fadeBegin == 299.0);
|
||||
CHECK(crossfadeWeight(lp, 298.0) == 0.0);
|
||||
CHECK(crossfadeWeight(lp, 299.0) == 0.0);
|
||||
CHECK(crossfadeWeight(lp, 300.0) == 0.0);
|
||||
CHECK(crossfadeWeight(lp, 350.0) == 0.5);
|
||||
CHECK(crossfadeWeight(lp, 375.0) == 0.75);
|
||||
CHECK(crossfadeWeight(lp, 400.0) == 1.0);
|
||||
CHECK(crossfadeWeight(lp, 349.0) == 0.5); // d = 50
|
||||
CHECK(crossfadeWeight(lp, 374.0) == 0.75); // d = 75
|
||||
CHECK(crossfadeWeight(lp, 399.0) == 1.0); // d = 100 == xf - 1, the ceiling's own threshold
|
||||
CHECK(crossfadeWeight(lp, 400.0) == 1.0); // past it too (the unwrapped-caller belt)
|
||||
}
|
||||
|
||||
// xf - 1 == 0 would divide by zero; the guard parks fadeInv at 0 instead. Unreachable via the
|
||||
// multiply branch anyway (the region's only frame has d == 0, caught by the d <= 0 check
|
||||
// first), but fadeInv must still be a sane value rather than +inf.
|
||||
static void testCrossfadeOfOneNeedsNoDivisionGuard() {
|
||||
const ResolvedLoop lp = resolveLoop(span(100, 400), 1, 1000, true);
|
||||
CHECK(lp.crossfade == 1);
|
||||
CHECK(lp.fadeInv == 0.0);
|
||||
CHECK(crossfadeWeight(lp, 399.0) == 0.0); // d == 0, the region's one frame
|
||||
CHECK(crossfadeWeight(lp, 400.0) == 1.0); // past it, the ceiling belt still holds
|
||||
}
|
||||
|
||||
static void testWeightIsMonotoneAndBoundedAcrossTheRegion() {
|
||||
@@ -158,6 +174,7 @@ int main() {
|
||||
testNegativeCrossfadeIsZero();
|
||||
testZeroCrossfadeWeighsNothingAnywhere();
|
||||
testWeightRunsZeroToOneAcrossTheFadeRegion();
|
||||
testCrossfadeOfOneNeedsNoDivisionGuard();
|
||||
testWeightIsMonotoneAndBoundedAcrossTheRegion();
|
||||
testWeightSaturatesPastTheLoopEnd();
|
||||
testDefaultBoundsSitInTheLastQuarterAndClearFrameZero();
|
||||
|
||||
Reference in New Issue
Block a user