From f6d05509cbf523e02dad0775a6bbe43b392a741f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 29 Sep 2020 23:52:10 +0200 Subject: [PATCH 01/13] Add opcode: loop_crossfade --- src/sfizz/Defaults.h | 2 ++ src/sfizz/Region.cpp | 3 +++ src/sfizz/Region.h | 1 + tests/RegionT.cpp | 8 ++++++++ 4 files changed, 14 insertions(+) diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 540b6944..bf4c4ae6 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -54,6 +54,8 @@ namespace Default constexpr Range sampleCountRange { 0, std::numeric_limits::max() }; constexpr SfzLoopMode loopMode { SfzLoopMode::no_loop }; constexpr Range loopRange { 0, std::numeric_limits::max() }; + constexpr float loopCrossfade { 1e-3 }; + constexpr Range loopCrossfadeRange { loopCrossfade, 1.0 }; // common defaults constexpr Range midi7Range { 0, 127 }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 2e9a1de5..8c85cca1 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -138,6 +138,9 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("loop_start"): // also loopstart setRangeStartFromOpcode(opcode, loopRange, Default::loopRange); break; + case hash("loop_crossfade"): + setValueFromOpcode(opcode, loopCrossfade, Default::loopCrossfadeRange); + break; // Wavetable oscillator case hash("oscillator_phase"): diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 76840c53..a8144d52 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -319,6 +319,7 @@ struct Region { absl::optional sampleCount {}; // count absl::optional loopMode {}; // loopmode Range loopRange { Default::loopRange }; //loopstart and loopend + float loopCrossfade { Default::loopCrossfade }; // loop_crossfade // Wavetable oscillator float oscillatorPhase { Default::oscillatorPhase }; diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 77aff25a..550da46f 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -174,6 +174,14 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.loopRange == Range(0, 4294967295)); } + SECTION("loop_crossfade") + { + region.parseOpcode({ "loop_crossfade", "0.5" }); + REQUIRE(region.loopCrossfade == Approx(0.5f)); + region.parseOpcode({ "loop_crossfade", "0" }); + REQUIRE(region.loopCrossfade > 0); + } + SECTION("group") { REQUIRE(region.group == 0); From 8a2e17ec921a4de64faee390599d45e535e1df30 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 30 Sep 2020 01:27:32 +0200 Subject: [PATCH 02/13] Implement loop xfade --- src/sfizz/Config.h | 2 +- src/sfizz/Voice.cpp | 299 +++++++++++++++++++++++++++++++++++++------- src/sfizz/Voice.h | 21 +++- 3 files changed, 277 insertions(+), 45 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 442a165c..3c724dad 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -31,7 +31,7 @@ namespace config { constexpr int maxBlockSize { 8192 }; constexpr int bufferPoolSize { 6 }; constexpr int stereoBufferPoolSize { 4 }; - constexpr int indexBufferPoolSize { 2 }; + constexpr int indexBufferPoolSize { 4 }; constexpr int preloadSize { 8192 }; constexpr int loggerQueueSize { 256 }; constexpr int voiceLoggerQueueSize { 256 }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 4a0fc5b0..a370044f 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -533,35 +533,133 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept auto source = currentPromise->getData(); - auto jumps = resources.bufferPool.getBuffer(numSamples); + // calculate interpolation data + // indices: integral position in the source audio + // coeffs: fractional position normalized 0-1 auto coeffs = resources.bufferPool.getBuffer(numSamples); auto indices = resources.bufferPool.getIndexBuffer(numSamples); - if (!jumps || !indices || !coeffs) + if (!indices || !coeffs) return; + { + auto jumps = resources.bufferPool.getBuffer(numSamples); + if (!jumps) + return; - fill(*jumps, pitchRatio * speedRatio); - pitchEnvelope(*jumps); + fill(*jumps, pitchRatio * speedRatio); + pitchEnvelope(*jumps); - jumps->front() += floatPositionOffset; - cumsum(*jumps, *jumps); - sfzInterpolationCast(*jumps, *indices, *coeffs); - add1(sourcePosition, *indices); + jumps->front() += floatPositionOffset; + cumsum(*jumps, *jumps); + sfzInterpolationCast(*jumps, *indices, *coeffs); + add1(sourcePosition, *indices); + } - if (region->shouldLoop() && region->loopEnd(currentPromise->oversamplingFactor) <= source.getNumFrames()) { - const auto loopEnd = static_cast(region->loopEnd(currentPromise->oversamplingFactor)); - const auto loopStart = static_cast(region->loopStart(currentPromise->oversamplingFactor)); - const auto loopSize = loopEnd + 1 - loopStart; - for (auto* it = indices->begin(), *end = indices->end(); it < end; ++it) { - auto index = *it; - *it = (index < loopEnd + 1) ? index : - (loopStart + (index - loopStart) % loopSize); + // calculate loop characteristics + bool isLooping = false; + int loopStart = 0; + int loopEnd = 0; + int loopSize = 0; + int loopXfadeSize = 0; + int loopXfOutStart = 0; + int loopXfInStart = 0; // Note: beware in case of negative index + SpanHolder> xfadeTemp[2]; + SpanHolder> xfadeIndexTemp[1]; + if (region->shouldLoop()) { + loopEnd = region->loopEnd(currentPromise->oversamplingFactor); + isLooping = static_cast(loopEnd) < source.getNumFrames(); + } + if (isLooping) { + loopStart = static_cast(region->loopStart(currentPromise->oversamplingFactor)); + loopSize = loopEnd + 1 - loopStart; + loopXfadeSize = static_cast(region->loopCrossfade * sampleRate + 0.5); + loopXfOutStart = loopEnd + 1 - loopXfadeSize; + loopXfInStart = loopStart - loopXfadeSize; + for (auto& buf : xfadeTemp) { + buf = resources.bufferPool.getBuffer(numSamples); + if (!buf) + return; } - } else { + for (auto& buf : xfadeIndexTemp) { + buf = resources.bufferPool.getIndexBuffer(numSamples); + if (!buf) + return; + } + } + + /* + loop start loop end + v | + /|---------------|\ | + / | | \ | + / | | \ v + /------|---------------|------\ + ^ ^ + xfin start xfout start + <------> <------> + xfade size xfade size + */ + + // loop crossfade partitioning + absl::Span partitionStarts; + absl::Span partitionTypes; + unsigned numPartitions = 0; + enum PartitionType { kPartitionNormal, kPartitionLoopXfade }; + + SpanHolder> partitionBuffers[2]; + if (!isLooping) { + static const int starts[1] = { 0 }; + static const int types[1] = { kPartitionNormal }; + partitionStarts = absl::MakeSpan(const_cast(starts), 1); + partitionTypes = absl::MakeSpan(const_cast(types), 1); + numPartitions = 1; + } + else { + for (auto& buf : partitionBuffers) { + buf = resources.bufferPool.getIndexBuffer(numSamples); + if (!buf) + return; + } + partitionStarts = *partitionBuffers[0]; + partitionTypes = *partitionBuffers[1]; + // Note: partitions will be alternance of Normal/Xfade + // computed along with index processing below + } + + // index preprocessing for loops + if (isLooping) { + int oldIndex {}; + int oldPartitionType {}; + for (unsigned i = 0; i < numSamples; ++i) { + int index = (*indices)[i]; + + // wrap indices post loop-entry around the loop segment + int wrappedIndex = (index <= loopEnd) ? index : + (loopStart + (index - loopStart) % loopSize); + (*indices)[i] = wrappedIndex; + + // identify the partition this index is in + bool xfading = wrappedIndex >= loopStart && wrappedIndex >= loopXfOutStart; + int partitionType = xfading ? kPartitionLoopXfade : kPartitionNormal; + // if looping or entering a different type, start a new partition + bool start = i == 0 || wrappedIndex < oldIndex || partitionType != oldPartitionType; + if (start) { + partitionStarts[numPartitions] = i; + partitionTypes[numPartitions] = partitionType; + ++numPartitions; + } + + oldIndex = wrappedIndex; + oldPartitionType = partitionType; + } + } + // index preprocessing for one-shots + else { + // cut short the voice at the instant of reaching end of sample const auto sampleEnd = min( static_cast(region->trueSampleEnd(currentPromise->oversamplingFactor)), static_cast(source.getNumFrames()) ) - 1; - for (unsigned i = 0; i < indices->size(); ++i) { + for (unsigned i = 0; i < numSamples; ++i) { if ((*indices)[i] >= sampleEnd) { #ifndef NDEBUG // Check for underflow @@ -581,25 +679,93 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept } } + // interpolation processing const int quality = getCurrentSampleQuality(); - switch (quality) { - default: - if (quality > 2) - goto high; // TODO sinc, not implemented - // fall through - case 1: - fillInterpolated(source, buffer, *indices, *coeffs); - break; - case 2: high: -#if 1 - // B-spline response has faster decay of aliasing, but not zero-crossings at integer positions - fillInterpolated(source, buffer, *indices, *coeffs); -#else - // Hermite polynomial - fillInterpolated(source, buffer, *indices, *coeffs); -#endif - break; + for (unsigned ptNo = 0; ptNo < numPartitions; ++ptNo) { + // current partition + const int ptType = partitionTypes[ptNo]; + const unsigned ptStart = partitionStarts[ptNo]; + const unsigned ptNextStart = (ptNo + 1 < numPartitions) ? partitionStarts[ptNo + 1] : numSamples; + const unsigned ptSize = ptNextStart - ptStart; + + // partition spans + AudioSpan ptBuffer = buffer.subspan(ptStart, ptSize); + absl::Span ptIndices = indices->subspan(ptStart, ptSize); + absl::Span ptCoeffs = coeffs->subspan(ptStart, ptSize); + + fillInterpolatedWithQuality( + source, ptBuffer, ptIndices, ptCoeffs, {}, quality); + + if (ptType == kPartitionLoopXfade) { + absl::Span xfCoeff = xfadeTemp[0]->first(ptSize); + + // compute crossfade coeffs + for (unsigned i = 0; i < ptSize; ++i) { + float pos = ptIndices[i] + ptCoeffs[i]; + xfCoeff[i] = (pos - loopXfOutStart) / loopXfadeSize; + } + + //----------------------------------------------------------------// + // Crossfade Out + // -> fade out signal nearing the loop end + { + // compute crossfade coeffs + for (unsigned i = 0; i < ptSize; ++i) { + float pos = ptIndices[i] + ptCoeffs[i]; + xfCoeff[i] = (pos - loopXfOutStart) / loopXfadeSize; + } + // compute out curve + const Curve& xfOut = resources.curves.getCurve(6); + absl::Span xfCurve = xfadeTemp[1]->first(ptSize); + for (unsigned i = 0; i < ptSize; ++i) + xfCurve[i] = xfOut.evalNormalized(xfCoeff[i]); + // apply out curve + if (0) + ptBuffer.applyGain(xfCurve); + else { + // scalar fallback: buffer and curve not aligned + size_t numChannels = ptBuffer.getNumChannels(); + for (size_t c = 0; c < numChannels; ++c) { + absl::Span channel = ptBuffer.getSpan(c); + for (unsigned i = 0; i < ptSize; ++i) + channel[i] *= xfCurve[i]; + } + } + } + //----------------------------------------------------------------// + // Crossfade In + // -> fade in signal preceding the loop start + { + // compute indices of the crossfade input segment + absl::Span xfInIndices = xfadeIndexTemp[0]->first(ptSize); + absl::c_copy(ptIndices, xfInIndices.begin()); + subtract1(loopXfOutStart - loopXfInStart, xfInIndices); + + // disregard the segment whose indices have been pushed + // into the negatives, take these virtually as zeroes. + unsigned applyOffset = 0; + while (applyOffset < ptSize && xfInIndices[applyOffset] < 0) + ++applyOffset; + unsigned applySize = ptSize - applyOffset; + + // offset the indices + xfInIndices = xfInIndices.subspan(applyOffset); + // offset the coeffs + absl::Span xfInCoeff = xfCoeff.subspan(applyOffset); + // offset the output buffer + AudioSpan xfInBuffer = ptBuffer.subspan(applyOffset); + + // compute in curve + const Curve& xfIn = resources.curves.getCurve(5); + absl::Span xfCurve = xfadeTemp[1]->first(applySize); + for (unsigned i = 0; i < applySize; ++i) + xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + // apply in curve + fillInterpolatedWithQuality( + source, xfInBuffer, xfInIndices, *coeffs, xfCurve, quality); + } + } } sourcePosition = indices->back(); @@ -613,31 +779,80 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept #endif } -template +template void sfz::Voice::fillInterpolated( const sfz::AudioSpan& source, const sfz::AudioSpan& dest, - absl::Span indices, absl::Span coeffs) + absl::Span indices, absl::Span coeffs, + absl::Span addingGains) { - auto ind = indices.data(); - auto coeff = coeffs.data(); + auto* ind = indices.data(); + auto* coeff = coeffs.data(); + auto* addingGain = addingGains.data(); auto leftSource = source.getConstSpan(0); auto left = dest.getChannel(0); if (source.getNumChannels() == 1) { while (ind < indices.end()) { - *left = sfz::interpolate(&leftSource[*ind], *coeff); + auto output = sfz::interpolate(&leftSource[*ind], *coeff); + IF_CONSTEXPR(Adding) { + float g = *addingGain++; + *left += g * output; + } + else + *left = output; incrementAll(ind, left, coeff); } } else { auto right = dest.getChannel(1); auto rightSource = source.getConstSpan(1); while (ind < indices.end()) { - *left = sfz::interpolate(&leftSource[*ind], *coeff); - *right = sfz::interpolate(&rightSource[*ind], *coeff); + auto leftOutput = sfz::interpolate(&leftSource[*ind], *coeff); + auto rightOutput = sfz::interpolate(&rightSource[*ind], *coeff); + IF_CONSTEXPR(Adding) { + float g = *addingGain++; + *left += g * leftOutput; + *right += g * rightOutput; + } + else { + *left = leftOutput; + *right = rightOutput; + } incrementAll(ind, left, right, coeff); } } } +template +void sfz::Voice::fillInterpolatedWithQuality( + const sfz::AudioSpan& source, const sfz::AudioSpan& dest, + absl::Span indices, absl::Span coeffs, + absl::Span addingGains, int quality) +{ + switch (quality) { + default: + if (quality > 2) + goto high; // TODO sinc, not implemented + // fall through + case 1: + { + constexpr auto itp = kInterpolatorLinear; + fillInterpolated(source, dest, indices, coeffs, addingGains); + } + break; + case 2: high: + { +#if 1 + // B-spline response has faster decay of aliasing, but not zero-crossings at integer positions + constexpr auto itp = kInterpolatorBspline3; +#else + // Hermite polynomial + constexpr auto itp = kInterpolatorHermite3; +#endif + fillInterpolated(source, dest, indices, coeffs, addingGains); + } + break; + } +} + void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept { const auto leftSpan = buffer.getSpan(0); diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 9d5bfceb..35f91efc 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -374,10 +374,27 @@ private: * @param indices the integral parts of the source positions * @param coeffs the fractional parts of the source positions */ - template + template static void fillInterpolated( const AudioSpan& source, const AudioSpan& dest, - absl::Span indices, absl::Span coeffs); + absl::Span indices, absl::Span coeffs, + absl::Span addingGains); + + /** + * @brief Fill a destination with an interpolated source, selecting + * interpolation type dynamically by quality level. + * + * @param source the source sample + * @param dest the destination buffer + * @param indices the integral parts of the source positions + * @param coeffs the fractional parts of the source positions + * @param quality the quality level 1-10 + */ + template + static void fillInterpolatedWithQuality( + const AudioSpan& source, const AudioSpan& dest, + absl::Span indices, absl::Span coeffs, + absl::Span addingGains, int quality); /** * @brief Compute the amplitude envelope, applied as a gain to a mono From 7a5fd53001ac916a9a2f4f900c073ef62410b5c7 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 30 Sep 2020 18:19:31 +0200 Subject: [PATCH 03/13] Let's make clang-tidy happy --- src/sfizz/Voice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index a370044f..0b1ee1aa 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -571,7 +571,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept if (isLooping) { loopStart = static_cast(region->loopStart(currentPromise->oversamplingFactor)); loopSize = loopEnd + 1 - loopStart; - loopXfadeSize = static_cast(region->loopCrossfade * sampleRate + 0.5); + loopXfadeSize = static_cast(lroundPositive(region->loopCrossfade * sampleRate)); loopXfOutStart = loopEnd + 1 - loopXfadeSize; loopXfInStart = loopStart - loopXfadeSize; for (auto& buf : xfadeTemp) { From fda4b5931c364655332a08fedb24adb6e4eba6d8 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 30 Sep 2020 19:35:43 +0200 Subject: [PATCH 04/13] Eliminate a code repetition --- src/sfizz/Voice.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 0b1ee1aa..dbeb6405 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -710,11 +710,6 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // Crossfade Out // -> fade out signal nearing the loop end { - // compute crossfade coeffs - for (unsigned i = 0; i < ptSize; ++i) { - float pos = ptIndices[i] + ptCoeffs[i]; - xfCoeff[i] = (pos - loopXfOutStart) / loopXfadeSize; - } // compute out curve const Curve& xfOut = resources.curves.getCurve(6); absl::Span xfCurve = xfadeTemp[1]->first(ptSize); From 453d489d4ae458175e599d77f2f776712bd3498e Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Wed, 30 Sep 2020 20:29:50 +0200 Subject: [PATCH 05/13] Use linear xfade curves --- src/sfizz/Voice.cpp | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index dbeb6405..e0f298bd 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -625,6 +625,9 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // computed along with index processing below } + // loop crossfade settings + constexpr bool loopXfadeUseCurves = false; // 0: linear, 1: use curves 5 & 6 + // index preprocessing for loops if (isLooping) { int oldIndex {}; @@ -711,10 +714,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // -> fade out signal nearing the loop end { // compute out curve - const Curve& xfOut = resources.curves.getCurve(6); absl::Span xfCurve = xfadeTemp[1]->first(ptSize); - for (unsigned i = 0; i < ptSize; ++i) - xfCurve[i] = xfOut.evalNormalized(xfCoeff[i]); + IF_CONSTEXPR (loopXfadeUseCurves) { + const Curve& xfOut = resources.curves.getCurve(6); + for (unsigned i = 0; i < ptSize; ++i) + xfCurve[i] = xfOut.evalNormalized(xfCoeff[i]); + } + else { + // TODO(jpc) vectorize this + for (unsigned i = 0; i < ptSize; ++i) + xfCurve[i] = clamp(1.0f - xfCoeff[i], 0.0f, 1.0f); + } // apply out curve if (0) ptBuffer.applyGain(xfCurve); @@ -752,10 +762,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept AudioSpan xfInBuffer = ptBuffer.subspan(applyOffset); // compute in curve - const Curve& xfIn = resources.curves.getCurve(5); absl::Span xfCurve = xfadeTemp[1]->first(applySize); - for (unsigned i = 0; i < applySize; ++i) - xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + IF_CONSTEXPR (loopXfadeUseCurves) { + const Curve& xfIn = resources.curves.getCurve(5); + for (unsigned i = 0; i < applySize; ++i) + xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + } + else { + // TODO(jpc) vectorize this + for (unsigned i = 0; i < applySize; ++i) + xfCurve[i] = clamp(xfInCoeff[i], 0.0f, 1.0f); + } // apply in curve fillInterpolatedWithQuality( source, xfInBuffer, xfInIndices, *coeffs, xfCurve, quality); From 893d0361c2015ebff83969a401d5fc3f559eecdf Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 1 Oct 2020 13:43:22 +0200 Subject: [PATCH 06/13] Crossfade with S-shape curve --- src/sfizz/Voice.cpp | 39 ++++++++++++++++++++++++++++++++++----- src/sfizz/Voice.h | 5 +++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index e0f298bd..574fd2a9 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -34,6 +34,9 @@ sfz::Voice::Voice(int voiceNumber, sfz::Resources& resources) gainSmoother.setSmoothing(config::gainSmoothing, sampleRate); xfadeSmoother.setSmoothing(config::xfadeSmoothing, sampleRate); + + // prepare curves + getSCurve(); } sfz::Voice::~Voice() @@ -626,7 +629,9 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept } // loop crossfade settings - constexpr bool loopXfadeUseCurves = false; // 0: linear, 1: use curves 5 & 6 + constexpr int loopXfadeUseCurves = 2; // 0: linear + // 1: use curves 5 & 6 + // 2: use S-shaped curve // index preprocessing for loops if (isLooping) { @@ -715,12 +720,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept { // compute out curve absl::Span xfCurve = xfadeTemp[1]->first(ptSize); - IF_CONSTEXPR (loopXfadeUseCurves) { + IF_CONSTEXPR (loopXfadeUseCurves == 2) { + const Curve& xfIn = getSCurve(); + for (unsigned i = 0; i < ptSize; ++i) + xfCurve[i] = xfIn.evalNormalized(1.0f - xfCoeff[i]); + } + else IF_CONSTEXPR (loopXfadeUseCurves == 1) { const Curve& xfOut = resources.curves.getCurve(6); for (unsigned i = 0; i < ptSize; ++i) xfCurve[i] = xfOut.evalNormalized(xfCoeff[i]); } - else { + else IF_CONSTEXPR (loopXfadeUseCurves == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < ptSize; ++i) xfCurve[i] = clamp(1.0f - xfCoeff[i], 0.0f, 1.0f); @@ -763,12 +773,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // compute in curve absl::Span xfCurve = xfadeTemp[1]->first(applySize); - IF_CONSTEXPR (loopXfadeUseCurves) { + IF_CONSTEXPR (loopXfadeUseCurves == 2) { + const Curve& xfIn = getSCurve(); + for (unsigned i = 0; i < applySize; ++i) + xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + } + else IF_CONSTEXPR (loopXfadeUseCurves == 1) { const Curve& xfIn = resources.curves.getCurve(5); for (unsigned i = 0; i < applySize; ++i) xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); } - else { + else IF_CONSTEXPR (loopXfadeUseCurves == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < applySize; ++i) xfCurve[i] = clamp(xfInCoeff[i], 0.0f, 1.0f); @@ -865,6 +880,20 @@ void sfz::Voice::fillInterpolatedWithQuality( } } +const sfz::Curve& sfz::Voice::getSCurve() +{ + static const Curve curve = []() -> Curve { + constexpr unsigned N = Curve::NumValues; + float values[N]; + for (unsigned i = 0; i < N; ++i) { + double x = i / static_cast(N - 1); + values[i] = (1.0 - std::cos(M_PI * x)) * 0.5; + } + return Curve::buildFromPoints(values); + }(); + return curve; +} + void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept { const auto leftSpan = buffer.getSpan(0); diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 35f91efc..95dbd9ec 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -396,6 +396,11 @@ private: absl::Span indices, absl::Span coeffs, absl::Span addingGains, int quality); + /** + * @brief Get a S-shaped curve that is applicable to loop crossfading. + */ + static const Curve& getSCurve(); + /** * @brief Compute the amplitude envelope, applied as a gain to a mono * or stereo buffer From ab35581786e7f563fb510704eb677d2cfa0e24c5 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 1 Oct 2020 14:03:10 +0200 Subject: [PATCH 07/13] Xfade related to file sample rate, with oversampling considered --- src/sfizz/Voice.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 574fd2a9..8f655f77 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -574,7 +574,8 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept if (isLooping) { loopStart = static_cast(region->loopStart(currentPromise->oversamplingFactor)); loopSize = loopEnd + 1 - loopStart; - loopXfadeSize = static_cast(lroundPositive(region->loopCrossfade * sampleRate)); + loopXfadeSize = static_cast( + lroundPositive(region->loopCrossfade * static_cast(currentPromise->oversamplingFactor) * currentPromise->sampleRate)); loopXfOutStart = loopEnd + 1 - loopXfadeSize; loopXfInStart = loopStart - loopXfadeSize; for (auto& buf : xfadeTemp) { From 049739f8f1550ec3e79c45dd57318db5abf0ee0f Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 10:49:41 +0200 Subject: [PATCH 08/13] Use the correct coeffs for xfIn --- src/sfizz/Voice.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 8f655f77..b1522174 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -707,12 +707,12 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept source, ptBuffer, ptIndices, ptCoeffs, {}, quality); if (ptType == kPartitionLoopXfade) { - absl::Span xfCoeff = xfadeTemp[0]->first(ptSize); + absl::Span xfCurvePos = xfadeTemp[0]->first(ptSize); - // compute crossfade coeffs + // compute crossfade positions for (unsigned i = 0; i < ptSize; ++i) { float pos = ptIndices[i] + ptCoeffs[i]; - xfCoeff[i] = (pos - loopXfOutStart) / loopXfadeSize; + xfCurvePos[i] = (pos - loopXfOutStart) / loopXfadeSize; } //----------------------------------------------------------------// @@ -724,17 +724,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept IF_CONSTEXPR (loopXfadeUseCurves == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < ptSize; ++i) - xfCurve[i] = xfIn.evalNormalized(1.0f - xfCoeff[i]); + xfCurve[i] = xfIn.evalNormalized(1.0f - xfCurvePos[i]); } else IF_CONSTEXPR (loopXfadeUseCurves == 1) { const Curve& xfOut = resources.curves.getCurve(6); for (unsigned i = 0; i < ptSize; ++i) - xfCurve[i] = xfOut.evalNormalized(xfCoeff[i]); + xfCurve[i] = xfOut.evalNormalized(xfCurvePos[i]); } else IF_CONSTEXPR (loopXfadeUseCurves == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < ptSize; ++i) - xfCurve[i] = clamp(1.0f - xfCoeff[i], 0.0f, 1.0f); + xfCurve[i] = clamp(1.0f - xfCurvePos[i], 0.0f, 1.0f); } // apply out curve if (0) @@ -765,10 +765,11 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept ++applyOffset; unsigned applySize = ptSize - applyOffset; - // offset the indices + // offset the indices and coeffs xfInIndices = xfInIndices.subspan(applyOffset); - // offset the coeffs - absl::Span xfInCoeff = xfCoeff.subspan(applyOffset); + absl::Span xfInCoeffs = ptCoeffs.subspan(applyOffset); + // offset the curve positions + absl::Span xfInCurvePos = xfCurvePos.subspan(applyOffset); // offset the output buffer AudioSpan xfInBuffer = ptBuffer.subspan(applyOffset); @@ -777,21 +778,21 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept IF_CONSTEXPR (loopXfadeUseCurves == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < applySize; ++i) - xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + xfCurve[i] = xfIn.evalNormalized(xfInCurvePos[i]); } else IF_CONSTEXPR (loopXfadeUseCurves == 1) { const Curve& xfIn = resources.curves.getCurve(5); for (unsigned i = 0; i < applySize; ++i) - xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]); + xfCurve[i] = xfIn.evalNormalized(xfInCurvePos[i]); } else IF_CONSTEXPR (loopXfadeUseCurves == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < applySize; ++i) - xfCurve[i] = clamp(xfInCoeff[i], 0.0f, 1.0f); + xfCurve[i] = clamp(xfInCurvePos[i], 0.0f, 1.0f); } // apply in curve fillInterpolatedWithQuality( - source, xfInBuffer, xfInIndices, *coeffs, xfCurve, quality); + source, xfInBuffer, xfInIndices, xfInCoeffs, xfCurve, quality); } } } From 9f698aea4cf3b1d6a55c9ac2de34674b91923ae7 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 11:54:02 +0200 Subject: [PATCH 09/13] Remove dead code --- src/sfizz/Voice.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index b1522174..654dba5b 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -737,16 +737,12 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept xfCurve[i] = clamp(1.0f - xfCurvePos[i], 0.0f, 1.0f); } // apply out curve - if (0) - ptBuffer.applyGain(xfCurve); - else { - // scalar fallback: buffer and curve not aligned - size_t numChannels = ptBuffer.getNumChannels(); - for (size_t c = 0; c < numChannels; ++c) { - absl::Span channel = ptBuffer.getSpan(c); - for (unsigned i = 0; i < ptSize; ++i) - channel[i] *= xfCurve[i]; - } + // (scalar fallback: buffer and curve not aligned) + size_t numChannels = ptBuffer.getNumChannels(); + for (size_t c = 0; c < numChannels; ++c) { + absl::Span channel = ptBuffer.getSpan(c); + for (unsigned i = 0; i < ptSize; ++i) + channel[i] *= xfCurve[i]; } } //----------------------------------------------------------------// From 8f3be5ca3c55da22896516fa2a01a7ada004b01e Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 5 Oct 2020 13:48:21 +0200 Subject: [PATCH 10/13] Compute loop information at the start of the voice The check for actual looping is still done in the render method, to see if we do have enough samples --- src/sfizz/Voice.cpp | 63 +++++++++++++++++++++++++++++---------------- src/sfizz/Voice.h | 19 ++++++++++++++ 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 654dba5b..c1b05e8a 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -99,6 +99,7 @@ void sfz::Voice::startVoice(Region* region, int delay, const TriggerEvent& event switchState(State::cleanMeUp); return; } + updateLoopInformation(); speedRatio = static_cast(currentPromise->sampleRate / this->sampleRate); } @@ -558,26 +559,11 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept } // calculate loop characteristics - bool isLooping = false; - int loopStart = 0; - int loopEnd = 0; - int loopSize = 0; - int loopXfadeSize = 0; - int loopXfOutStart = 0; - int loopXfInStart = 0; // Note: beware in case of negative index + const bool isLooping = region->shouldLoop() + && (static_cast(loop.end) < source.getNumFrames()); SpanHolder> xfadeTemp[2]; SpanHolder> xfadeIndexTemp[1]; - if (region->shouldLoop()) { - loopEnd = region->loopEnd(currentPromise->oversamplingFactor); - isLooping = static_cast(loopEnd) < source.getNumFrames(); - } if (isLooping) { - loopStart = static_cast(region->loopStart(currentPromise->oversamplingFactor)); - loopSize = loopEnd + 1 - loopStart; - loopXfadeSize = static_cast( - lroundPositive(region->loopCrossfade * static_cast(currentPromise->oversamplingFactor) * currentPromise->sampleRate)); - loopXfOutStart = loopEnd + 1 - loopXfadeSize; - loopXfInStart = loopStart - loopXfadeSize; for (auto& buf : xfadeTemp) { buf = resources.bufferPool.getBuffer(numSamples); if (!buf) @@ -642,12 +628,12 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept int index = (*indices)[i]; // wrap indices post loop-entry around the loop segment - int wrappedIndex = (index <= loopEnd) ? index : - (loopStart + (index - loopStart) % loopSize); + int wrappedIndex = (index <= loop.end) ? index : + (loop.start + (index - loop.start) % loop.size); (*indices)[i] = wrappedIndex; // identify the partition this index is in - bool xfading = wrappedIndex >= loopStart && wrappedIndex >= loopXfOutStart; + bool xfading = wrappedIndex >= loop.start && wrappedIndex >= loop.xfOutStart; int partitionType = xfading ? kPartitionLoopXfade : kPartitionNormal; // if looping or entering a different type, start a new partition bool start = i == 0 || wrappedIndex < oldIndex || partitionType != oldPartitionType; @@ -712,7 +698,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // compute crossfade positions for (unsigned i = 0; i < ptSize; ++i) { float pos = ptIndices[i] + ptCoeffs[i]; - xfCurvePos[i] = (pos - loopXfOutStart) / loopXfadeSize; + xfCurvePos[i] = (pos - loop.xfOutStart) / loop.xfSize; } //----------------------------------------------------------------// @@ -752,7 +738,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // compute indices of the crossfade input segment absl::Span xfInIndices = xfadeIndexTemp[0]->first(ptSize); absl::c_copy(ptIndices, xfInIndices.begin()); - subtract1(loopXfOutStart - loopXfInStart, xfInIndices); + subtract1(loop.xfOutStart - loop.xfInStart, xfInIndices); // disregard the segment whose indices have been pushed // into the negatives, take these virtually as zeroes. @@ -1067,6 +1053,8 @@ void sfz::Voice::reset() noexcept floatPositionOffset = 0.0f; noteIsOff = false; + resetLoopInformation(); + powerFollower.clear(); for (auto& filter : filters) @@ -1078,6 +1066,37 @@ void sfz::Voice::reset() noexcept removeVoiceFromRing(); } +void sfz::Voice::resetLoopInformation() noexcept +{ + loop.start = 0; + loop.end = 0; + loop.size = 0; + loop.xfSize = 0; + loop.xfOutStart = 0; + loop.xfInStart = 0; +} + +void sfz::Voice::updateLoopInformation() noexcept +{ + if (!region || !currentPromise) + return; + + if (!region->shouldLoop()) + return; + + const auto factor = currentPromise->oversamplingFactor; + const auto rate = currentPromise->sampleRate; + + loop.end = static_cast(region->loopEnd(factor)); + loop.start = static_cast(region->loopStart(factor)); + loop.size = loop.end + 1 - loop.start; + loop.xfSize = static_cast( + lroundPositive(region->loopCrossfade * static_cast(factor) * rate) + ); + loop.xfOutStart = loop.end + 1 - loop.xfSize; + loop.xfInStart = loop.start - loop.xfSize; +} + void sfz::Voice::setNextSisterVoice(Voice* voice) noexcept { // Should never be null diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 95dbd9ec..1c1d2d86 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -496,6 +496,25 @@ private: int sourcePosition { 0 }; int initialDelay { 0 }; int age { 0 }; + struct { + int start { 0 }; + int end { 0 }; + int size { 0 }; + int xfSize { 0 }; + int xfOutStart { 0 }; + int xfInStart { 0 }; + } loop; + /** + * @brief Reset the loop information + * + */ + void resetLoopInformation() noexcept; + /** + * @brief Read the loop information data from the region. + * This requires that the region and promise is properly set. + * + */ + void updateLoopInformation() noexcept; FilePromisePtr currentPromise { nullptr }; From cab43b6641462fa24da86e3be9e095670972a8a0 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Mon, 5 Oct 2020 14:33:57 +0200 Subject: [PATCH 11/13] Move the crossfade spans closer to their actual use --- src/sfizz/Voice.cpp | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index c1b05e8a..66c7bb1e 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -561,20 +561,6 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // calculate loop characteristics const bool isLooping = region->shouldLoop() && (static_cast(loop.end) < source.getNumFrames()); - SpanHolder> xfadeTemp[2]; - SpanHolder> xfadeIndexTemp[1]; - if (isLooping) { - for (auto& buf : xfadeTemp) { - buf = resources.bufferPool.getBuffer(numSamples); - if (!buf) - return; - } - for (auto& buf : xfadeIndexTemp) { - buf = resources.bufferPool.getIndexBuffer(numSamples); - if (!buf) - return; - } - } /* loop start loop end @@ -693,7 +679,13 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept source, ptBuffer, ptIndices, ptCoeffs, {}, quality); if (ptType == kPartitionLoopXfade) { - absl::Span xfCurvePos = xfadeTemp[0]->first(ptSize); + auto xfTemp1 = resources.bufferPool.getBuffer(numSamples); + auto xfTemp2 = resources.bufferPool.getBuffer(numSamples); + auto xfIndicesTemp = resources.bufferPool.getIndexBuffer(numSamples); + if (!xfTemp1 || !xfTemp2 || !xfIndicesTemp) + return; + + absl::Span xfCurvePos = xfTemp1->first(ptSize); // compute crossfade positions for (unsigned i = 0; i < ptSize; ++i) { @@ -706,7 +698,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // -> fade out signal nearing the loop end { // compute out curve - absl::Span xfCurve = xfadeTemp[1]->first(ptSize); + absl::Span xfCurve = xfTemp2->first(ptSize); IF_CONSTEXPR (loopXfadeUseCurves == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < ptSize; ++i) @@ -736,7 +728,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // -> fade in signal preceding the loop start { // compute indices of the crossfade input segment - absl::Span xfInIndices = xfadeIndexTemp[0]->first(ptSize); + absl::Span xfInIndices = xfIndicesTemp->first(ptSize); absl::c_copy(ptIndices, xfInIndices.begin()); subtract1(loop.xfOutStart - loop.xfInStart, xfInIndices); @@ -756,7 +748,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept AudioSpan xfInBuffer = ptBuffer.subspan(applyOffset); // compute in curve - absl::Span xfCurve = xfadeTemp[1]->first(applySize); + absl::Span xfCurve = xfTemp2->first(applySize); IF_CONSTEXPR (loopXfadeUseCurves == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < applySize; ++i) From 24624abef15708d9cd962848fdcdb32bb07c112f Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Tue, 6 Oct 2020 09:21:49 +0200 Subject: [PATCH 12/13] Move the crossfade curve config to the config --- src/sfizz/Config.h | 4 ++++ src/sfizz/Voice.cpp | 20 +++++++------------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 3c724dad..023e7fed 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -126,6 +126,10 @@ namespace config { @brief Ratio to target under which smoothing is considered as completed */ static constexpr float smoothingShortcutThreshold = 5e-3; + // loop crossfade settings + static constexpr int loopXfadeCurve = 2; // 0: linear + // 1: use curves 5 & 6 + // 2: use S-shaped curve } // namespace config } // namespace sfz diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 66c7bb1e..680f819e 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -588,8 +588,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept partitionStarts = absl::MakeSpan(const_cast(starts), 1); partitionTypes = absl::MakeSpan(const_cast(types), 1); numPartitions = 1; - } - else { + } else { for (auto& buf : partitionBuffers) { buf = resources.bufferPool.getIndexBuffer(numSamples); if (!buf) @@ -601,11 +600,6 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // computed along with index processing below } - // loop crossfade settings - constexpr int loopXfadeUseCurves = 2; // 0: linear - // 1: use curves 5 & 6 - // 2: use S-shaped curve - // index preprocessing for loops if (isLooping) { int oldIndex {}; @@ -699,17 +693,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept { // compute out curve absl::Span xfCurve = xfTemp2->first(ptSize); - IF_CONSTEXPR (loopXfadeUseCurves == 2) { + IF_CONSTEXPR (config::loopXfadeCurve == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < ptSize; ++i) xfCurve[i] = xfIn.evalNormalized(1.0f - xfCurvePos[i]); } - else IF_CONSTEXPR (loopXfadeUseCurves == 1) { + else IF_CONSTEXPR (config::loopXfadeCurve == 1) { const Curve& xfOut = resources.curves.getCurve(6); for (unsigned i = 0; i < ptSize; ++i) xfCurve[i] = xfOut.evalNormalized(xfCurvePos[i]); } - else IF_CONSTEXPR (loopXfadeUseCurves == 0) { + else IF_CONSTEXPR (config::loopXfadeCurve == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < ptSize; ++i) xfCurve[i] = clamp(1.0f - xfCurvePos[i], 0.0f, 1.0f); @@ -749,17 +743,17 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept // compute in curve absl::Span xfCurve = xfTemp2->first(applySize); - IF_CONSTEXPR (loopXfadeUseCurves == 2) { + IF_CONSTEXPR (config::loopXfadeCurve == 2) { const Curve& xfIn = getSCurve(); for (unsigned i = 0; i < applySize; ++i) xfCurve[i] = xfIn.evalNormalized(xfInCurvePos[i]); } - else IF_CONSTEXPR (loopXfadeUseCurves == 1) { + else IF_CONSTEXPR (config::loopXfadeCurve == 1) { const Curve& xfIn = resources.curves.getCurve(5); for (unsigned i = 0; i < applySize; ++i) xfCurve[i] = xfIn.evalNormalized(xfInCurvePos[i]); } - else IF_CONSTEXPR (loopXfadeUseCurves == 0) { + else IF_CONSTEXPR (config::loopXfadeCurve == 0) { // TODO(jpc) vectorize this for (unsigned i = 0; i < applySize; ++i) xfCurve[i] = clamp(xfInCurvePos[i], 0.0f, 1.0f); From 809edfa76cee428a29a59f6683c271d402ad7af0 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 6 Oct 2020 21:06:03 +0200 Subject: [PATCH 13/13] Make local copy of the loop characteristics --- src/sfizz/Voice.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 680f819e..1ba67db3 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -559,6 +559,7 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept } // calculate loop characteristics + const auto loop = this->loop; const bool isLooping = region->shouldLoop() && (static_cast(loop.end) < source.getNumFrames());