Move the stateful region value computations in a separate file

This commit is contained in:
Paul Fd 2021-07-04 19:32:17 +02:00
parent 5527282159
commit 66ec8c4423
10 changed files with 476 additions and 379 deletions

View file

@ -107,6 +107,7 @@ SFIZZ_SOURCES = \
src/sfizz/PowerFollower.cpp \
src/sfizz/Region.cpp \
src/sfizz/RegionSet.cpp \
src/sfizz/RegionStateful.cpp \
src/sfizz/Resources.cpp \
src/sfizz/RTSemaphore.cpp \
src/sfizz/ScopedFTZ.cpp \

View file

@ -14,6 +14,7 @@ clang-tidy \
src/sfizz/Panning.cpp \
src/sfizz/sfizz.cpp \
src/sfizz/Region.cpp \
src/sfizz/RegionStateful.cpp \
src/sfizz/SIMDHelpers.cpp \
src/sfizz/simd/HelpersSSE.cpp \
src/sfizz/simd/HelpersAVX.cpp \

View file

@ -96,6 +96,7 @@ set(SFIZZ_HEADERS
sfizz/railsback/4-1.h
sfizz/railsback/4-2.h
sfizz/Region.h
sfizz/RegionStateful.h
sfizz/RegionSet.h
sfizz/Resources.h
sfizz/RTSemaphore.h
@ -131,6 +132,7 @@ set(SFIZZ_SOURCES
sfizz/AudioReader.cpp
sfizz/FilterPool.cpp
sfizz/EQPool.cpp
sfizz/RegionStateful.cpp
sfizz/Region.cpp
sfizz/Voice.cpp
sfizz/ScopedFTZ.cpp

View file

@ -6,13 +6,11 @@
#include "Region.h"
#include "Opcode.h"
#include "MidiState.h"
#include "MathHelpers.h"
#include "utility/SwapAndPop.h"
#include "utility/StringViewHelpers.h"
#include "utility/Macros.h"
#include "utility/Debug.h"
#include "ModifierHelpers.h"
#include "modulations/ModId.h"
#include "absl/strings/str_replace.h"
#include "absl/strings/str_cat.h"
@ -1739,18 +1737,6 @@ float sfz::Region::getBasePitchVariation(float noteNumber, float velocity) const
return centsFactor(pitchVariationInCents);
}
float sfz::Region::getBaseVolumedB(const MidiState& midiState, int noteNumber) const noexcept
{
fast_real_distribution<float> volumeDistribution { 0.0f, ampRandom };
auto baseVolumedB = volume + volumeDistribution(Random::randomGenerator);
baseVolumedB += globalVolume;
baseVolumedB += masterVolume;
baseVolumedB += groupVolume;
if (trigger == Trigger::release || trigger == Trigger::release_key)
baseVolumedB -= rtDecay * midiState.getNoteDuration(noteNumber);
return baseVolumedB;
}
float sfz::Region::getBaseGain() const noexcept
{
float baseGain = amplitude;
@ -1774,115 +1760,6 @@ float sfz::Region::getPhase() const noexcept
return phase;
}
uint64_t sfz::Region::getOffset(const MidiState& midiState) const noexcept
{
std::uniform_int_distribution<int64_t> offsetDistribution { 0, offsetRandom };
uint64_t finalOffset = offset + offsetDistribution(Random::randomGenerator);
for (const auto& mod: offsetCC)
finalOffset += static_cast<uint64_t>(mod.data * midiState.getCCValue(mod.cc));
return Default::offset.bounds.clamp(finalOffset);
}
float sfz::Region::getDelay(const MidiState& midiState) const noexcept
{
fast_real_distribution<float> delayDistribution { 0, delayRandom };
float finalDelay { delay };
finalDelay += delayDistribution(Random::randomGenerator);
for (const auto& mod: delayCC)
finalDelay += mod.data * midiState.getCCValue(mod.cc);
return Default::delay.bounds.clamp(finalDelay);
}
uint32_t sfz::Region::getSampleEnd(MidiState& midiState) const noexcept
{
int64_t end = sampleEnd;
for (const auto& mod: endCC)
end += static_cast<int64_t>(mod.data * midiState.getCCValue(mod.cc));
end = clamp(end, int64_t { 0 }, sampleEnd);
return static_cast<uint32_t>(end);
}
uint32_t sfz::Region::loopStart(MidiState& midiState) const noexcept
{
auto start = loopRange.getStart();
for (const auto& mod: loopStartCC)
start += static_cast<int64_t>(mod.data * midiState.getCCValue(mod.cc));
start = clamp(start, int64_t { 0 }, sampleEnd);
return static_cast<uint32_t>(start);
}
uint32_t sfz::Region::loopEnd(MidiState& midiState) const noexcept
{
auto end = loopRange.getEnd();
for (const auto& mod: loopEndCC)
end += static_cast<int64_t>(mod.data * midiState.getCCValue(mod.cc));
end = clamp(end, int64_t { 0 }, sampleEnd);
return static_cast<uint32_t>(end);
}
float sfz::Region::getNoteGain(int noteNumber, float velocity) const noexcept
{
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
float baseGain { 1.0f };
// Amplitude key tracking
baseGain *= db2mag(ampKeytrack * static_cast<float>(noteNumber - ampKeycenter));
// Crossfades related to the note number
baseGain *= crossfadeIn(crossfadeKeyInRange, noteNumber, crossfadeKeyCurve);
baseGain *= crossfadeOut(crossfadeKeyOutRange, noteNumber, crossfadeKeyCurve);
// Amplitude velocity tracking
baseGain *= velocityCurve(velocity);
// Crossfades related to velocity
baseGain *= crossfadeIn(crossfadeVelInRange, velocity, crossfadeVelCurve);
baseGain *= crossfadeOut(crossfadeVelOutRange, velocity, crossfadeVelCurve);
return baseGain;
}
float sfz::Region::getCrossfadeGain(const MidiState& midiState) const noexcept
{
float gain { 1.0f };
// Crossfades due to CC states
for (const auto& ccData : crossfadeCCInRange) {
const auto ccValue = midiState.getCCValue(ccData.cc);
const auto crossfadeRange = ccData.data;
gain *= crossfadeIn(crossfadeRange, ccValue, crossfadeCCCurve);
}
for (const auto& ccData : crossfadeCCOutRange) {
const auto ccValue = midiState.getCCValue(ccData.cc);
const auto crossfadeRange = ccData.data;
gain *= crossfadeOut(crossfadeRange, ccValue, crossfadeCCCurve);
}
return gain;
}
float sfz::Region::velocityCurve(float velocity) const noexcept
{
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
float gain;
if (velCurve)
gain = velCurve->evalNormalized(velocity);
else
gain = velocity * velocity;
gain = std::fabs(ampVeltrack) * (1.0f - gain);
gain = (ampVeltrack < 0) ? gain : (1.0f - gain);
return gain;
}
void sfz::Region::offsetAllKeys(int offset) noexcept
{
// Offset key range

View file

@ -108,32 +108,6 @@ struct Region {
* @return float
*/
float getBasePitchVariation(float noteNumber, float velocity) const noexcept;
/**
* @brief Get the note-related gain of the region depending on which note has been
* pressed and at which velocity.
*
* @param noteNumber
* @param velocity
* @return float
*/
float getNoteGain(int noteNumber, float velocity) const noexcept;
/**
* @brief Get the additional crossfade gain of the region depending on the
* CC values
*
* @param midiState
* @return float
*/
float getCrossfadeGain(const MidiState& midiState) const noexcept;
/**
* @brief Get the base volume of the region depending on which note has been
* pressed to trigger the region.
*
* @param midiState
* @param noteNumber
* @return float
*/
float getBaseVolumedB(const MidiState& midiState, int noteNumber) const noexcept;
/**
* @brief Get the base gain of the region.
*
@ -146,12 +120,6 @@ struct Region {
* @return float
*/
float getPhase() const noexcept;
/**
* @brief Computes the gain value related to the velocity of the note
*
* @return float
*/
float velocityCurve(float velocity) const noexcept;
/**
* @brief Get the detuning in cents for a given bend value between -1 and 1
*
@ -160,27 +128,6 @@ struct Region {
*/
float getBendInCents(float bend) const noexcept;
/**
* @brief Get the region offset in samples
*
* @param midiState
* @return uint32_t
*/
uint64_t getOffset(const MidiState& midiState) const noexcept;
/**
* @brief Get the region delay in seconds
*
* @param midiState
* @return float
*/
float getDelay(const MidiState& midiState) const noexcept;
/**
* @brief Get the index of the sample end, either natural end or forced
* loop.
*
* @return uint32_t
*/
uint32_t getSampleEnd(MidiState& midiState) const noexcept;
/**
* @brief Parse a new opcode into the region to fill in the proper parameters.
* This must be called multiple times for each opcode applying to this region.
@ -260,9 +207,6 @@ struct Region {
void offsetAllKeys(int offset) noexcept;
uint32_t loopStart(MidiState& midiState) const noexcept;
uint32_t loopEnd(MidiState& midiState) const noexcept;
/**
* @brief Get the gain this region contributes into the input of the Nth
* effect bus

View file

@ -0,0 +1,134 @@
// SPDX-License-Identifier: BSD-2-Clause
// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#include "RegionStateful.h"
#include "ModifierHelpers.h"
namespace sfz {
float getBaseVolumedB(const Region& region, const MidiState& midiState, int noteNumber) noexcept
{
fast_real_distribution<float> volumeDistribution { 0.0f, region.ampRandom };
auto baseVolumedB = region.volume + volumeDistribution(Random::randomGenerator);
baseVolumedB += region.globalVolume;
baseVolumedB += region.masterVolume;
baseVolumedB += region.groupVolume;
if (region.trigger == Trigger::release || region.trigger == Trigger::release_key)
baseVolumedB -= region.rtDecay * midiState.getNoteDuration(noteNumber);
return baseVolumedB;
}
uint64_t getOffset(const Region& region, const MidiState& midiState) noexcept
{
std::uniform_int_distribution<int64_t> offsetDistribution { 0, region.offsetRandom };
uint64_t finalOffset = region.offset + offsetDistribution(Random::randomGenerator);
for (const auto& mod: region.offsetCC)
finalOffset += static_cast<uint64_t>(mod.data * midiState.getCCValue(mod.cc));
return Default::offset.bounds.clamp(finalOffset);
}
float getDelay(const Region& region, const MidiState& midiState) noexcept
{
fast_real_distribution<float> delayDistribution { 0, region.delayRandom };
float finalDelay { region.delay };
finalDelay += delayDistribution(Random::randomGenerator);
for (const auto& mod: region.delayCC)
finalDelay += mod.data * midiState.getCCValue(mod.cc);
return Default::delay.bounds.clamp(finalDelay);
}
uint32_t getSampleEnd(const Region& region, MidiState& midiState) noexcept
{
int64_t end = region.sampleEnd;
for (const auto& mod: region.endCC)
end += static_cast<int64_t>(mod.data * midiState.getCCValue(mod.cc));
end = clamp(end, int64_t { 0 }, region.sampleEnd);
return static_cast<uint32_t>(end);
}
uint32_t loopStart(const Region& region, MidiState& midiState) noexcept
{
auto start = region.loopRange.getStart();
for (const auto& mod: region.loopStartCC)
start += static_cast<int64_t>(mod.data * midiState.getCCValue(mod.cc));
start = clamp(start, int64_t { 0 }, region.sampleEnd);
return static_cast<uint32_t>(start);
}
uint32_t loopEnd(const Region& region, MidiState& midiState) noexcept
{
auto end = region.loopRange.getEnd();
for (const auto& mod: region.loopEndCC)
end += static_cast<int64_t>(mod.data * midiState.getCCValue(mod.cc));
end = clamp(end, int64_t { 0 }, region.sampleEnd);
return static_cast<uint32_t>(end);
}
float getNoteGain(const Region& region, int noteNumber, float velocity, const MidiState& midiState, const CurveSet& curveSet) noexcept
{
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
float baseGain { 1.0f };
// Amplitude key tracking
baseGain *= db2mag(region.ampKeytrack * static_cast<float>(noteNumber - region.ampKeycenter));
// Crossfades related to the note number
baseGain *= crossfadeIn(region.crossfadeKeyInRange, noteNumber, region.crossfadeKeyCurve);
baseGain *= crossfadeOut(region.crossfadeKeyOutRange, noteNumber, region.crossfadeKeyCurve);
// Amplitude velocity tracking
baseGain *= velocityCurve(region, velocity, midiState, curveSet);
// Crossfades related to velocity
baseGain *= crossfadeIn(region.crossfadeVelInRange, velocity, region.crossfadeVelCurve);
baseGain *= crossfadeOut(region.crossfadeVelOutRange, velocity, region.crossfadeVelCurve);
return baseGain;
}
float getCrossfadeGain(const Region& region, const MidiState& midiState) noexcept
{
float gain { 1.0f };
// Crossfades due to CC states
for (const auto& ccData : region.crossfadeCCInRange) {
const auto ccValue = midiState.getCCValue(ccData.cc);
const auto crossfadeRange = ccData.data;
gain *= crossfadeIn(crossfadeRange, ccValue, region.crossfadeCCCurve);
}
for (const auto& ccData : region.crossfadeCCOutRange) {
const auto ccValue = midiState.getCCValue(ccData.cc);
const auto crossfadeRange = ccData.data;
gain *= crossfadeOut(crossfadeRange, ccValue, region.crossfadeCCCurve);
}
return gain;
}
float velocityCurve(const Region& region, float velocity, const MidiState& midiState, const CurveSet& curveSet) noexcept
{
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
float gain;
if (region.velCurve)
gain = region.velCurve->evalNormalized(velocity);
else
gain = velocity * velocity;
gain = std::fabs(region.ampVeltrack) * (1.0f - gain);
gain = (region.ampVeltrack < 0) ? gain : (1.0f - gain);
return gain;
}
}

View file

@ -0,0 +1,97 @@
// SPDX-License-Identifier: BSD-2-Clause
// This code is part of the sfizz library and is licensed under a BSD 2-clause
// license. You should have receive a LICENSE.md file along with the code.
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#include "Region.h"
#include "MidiState.h"
#include "Curve.h"
namespace sfz {
/**
* @brief Get the note-related gain of the region depending on which note has been
* pressed and at which velocity.
*
* @param region
* @param noteNumber
* @param velocity
* @param midiState
* @param curveSet
* @return float
*/
float getNoteGain(const Region& region, int noteNumber, float velocity, const MidiState& midiState, const CurveSet& curveSet) noexcept;
/**
* @brief Get the additional crossfade gain of the region depending on the
* CC values
*
* @param region
* @param midiState
* @return float
*/
float getCrossfadeGain(const Region& region, const MidiState& midiState) noexcept;
/**
* @brief Get the base volume of the region depending on which note has been
* pressed to trigger the region.
*
* @param region
* @param midiState
* @param noteNumber
* @return float
*/
float getBaseVolumedB(const Region& region, const MidiState& midiState, int noteNumber) noexcept;
/**
* @brief Get the region offset in samples
*
* @param region
* @param midiState
* @return uint32_t
*/
uint64_t getOffset(const Region& region, const MidiState& midiState) noexcept;
/**
* @brief Get the region delay in seconds
*
* @param region
* @param midiState
* @return float
*/
float getDelay(const Region& region, const MidiState& midiState) noexcept;
/**
* @brief Get the index of the sample end, either natural end or forced
* loop.
*
* @param region
* @param midiState
* @return uint32_t
*/
uint32_t getSampleEnd(const Region& region, MidiState& midiState) noexcept;
/**
* @brief Computes the gain value related to the velocity of the note
*
* @return float
*/
float velocityCurve(const Region& region, float velocity, const MidiState& midiState, const CurveSet& curveSet) noexcept;
/**
* @brief Returns the start of the loop for a given region
*
* @param region
* @param midiState
* @return uint32_t
*/
uint32_t loopStart(const Region& region, MidiState& midiState) noexcept;
/**
* @brief Returns the end of the loop for a given region
*
* @param region
* @param midiState
* @return uint32_t
*/
uint32_t loopEnd(const Region& region, MidiState& midiState) noexcept;
}

View file

@ -16,6 +16,7 @@
#include "LFO.h"
#include "MathHelpers.h"
#include "ModifierHelpers.h"
#include "RegionStateful.h"
#include "TriggerEvent.h"
#include "modulations/ModId.h"
#include "modulations/ModKey.h"
@ -407,6 +408,7 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc
Resources& resources = impl.resources_;
MidiState& midiState = resources.getMidiState();
CurveSet& curveSet = resources.getCurves();
const Region& region = layer->getRegion();
impl.region_ = &region;
@ -474,7 +476,7 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc
}
impl.updateLoopInformation();
impl.speedRatio_ = static_cast<float>(impl.currentPromise_->information.sampleRate / impl.sampleRate_);
impl.sourcePosition_ = region.getOffset(midiState);
impl.sourcePosition_ = getOffset(region, midiState);
}
// do Scala retuning and reconvert the frequency into a 12TET key number
@ -488,10 +490,10 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc
impl.pitchRatio_ *= stretch->getRatioForFractionalKey(numberRetuned);
impl.pitchKeycenter_ = region.pitchKeycenter;
impl.baseVolumedB_ = region.getBaseVolumedB(midiState, impl.triggerEvent_.number);
impl.baseVolumedB_ = getBaseVolumedB(region, midiState, impl.triggerEvent_.number);
impl.baseGain_ = region.getBaseGain();
if (impl.triggerEvent_.type != TriggerEventType::CC || region.velocityOverride == VelocityOverride::previous)
impl.baseGain_ *= region.getNoteGain(impl.triggerEvent_.number, impl.triggerEvent_.value);
impl.baseGain_ *= getNoteGain(region, impl.triggerEvent_.number, impl.triggerEvent_.value, midiState, curveSet);
impl.gainSmoother_.reset();
impl.resetCrossfades();
@ -505,9 +507,9 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc
}
impl.triggerDelay_ = delay;
impl.initialDelay_ = delay + static_cast<int>(region.getDelay(midiState) * impl.sampleRate_);
impl.initialDelay_ = delay + static_cast<int>(getDelay(region, midiState) * impl.sampleRate_);
impl.baseFrequency_ = tuning.getFrequencyOfKey(impl.triggerEvent_.number);
impl.sampleEnd_ = int(region.getSampleEnd(midiState));
impl.sampleEnd_ = int(getSampleEnd(region, midiState));
impl.sampleSize_ = impl.sampleEnd_- impl.sourcePosition_ - 1;
impl.bendSmoother_.setSmoothing(region.bendSmooth, impl.sampleRate_);
impl.bendSmoother_.reset(region.getBendInCents(midiState.getPitchBend()));
@ -1724,14 +1726,15 @@ void Voice::Impl::updateLoopInformation() noexcept
if (!region_->shouldLoop())
return;
const Region& region = *region_;
MidiState& midiState = resources_.getMidiState();
const FileInformation& info = currentPromise_->information;
const double rate = info.sampleRate;
loop_.start = static_cast<int>(region_->loopStart(midiState));
loop_.end = max(static_cast<int>(region_->loopEnd(midiState)), loop_.start);
loop_.start = static_cast<int>(loopStart(region, midiState));
loop_.end = max(static_cast<int>(loopEnd(region, midiState)), loop_.start);
loop_.size = loop_.end + 1 - loop_.start;
loop_.xfSize = static_cast<int>(lroundPositive(region_->loopCrossfade * rate));
loop_.xfSize = static_cast<int>(lroundPositive(region.loopCrossfade * rate));
// Clamp the crossfade to the part available before the loop starts
loop_.xfSize = min(loop_.start, loop_.xfSize);
loop_.xfOutStart = loop_.end + 1 - loop_.xfSize;

