loop: crossfade the Gate sustain seam, and unshadow the loop handles that made loop points look gone

This commit is contained in:
2026-07-31 17:36:36 -04:00
parent a90ccd9a00
commit 0fe4166d7d
25 changed files with 991 additions and 64 deletions
+14 -6
View File
@@ -66,6 +66,10 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
readPos_ = static_cast<double>(start);
startFrame_ = start; // the span-offset origin: readPos - startFrame
// The one fold of the stored span + crossfade into what the read path wraps on.
loop_ = instrument::engine::loop::resolveLoop(sample.loop, sample.loopCrossfadeFrames,
frameCount, playMode_ == PlayMode::Gate);
// Amplitude envelope: Gate = AHDSR (all five fields read from play.adsr, resolved to
// frames from stored seconds at load time); Trigger = the staged AHD over the % play span.
const std::int64_t postStart = frameCount - start; // >= 1 (start clamped < frameCount)
@@ -162,9 +166,7 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
// no per-frame shifter cost.
if (pitchEngine_ == PitchEngine::Preserve && shiftL_.configured()) {
const std::int64_t w = shiftL_.window();
const bool loopWrap = sustainLoopUsable();
const SampleLoop& loop = sample.loop;
const std::int64_t loopLen = loopWrap ? (loop.end - loop.start) : 0;
const bool loopWrap = loop_.active;
const bool stereoSample = sample.channelCount() == 2 && shiftR_.configured();
// The prime may only carry playable source. The per-frame feed stops at feedBound
// (playEnd_ for a bounded Trigger span, the sample end for Gate) and freezes the
@@ -189,12 +191,18 @@ void Voice::start(int note, int velocity, const SampleData& sample, bool declick
std::int64_t q = start;
for (std::int64_t i = 0; i < primeCount; ++i) {
if (loopWrap) {
while (q >= loop.end) q -= loopLen;
while (q >= loop_.end) q -= loop_.length;
}
// q < frameCount holds by construction on the non-loop path (primeCount is
// bounded); the guard stays as a belt for the loop-wrap walk.
// bounded); the guard stays as a belt for the loop-wrap walk. The prime runs
// the SAME crossfade the per-frame feed does — a ring primed with an un-faded
// seam would put the click back one window into the note.
primeBuf_[static_cast<std::size_t>(i)] =
(q < frameCount) ? pcmCh[static_cast<std::size_t>(q)] : 0.0f;
(q < frameCount)
? crossfadedSource(pcmCh, q,
instrument::engine::loop::crossfadeWeight(
loop_, static_cast<double>(q)))
: 0.0f;
++q;
}
(ch == 0 ? shiftL_ : shiftR_).prime(primeBuf_.data(), primeCount);