diff --git a/src/sfizz/modulations/ModKey.cpp b/src/sfizz/modulations/ModKey.cpp index c5ee2a37..9012fb46 100644 --- a/src/sfizz/modulations/ModKey.cpp +++ b/src/sfizz/modulations/ModKey.cpp @@ -71,22 +71,22 @@ std::string ModKey::toString() const " {curve=", params_.curve, ", smooth=", params_.smooth, ", value=", params_.value, ", step=", params_.step, "}"); case ModId::Envelope: - return absl::StrCat("EG ", 1 + params_.N); + return absl::StrCat("EG ", 1 + params_.N, " {region=", region_.number(), "}"); case ModId::LFO: - return absl::StrCat("LFO ", 1 + params_.N); + return absl::StrCat("LFO ", 1 + params_.N, " {region=", region_.number(), "}"); case ModId::Amplitude: - return "Amplitude"; + return absl::StrCat("Amplitude", " {region=", region_.number(), "}"); case ModId::Pan: - return "Pan"; + return absl::StrCat("Pan", " {region=", region_.number(), "}"); case ModId::Width: - return "Width"; + return absl::StrCat("Width", " {region=", region_.number(), "}"); case ModId::Position: - return "Position"; + return absl::StrCat("Position", " {region=", region_.number(), "}"); case ModId::Pitch: - return "Pitch"; + return absl::StrCat("Pitch", " {region=", region_.number(), "}"); case ModId::Volume: - return "Volume"; + return absl::StrCat("Volume", " {region=", region_.number(), "}"); default: return {}; diff --git a/tests/ADSREnvelopeT.cpp b/tests/ADSREnvelopeT.cpp index df104407..424d8273 100644 --- a/tests/ADSREnvelopeT.cpp +++ b/tests/ADSREnvelopeT.cpp @@ -5,6 +5,7 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "sfizz/ADSREnvelope.h" +#include "TestHelpers.h" #include "catch2/catch.hpp" #include #include @@ -13,21 +14,6 @@ #include using namespace Catch::literals; -template -inline bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) -{ - if (lhs.size() != rhs.size()) - return false; - - for (size_t i = 0; i < rhs.size(); ++i) - if (rhs[i] != Approx(lhs[i]).epsilon(eps)) { - std::cerr << lhs[i] << " != " << rhs[i] << " at index " << i << '\n'; - return false; - } - - return true; -} - TEST_CASE("[ADSREnvelope] Basic state") { sfz::ADSREnvelope envelope; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 61b16360..0090620c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,7 @@ set(SFIZZ_TEST_SOURCES # If we're tweaking the curves this kind of tests does not make sense # Use integration tests with comparison curves # ADSREnvelopeT.cpp + FlexEGT.cpp EventEnvelopesT.cpp MainT.cpp SynthT.cpp diff --git a/tests/FlexEGT.cpp b/tests/FlexEGT.cpp new file mode 100644 index 00000000..f48e5455 --- /dev/null +++ b/tests/FlexEGT.cpp @@ -0,0 +1,263 @@ +// 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 "sfizz/Synth.h" +#include "sfizz/FlexEnvelope.h" +#include "catch2/catch.hpp" +#include "TestHelpers.h" +using namespace Catch::literals; +using namespace sfz::literals; + +TEST_CASE("[FlexEG] Values") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_amplitude=1 + eg1_time1=.1 eg1_level1=.25 + eg1_time2=.2 eg1_level2=1 + eg1_time3=.2 eg1_level3=.5 eg1_sustain=3 + eg1_time4=.4 eg1_level4=1 + )"); + REQUIRE(synth.getNumRegions() == 1); + const auto* region = synth.getRegionView(0); + REQUIRE( region->flexEGs.size() == 1 ); + const auto& egDescription = region->flexEGs[0]; + REQUIRE( egDescription.points.size() == 5 ); + REQUIRE( egDescription.points[0].time == 0.0_a ); + REQUIRE( egDescription.points[0].level == 0.0_a ); + REQUIRE( egDescription.points[1].time == .1_a ); + REQUIRE( egDescription.points[1].level == .25_a ); + REQUIRE( egDescription.points[2].time == .2_a ); + REQUIRE( egDescription.points[2].level == 1.0_a ); + REQUIRE( egDescription.points[3].time == .2_a ); + REQUIRE( egDescription.points[3].level == .5_a ); + REQUIRE( egDescription.points[4].time == .4_a ); + REQUIRE( egDescription.points[4].level == 1.0_a ); + REQUIRE( egDescription.sustain == 3 ); + REQUIRE(synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ + R"("EG 1 {region=0}" -> "Amplitude {region=0}")", + })); +} + +TEST_CASE("[FlexEG] Default values") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg3_time2=.1 eg3_level2=.25 + )"); + REQUIRE(synth.getNumRegions() == 1); + const auto* region = synth.getRegionView(0); + REQUIRE( region->flexEGs.size() == 3 ); + REQUIRE( region->flexEGs[0].points.size() == 0 ); + REQUIRE( region->flexEGs[1].points.size() == 0 ); + const auto& egDescription = region->flexEGs[2]; + REQUIRE( egDescription.points.size() == 3 ); + REQUIRE( egDescription.points[0].time == 0.0_a ); + REQUIRE( egDescription.points[0].level == 0.0_a ); + REQUIRE( egDescription.points[1].time == 0.0_a ); + REQUIRE( egDescription.points[1].level == 0.0_a ); + REQUIRE( egDescription.points[2].time == .1_a ); + REQUIRE( egDescription.points[2].level == .25_a ); + REQUIRE( synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({}) ); +} + +TEST_CASE("[FlexEG] Connections") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine eg1_amplitude=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_pan=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_width=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_position=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_pitch=1 eg1_time1=.1 eg1_level1=.25 + sample=*sine eg1_volume=1 eg1_time1=.1 eg1_level1=.25 + )"); + REQUIRE(synth.getNumRegions() == 6); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + REQUIRE( synth.getRegionView(0)->flexEGs[0].points.size() == 2 ); + REQUIRE( synth.getResources().modMatrix.toDotGraph() == createReferenceGraph({ + R"("EG 1 {region=0}" -> "Amplitude {region=0}")", + R"("EG 1 {region=1}" -> "Pan {region=1}")", + R"("EG 1 {region=2}" -> "Width {region=2}")", + R"("EG 1 {region=3}" -> "Position {region=3}")", + R"("EG 1 {region=4}" -> "Pitch {region=4}")", + R"("EG 1 {region=5}" -> "Volume {region=5}")", + }, 6)); +} + +TEST_CASE("[FlexEG] Coarse numerical envelope test (No release)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + envelope.setSampleRate(10); + output.resize(16); + envelope.start(1); + envelope.process(absl::MakeSpan(output)); + REQUIRE( output[0] == 0.0_a ); // Trigger delay + REQUIRE( output[5] == 0.25_a ); // 0.25 at time == 0.5s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[10] == 1.0_a ); // 1 at time == 1s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[15] == 1.0_a ); // sustaining +} + +TEST_CASE("[FlexEG] Detailed numerical envelope test") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + std::vector expected { 0.0f, 0.05f, 0.1f, 0.15f, 0.2f, 0.25f, 0.4f, 0.55f, 0.7f, 0.85f, 1.0f, 1.0f, 1.0f }; + output.resize(expected.size()); + envelope.setSampleRate(10); + envelope.start(1); + envelope.process(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[FlexEG] Coarse numerical envelope test (with release)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + envelope.setSampleRate(10); + output.resize(32); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( output[0] == 0.0_a ); // Trigger delay + REQUIRE( output[5] == 0.25_a ); // 0.25 at time == 0.5s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[10] == 1.0_a ); // 1 at time == 1s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[15] == 1.0_a ); // sustaining + REQUIRE( output[16] == 0.0_a ); // released + REQUIRE( output[31] == 0.0_a ); // released +} + +TEST_CASE("[FlexEG] Detailed numerical envelope test (with release and release ramp)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 + eg1_time2=0.5 eg1_level2=1 + eg1_time3=0.5 eg1_level3=0 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + std::vector expected { + 0.0f, + 0.05f, 0.1f, 0.15f, 0.2f, 0.25f, + 0.4f, 0.55f, 0.7f, 0.85f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.8f, 0.6f, 0.4f, 0.2f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f + }; + output.resize(expected.size()); + envelope.setSampleRate(10); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[FlexEG] Coarse numerical envelope test (with shapes)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 eg1_shape1=2 + eg1_time2=0.5 eg1_level2=1 eg1_shape2=0.5 + eg1_sustain=2 + eg1_time3=0.5 eg1_level3=0 eg1_shape3=4 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + envelope.setSampleRate(10); + output.resize(32); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( output[0] == 0.0_a ); // Trigger delay + REQUIRE( output[5] == 0.25_a ); // 0.25 at time == 0.5s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[10] == 1.0_a ); // 1 at time == 1s (5 samples at samplerate 10 + trigger delay) + REQUIRE( output[15] == 1.0_a ); // sustaining + REQUIRE( output[31] == 0.0_a ); // released +} + +TEST_CASE("[FlexEG] Detailed numerical envelope test (with shapes)") +{ + sfz::Synth synth; + + synth.loadSfzString(fs::current_path(), R"( + sample=*sine + eg1_time1=.5 eg1_level1=.25 eg1_shape1=2 + eg1_time2=0.5 eg1_level2=1 eg1_shape2=0.5 + eg1_time3=0.5 eg1_level3=0 eg1_shape3=4 + eg1_sustain=2 + )"); + sfz::FlexEnvelope envelope; + REQUIRE(synth.getNumRegions() == 1); + REQUIRE( synth.getRegionView(0)->flexEGs.size() == 1 ); + envelope.configure(&synth.getRegionView(0)->flexEGs[0]); + std::vector output; + std::vector expected { + 0.0f, + 0.01f, 0.04f, 0.09f, 0.16f, 0.25f, + 0.58f, 0.72f, 0.83f, 0.92f, 1.0f, + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.99f, 0.97f, 0.87f, 0.59f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f + }; + output.resize(expected.size()); + envelope.setSampleRate(10); + envelope.start(1); + envelope.release(15); + envelope.process(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected, 0.01f) ); +} diff --git a/tests/ModulationsT.cpp b/tests/ModulationsT.cpp index 48203a2e..627714d6 100644 --- a/tests/ModulationsT.cpp +++ b/tests/ModulationsT.cpp @@ -7,6 +7,7 @@ #include "sfizz/modulations/ModId.h" #include "sfizz/modulations/ModKey.h" #include "sfizz/Synth.h" +#include "TestHelpers.h" #include "catch2/catch.hpp" TEST_CASE("[Modulations] Identifiers") @@ -78,32 +79,6 @@ TEST_CASE("[Modulations] Display names") }); } -static std::string createReferenceGraph(std::vector lines) -{ - const char* defaultConnections[] = { - R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude")", - R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan")" - }; - - for (const char* line : defaultConnections) - lines.push_back(line); - - std::sort(lines.begin(), lines.end()); - - std::string graph; - graph.reserve(1024); - - graph += "digraph {\n"; - for (const std::string& line : lines) { - graph.push_back('\t'); - graph += line; - graph.push_back('\n'); - } - graph += "}\n"; - - return graph; -}; - TEST_CASE("[Modulations] Connection graph from SFZ") { sfz::Synth synth; @@ -118,9 +93,9 @@ width_oncc425=29 const std::string graph = synth.getResources().modMatrix.toDotGraph(); REQUIRE(graph == createReferenceGraph({ - R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude")", - R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch")", - R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan")", - R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width")", + R"("Controller 20 {curve=3, smooth=0, value=59, step=0}" -> "Amplitude {region=0}")", + R"("Controller 42 {curve=0, smooth=32, value=71, step=0}" -> "Pitch {region=0}")", + R"("Controller 36 {curve=0, smooth=0, value=14.5, step=1.5}" -> "Pan {region=0}")", + R"("Controller 425 {curve=0, smooth=0, value=29, step=0}" -> "Width {region=0}")", })); } diff --git a/tests/TestHelpers.cpp b/tests/TestHelpers.cpp index f58eee73..fb4bd233 100644 --- a/tests/TestHelpers.cpp +++ b/tests/TestHelpers.cpp @@ -68,3 +68,34 @@ unsigned numPlayingVoices(const sfz::Synth& synth) return !v->releasedOrFree(); }); } + +std::string createReferenceGraph(std::vector lines, int numRegions) +{ + for (int regionIdx = 0; regionIdx < numRegions; ++regionIdx) { + lines.push_back(absl::StrCat( + R"("Controller 7 {curve=4, smooth=10, value=100, step=0}" -> "Amplitude {region=)", + regionIdx, + R"(}")" + )); + lines.push_back(absl::StrCat( + R"("Controller 10 {curve=1, smooth=10, value=100, step=0}" -> "Pan {region=)", + regionIdx, + R"(}")" + )); + } + + std::sort(lines.begin(), lines.end()); + + std::string graph; + graph.reserve(1024); + + graph += "digraph {\n"; + for (const std::string& line : lines) { + graph.push_back('\t'); + graph += line; + graph.push_back('\n'); + } + graph += "}\n"; + + return graph; +}; diff --git a/tests/TestHelpers.h b/tests/TestHelpers.h index e5e48671..272fb612 100644 --- a/tests/TestHelpers.h +++ b/tests/TestHelpers.h @@ -65,3 +65,24 @@ const std::vector getPlayingVoices(const sfz::Synth& synth); * @return unsigned */ unsigned numPlayingVoices(const sfz::Synth& synth); + +/** + * @brief Create the dot graph representation from a list of strings + * + */ +std::string createReferenceGraph(std::vector lines, int numRegions = 1); + +template +inline bool approxEqual(absl::Span lhs, absl::Span rhs, Type eps = 1e-3) +{ + if (lhs.size() != rhs.size()) + return false; + + for (size_t i = 0; i < rhs.size(); ++i) + if (rhs[i] != Approx(lhs[i]).epsilon(eps)) { + std::cerr << lhs[i] << " != " << rhs[i] << " at index " << i << '\n'; + return false; + } + + return true; +}