Implement loop xfade
This commit is contained in:
parent
f6d05509cb
commit
8a2e17ec92
3 changed files with 277 additions and 45 deletions
|
|
@ -31,7 +31,7 @@ namespace config {
|
||||||
constexpr int maxBlockSize { 8192 };
|
constexpr int maxBlockSize { 8192 };
|
||||||
constexpr int bufferPoolSize { 6 };
|
constexpr int bufferPoolSize { 6 };
|
||||||
constexpr int stereoBufferPoolSize { 4 };
|
constexpr int stereoBufferPoolSize { 4 };
|
||||||
constexpr int indexBufferPoolSize { 2 };
|
constexpr int indexBufferPoolSize { 4 };
|
||||||
constexpr int preloadSize { 8192 };
|
constexpr int preloadSize { 8192 };
|
||||||
constexpr int loggerQueueSize { 256 };
|
constexpr int loggerQueueSize { 256 };
|
||||||
constexpr int voiceLoggerQueueSize { 256 };
|
constexpr int voiceLoggerQueueSize { 256 };
|
||||||
|
|
|
||||||
|
|
@ -533,35 +533,133 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
|
||||||
|
|
||||||
auto source = currentPromise->getData();
|
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 coeffs = resources.bufferPool.getBuffer(numSamples);
|
||||||
auto indices = resources.bufferPool.getIndexBuffer(numSamples);
|
auto indices = resources.bufferPool.getIndexBuffer(numSamples);
|
||||||
if (!jumps || !indices || !coeffs)
|
if (!indices || !coeffs)
|
||||||
return;
|
return;
|
||||||
|
{
|
||||||
|
auto jumps = resources.bufferPool.getBuffer(numSamples);
|
||||||
|
if (!jumps)
|
||||||
|
return;
|
||||||
|
|
||||||
fill(*jumps, pitchRatio * speedRatio);
|
fill(*jumps, pitchRatio * speedRatio);
|
||||||
pitchEnvelope(*jumps);
|
pitchEnvelope(*jumps);
|
||||||
|
|
||||||
jumps->front() += floatPositionOffset;
|
jumps->front() += floatPositionOffset;
|
||||||
cumsum<float>(*jumps, *jumps);
|
cumsum<float>(*jumps, *jumps);
|
||||||
sfzInterpolationCast<float>(*jumps, *indices, *coeffs);
|
sfzInterpolationCast<float>(*jumps, *indices, *coeffs);
|
||||||
add1<int>(sourcePosition, *indices);
|
add1<int>(sourcePosition, *indices);
|
||||||
|
}
|
||||||
|
|
||||||
if (region->shouldLoop() && region->loopEnd(currentPromise->oversamplingFactor) <= source.getNumFrames()) {
|
// calculate loop characteristics
|
||||||
const auto loopEnd = static_cast<int>(region->loopEnd(currentPromise->oversamplingFactor));
|
bool isLooping = false;
|
||||||
const auto loopStart = static_cast<int>(region->loopStart(currentPromise->oversamplingFactor));
|
int loopStart = 0;
|
||||||
const auto loopSize = loopEnd + 1 - loopStart;
|
int loopEnd = 0;
|
||||||
for (auto* it = indices->begin(), *end = indices->end(); it < end; ++it) {
|
int loopSize = 0;
|
||||||
auto index = *it;
|
int loopXfadeSize = 0;
|
||||||
*it = (index < loopEnd + 1) ? index :
|
int loopXfOutStart = 0;
|
||||||
(loopStart + (index - loopStart) % loopSize);
|
int loopXfInStart = 0; // Note: beware in case of negative index
|
||||||
|
SpanHolder<absl::Span<float>> xfadeTemp[2];
|
||||||
|
SpanHolder<absl::Span<int>> xfadeIndexTemp[1];
|
||||||
|
if (region->shouldLoop()) {
|
||||||
|
loopEnd = region->loopEnd(currentPromise->oversamplingFactor);
|
||||||
|
isLooping = static_cast<size_t>(loopEnd) < source.getNumFrames();
|
||||||
|
}
|
||||||
|
if (isLooping) {
|
||||||
|
loopStart = static_cast<int>(region->loopStart(currentPromise->oversamplingFactor));
|
||||||
|
loopSize = loopEnd + 1 - loopStart;
|
||||||
|
loopXfadeSize = static_cast<int>(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<int> partitionStarts;
|
||||||
|
absl::Span<int> partitionTypes;
|
||||||
|
unsigned numPartitions = 0;
|
||||||
|
enum PartitionType { kPartitionNormal, kPartitionLoopXfade };
|
||||||
|
|
||||||
|
SpanHolder<absl::Span<int>> partitionBuffers[2];
|
||||||
|
if (!isLooping) {
|
||||||
|
static const int starts[1] = { 0 };
|
||||||
|
static const int types[1] = { kPartitionNormal };
|
||||||
|
partitionStarts = absl::MakeSpan(const_cast<int*>(starts), 1);
|
||||||
|
partitionTypes = absl::MakeSpan(const_cast<int*>(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(
|
const auto sampleEnd = min(
|
||||||
static_cast<int>(region->trueSampleEnd(currentPromise->oversamplingFactor)),
|
static_cast<int>(region->trueSampleEnd(currentPromise->oversamplingFactor)),
|
||||||
static_cast<int>(source.getNumFrames())
|
static_cast<int>(source.getNumFrames())
|
||||||
) - 1;
|
) - 1;
|
||||||
for (unsigned i = 0; i < indices->size(); ++i) {
|
for (unsigned i = 0; i < numSamples; ++i) {
|
||||||
if ((*indices)[i] >= sampleEnd) {
|
if ((*indices)[i] >= sampleEnd) {
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
// Check for underflow
|
// Check for underflow
|
||||||
|
|
@ -581,25 +679,93 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// interpolation processing
|
||||||
const int quality = getCurrentSampleQuality();
|
const int quality = getCurrentSampleQuality();
|
||||||
|
|
||||||
switch (quality) {
|
for (unsigned ptNo = 0; ptNo < numPartitions; ++ptNo) {
|
||||||
default:
|
// current partition
|
||||||
if (quality > 2)
|
const int ptType = partitionTypes[ptNo];
|
||||||
goto high; // TODO sinc, not implemented
|
const unsigned ptStart = partitionStarts[ptNo];
|
||||||
// fall through
|
const unsigned ptNextStart = (ptNo + 1 < numPartitions) ? partitionStarts[ptNo + 1] : numSamples;
|
||||||
case 1:
|
const unsigned ptSize = ptNextStart - ptStart;
|
||||||
fillInterpolated<kInterpolatorLinear>(source, buffer, *indices, *coeffs);
|
|
||||||
break;
|
// partition spans
|
||||||
case 2: high:
|
AudioSpan<float> ptBuffer = buffer.subspan(ptStart, ptSize);
|
||||||
#if 1
|
absl::Span<const int> ptIndices = indices->subspan(ptStart, ptSize);
|
||||||
// B-spline response has faster decay of aliasing, but not zero-crossings at integer positions
|
absl::Span<const float> ptCoeffs = coeffs->subspan(ptStart, ptSize);
|
||||||
fillInterpolated<kInterpolatorBspline3>(source, buffer, *indices, *coeffs);
|
|
||||||
#else
|
fillInterpolatedWithQuality<false>(
|
||||||
// Hermite polynomial
|
source, ptBuffer, ptIndices, ptCoeffs, {}, quality);
|
||||||
fillInterpolated<kInterpolatorHermite3>(source, buffer, *indices, *coeffs);
|
|
||||||
#endif
|
if (ptType == kPartitionLoopXfade) {
|
||||||
break;
|
absl::Span<float> 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<float> 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<float> 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<int> 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<float> xfInCoeff = xfCoeff.subspan(applyOffset);
|
||||||
|
// offset the output buffer
|
||||||
|
AudioSpan<float> xfInBuffer = ptBuffer.subspan(applyOffset);
|
||||||
|
|
||||||
|
// compute in curve
|
||||||
|
const Curve& xfIn = resources.curves.getCurve(5);
|
||||||
|
absl::Span<float> xfCurve = xfadeTemp[1]->first(applySize);
|
||||||
|
for (unsigned i = 0; i < applySize; ++i)
|
||||||
|
xfCurve[i] = xfIn.evalNormalized(xfInCoeff[i]);
|
||||||
|
// apply in curve
|
||||||
|
fillInterpolatedWithQuality<true>(
|
||||||
|
source, xfInBuffer, xfInIndices, *coeffs, xfCurve, quality);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourcePosition = indices->back();
|
sourcePosition = indices->back();
|
||||||
|
|
@ -613,31 +779,80 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
template <sfz::InterpolatorModel M>
|
template <sfz::InterpolatorModel M, bool Adding>
|
||||||
void sfz::Voice::fillInterpolated(
|
void sfz::Voice::fillInterpolated(
|
||||||
const sfz::AudioSpan<const float>& source, const sfz::AudioSpan<float>& dest,
|
const sfz::AudioSpan<const float>& source, const sfz::AudioSpan<float>& dest,
|
||||||
absl::Span<const int> indices, absl::Span<const float> coeffs)
|
absl::Span<const int> indices, absl::Span<const float> coeffs,
|
||||||
|
absl::Span<const float> addingGains)
|
||||||
{
|
{
|
||||||
auto ind = indices.data();
|
auto* ind = indices.data();
|
||||||
auto coeff = coeffs.data();
|
auto* coeff = coeffs.data();
|
||||||
|
auto* addingGain = addingGains.data();
|
||||||
auto leftSource = source.getConstSpan(0);
|
auto leftSource = source.getConstSpan(0);
|
||||||
auto left = dest.getChannel(0);
|
auto left = dest.getChannel(0);
|
||||||
if (source.getNumChannels() == 1) {
|
if (source.getNumChannels() == 1) {
|
||||||
while (ind < indices.end()) {
|
while (ind < indices.end()) {
|
||||||
*left = sfz::interpolate<M>(&leftSource[*ind], *coeff);
|
auto output = sfz::interpolate<M>(&leftSource[*ind], *coeff);
|
||||||
|
IF_CONSTEXPR(Adding) {
|
||||||
|
float g = *addingGain++;
|
||||||
|
*left += g * output;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
*left = output;
|
||||||
incrementAll(ind, left, coeff);
|
incrementAll(ind, left, coeff);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
auto right = dest.getChannel(1);
|
auto right = dest.getChannel(1);
|
||||||
auto rightSource = source.getConstSpan(1);
|
auto rightSource = source.getConstSpan(1);
|
||||||
while (ind < indices.end()) {
|
while (ind < indices.end()) {
|
||||||
*left = sfz::interpolate<M>(&leftSource[*ind], *coeff);
|
auto leftOutput = sfz::interpolate<M>(&leftSource[*ind], *coeff);
|
||||||
*right = sfz::interpolate<M>(&rightSource[*ind], *coeff);
|
auto rightOutput = sfz::interpolate<M>(&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);
|
incrementAll(ind, left, right, coeff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <bool Adding>
|
||||||
|
void sfz::Voice::fillInterpolatedWithQuality(
|
||||||
|
const sfz::AudioSpan<const float>& source, const sfz::AudioSpan<float>& dest,
|
||||||
|
absl::Span<const int> indices, absl::Span<const float> coeffs,
|
||||||
|
absl::Span<const float> addingGains, int quality)
|
||||||
|
{
|
||||||
|
switch (quality) {
|
||||||
|
default:
|
||||||
|
if (quality > 2)
|
||||||
|
goto high; // TODO sinc, not implemented
|
||||||
|
// fall through
|
||||||
|
case 1:
|
||||||
|
{
|
||||||
|
constexpr auto itp = kInterpolatorLinear;
|
||||||
|
fillInterpolated<itp, Adding>(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<itp, Adding>(source, dest, indices, coeffs, addingGains);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void sfz::Voice::fillWithGenerator(AudioSpan<float> buffer) noexcept
|
void sfz::Voice::fillWithGenerator(AudioSpan<float> buffer) noexcept
|
||||||
{
|
{
|
||||||
const auto leftSpan = buffer.getSpan(0);
|
const auto leftSpan = buffer.getSpan(0);
|
||||||
|
|
|
||||||
|
|
@ -374,10 +374,27 @@ private:
|
||||||
* @param indices the integral parts of the source positions
|
* @param indices the integral parts of the source positions
|
||||||
* @param coeffs the fractional parts of the source positions
|
* @param coeffs the fractional parts of the source positions
|
||||||
*/
|
*/
|
||||||
template <InterpolatorModel M>
|
template <InterpolatorModel M, bool Adding>
|
||||||
static void fillInterpolated(
|
static void fillInterpolated(
|
||||||
const AudioSpan<const float>& source, const AudioSpan<float>& dest,
|
const AudioSpan<const float>& source, const AudioSpan<float>& dest,
|
||||||
absl::Span<const int> indices, absl::Span<const float> coeffs);
|
absl::Span<const int> indices, absl::Span<const float> coeffs,
|
||||||
|
absl::Span<const float> 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 <bool Adding>
|
||||||
|
static void fillInterpolatedWithQuality(
|
||||||
|
const AudioSpan<const float>& source, const AudioSpan<float>& dest,
|
||||||
|
absl::Span<const int> indices, absl::Span<const float> coeffs,
|
||||||
|
absl::Span<const float> addingGains, int quality);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Compute the amplitude envelope, applied as a gain to a mono
|
* @brief Compute the amplitude envelope, applied as a gain to a mono
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue