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:
@@ -29,6 +29,8 @@ using instrument::engine::VelocityCurve;
|
||||
using instrument::engine::VelocityPoint;
|
||||
using instrument::engine::loop::ResolvedLoop;
|
||||
using instrument::engine::loop::crossfadeWeight;
|
||||
using instrument::engine::loop::crossfadedSource;
|
||||
using instrument::engine::loop::lerpSource;
|
||||
|
||||
// 2^((note - rootNote) / 12). note == rootNote -> 1.0. Pure equal temperament; no
|
||||
// reference-frequency needed.
|
||||
@@ -154,32 +156,6 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
// Linear-interpolated read at a plain (non-wrapping) fractional source position. The
|
||||
// crossfade tap sits one loop length behind the head, i.e. BEFORE the loop start, so it
|
||||
// never needs the wrap partner the main read uses.
|
||||
static double lerpSource(const std::vector<AudioSample>& pcm, std::int64_t frameCount,
|
||||
double pos) {
|
||||
const std::int64_t i0 = static_cast<std::int64_t>(pos);
|
||||
const std::int64_t i1 = i0 + 1;
|
||||
const double frac = pos - static_cast<double>(i0);
|
||||
const double a = (i0 >= 0 && i0 < frameCount) ? static_cast<double>(pcm[i0]) : 0.0;
|
||||
const double b = (i1 >= 0 && i1 < frameCount) ? static_cast<double>(pcm[i1]) : 0.0;
|
||||
return a + (b - a) * frac;
|
||||
}
|
||||
|
||||
// One integer source frame with the loop crossfade already blended in — the Preserve
|
||||
// path's read, and the start()-time ring prime's. `pos` must be a valid index; `xw` is
|
||||
// crossfadeWeight at that position (0 blends nothing).
|
||||
AudioSample crossfadedSource(const std::vector<AudioSample>& pcm, std::int64_t pos,
|
||||
double xw) const {
|
||||
const double v = static_cast<double>(pcm[static_cast<std::size_t>(pos)]);
|
||||
if (xw <= 0.0) return static_cast<AudioSample>(v);
|
||||
const std::int64_t tap = pos - loop_.length;
|
||||
if (tap < 0) return static_cast<AudioSample>(v);
|
||||
const double in = static_cast<double>(pcm[static_cast<std::size_t>(tap)]);
|
||||
return static_cast<AudioSample>(v + xw * (in - v));
|
||||
}
|
||||
|
||||
// This frame's amplitude in [0,1] from the active envelope. Gate: AHDSR ticks once per
|
||||
// output frame (envelope time is wall-clock, independent of read rate). Trigger: the AHD
|
||||
// is evaluated at the source offset (readPos - startFrame) so its stages anchor to source
|
||||
@@ -449,7 +425,7 @@ private:
|
||||
// shift the output.
|
||||
const double feedXw = crossfadeWeight(loop, static_cast<double>(feedPos_));
|
||||
const AudioSample feedL =
|
||||
feedOk ? crossfadedSource(pcm, feedPos_, feedXw) : 0.0f;
|
||||
feedOk ? crossfadedSource(pcm, loop, feedPos_, feedXw) : 0.0f;
|
||||
const double shift = baseRatio_ * envFactor;
|
||||
shiftL_.setShiftRatio(shift);
|
||||
const double shiftedL = static_cast<double>(shiftL_.process(feedL));
|
||||
@@ -468,7 +444,7 @@ private:
|
||||
// not leak a previous note.
|
||||
if (exhausted) shiftR_.freezeTail();
|
||||
const AudioSample feedR =
|
||||
feedOk ? crossfadedSource(pcmR, feedPos_, feedXw) : 0.0f;
|
||||
feedOk ? crossfadedSource(pcmR, loop, feedPos_, feedXw) : 0.0f;
|
||||
shiftR_.setShiftRatio(shift);
|
||||
outRlocal =
|
||||
static_cast<double>(shiftR_.processLinked(feedR, shiftL_.lastSplice()));
|
||||
|
||||
Reference in New Issue
Block a user