Skip to content

Commit

Permalink
Follower, Slew, Average inspired by BogAudio DSP
Browse files Browse the repository at this point in the history
Add a low pass envelope follower, based on Puckett/Bog

Running Average, Slew Limiter, and tests thereof
  • Loading branch information
baconpaul committed May 5, 2024
1 parent 4276b8f commit 3f3ffc7
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 0 deletions.
177 changes: 177 additions & 0 deletions include/sst/basic-blocks/dsp/FollowSlewAndSmooth.h
Original file line number Diff line number Diff line change
@@ -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 <algorithm>
#include <utility>

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
57 changes: 57 additions & 0 deletions tests/dsp_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

#include <iostream>
#include "sst/basic-blocks/dsp/SSESincDelayLine.h"
#include "sst/basic-blocks/dsp/FollowSlewAndSmooth.h"

TEST_CASE("lipol_sse basic", "[dsp]")
{
Expand Down Expand Up @@ -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<float, 1000> 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<float, 101> 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));
}
}
}
}

0 comments on commit 3f3ffc7

Please sign in to comment.