commit
4e13493ab4
7 changed files with 364 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 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
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ namespace Default
|
|||
constexpr Range<uint32_t> sampleCountRange { 0, std::numeric_limits<uint32_t>::max() };
|
||||
constexpr SfzLoopMode loopMode { SfzLoopMode::no_loop };
|
||||
constexpr Range<uint32_t> loopRange { 0, std::numeric_limits<uint32_t>::max() };
|
||||
constexpr float loopCrossfade { 1e-3 };
|
||||
constexpr Range<float> loopCrossfadeRange { loopCrossfade, 1.0 };
|
||||
|
||||
// common defaults
|
||||
constexpr Range<uint8_t> midi7Range { 0, 127 };
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -319,6 +319,7 @@ struct Region {
|
|||
absl::optional<uint32_t> sampleCount {}; // count
|
||||
absl::optional<SfzLoopMode> loopMode {}; // loopmode
|
||||
Range<uint32_t> loopRange { Default::loopRange }; //loopstart and loopend
|
||||
float loopCrossfade { Default::loopCrossfade }; // loop_crossfade
|
||||
|
||||
// Wavetable oscillator
|
||||
float oscillatorPhase { Default::oscillatorPhase };
|
||||
|
|
|
|||
|
|
@ -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<float>(currentPromise->sampleRate / this->sampleRate);
|
||||
sourcePosition = region->getOffset(currentPromise->oversamplingFactor);
|
||||
}
|
||||
|
|
@ -548,35 +552,105 @@ 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
|
||||
const auto loop = this->loop;
|
||||
const bool isLooping = region->shouldLoop()
|
||||
&& (static_cast<size_t>(loop.end) < source.getNumFrames());
|
||||
|
||||
/*
|
||||
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 <= 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<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
|
||||
|
|
@ -602,25 +676,115 @@ 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) {
|
||||
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<float> 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<float> 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<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 = 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<const float> xfInCoeffs = ptCoeffs.subspan(applyOffset);
|
||||
// offset the curve positions
|
||||
absl::Span<float> xfInCurvePos = xfCurvePos.subspan(applyOffset);
|
||||
// offset the output buffer
|
||||
AudioSpan<float> xfInBuffer = ptBuffer.subspan(applyOffset);
|
||||
|
||||
// compute in curve
|
||||
absl::Span<float> 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<true>(
|
||||
source, xfInBuffer, xfInIndices, xfInCoeffs, xfCurve, quality);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourcePosition = indices->back();
|
||||
|
|
@ -634,31 +798,94 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
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<double>(N - 1);
|
||||
values[i] = (1.0 - std::cos(M_PI * x)) * 0.5;
|
||||
}
|
||||
return Curve::buildFromPoints(values);
|
||||
}();
|
||||
return curve;
|
||||
}
|
||||
|
||||
void sfz::Voice::fillWithGenerator(AudioSpan<float> 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<int>(region->loopEnd(factor));
|
||||
loop.start = static_cast<int>(region->loopStart(factor));
|
||||
loop.size = loop.end + 1 - loop.start;
|
||||
loop.xfSize = static_cast<int>(
|
||||
lroundPositive(region->loopCrossfade * static_cast<int>(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
|
||||
|
|
|
|||
|
|
@ -385,10 +385,32 @@ 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 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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -174,6 +174,14 @@ TEST_CASE("[Region] Parsing opcodes")
|
|||
REQUIRE(region.loopRange == Range<uint32_t>(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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue