From 3f3ffc776c34ba171eab5004d1586a626665536b Mon Sep 17 00:00:00 2001 From: Paul Walker Date: Thu, 2 May 2024 18:36:24 -0400 Subject: [PATCH] Follower, Slew, Average inspired by BogAudio DSP Add a low pass envelope follower, based on Puckett/Bog Running Average, Slew Limiter, and tests thereof --- .../basic-blocks/dsp/FollowSlewAndSmooth.h | 177 ++++++++++++++++++ tests/dsp_tests.cpp | 57 ++++++ 2 files changed, 234 insertions(+) create mode 100644 include/sst/basic-blocks/dsp/FollowSlewAndSmooth.h diff --git a/include/sst/basic-blocks/dsp/FollowSlewAndSmooth.h b/include/sst/basic-blocks/dsp/FollowSlewAndSmooth.h new file mode 100644 index 0000000..8e3e393 --- /dev/null +++ b/include/sst/basic-blocks/dsp/FollowSlewAndSmooth.h @@ -0,0 +1,177 @@ +/* + * sst-basic-blocks - an open source library of core audio utilities + * built by Surge Synth Team. + * + * Provides a collection of tools useful on the audio thread for blocks, + * modulation, etc... or useful for adapting code to multiple environments. + * + * Copyright 2023, various authors, as described in the GitHub + * transaction log. Parts of this code are derived from similar + * functions original in Surge or ShortCircuit. + * + * sst-basic-blocks is released under the GNU General Public Licence v3 + * or later (GPL-3.0-or-later). The license is found in the "LICENSE" + * file in the root of this repository, or at + * https://www.gnu.org/licenses/gpl-3.0.en.html. + * + * A very small number of explicitly chosen header files can also be + * used in an MIT/BSD context. Please see the README.md file in this + * repo or the comments in the individual files. Only headers with an + * explicit mention that they are dual licensed may be copied and reused + * outside the GPL3 terms. + * + * All source in sst-basic-blocks available at + * https://github.com/surge-synthesizer/sst-basic-blocks + */ + +#ifndef INCLUDE_SST_BASIC_BLOCKS_DSP_ENVELOPEFOLLOWER_H +#define INCLUDE_SST_BASIC_BLOCKS_DSP_ENVELOPEFOLLOWER_H + +#include +#include + +namespace sst::basic_blocks::dsp +{ +/* + * An LPF on the abs of the signal; equivalent roughly + * to the BogAudio PucketEnvelopeFollower + */ +struct LowPassEnvelopeFollower +{ + float yp[2]{0, 0}, xp[2]{0, 0}; + float a[3]{1., 0, 0}, b[3]{1., 0, 0}; + float xc[3]{0, 0, 0}, yc[3]{0, 0, 0}; + LowPassEnvelopeFollower() { reset(); } + + void setSensitivity01(float sens01, float sampleRate) + { + static constexpr float maxCutoff = 10000.0f; + static constexpr float minCutoff = 100.0f; + auto s01 = std::clamp(sens01, 0.f, 1.f); + auto co = (maxCutoff - minCutoff) * s01 + minCutoff; + + // todo - setup coefficients here + static constexpr float Q{0.001}; + auto omega = 2 * M_PI * co / sampleRate; + auto alpha = sin(omega) / (2 * Q); + auto cosw = cos(omega); + + a[0] = 1 + alpha; + a[1] = -2 * cosw; + a[2] = 1 - alpha; + b[0] = (1 - cosw) / 2; + b[1] = 2 * b[0]; + b[2] = b[0]; + resetCoeff(); + } + + void reset() + { + a[0] = 1.; + a[1] = 0.; + a[2] = 0.; + b[0] = 1.; + b[1] = 0.; + b[2] = 0.; + yp[0] = 0; + yp[1] = 0; + xp[0] = 0; + xp[1] = 0; + resetCoeff(); + } + + void resetCoeff() + { + auto oa0 = 1.0 / a[0]; + xc[0] = b[0] * oa0; + xc[1] = b[1] * oa0; + xc[2] = b[2] * oa0; + + yc[0] = 0; + yc[1] = -a[1] * oa0; + yc[2] = -a[2] * oa0; + } + + float step(float x) + { + x = std::fabs(x); + auto r = xc[0] * x + xc[1] * xp[0] + xc[2] * xp[1] + yc[1] * yp[0] + yc[2] * yp[1]; + + yp[1] = yp[0]; + yp[0] = r; + xp[1] = xp[0]; + xp[0] = x; + + return r; + } +}; + +struct SlewLimiter +{ + float delta{0}; + float last{0}; + + void setParams(float ms, float range, float sampleRate) + { + delta = range / ((ms / 1000.0f) * sampleRate); + } + + void setLast(float l) { last = l; } + void reset() { setLast(0.f); } + + float step(float x) + { + float res = x; + if (x > last) + { + res = std::min(last + delta, x); + } + else if (x < last) + { + res = std::max(last - delta, x); + } + + last = res; + return res; + } +}; + +struct RunningAverage +{ + float *storage{nullptr}; + size_t nPoints{0}; + size_t head{0}, tail{0}; + float avg{0}, oneOverN{1}; + RunningAverage(float *ontoStorage, size_t np) : storage{ontoStorage}, nPoints{np} + { + reset(); + oneOverN = 1.0 / (nPoints-1); + } + RunningAverage() = delete; + + void reset() + { + std::fill(storage, storage + nPoints, 0.f); + head = 0; + tail = 1; + avg = 0.f; + } + + float step(float x) + { + storage[head] = x; + + avg += (storage[head] - storage[tail]) * oneOverN; + head++; + if (head >= nPoints) + head = 0; + tail++; + if (tail >= nPoints) + tail = 0; + + return avg; + } +}; +} // namespace sst::basic_blocks::dsp + +#endif // BACONMUSIC_ENVELOPEFOLLOWER_H diff --git a/tests/dsp_tests.cpp b/tests/dsp_tests.cpp index e15ca29..d1fc8d0 100644 --- a/tests/dsp_tests.cpp +++ b/tests/dsp_tests.cpp @@ -39,6 +39,7 @@ #include #include "sst/basic-blocks/dsp/SSESincDelayLine.h" +#include "sst/basic-blocks/dsp/FollowSlewAndSmooth.h" TEST_CASE("lipol_sse basic", "[dsp]") { @@ -1084,4 +1085,60 @@ TEST_CASE("UIComponentLagHandler", "[dsp]") lag.process(); REQUIRE(g < 1.0); } +} + +TEST_CASE("Slew", "[dsp]") +{ + auto sl = sst::basic_blocks::dsp::SlewLimiter(); + sl.setParams(100, 1.0, 1000); + + for (int i=0; i<100; ++i) + { + auto val = sl.step(0.5); + if (i < 50) + REQUIRE(val == Approx((i+1) * 0.01)); + else + REQUIRE(val == 0.5); + } + + for (int i=0; i<200; ++i) + { + auto val = sl.step(-0.5); + if (i < 100) + REQUIRE(val == Approx(0.5 - (i+1) * 0.01).margin(1e-5)); + else + REQUIRE(val == -0.5); + + } +} + +TEST_CASE("Running Avg", "[dsp]") +{ + SECTION("Constants") + { + std::array data{}; + auto ra = sst::basic_blocks::dsp::RunningAverage(data.data(), data.size()); + for (int i = 0; i < data.size() - 1; ++i) + { + auto val = ra.step(3.2); + REQUIRE(val == Approx(3.2 * (i + 1) / 1000.0).margin(0.005)); + } + } + + + SECTION("RAMP") + { + std::array data{}; + auto ra = sst::basic_blocks::dsp::RunningAverage(data.data(), data.size()); + for (int i = 0; i < 500; ++i) + { + auto val = ra.step(i * 0.1); + if (i > data.size() - 1) + { + // Filled with a ramp. Average is start - end / count + auto avg = (i + (i - (data.size()-1 -1))) * 0.5 * 0.1; + REQUIRE( val == Approx(avg).margin(0.005)); + } + } + } } \ No newline at end of file