From b909e468bf120144ec29c7ee9411c36b88b28927 Mon Sep 17 00:00:00 2001 From: paul Date: Thu, 22 Aug 2019 17:18:09 +0200 Subject: [PATCH] Wrapping up the ADSR envelope and tests --- CMakeLists.txt | 7 +- benchmarks/BM_ADSR.cpp | 45 ++++++++ sources/ADSREnvelope.cpp | 217 +++++++++++++++++++++++++++++++++++++++ sources/ADSREnvelope.h | 45 ++++---- sources/SIMDDummy.cpp | 8 +- sources/SIMDHelpers.h | 10 +- sources/SIMDSSE.cpp | 6 +- tests/ADSREnvelopeT.cpp | 189 ++++++++++++++++++++++++++++++++++ 8 files changed, 491 insertions(+), 36 deletions(-) create mode 100644 benchmarks/BM_ADSR.cpp create mode 100644 sources/ADSREnvelope.cpp create mode 100644 tests/ADSREnvelopeT.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 067f339e..ca59b2e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,7 @@ set(COMMON_SOURCES sources/Synth.cpp sources/FilePool.cpp sources/Region.cpp + sources/ADSREnvelope.cpp sources/Parser.cpp ) @@ -110,6 +111,7 @@ set(TEST_SOURCES tests/FilesT.cpp tests/OnePoleFilterT.cpp tests/RegionActivationT.cpp + tests/ADSREnvelopeT.cpp tests/MainT.cpp ) @@ -168,4 +170,7 @@ add_executable(bm_looping benchmarks/BM_looping.cpp sources/SIMDSSE.cpp) target_link_libraries(bm_looping benchmark absl::span absl::algorithm) add_executable(bm_ramp benchmarks/BM_ramp.cpp sources/SIMDSSE.cpp) -target_link_libraries(bm_ramp benchmark absl::span absl::algorithm) \ No newline at end of file +target_link_libraries(bm_ramp benchmark absl::span absl::algorithm) + +add_executable(bm_ADSR benchmarks/BM_ADSR.cpp sources/SIMDSSE.cpp) +target_link_libraries(bm_ADSR benchmark absl::span absl::algorithm) \ No newline at end of file diff --git a/benchmarks/BM_ADSR.cpp b/benchmarks/BM_ADSR.cpp new file mode 100644 index 00000000..264f88aa --- /dev/null +++ b/benchmarks/BM_ADSR.cpp @@ -0,0 +1,45 @@ +#include +#include "../sources/ADSREnvelope.h" +#include +#include +#include + +constexpr int fixedAmount { 12 }; +constexpr int envelopeSize { 2 << 16 }; +constexpr int attack = static_cast(envelopeSize / 4) - fixedAmount; +constexpr int decay = attack; +constexpr int release = attack; +constexpr int releaseTime = envelopeSize - static_cast(envelopeSize / 4); + + +static void Point(benchmark::State& state) { + std::vector output(state.range(0)); + sfz::ADSREnvelope envelope; + for (auto _ : state) { + envelope.reset(attack, release, 1.0, fixedAmount, decay, fixedAmount); + envelope.startRelease(releaseTime); + for(int offset = 0; offset < envelopeSize; offset += state.range(0)) + for (auto& out: output) + out = envelope.getNextValue(); + benchmark::DoNotOptimize(output); + } + state.counters["Per block"] = benchmark::Counter(envelopeSize / state.range(0), benchmark::Counter::kIsIterationInvariantRate | benchmark::Counter::kInvert); +} + +static void Block(benchmark::State& state) { + std::vector output(state.range(0)); + sfz::ADSREnvelope envelope; + for (auto _ : state) { + envelope.reset(attack, release, 1.0, fixedAmount, decay, fixedAmount); + envelope.startRelease(releaseTime); + for(int offset = 0; offset < envelopeSize; offset += state.range(0)) + envelope.getBlock(absl::MakeSpan(output)); + benchmark::DoNotOptimize(output); + } + + state.counters["Per block"] = benchmark::Counter(envelopeSize / state.range(0), benchmark::Counter::kIsIterationInvariantRate | benchmark::Counter::kInvert); +} + +BENCHMARK(Point)->RangeMultiplier(2)->Range((2<<6), (2<<11)); +BENCHMARK(Block)->RangeMultiplier(2)->Range((2<<6), (2<<11)); +BENCHMARK_MAIN(); \ No newline at end of file diff --git a/sources/ADSREnvelope.cpp b/sources/ADSREnvelope.cpp new file mode 100644 index 00000000..2bc6f41c --- /dev/null +++ b/sources/ADSREnvelope.cpp @@ -0,0 +1,217 @@ +#include "Globals.h" +#include "SIMDHelpers.h" +#include "Helpers.h" +#include "ADSREnvelope.h" +#include + +namespace sfz +{ + +template +void ADSREnvelope::reset(int attack, int release, Type sustain, int delay, int decay, int hold, Type start, Type depth) noexcept +{ + ASSERT(start <= 1.0f); + ASSERT(sustain <= 1.0f); + + sustain = std::clamp(sustain, 0.0, 1.0); + start = std::clamp(start, 0.0, 1.0); + + currentState = State::Done; + this->delay = delay; + this->attack = attack; + this->decay = decay; + this->release = release; + this->hold = hold; + this->start = depth * start; + this->sustain = depth * sustain; + this->peak = depth; + releaseDelay = 0; + shouldRelease = false; + step = 0.0; + currentValue = this->start; + currentState = State::Delay; +} + +template +Type ADSREnvelope::getNextValue() noexcept +{ + if (shouldRelease && releaseDelay-- == 0) + { + currentState = State::Release; + step = std::exp((std::log(config::virtuallyZero) - std::log(currentValue)) / (release > 0 ? release : 1)); + } + + switch(currentState) + { + case State::Delay: + if (delay-- > 0) + return start; + + currentState = State::Attack; + step = (1.0 - currentValue) / (attack > 0 ? attack : 1); + [[fallthrough]]; + case State::Attack: + if (attack-- > 0) + { + currentValue += step; + return currentValue; + } + + currentState = State::Hold; + currentValue = 1.0; + [[fallthrough]]; + case State::Hold: + if (hold-- > 0) + return currentValue; + + step = std::exp(std::log(sustain) / (decay > 0 ? decay : 1)); + currentState = State::Decay; + [[fallthrough]]; + case State::Decay: + if (decay-- > 0) + { + currentValue *= step; + return currentValue; + } + + currentState = State::Sustain; + currentValue = sustain; + [[fallthrough]]; + case State::Sustain: + return currentValue; + case State::Release: + if (release-- > 0) + { + currentValue *= step; + return currentValue; + } + + currentState = State::Done; + currentValue = 0.0; + [[fallthrough]]; + default: + return 0.0; + } +} + +template +void ADSREnvelope::getBlock(absl::Span output) noexcept +{ + auto originalSpan = output; + auto remainingSamples = static_cast(output.size()); + int length; + switch(currentState) + { + case State::Delay: + length = min(remainingSamples, delay); + ::fill(output, currentValue); + output.remove_prefix(length); + remainingSamples -= length; + delay -= length; + if (remainingSamples == 0) + break; + + currentState = State::Attack; + step = (peak - start) / (attack > 0 ? attack : 1); + [[fallthrough]]; + case State::Attack: + length = min(remainingSamples, attack); + currentValue = ::linearRamp(output, currentValue, step); + output.remove_prefix(length); + remainingSamples -= length; + attack -= length; + if (remainingSamples == 0) + break; + + currentValue = peak; + currentState = State::Hold; + [[fallthrough]]; + case State::Hold: + length = min(remainingSamples, hold); + ::fill(output, currentValue); + output.remove_prefix(length); + remainingSamples -= length; + hold -= length; + if (remainingSamples == 0) + break; + + step = std::exp(std::log(sustain) / (decay > 0 ? decay : 1)); + currentState = State::Decay; + [[fallthrough]]; + case State::Decay: + length = min(remainingSamples, decay); + currentValue = ::multiplicativeRamp(output, currentValue, step); + output.remove_prefix(length); + remainingSamples -= length; + decay -= length; + if (remainingSamples == 0) + break; + + currentValue = sustain; + currentState = State::Sustain; + [[fallthrough]]; + case State::Sustain: + break; + case State::Release: + length = min(remainingSamples, release); + currentValue = ::multiplicativeRamp(output, currentValue, step); + output.remove_prefix(length); + remainingSamples -= length; + release -= length; + if (remainingSamples == 0) + break; + + currentValue = 0.0; + currentState = State::Done; + [[fallthrough]]; + case State::Done: + [[fallthrough]]; + default: + break; + } + ::fill(output, currentValue); + + if (shouldRelease) + { + remainingSamples = static_cast(originalSpan.size()); + if (releaseDelay > remainingSamples) + { + releaseDelay -= remainingSamples; + return; + } + + originalSpan.remove_prefix(releaseDelay); + currentValue = originalSpan.front(); + step = std::exp((std::log(config::virtuallyZero) - std::log(currentValue)) / (release > 0 ? release : 1)); + remainingSamples -= releaseDelay; + length = min(remainingSamples, release); + currentState = State::Release; + currentValue = ::multiplicativeRamp(originalSpan, currentValue, step); + originalSpan.remove_prefix(length); + release -= length; + + if (release == 0) + { + currentValue = 0.0; + currentState = State::Done; + ::fill(originalSpan, 0.0); + } + + } +} + +template +void ADSREnvelope::startRelease(int releaseDelay) noexcept +{ + shouldRelease = true; + this->releaseDelay = releaseDelay; +} + +template +constexpr bool ADSREnvelope::isSmoothing() noexcept +{ + return currentState != State::Done; +} + +template class ADSREnvelope; +} diff --git a/sources/ADSREnvelope.h b/sources/ADSREnvelope.h index a5e1e8cd..dff7913d 100644 --- a/sources/ADSREnvelope.h +++ b/sources/ADSREnvelope.h @@ -1,5 +1,4 @@ -#include "Globals.h" - +#include namespace sfz { @@ -7,34 +6,30 @@ template class ADSREnvelope { public: - struct Description - { - Description() - : depth(1) {} - Descrition(Type depth) - : depth(depth) {} - - int delay { 0 }; - int attack { 0 }; - int decay { 0 }; - int release { 0 }; - int hold { 0 }; - float start { 0 }; - float sustain { 0 }; - Type depth; - }; - ADSREnvelope() = default; - void reset(Description desc) - { - - } + void reset(int attack, int release, Type sustain=1.0, int delay = 0, int decay = 0, int hold = 0, Type start=0.0, Type depth=1) noexcept; + Type getNextValue() noexcept; + void getBlock(absl::Span output) noexcept; + void startRelease(int releaseDelay) noexcept; + constexpr bool isSmoothing() noexcept; private: enum class State { - Delay, Attack, Hold, Sustain, Release, Done + Delay, Attack, Hold, Decay, Sustain, Release, Done }; - State currentState { Done }; + State currentState { State::Done }; + Type currentValue { 0.0 }; + Type step { 0.0 }; + int delay { 0 }; + int attack { 0 }; + int decay { 0 }; + int release { 0 }; + int hold { 0 }; + Type start { 0 }; + Type peak { 0 }; + Type sustain { 0 }; + int releaseDelay { 0 }; + bool shouldRelease { false }; }; } \ No newline at end of file diff --git a/sources/SIMDDummy.cpp b/sources/SIMDDummy.cpp index 829c35e5..e9b4654d 100644 --- a/sources/SIMDDummy.cpp +++ b/sources/SIMDDummy.cpp @@ -68,13 +68,13 @@ void loopingSFZIndex(absl::Span jumps, absl::Span -void linearRamp(absl::Span output, float start, float step) noexcept +float linearRamp(absl::Span output, float start, float step) noexcept { - linearRamp(output, start, step); + return linearRamp(output, start, step); } template<> -void multiplicativeRamp(absl::Span output, float start, float step) noexcept +float multiplicativeRamp(absl::Span output, float start, float step) noexcept { - multiplicativeRamp(output, start, step); + return multiplicativeRamp(output, start, step); } \ No newline at end of file diff --git a/sources/SIMDHelpers.h b/sources/SIMDHelpers.h index d05516ff..8b5add13 100644 --- a/sources/SIMDHelpers.h +++ b/sources/SIMDHelpers.h @@ -206,11 +206,12 @@ inline void snippetRampLinear(T*& output, T& value, T step) } template -void linearRamp(absl::Span output, T start, T step) noexcept +T linearRamp(absl::Span output, T start, T step) noexcept { auto* out = output.begin(); while(out < output.end()) snippetRampLinear(out, start, step); + return start; } template @@ -221,16 +222,17 @@ inline void snippetRampMultiplicative(T*& output, T& value, T step) } template -void multiplicativeRamp(absl::Span output, T start, T step) noexcept +T multiplicativeRamp(absl::Span output, T start, T step) noexcept { auto* out = output.begin(); while(out < output.end()) snippetRampMultiplicative(out, start, step); + return start; } template<> -void linearRamp(absl::Span output, float start, float step) noexcept; +float linearRamp(absl::Span output, float start, float step) noexcept; template<> -void multiplicativeRamp(absl::Span output, float start, float step) noexcept; \ No newline at end of file +float multiplicativeRamp(absl::Span output, float start, float step) noexcept; \ No newline at end of file diff --git a/sources/SIMDSSE.cpp b/sources/SIMDSSE.cpp index bce03f72..aed75759 100644 --- a/sources/SIMDSSE.cpp +++ b/sources/SIMDSSE.cpp @@ -306,7 +306,7 @@ void loopingSFZIndex(absl::Span jumps, absl::Span -void linearRamp(absl::Span output, float value, float step) noexcept +float linearRamp(absl::Span output, float value, float step) noexcept { auto* out = output.begin(); const auto* lastAligned = prevAligned(output.end()); @@ -328,10 +328,11 @@ void linearRamp(absl::Span output, float value, float step) value = _mm_cvtss_f32(mmValue); while(out < output.end()) snippetRampLinear(out, value, step); + return value; } template<> -void multiplicativeRamp(absl::Span output, float value, float step) noexcept +float multiplicativeRamp(absl::Span output, float value, float step) noexcept { auto* out = output.begin(); const auto* lastAligned = prevAligned(output.end()); @@ -353,4 +354,5 @@ void multiplicativeRamp(absl::Span output, float value, floa value = _mm_cvtss_f32(mmValue); while(out < output.end()) snippetRampMultiplicative(out, value, step); + return value; } \ No newline at end of file diff --git a/tests/ADSREnvelopeT.cpp b/tests/ADSREnvelopeT.cpp new file mode 100644 index 00000000..1bec2fa0 --- /dev/null +++ b/tests/ADSREnvelopeT.cpp @@ -0,0 +1,189 @@ +#include "catch2/catch.hpp" +#include "../sources/ADSREnvelope.h" +#include +#include +#include +#include +#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; + std::array output; + std::array expected { 0.0, 0.0, 0.0, 0.0, 0.0 }; + for (auto& out: output) + out = envelope.getNextValue(); + REQUIRE( approxEqual(output, expected) ); + + absl::c_fill(output, -1.0); + envelope.getBlock(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[ADSREnvelope] Attack") +{ + sfz::ADSREnvelope envelope; + envelope.reset(2, 0); + std::array output; + std::array expected { 0.5, 1.0, 1.0, 1.0, 1.0 }; + for (auto& out: output) + out = envelope.getNextValue(); + REQUIRE( approxEqual(output, expected) ); + + envelope.reset(2, 0); + absl::c_fill(output, -1.0); + envelope.getBlock(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[ADSREnvelope] Attack again") +{ + sfz::ADSREnvelope envelope; + envelope.reset(3, 0); + std::array output; + std::array expected { 0.33333, 0.66667, 1.0, 1.0, 1.0 }; + for (auto& out: output) + out = envelope.getNextValue(); + REQUIRE( approxEqual(output, expected) ); + + envelope.reset(3, 0); + absl::c_fill(output, -1.0); + envelope.getBlock(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[ADSREnvelope] Release") +{ + sfz::ADSREnvelope envelope; + envelope.reset(2, 4); + envelope.startRelease(2); + std::array output; + std::array expected { 0.5, 1.0, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f }; + for (auto& out: output) + out = envelope.getNextValue(); + REQUIRE( approxEqual(output, expected) ); + + envelope.reset(2, 4); + envelope.startRelease(2); + absl::c_fill(output, -1.0); + envelope.getBlock(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[ADSREnvelope] Delay") +{ + sfz::ADSREnvelope envelope; + envelope.reset(2, 4, 1.0f, 2); + std::array output; + envelope.startRelease(4); + std::array expected { 0.0, 0.0, 0.5, 1.0, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f }; + for (auto& out: output) + out = envelope.getNextValue(); + REQUIRE( approxEqual(output, expected) ); + + envelope.reset(2, 4, 1.0f, 2); + envelope.startRelease(4); + absl::c_fill(output, -1.0); + envelope.getBlock(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[ADSREnvelope] Lower sustain") +{ + sfz::ADSREnvelope envelope; + envelope.reset(2, 4, 0.5, 2); + std::array output; + std::array expected { 0.0, 0.0, 0.5, 1.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 }; + for (auto& out: output) + out = envelope.getNextValue(); + REQUIRE( approxEqual(output, expected) ); + + envelope.reset(2, 4, 0.5, 2); + absl::c_fill(output, -1.0); + envelope.getBlock(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[ADSREnvelope] Decay") +{ + sfz::ADSREnvelope envelope; + envelope.reset(2, 4, 0.5, 2, 2); + std::array output; + std::array expected { 0.0, 0.0, 0.5, 1.0, 0.707107, 0.5, 0.5, 0.5, 0.5, 0.5 }; + for (auto& out: output) + out = envelope.getNextValue(); + REQUIRE( approxEqual(output, expected) ); + + envelope.reset(2, 4, 0.5, 2, 2); + absl::c_fill(output, -1.0); + envelope.getBlock(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[ADSREnvelope] Hold") +{ + sfz::ADSREnvelope envelope; + envelope.reset(2, 4, 0.5, 2, 2, 2); + std::array output; + std::array expected { 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 0.707107, 0.5, 0.5, 0.5, 0.5, 0.5 }; + for (auto& out: output) + out = envelope.getNextValue(); + REQUIRE( approxEqual(output, expected) ); + + envelope.reset(2, 4, 0.5, 2, 2, 2); + absl::c_fill(output, -1.0); + envelope.getBlock(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[ADSREnvelope] Hold with release") +{ + sfz::ADSREnvelope envelope; + envelope.reset(2, 4, 0.5, 2, 2, 2); + envelope.startRelease(8); + std::array output; + std::array expected { 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 0.707107, 0.5, 0.05, 0.005, 0.0005, 0.00005, 0.0, 0.0 }; + for (auto& out: output) + out = envelope.getNextValue(); + + REQUIRE( approxEqual(output, expected) ); + envelope.reset(2, 4, 0.5, 2, 2, 2); + envelope.startRelease(8); + absl::c_fill(output, -1.0); + envelope.getBlock(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} + +TEST_CASE("[ADSREnvelope] Hold with release 2") +{ + sfz::ADSREnvelope envelope; + envelope.reset(2, 4, 0.5, 2, 2, 2); + envelope.startRelease(4); + std::array output; + std::array expected { 0.0, 0.0, 0.5, 1.0, 0.08409, 0.00707, 0.000594604, 0.00005, 0.0, 0.0, 0.0, 0.0 }; + for (auto& out: output) + out = envelope.getNextValue(); + REQUIRE( approxEqual(output, expected) ); + envelope.reset(2, 4, 0.5, 2, 2, 2); + envelope.startRelease(4); + absl::c_fill(output, -1.0); + envelope.getBlock(absl::MakeSpan(output)); + REQUIRE( approxEqual(output, expected) ); +} \ No newline at end of file