diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index e99c83b3..477c3ce3 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 bool loadInRam { false }; constexpr int loggerQueueSize { 256 }; @@ -127,6 +127,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/Defaults.h b/src/sfizz/Defaults.h index 71473749..24fc06c3 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 15e183cf..be7959ef 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 62692437..8cf68067 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/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index def9cde2..7dd2f948 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() @@ -96,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); sourcePosition = region->getOffset(currentPromise->oversamplingFactor); } @@ -548,35 +552,105 @@ 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 + const auto loop = this->loop; + const bool isLooping = region->shouldLoop() + && (static_cast(loop.end) < source.getNumFrames()); + + /* + 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 <= loop.end) ? index : + (loop.start + (index - loop.start) % loop.size); + (*indices)[i] = wrappedIndex; + + // identify the partition this index is in + 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; + 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 @@ -602,25 +676,115 @@ 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) { + 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) { + float pos = ptIndices[i] + ptCoeffs[i]; + xfCurvePos[i] = (pos - loop.xfOutStart) / loop.xfSize; + } + + //----------------------------------------------------------------// + // Crossfade Out + // -> fade out signal nearing the loop end + { + // compute out curve + absl::Span xfCurve = xfTemp2->first(ptSize); + 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 (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 (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); + } + // apply out curve + // (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 = xfIndicesTemp->first(ptSize); + absl::c_copy(ptIndices, xfInIndices.begin()); + subtract1(loop.xfOutStart - loop.xfInStart, 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 and coeffs + xfInIndices = xfInIndices.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); + + // compute in curve + absl::Span xfCurve = xfTemp2->first(applySize); + 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 (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 (config::loopXfadeCurve == 0) { + // TODO(jpc) vectorize this + for (unsigned i = 0; i < applySize; ++i) + xfCurve[i] = clamp(xfInCurvePos[i], 0.0f, 1.0f); + } + // apply in curve + fillInterpolatedWithQuality( + source, xfInBuffer, xfInIndices, xfInCoeffs, xfCurve, quality); + } + } } sourcePosition = indices->back(); @@ -634,31 +798,94 @@ 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; + } +} + +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); @@ -834,6 +1061,8 @@ void sfz::Voice::reset() noexcept floatPositionOffset = 0.0f; noteIsOff = false; + resetLoopInformation(); + powerFollower.clear(); for (auto& filter : filters) @@ -845,6 +1074,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 ffbdea4b..dbcae7d7 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -385,10 +385,32 @@ 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 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 @@ -485,6 +507,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 }; diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index bf914d36..8bd77f62 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);