View file

@ -6,6 +6,7 @@
#include "sfizz/Defaults.h"
#include "sfizz/Region.h"
#include "sfizz/RegionStateful.h"
#include "sfizz/MidiState.h"
#include "sfizz/SfzHelpers.h"
#include "catch2/catch.hpp"
@ -19,137 +20,155 @@ constexpr int numRandomTests { 64 };
TEST_CASE("[Region] Crossfade in on key")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "3" });
REQUIRE(region.getNoteGain(2, 127_norm) == 0.70711_a);
REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a);
REQUIRE(region.getNoteGain(3, 127_norm) == 1.0_a);
REQUIRE(getNoteGain(region, 2, 127_norm, midiState, curveSet) == 0.70711_a);
REQUIRE(getNoteGain(region, 1, 127_norm, midiState, curveSet) == 0.0_a);
REQUIRE(getNoteGain(region, 3, 127_norm, midiState, curveSet) == 1.0_a);
}
TEST_CASE("[Region] Crossfade in on key - 2")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "5" });
REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a);
REQUIRE(region.getNoteGain(2, 127_norm) == 0.5_a);
REQUIRE(region.getNoteGain(3, 127_norm) == 0.70711_a);
REQUIRE(region.getNoteGain(4, 127_norm) == 0.86603_a);
REQUIRE(region.getNoteGain(5, 127_norm) == 1.0_a);
REQUIRE(region.getNoteGain(6, 127_norm) == 1.0_a);
REQUIRE(getNoteGain(region, 1, 127_norm, midiState, curveSet) == 0.0_a);
REQUIRE(getNoteGain(region, 2, 127_norm, midiState, curveSet) == 0.5_a);
REQUIRE(getNoteGain(region, 3, 127_norm, midiState, curveSet) == 0.70711_a);
REQUIRE(getNoteGain(region, 4, 127_norm, midiState, curveSet) == 0.86603_a);
REQUIRE(getNoteGain(region, 5, 127_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 6, 127_norm, midiState, curveSet) == 1.0_a);
}
TEST_CASE("[Region] Crossfade in on key - gain")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "5" });
region.parseOpcode({ "xf_keycurve", "gain" });
REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a);
REQUIRE(region.getNoteGain(2, 127_norm) == 0.25_a);
REQUIRE(region.getNoteGain(3, 127_norm) == 0.5_a);
REQUIRE(region.getNoteGain(4, 127_norm) == 0.75_a);
REQUIRE(region.getNoteGain(5, 127_norm) == 1.0_a);
REQUIRE(getNoteGain(region, 1, 127_norm, midiState, curveSet) == 0.0_a);
REQUIRE(getNoteGain(region, 2, 127_norm, midiState, curveSet) == 0.25_a);
REQUIRE(getNoteGain(region, 3, 127_norm, midiState, curveSet) == 0.5_a);
REQUIRE(getNoteGain(region, 4, 127_norm, midiState, curveSet) == 0.75_a);
REQUIRE(getNoteGain(region, 5, 127_norm, midiState, curveSet) == 1.0_a);
}
TEST_CASE("[Region] Crossfade out on key")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lokey", "51" });
region.parseOpcode({ "xfout_hikey", "55" });
REQUIRE(region.getNoteGain(50, 127_norm) == 1.0_a);
REQUIRE(region.getNoteGain(51, 127_norm) == 1.0_a);
REQUIRE(region.getNoteGain(52, 127_norm) == 0.86603_a);
REQUIRE(region.getNoteGain(53, 127_norm) == 0.70711_a);
REQUIRE(region.getNoteGain(54, 127_norm) == 0.5_a);
REQUIRE(region.getNoteGain(55, 127_norm) == 0.0_a);
REQUIRE(region.getNoteGain(56, 127_norm) == 0.0_a);
REQUIRE(getNoteGain(region, 50, 127_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 51, 127_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 52, 127_norm, midiState, curveSet) == 0.86603_a);
REQUIRE(getNoteGain(region, 53, 127_norm, midiState, curveSet) == 0.70711_a);
REQUIRE(getNoteGain(region, 54, 127_norm, midiState, curveSet) == 0.5_a);
REQUIRE(getNoteGain(region, 55, 127_norm, midiState, curveSet) == 0.0_a);
REQUIRE(getNoteGain(region, 56, 127_norm, midiState, curveSet) == 0.0_a);
}
TEST_CASE("[Region] Crossfade out on key - gain")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lokey", "51" });
region.parseOpcode({ "xfout_hikey", "55" });
region.parseOpcode({ "xf_keycurve", "gain" });
REQUIRE(region.getNoteGain(50, 127_norm) == 1.0_a);
REQUIRE(region.getNoteGain(51, 127_norm) == 1.0_a);
REQUIRE(region.getNoteGain(52, 127_norm) == 0.75_a);
REQUIRE(region.getNoteGain(53, 127_norm) == 0.5_a);
REQUIRE(region.getNoteGain(54, 127_norm) == 0.25_a);
REQUIRE(region.getNoteGain(55, 127_norm) == 0.0_a);
REQUIRE(region.getNoteGain(56, 127_norm) == 0.0_a);
REQUIRE(getNoteGain(region, 50, 127_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 51, 127_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 52, 127_norm, midiState, curveSet) == 0.75_a);
REQUIRE(getNoteGain(region, 53, 127_norm, midiState, curveSet) == 0.5_a);
REQUIRE(getNoteGain(region, 54, 127_norm, midiState, curveSet) == 0.25_a);
REQUIRE(getNoteGain(region, 55, 127_norm, midiState, curveSet) == 0.0_a);
REQUIRE(getNoteGain(region, 56, 127_norm, midiState, curveSet) == 0.0_a);
}
TEST_CASE("[Region] Crossfade in on velocity")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lovel", "20" });
region.parseOpcode({ "xfin_hivel", "24" });
region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE(region.getNoteGain(1, 19_norm) == 0.0_a);
REQUIRE(region.getNoteGain(1, 20_norm) == 0.0_a);
REQUIRE(region.getNoteGain(2, 21_norm) == 0.5_a);
REQUIRE(region.getNoteGain(3, 22_norm) == 0.70711_a);
REQUIRE(region.getNoteGain(4, 23_norm) == 0.86603_a);
REQUIRE(region.getNoteGain(5, 24_norm) == 1.0_a);
REQUIRE(region.getNoteGain(6, 25_norm) == 1.0_a);
REQUIRE(getNoteGain(region, 1, 19_norm, midiState, curveSet) == 0.0_a);
REQUIRE(getNoteGain(region, 1, 20_norm, midiState, curveSet) == 0.0_a);
REQUIRE(getNoteGain(region, 2, 21_norm, midiState, curveSet) == 0.5_a);
REQUIRE(getNoteGain(region, 3, 22_norm, midiState, curveSet) == 0.70711_a);
REQUIRE(getNoteGain(region, 4, 23_norm, midiState, curveSet) == 0.86603_a);
REQUIRE(getNoteGain(region, 5, 24_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 6, 25_norm, midiState, curveSet) == 1.0_a);
}
TEST_CASE("[Region] Crossfade in on vel - gain")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lovel", "20" });
region.parseOpcode({ "xfin_hivel", "24" });
region.parseOpcode({ "xf_velcurve", "gain" });
region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE(region.getNoteGain(1, 19_norm) == 0.0_a);
REQUIRE(region.getNoteGain(1, 20_norm) == 0.0_a);
REQUIRE(region.getNoteGain(2, 21_norm) == 0.25_a);
REQUIRE(region.getNoteGain(3, 22_norm) == 0.5_a);
REQUIRE(region.getNoteGain(4, 23_norm) == 0.75_a);
REQUIRE(region.getNoteGain(5, 24_norm) == 1.0_a);
REQUIRE(region.getNoteGain(5, 25_norm) == 1.0_a);
REQUIRE(getNoteGain(region, 1, 19_norm, midiState, curveSet) == 0.0_a);
REQUIRE(getNoteGain(region, 1, 20_norm, midiState, curveSet) == 0.0_a);
REQUIRE(getNoteGain(region, 2, 21_norm, midiState, curveSet) == 0.25_a);
REQUIRE(getNoteGain(region, 3, 22_norm, midiState, curveSet) == 0.5_a);
REQUIRE(getNoteGain(region, 4, 23_norm, midiState, curveSet) == 0.75_a);
REQUIRE(getNoteGain(region, 5, 24_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 5, 25_norm, midiState, curveSet) == 1.0_a);
}
TEST_CASE("[Region] Crossfade out on vel")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lovel", "51" });
region.parseOpcode({ "xfout_hivel", "55" });
region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE(region.getNoteGain(5, 50_norm) == 1.0_a);
REQUIRE(region.getNoteGain(5, 51_norm) == 1.0_a);
REQUIRE(region.getNoteGain(5, 52_norm) == 0.86603_a);
REQUIRE(region.getNoteGain(5, 53_norm) == 0.70711_a);
REQUIRE(region.getNoteGain(5, 54_norm) == 0.5_a);
REQUIRE(region.getNoteGain(5, 55_norm) == 0.0_a);
REQUIRE(region.getNoteGain(5, 56_norm) == 0.0_a);
REQUIRE(getNoteGain(region, 5, 50_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 5, 51_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 5, 52_norm, midiState, curveSet) == 0.86603_a);
REQUIRE(getNoteGain(region, 5, 53_norm, midiState, curveSet) == 0.70711_a);
REQUIRE(getNoteGain(region, 5, 54_norm, midiState, curveSet) == 0.5_a);
REQUIRE(getNoteGain(region, 5, 55_norm, midiState, curveSet) == 0.0_a);
REQUIRE(getNoteGain(region, 5, 56_norm, midiState, curveSet) == 0.0_a);
}
TEST_CASE("[Region] Crossfade out on vel - gain")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lovel", "51" });
region.parseOpcode({ "xfout_hivel", "55" });
region.parseOpcode({ "xf_velcurve", "gain" });
region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE(region.getNoteGain(56, 50_norm) == 1.0_a);
REQUIRE(region.getNoteGain(56, 51_norm) == 1.0_a);
REQUIRE(region.getNoteGain(56, 52_norm) == 0.75_a);
REQUIRE(region.getNoteGain(56, 53_norm) == 0.5_a);
REQUIRE(region.getNoteGain(56, 54_norm) == 0.25_a);
REQUIRE(region.getNoteGain(56, 55_norm) == 0.0_a);
REQUIRE(region.getNoteGain(56, 56_norm) == 0.0_a);
REQUIRE(getNoteGain(region, 56, 50_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 56, 51_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 56, 52_norm, midiState, curveSet) == 0.75_a);
REQUIRE(getNoteGain(region, 56, 53_norm, midiState, curveSet) == 0.5_a);
REQUIRE(getNoteGain(region, 56, 54_norm, midiState, curveSet) == 0.25_a);
REQUIRE(getNoteGain(region, 56, 55_norm, midiState, curveSet) == 0.0_a);
REQUIRE(getNoteGain(region, 56, 56_norm, midiState, curveSet) == 0.0_a);
}
TEST_CASE("[Region] Crossfade in on CC")
@ -161,19 +180,19 @@ TEST_CASE("[Region] Crossfade in on CC")
region.parseOpcode({ "xfin_hicc24", "24" });
region.parseOpcode({ "amp_veltrack", "0" });
midiState.ccEvent(0, 24, 19_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.0_a);
midiState.ccEvent(0, 24, 20_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.0_a);
midiState.ccEvent(0, 24, 21_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.5_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.5_a);
midiState.ccEvent(0, 24, 22_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.70711_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.70711_a);
midiState.ccEvent(0, 24, 23_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.86603_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.86603_a);
midiState.ccEvent(0, 24, 24_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 1.0_a);
midiState.ccEvent(0, 24, 25_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 1.0_a);
}
TEST_CASE("[Region] Crossfade in on CC - gain")
@ -186,19 +205,19 @@ TEST_CASE("[Region] Crossfade in on CC - gain")
region.parseOpcode({ "amp_veltrack", "0" });
region.parseOpcode({ "xf_cccurve", "gain" });
midiState.ccEvent(0, 24, 19_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.0_a);
midiState.ccEvent(0, 24, 20_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.0_a);
midiState.ccEvent(0, 24, 21_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.25_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.25_a);
midiState.ccEvent(0, 24, 22_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.5_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.5_a);
midiState.ccEvent(0, 24, 23_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.75_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.75_a);
midiState.ccEvent(0, 24, 24_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 1.0_a);
midiState.ccEvent(0, 24, 25_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 1.0_a);
}
TEST_CASE("[Region] Crossfade out on CC")
{
@ -209,19 +228,19 @@ TEST_CASE("[Region] Crossfade out on CC")
region.parseOpcode({ "xfout_hicc24", "24" });
region.parseOpcode({ "amp_veltrack", "0" });
midiState.ccEvent(0, 24, 19_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 1.0_a);
midiState.ccEvent(0, 24, 20_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 1.0_a);
midiState.ccEvent(0, 24, 21_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.86603_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.86603_a);
midiState.ccEvent(0, 24, 22_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.70711_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.70711_a);
midiState.ccEvent(0, 24, 23_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.5_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.5_a);
midiState.ccEvent(0, 24, 24_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.0_a);
midiState.ccEvent(0, 24, 25_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.0_a);
}
TEST_CASE("[Region] Crossfade out on CC - gain")
@ -234,47 +253,53 @@ TEST_CASE("[Region] Crossfade out on CC - gain")
region.parseOpcode({ "amp_veltrack", "0" });
region.parseOpcode({ "xf_cccurve", "gain" });
midiState.ccEvent(0, 24, 19_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 1.0_a);
midiState.ccEvent(0, 24, 20_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 1.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 1.0_a);
midiState.ccEvent(0, 24, 21_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.75_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.75_a);
midiState.ccEvent(0, 24, 22_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.5_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.5_a);
midiState.ccEvent(0, 24, 23_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.25_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.25_a);
midiState.ccEvent(0, 24, 24_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.0_a);
midiState.ccEvent(0, 24, 25_norm);
REQUIRE(region.getCrossfadeGain(midiState) == 0.0_a);
REQUIRE(getCrossfadeGain(region, midiState) == 0.0_a);
}
TEST_CASE("[Region] Velocity bug for extreme values - veltrack at 0")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a);
REQUIRE(region.getNoteGain(64, 0_norm) == 1.0_a);
REQUIRE(getNoteGain(region, 64, 127_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 64, 0_norm, midiState, curveSet) == 1.0_a);
}
TEST_CASE("[Region] Velocity bug for extreme values - positive veltrack")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "100" });
REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a);
REQUIRE(region.getNoteGain(64, 0_norm) == Approx(0.0).margin(0.0001));
REQUIRE(getNoteGain(region, 64, 127_norm, midiState, curveSet) == 1.0_a);
REQUIRE(getNoteGain(region, 64, 0_norm, midiState, curveSet) == Approx(0.0).margin(0.0001));
}
TEST_CASE("[Region] Velocity bug for extreme values - negative veltrack")
{
Region region { 0 };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "-100" });
REQUIRE(region.getNoteGain(64, 127_norm) == Approx(0.0).margin(0.0001));
REQUIRE(region.getNoteGain(64, 0_norm) == 1.0_a);
REQUIRE(getNoteGain(region, 64, 127_norm, midiState, curveSet) == Approx(0.0).margin(0.0001));
REQUIRE(getNoteGain(region, 64, 0_norm, midiState, curveSet) == 1.0_a);
}
TEST_CASE("[Region] rt_decay")
@ -287,15 +312,15 @@ TEST_CASE("[Region] rt_decay")
region.parseOpcode({ "rt_decay", "10" });
midiState.noteOnEvent(0, 64, 64_norm);
midiState.advanceTime(100);
REQUIRE( region.getBaseVolumedB(midiState, 64) == Approx(Default::volume - 1.0f).margin(0.1) );
REQUIRE( getBaseVolumedB(region, midiState, 64) == Approx(Default::volume - 1.0f).margin(0.1) );
region.parseOpcode({ "rt_decay", "20" });
midiState.noteOnEvent(0, 64, 64_norm);
midiState.advanceTime(100);
REQUIRE( region.getBaseVolumedB(midiState, 64) == Approx(Default::volume - 2.0f).margin(0.1) );
REQUIRE( getBaseVolumedB(region, midiState, 64) == Approx(Default::volume - 2.0f).margin(0.1) );
region.parseOpcode({ "trigger", "attack" });
midiState.noteOnEvent(0, 64, 64_norm);
midiState.advanceTime(100);
REQUIRE( region.getBaseVolumedB(midiState, 64) == Approx(Default::volume).margin(0.1) );
REQUIRE( getBaseVolumedB(region, midiState, 64) == Approx(Default::volume).margin(0.1) );
}
TEST_CASE("[Region] Base delay")
@ -304,12 +329,12 @@ TEST_CASE("[Region] Base delay")
Region region { 0 };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "delay", "10" });
REQUIRE( region.getDelay(midiState) == 10.0f );
REQUIRE( getDelay(region, midiState) == 10.0f );
region.parseOpcode({ "delay_random", "10" });
Random::randomGenerator.seed(42);
for (int i = 0; i < numRandomTests; ++i)
{
auto delay = region.getDelay(midiState);
auto delay = getDelay(region, midiState);
REQUIRE( (delay >= 10.0 && delay <= 20.0) );
}
}
@ -321,15 +346,15 @@ TEST_CASE("[Region] Offsets with CCs")
region.parseOpcode({ "offset_cc4", "255" });
region.parseOpcode({ "offset", "10" });
REQUIRE( region.getOffset(midiState) == 10 );
REQUIRE( getOffset(region, midiState) == 10 );
midiState.ccEvent(0, 4, 127_norm);
REQUIRE( region.getOffset(midiState) == 265 );
REQUIRE( getOffset(region, midiState) == 265 );
midiState.ccEvent(0, 4, 100_norm);
REQUIRE( region.getOffset(midiState) == 210 );
REQUIRE( getOffset(region, midiState) == 210 );
midiState.ccEvent(0, 4, 10_norm);
REQUIRE( region.getOffset(midiState) == 30 );
REQUIRE( getOffset(region, midiState) == 30 );
midiState.ccEvent(0, 4, 0);
REQUIRE( region.getOffset(midiState) == 10 );
REQUIRE( getOffset(region, midiState) == 10 );
}
TEST_CASE("[Region] Pitch variation with veltrack")
@ -344,3 +369,108 @@ TEST_CASE("[Region] Pitch variation with veltrack")
REQUIRE(region.getBasePitchVariation(60.0, 64_norm) == Approx(centsFactor(600.0)).margin(0.01f));
REQUIRE(region.getBasePitchVariation(60.0, 127_norm) == Approx(centsFactor(1200.0)).margin(0.01f));
}
TEST_CASE("[Synth] velcurve")
{
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
struct VelocityData { float velocity, gain; bool exact; };
static const VelocityData veldata[] = {
{ 0_norm, 0.0, true },
{ 32_norm, 0.5f, false },
{ 64_norm, 1.0, true },
{ 96_norm, 1.0, true },
{ 127_norm, 1.0, true },
};
SECTION("Default veltrack")
{
sfz::Region region { 0 };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_velcurve_064", "1" });
region.velCurve = Curve::buildFromVelcurvePoints(
region.velocityPoints, Curve::Interpolator::Linear);
for (const VelocityData& vd : veldata) {
if (vd.exact) {
REQUIRE(velocityCurve(region, vd.velocity, midiState, curveSet) == vd.gain);
} else {
REQUIRE(velocityCurve(region, vd.velocity, midiState, curveSet) == Approx(vd.gain).margin(1e-2));
}
}
}
SECTION("Inverted veltrack")
{
sfz::Region region { 0 };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_velcurve_064", "1" });
region.parseOpcode({ "amp_veltrack", "-100" });
region.velCurve = Curve::buildFromVelcurvePoints(
region.velocityPoints, Curve::Interpolator::Linear);
for (const VelocityData& vd : veldata) {
if (vd.exact) {
REQUIRE(velocityCurve(region, vd.velocity, midiState, curveSet) == 1.0f - vd.gain);
} else {
REQUIRE(velocityCurve(region, vd.velocity, midiState, curveSet) == Approx( 1.0f - vd.gain).margin(1e-2));
}
}
}
}
TEST_CASE("[Synth] veltrack")
{
struct VelocityData { float velocity, dBGain; };
struct VeltrackData { float veltrack; absl::Span<const VelocityData> veldata; };
MidiState midiState;
CurveSet curveSet { CurveSet::createPredefined() };
// measured on ARIA
const VelocityData veldata25[] = {
{ 127_norm, 0.0 },
{ 96_norm, -1 },
{ 64_norm, -1.8 },
{ 32_norm, -2.3 },
{ 1_norm, -2.5 },
};
const VelocityData veldata50[] = {
{ 127_norm, 0.0 },
{ 96_norm, -2.1 },
{ 64_norm, -4.1 },
{ 32_norm, -5.5 },
{ 1_norm, -6.0 },
};
const VelocityData veldata75[] = {
{ 127_norm, 0.0 },
{ 96_norm, -3.4 },
{ 64_norm, -7.2 },
{ 32_norm, -10.5 },
{ 1_norm, -12.0 },
};
const VelocityData veldata100[] = {
{ 127_norm, 0.0 },
{ 96_norm, -4.9 },
{ 64_norm, -12.0 },
{ 32_norm, -24.0 },
{ 1_norm, -84.1 },
};
const VeltrackData veltrackdata[] = {
{ 25, absl::MakeConstSpan(veldata25) },
{ 50, absl::MakeConstSpan(veldata50) },
{ 75, absl::MakeConstSpan(veldata75) },
{ 100, absl::MakeConstSpan(veldata100) },
};
for (const VeltrackData& vt : veltrackdata) {
sfz::Region region { 0 };
region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", std::to_string(vt.veltrack) });
for (const VelocityData& vd : vt.veldata) {
float dBGain = 20.0f * std::log10(velocityCurve(region, vd.velocity, midiState, curveSet));
REQUIRE(dBGain == Approx(vd.dBGain).margin(0.1));
}
}
}

