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 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 };
|
||||
|
|
|
|||
|
|
@ -533,35 +533,133 @@ void sfz::Voice::fillWithData(AudioSpan<float> 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<float>(*jumps, *jumps);
|
||||
sfzInterpolationCast<float>(*jumps, *indices, *coeffs);
|
||||
add1<int>(sourcePosition, *indices);
|
||||
jumps->front() += floatPositionOffset;
|
||||
cumsum<float>(*jumps, *jumps);
|
||||
sfzInterpolationCast<float>(*jumps, *indices, *coeffs);
|
||||
add1<int>(sourcePosition, *indices);
|
||||
}
|
||||
|
||||
if (region->shouldLoop() && region->loopEnd(currentPromise->oversamplingFactor) <= source.getNumFrames()) {
|
||||
const auto loopEnd = static_cast<int>(region->loopEnd(currentPromise->oversamplingFactor));
|
||||
const auto loopStart = static_cast<int>(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<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(
|
||||
static_cast<int>(region->trueSampleEnd(currentPromise->oversamplingFactor)),
|
||||
static_cast<int>(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<float> 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<kInterpolatorLinear>(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<kInterpolatorBspline3>(source, buffer, *indices, *coeffs);
|
||||
#else
|
||||
// Hermite polynomial
|
||||
fillInterpolated<kInterpolatorHermite3>(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<float> ptBuffer = buffer.subspan(ptStart, ptSize);
|
||||
absl::Span<const int> ptIndices = indices->subspan(ptStart, ptSize);
|
||||
absl::Span<const float> ptCoeffs = coeffs->subspan(ptStart, ptSize);
|
||||
|
||||
fillInterpolatedWithQuality<false>(
|
||||
source, ptBuffer, ptIndices, ptCoeffs, {}, quality);
|
||||
|
||||
if (ptType == kPartitionLoopXfade) {
|
||||
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();
|
||||
|
|
@ -613,31 +779,80 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
|
|||
#endif
|
||||
}
|
||||
|
||||
template <sfz::InterpolatorModel M>
|
||||
template <sfz::InterpolatorModel M, bool Adding>
|
||||
void sfz::Voice::fillInterpolated(
|
||||
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 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<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);
|
||||
}
|
||||
} else {
|
||||
auto right = dest.getChannel(1);
|
||||
auto rightSource = source.getConstSpan(1);
|
||||
while (ind < indices.end()) {
|
||||
*left = sfz::interpolate<M>(&leftSource[*ind], *coeff);
|
||||
*right = sfz::interpolate<M>(&rightSource[*ind], *coeff);
|
||||
auto leftOutput = sfz::interpolate<M>(&leftSource[*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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
const auto leftSpan = buffer.getSpan(0);
|
||||
|
|
|
|||
|
|
@ -374,10 +374,27 @@ private:
|
|||
* @param indices the integral 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(
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue