diff --git a/CMakeLists.txt b/CMakeLists.txt index 8030a21b..406d01db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,7 @@ set(TEST_SOURCES tests/FilesT.cpp tests/OnePoleFilterT.cpp tests/RegionActivationT.cpp + tests/RegionCrossfadesT.cpp tests/ADSREnvelopeT.cpp tests/LinearEnvelopeT.cpp tests/MainT.cpp diff --git a/sources/Region.cpp b/sources/Region.cpp index 111b224e..9f2f9356 100644 --- a/sources/Region.cpp +++ b/sources/Region.cpp @@ -631,55 +631,56 @@ bool sfz::Region::isStereo() const noexcept return this->numChannels == 2; } -// TODO: lots and lots of repetition here... +template +float crossfadeIn(const Range& crossfadeRange, U value, SfzCrossfadeCurve curve) +{ + if (value < crossfadeRange.getStart()) + return 0.0f; + else if (value < crossfadeRange.getEnd()) { + const auto crossfadePosition = static_cast(value - crossfadeRange.getStart()) / std::max(static_cast(crossfadeRange.length()), 1.0f); + if (curve == SfzCrossfadeCurve::power) + return sqrt(crossfadePosition); + if (curve == SfzCrossfadeCurve::gain) + return crossfadePosition; + } + + return 1.0f; +} + +template +float crossfadeOut(const Range& crossfadeRange, U value, SfzCrossfadeCurve curve) +{ + if (value > crossfadeRange.getEnd()) + return 0.0f; + else if (value > crossfadeRange.getStart()) { + const auto crossfadePosition = static_cast(value - crossfadeRange.getStart()) / std::max(static_cast(crossfadeRange.length()), 1.0f); + if (curve == SfzCrossfadeCurve::power) + return sqrt(1 - crossfadePosition); + if (curve == SfzCrossfadeCurve::gain) + return 1 - crossfadePosition; + } + + return 1.0f; +} + float sfz::Region::getNoteGain(int noteNumber, uint8_t velocity) noexcept { float baseGain { 1.0f }; + // Amplitude velocity tracking if (trigger == SfzTrigger::release_key) baseGain *= velocityGain(lastNoteVelocities[noteNumber]); else baseGain *= velocityGain(velocity); - if (noteNumber < crossfadeKeyInRange.getStart()) - baseGain = 0.0f; - else if (noteNumber < crossfadeKeyInRange.getEnd()) { - const auto crossfadePosition = static_cast(noteNumber - crossfadeKeyInRange.getStart()) / (crossfadeKeyInRange.length() > 0 ? crossfadeKeyInRange.length() : 1); - if (crossfadeKeyCurve == SfzCrossfadeCurve::power) - baseGain *= sqrt(crossfadePosition); - if (crossfadeKeyCurve == SfzCrossfadeCurve::gain) - baseGain *= crossfadePosition; - } + // Amplitude key tracking + baseGain *= db2pow(ampKeytrack * static_cast(noteNumber - pitchKeycenter)); - if (noteNumber > crossfadeKeyOutRange.getEnd()) - baseGain = 0.0f; - else if (noteNumber > crossfadeKeyOutRange.getStart()) { - const auto crossfadePosition = static_cast(noteNumber - crossfadeKeyOutRange.getStart()) / (crossfadeKeyOutRange.length() > 0 ? crossfadeKeyOutRange.length() : 1); - if (crossfadeKeyCurve == SfzCrossfadeCurve::power) - baseGain *= sqrt(1 - crossfadePosition); - if (crossfadeKeyCurve == SfzCrossfadeCurve::gain) - baseGain *= 1 - crossfadePosition; - } - - if (velocity < crossfadeVelInRange.getStart()) - baseGain = 0; - else if (velocity < crossfadeVelInRange.getEnd()) { - const auto crossfadePosition = static_cast(noteNumber - crossfadeVelInRange.getStart()) / (crossfadeVelInRange.length() > 0 ? crossfadeVelInRange.length() : 1); - if (crossfadeVelCurve == SfzCrossfadeCurve::power) - baseGain *= sqrt(crossfadePosition); - if (crossfadeVelCurve == SfzCrossfadeCurve::gain) - baseGain *= crossfadePosition; - } - - if (velocity > crossfadeVelOutRange.getEnd()) - baseGain = 0; - else if (velocity > crossfadeVelOutRange.getStart()) { - const auto crossfadePosition = static_cast(noteNumber - crossfadeVelOutRange.getStart()) / (crossfadeVelOutRange.length() > 0 ? crossfadeVelOutRange.length() : 1); - if (crossfadeVelCurve == SfzCrossfadeCurve::power) - baseGain *= sqrt(1 - crossfadePosition); - if (crossfadeVelCurve == SfzCrossfadeCurve::gain) - baseGain *= 1 - crossfadePosition; - } + // Crossfades related to key and velocity + baseGain *= crossfadeIn(crossfadeKeyInRange, noteNumber, crossfadeKeyCurve); + baseGain *= crossfadeOut(crossfadeKeyOutRange, noteNumber, crossfadeKeyCurve); + baseGain *= crossfadeIn(crossfadeVelInRange, velocity, crossfadeVelCurve); + baseGain *= crossfadeOut(crossfadeVelOutRange, velocity, crossfadeVelCurve); return baseGain; } @@ -688,32 +689,20 @@ float sfz::Region::getCCGain(const sfz::CCValueArray& ccState) noexcept { float gain { 1.0f }; + if (amplitudeCC) + gain *= *amplitudeCC + + // Crossfades due to CC states for (const auto& valuePair : crossfadeCCInRange) { const auto ccValue = ccState[valuePair.first]; const auto crossfadeRange = valuePair.second; - if (ccValue < crossfadeRange.getStart()) { - gain = 0.0f; - } else if (ccValue < crossfadeRange.getEnd()) { - const auto crossfadePosition = static_cast(ccValue - crossfadeRange.getStart()) / (crossfadeRange.length() > 0 ? crossfadeRange.length() : 1); - if (crossfadeCCCurve == SfzCrossfadeCurve::power) - gain *= sqrt(crossfadePosition); - if (crossfadeVelCurve == SfzCrossfadeCurve::gain) - gain *= crossfadePosition; - } + gain *= crossfadeIn(crossfadeRange, ccValue, crossfadeCCCurve); } for (const auto& valuePair : crossfadeCCOutRange) { const auto ccValue = ccState[valuePair.first]; const auto crossfadeRange = valuePair.second; - if (ccValue > crossfadeRange.getEnd()) { - gain = 0.0f; - } else if (ccValue > crossfadeRange.getStart()) { - const auto crossfadePosition = static_cast(ccValue - crossfadeRange.getStart()) / (crossfadeRange.length() > 0 ? crossfadeRange.length() : 1); - if (crossfadeCCCurve == SfzCrossfadeCurve::power) - gain *= sqrt(1 - crossfadePosition); - if (crossfadeVelCurve == SfzCrossfadeCurve::gain) - gain *= 1 - crossfadePosition; - } + gain *= crossfadeOut(crossfadeRange, ccValue, crossfadeCCCurve); } return gain; diff --git a/tests/RegionCrossfadesT.cpp b/tests/RegionCrossfadesT.cpp new file mode 100644 index 00000000..6289e2bd --- /dev/null +++ b/tests/RegionCrossfadesT.cpp @@ -0,0 +1,209 @@ +#include "../sources/Region.h" +#include "catch2/catch.hpp" +#include +using namespace Catch::literals; + +TEST_CASE("[Region] Crossfade in on key") +{ + sfz::Region region {}; + region.parseOpcode({ "sample", "*sine" }); + region.parseOpcode({ "xfin_lokey", "1" }); + region.parseOpcode({ "xfin_hikey", "3" }); + REQUIRE( region.getNoteGain(2, 127) == 0.70711_a ); + REQUIRE( region.getNoteGain(1, 127) == 0.0_a ); + REQUIRE( region.getNoteGain(3, 127) == 1.0_a ); +} + +TEST_CASE("[Region] Crossfade in on key - 2") +{ + sfz::Region region {}; + region.parseOpcode({ "sample", "*sine" }); + region.parseOpcode({ "xfin_lokey", "1" }); + region.parseOpcode({ "xfin_hikey", "5" }); + REQUIRE( region.getNoteGain(1, 127) == 0.0_a ); + REQUIRE( region.getNoteGain(2, 127) == 0.5_a ); + REQUIRE( region.getNoteGain(3, 127) == 0.70711_a ); + REQUIRE( region.getNoteGain(4, 127) == 0.86603_a ); + REQUIRE( region.getNoteGain(5, 127) == 1.0_a ); + REQUIRE( region.getNoteGain(6, 127) == 1.0_a ); +} + +TEST_CASE("[Region] Crossfade in on key - gain") +{ + sfz::Region region {}; + region.parseOpcode({ "sample", "*sine" }); + region.parseOpcode({ "xfin_lokey", "1" }); + region.parseOpcode({ "xfin_hikey", "5" }); + region.parseOpcode({ "xf_keycurve", "gain" }); + REQUIRE( region.getNoteGain(1, 127) == 0.0_a ); + REQUIRE( region.getNoteGain(2, 127) == 0.25_a ); + REQUIRE( region.getNoteGain(3, 127) == 0.5_a ); + REQUIRE( region.getNoteGain(4, 127) == 0.75_a ); + REQUIRE( region.getNoteGain(5, 127) == 1.0_a ); +} + +TEST_CASE("[Region] Crossfade out on key") +{ + sfz::Region region {}; + region.parseOpcode({ "sample", "*sine" }); + region.parseOpcode({ "xfout_lokey", "51" }); + region.parseOpcode({ "xfout_hikey", "55" }); + REQUIRE( region.getNoteGain(50, 127) == 1.0_a ); + REQUIRE( region.getNoteGain(51, 127) == 1.0_a ); + REQUIRE( region.getNoteGain(52, 127) == 0.86603_a ); + REQUIRE( region.getNoteGain(53, 127) == 0.70711_a ); + REQUIRE( region.getNoteGain(54, 127) == 0.5_a ); + REQUIRE( region.getNoteGain(55, 127) == 0.0_a ); + REQUIRE( region.getNoteGain(56, 127) == 0.0_a ); +} + +TEST_CASE("[Region] Crossfade out on key - gain") +{ + sfz::Region region {}; + region.parseOpcode({ "sample", "*sine" }); + region.parseOpcode({ "xfout_lokey", "51" }); + region.parseOpcode({ "xfout_hikey", "55" }); + region.parseOpcode({ "xf_keycurve", "gain" }); + REQUIRE( region.getNoteGain(50, 127) == 1.0_a ); + REQUIRE( region.getNoteGain(51, 127) == 1.0_a ); + REQUIRE( region.getNoteGain(52, 127) == 0.75_a ); + REQUIRE( region.getNoteGain(53, 127) == 0.5_a ); + REQUIRE( region.getNoteGain(54, 127) == 0.25_a ); + REQUIRE( region.getNoteGain(55, 127) == 0.0_a ); + REQUIRE( region.getNoteGain(56, 127) == 0.0_a ); +} + +TEST_CASE("[Region] Crossfade in on velocity") +{ + sfz::Region region {}; + region.parseOpcode({ "sample", "*sine" }); + region.parseOpcode({ "xfin_lovel", "20" }); + region.parseOpcode({ "xfin_hivel", "24" }); + region.parseOpcode({ "amp_veltrack", "0" }); + REQUIRE( region.getNoteGain(1, 19) == 0.0_a ); + REQUIRE( region.getNoteGain(1, 20) == 0.0_a ); + REQUIRE( region.getNoteGain(2, 21) == 0.5_a ); + REQUIRE( region.getNoteGain(3, 22) == 0.70711_a ); + REQUIRE( region.getNoteGain(4, 23) == 0.86603_a ); + REQUIRE( region.getNoteGain(5, 24) == 1.0_a ); + REQUIRE( region.getNoteGain(6, 25) == 1.0_a ); +} + +TEST_CASE("[Region] Crossfade in on vel - gain") +{ + sfz::Region region {}; + 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) == 0.0_a ); + REQUIRE( region.getNoteGain(1, 20) == 0.0_a ); + REQUIRE( region.getNoteGain(2, 21) == 0.25_a ); + REQUIRE( region.getNoteGain(3, 22) == 0.5_a ); + REQUIRE( region.getNoteGain(4, 23) == 0.75_a ); + REQUIRE( region.getNoteGain(5, 24) == 1.0_a ); + REQUIRE( region.getNoteGain(5, 25) == 1.0_a ); +} + +TEST_CASE("[Region] Crossfade out on vel") +{ + sfz::Region region {}; + region.parseOpcode({ "sample", "*sine" }); + region.parseOpcode({ "xfout_lovel", "51" }); + region.parseOpcode({ "xfout_hivel", "55" }); + region.parseOpcode({ "amp_veltrack", "0" }); + REQUIRE( region.getNoteGain(5, 50) == 1.0_a ); + REQUIRE( region.getNoteGain(5, 51) == 1.0_a ); + REQUIRE( region.getNoteGain(5, 52) == 0.86603_a ); + REQUIRE( region.getNoteGain(5, 53) == 0.70711_a ); + REQUIRE( region.getNoteGain(5, 54) == 0.5_a ); + REQUIRE( region.getNoteGain(5, 55) == 0.0_a ); + REQUIRE( region.getNoteGain(5, 56) == 0.0_a ); +} + +TEST_CASE("[Region] Crossfade out on vel - gain") +{ + sfz::Region region {}; + 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) == 1.0_a ); + REQUIRE( region.getNoteGain(56, 51) == 1.0_a ); + REQUIRE( region.getNoteGain(56, 52) == 0.75_a ); + REQUIRE( region.getNoteGain(56, 53) == 0.5_a ); + REQUIRE( region.getNoteGain(56, 54) == 0.25_a ); + REQUIRE( region.getNoteGain(56, 55) == 0.0_a ); + REQUIRE( region.getNoteGain(56, 56) == 0.0_a ); +} + +TEST_CASE("[Region] Crossfade in on CC") +{ + sfz::Region region {}; + sfz::CCValueArray ccState; + region.parseOpcode({ "sample", "*sine" }); + region.parseOpcode({ "xfin_locc24", "20" }); + region.parseOpcode({ "xfin_hicc24", "24" }); + region.parseOpcode({ "amp_veltrack", "0" }); + ccState[24] = 19; REQUIRE( region.getCCGain(ccState) == 0.0_a ); + ccState[24] = 20; REQUIRE( region.getCCGain(ccState) == 0.0_a ); + ccState[24] = 21; REQUIRE( region.getCCGain(ccState) == 0.5_a ); + ccState[24] = 22; REQUIRE( region.getCCGain(ccState) == 0.70711_a ); + ccState[24] = 23; REQUIRE( region.getCCGain(ccState) == 0.86603_a ); + ccState[24] = 24; REQUIRE( region.getCCGain(ccState) == 1.0_a ); + ccState[24] = 25; REQUIRE( region.getCCGain(ccState) == 1.0_a ); +} + +TEST_CASE("[Region] Crossfade in on CC - gain") +{ + sfz::Region region {}; + sfz::CCValueArray ccState; + region.parseOpcode({ "sample", "*sine" }); + region.parseOpcode({ "xfin_locc24", "20" }); + region.parseOpcode({ "xfin_hicc24", "24" }); + region.parseOpcode({ "amp_veltrack", "0" }); + region.parseOpcode({ "xf_cccurve", "gain" }); + ccState[24] = 19; REQUIRE( region.getCCGain(ccState) == 0.0_a ); + ccState[24] = 20; REQUIRE( region.getCCGain(ccState) == 0.0_a ); + ccState[24] = 21; REQUIRE( region.getCCGain(ccState) == 0.25_a ); + ccState[24] = 22; REQUIRE( region.getCCGain(ccState) == 0.5_a ); + ccState[24] = 23; REQUIRE( region.getCCGain(ccState) == 0.75_a ); + ccState[24] = 24; REQUIRE( region.getCCGain(ccState) == 1.0_a ); + ccState[24] = 25; REQUIRE( region.getCCGain(ccState) == 1.0_a ); +} +TEST_CASE("[Region] Crossfade out on CC") +{ + sfz::Region region {}; + sfz::CCValueArray ccState; + region.parseOpcode({ "sample", "*sine" }); + region.parseOpcode({ "xfout_locc24", "20" }); + region.parseOpcode({ "xfout_hicc24", "24" }); + region.parseOpcode({ "amp_veltrack", "0" }); + ccState[24] = 19; REQUIRE( region.getCCGain(ccState) == 1.0_a ); + ccState[24] = 20; REQUIRE( region.getCCGain(ccState) == 1.0_a ); + ccState[24] = 21; REQUIRE( region.getCCGain(ccState) == 0.86603_a ); + ccState[24] = 22; REQUIRE( region.getCCGain(ccState) == 0.70711_a ); + ccState[24] = 23; REQUIRE( region.getCCGain(ccState) == 0.5_a ); + ccState[24] = 24; REQUIRE( region.getCCGain(ccState) == 0.0_a ); + ccState[24] = 25; REQUIRE( region.getCCGain(ccState) == 0.0_a ); +} + +TEST_CASE("[Region] Crossfade out on CC - gain") +{ + sfz::Region region {}; + sfz::CCValueArray ccState; + region.parseOpcode({ "sample", "*sine" }); + region.parseOpcode({ "xfout_locc24", "20" }); + region.parseOpcode({ "xfout_hicc24", "24" }); + region.parseOpcode({ "amp_veltrack", "0" }); + region.parseOpcode({ "xf_cccurve", "gain" }); + ccState[24] = 19; REQUIRE( region.getCCGain(ccState) == 1.0_a ); + ccState[24] = 20; REQUIRE( region.getCCGain(ccState) == 1.0_a ); + ccState[24] = 21; REQUIRE( region.getCCGain(ccState) == 0.75_a ); + ccState[24] = 22; REQUIRE( region.getCCGain(ccState) == 0.5_a ); + ccState[24] = 23; REQUIRE( region.getCCGain(ccState) == 0.25_a ); + ccState[24] = 24; REQUIRE( region.getCCGain(ccState) == 0.0_a ); + ccState[24] = 25; REQUIRE( region.getCCGain(ccState) == 0.0_a ); +} \ No newline at end of file