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