View file

@ -431,98 +431,6 @@ TEST_CASE("[Synth] Velocity points")
REQUIRE( synth.getRegionView(1)->velocityPoints[0].second == 1.0_a );
}
TEST_CASE("[Synth] velcurve")
{
sfz::Synth synth;
synth.loadSfzString(fs::current_path() / "tests/TestFiles/velocity_endpoints.sfz", R"(
<region> amp_velcurve_064=1 sample=*sine
<region> amp_velcurve_064=1 amp_veltrack=-100 sample=*sine
)");
struct VelocityData { float velocity, gain; bool exact; };
static const VelocityData veldata[] = {
{ 0_norm, 0.0, true },
{ 32_norm, 0.5f, false },
{ 64_norm, 1.0, true },
{ 96_norm, 1.0, true },
{ 127_norm, 1.0, true },
};
REQUIRE(synth.getNumRegions() == 2);
const sfz::Region* r1 = synth.getRegionView(0);
const sfz::Region* r2 = synth.getRegionView(1);
for (const VelocityData& vd : veldata) {
if (vd.exact) {
REQUIRE(r1->velocityCurve(vd.velocity) == vd.gain);
REQUIRE(r2->velocityCurve(vd.velocity) == 1.0f - vd.gain);
}
else {
REQUIRE(r1->velocityCurve(vd.velocity) == Approx(vd.gain).margin(1e-2));
REQUIRE(r2->velocityCurve(vd.velocity) == Approx(1.0f - vd.gain).margin(1e-2));
}
}
}
TEST_CASE("[Synth] veltrack")
{
struct VelocityData { float velocity, dBGain; };
struct VeltrackData { float veltrack; absl::Span<const VelocityData> veldata; };
// measured on ARIA
const VelocityData veldata25[] = {
{ 127_norm, 0.0 },
{ 96_norm, -1 },
{ 64_norm, -1.8 },
{ 32_norm, -2.3 },
{ 1_norm, -2.5 },
};
const VelocityData veldata50[] = {
{ 127_norm, 0.0 },
{ 96_norm, -2.1 },
{ 64_norm, -4.1 },
{ 32_norm, -5.5 },
{ 1_norm, -6.0 },
};
const VelocityData veldata75[] = {
{ 127_norm, 0.0 },
{ 96_norm, -3.4 },
{ 64_norm, -7.2 },
{ 32_norm, -10.5 },
{ 1_norm, -12.0 },
};
const VelocityData veldata100[] = {
{ 127_norm, 0.0 },
{ 96_norm, -4.9 },
{ 64_norm, -12.0 },
{ 32_norm, -24.0 },
{ 1_norm, -84.1 },
};
const VeltrackData veltrackdata[] = {
{ 25, absl::MakeConstSpan(veldata25) },
{ 50, absl::MakeConstSpan(veldata50) },
{ 75, absl::MakeConstSpan(veldata75) },
{ 100, absl::MakeConstSpan(veldata100) },
};
for (const VeltrackData& vt : veltrackdata) {
sfz::Synth synth;
const std::string sfzCode = "<region>sample=*sine amp_veltrack=" +
std::to_string(vt.veltrack);
synth.loadSfzString(fs::current_path() / "tests/TestFiles/veltrack.sfz", sfzCode);
REQUIRE(synth.getNumRegions() == 1);
const sfz::Region* r = synth.getRegionView(0);
for (const VelocityData& vd : vt.veldata) {
float dBGain = 20.0f * std::log10(r->velocityCurve(vd.velocity));
REQUIRE(dBGain == Approx(vd.dBGain).margin(0.1));
}
}
}
TEST_CASE("[Synth] Region by identifier")
{
sfz::Synth synth;