Skip to content

Instantly share code, notes, and snippets.

@rygrob
Last active October 15, 2024 18:44
Show Gist options
  • Save rygrob/be9dfc6391800bf7ba77ec8cad46f5ae to your computer and use it in GitHub Desktop.
Save rygrob/be9dfc6391800bf7ba77ec8cad46f5ae to your computer and use it in GitHub Desktop.
TPT Filters
#ifndef tpt_filters_h
#define tpt_filters_h
#include <cmath>
#include <numbers>
// MARK: - One-pole Filter
template<typename X>
struct Tpt_1pole {
enum class Type {
lpf, hpf, apf
};
Tpt_1pole() = default;
Tpt_1pole(Type type) : _type{type} {}
struct Params {
X fc = 1000;
};
auto reset(float sr) -> void
{
sp = 1 / sr;
_s = 0;
prepare({});
}
auto prepare(const Params& params) -> void
{
this->params = params;
const auto g = std::tan(std::numbers::pi_v<X> * params.fc * sp);
_G = g / (1 + g);
}
auto process(X x) -> X
{
// See: https://www.discodsp.net/VAFilterDesign_2.1.2.pdf (p. 76)
const auto v = _G * (x - _s);
const auto yl = v + _s;
const auto yh = x - yl;
const auto ya = yl - yh;
_s = yl + v;
switch (_type) {
case Type::lpf:
return yl;
case Type::hpf:
return yh;
case Type::apf:
return ya;
}
}
private:
const Type _type{Type::lpf};
Params params{};
X sp = 1 / 48000.f;
X _G{};
X _s{};
};
// MARK: - SVF
template<typename X>
struct Tpt_svf {
enum class Type {
lpf, hpf, bpfq, bpf1
};
Tpt_svf() = default;
Tpt_svf(Type type) : _type(type) {};
struct Params {
X fc = 1000;
X q = 0.707f;
};
auto reset(float sr) -> void
{
sp = 1 / sr;
_s1 = _s2 = 0;
prepare({});
}
auto prepare(const Params& params) -> void
{
this->params = params;
_g = std::tan(std::numbers::pi_v<X> * params.fc * sp);
_R2 = 1 / params.q;
_d = 1 / (1 + _R2 * _g + _g * _g);
}
auto process(X x) -> X
{
// See: https://www.discodsp.net/VAFilterDesign_2.1.2.pdf (p. 109)
const auto g1 = _R2 + _g;
const auto yh = _d * (x - g1 * _s1 - _s2);
const auto yb = yh * _g + _s1;
const auto yl = yb * _g + _s2;
_s1 = yh * _g + yb;
_s2 = yb * _g + yl;
switch (_type) {
case Type::lpf:
return yl;
case Type::hpf:
return yh;
case Type::bpfq:
return yb;
case Type::bpf1:
return _R2 * yb;
}
}
private:
const Type _type{Type::lpf};
Params params{};
X sp = 1 / 48000.f;
X _g{};
X _d{};
X _R2{};
X _s1{};
X _s2{};
};
// MARK: - Ladder Filter
template<typename X>
struct Tpt_ladder {
enum class Type {
lpf, hpf
};
Tpt_ladder() = default;
Tpt_ladder(Type type) : _type{type} {};
struct Params {
X fc = 1000;
X k = 0; // 0...4
};
auto reset(float sr) -> void
{
sp = 1 / sr;
_s0 = _s1 = _s2 = _s3 = 0;
prepare({});
}
auto prepare(const Params& params) -> void
{
this->params = params;
const auto g = std::tan(std::numbers::pi_v<X> * params.fc * sp);
_g = g;
_G = [&]() {
switch (_type) {
case Type::lpf:
return g / (1 + g);
case Type::hpf:
return 1 / (1 + g);
}
}();
}
auto process(X x) -> X
{
// See: https://www.discodsp.net/VAFilterDesign_2.1.2.pdf (p. 137)
const auto k = params.k;
const auto G = _G;
// Get state scalar, so we can say y = G * x + S.
const auto gp = [&]() {
switch (_type) {
case Type::lpf:
return 1 - G; // 1 / (1 + g)
case Type::hpf:
return -G;
}
}();
const auto s0 = _s0 * gp;
const auto s1 = _s1 * gp;
const auto s2 = _s2 * gp;
const auto s3 = _s3 * gp;
const auto big_G = G*G*G*G;
const auto S = G*G*G*s0 + G*G*s1 + G*s2 + s3;
const auto u = (x - k * S) / (1 + k * big_G);
const auto y0 = G * u + s0;
const auto y1 = G * y0 + s1;
const auto y2 = G * y1 + s2;
const auto y3 = G * y2 + s3;
_s0 = next_state(y0, _s0);
_s1 = next_state(y1, _s1);
_s2 = next_state(y2, _s2);
_s3 = next_state(y3, _s3);
return y3;
}
private:
const Type _type{Type::lpf};
Params params{};
X sp = 1 / 48000.f;
X _g{};
X _G{};
X _s0{};
X _s1{};
X _s2{};
X _s3{};
auto next_state(X y, X s) -> X
{
switch (_type) {
case Type::lpf:
return 2 * y - s;
case Type::hpf:
return 2 * _g * y + s; // This is indeed 2 * ylp - s
}
}
};
// MARK: - Transposed Sallen-Key Filter
template<typename X>
struct Tpt_tsk {
enum class Type {
lpf, hpf
};
Tpt_tsk() = default;
Tpt_tsk(Type type) : _type{type} {};
struct Params {
X fc = 1000;
X k = 0; // 0...2
};
auto reset(float sr) -> void
{
sp = 1 / sr;
_s0 = _s1 = 0;
prepare({});
}
auto prepare(const Params& params) -> void
{
this->params = params;
const auto g = std::tan(std::numbers::pi_v<X> * params.fc * sp);
_g = g;
_G = [&]() {
switch (_type) {
case Type::lpf:
return g / (1 + g);
case Type::hpf:
return 1 / (1 + g);
}
}();
}
auto process(X x) -> X
{
// See: https://www.discodsp.net/VAFilterDesign_2.1.2.pdf (p. 153)
const auto k = params.k;
const auto G = _G;
// Get state scalar, so we can say y = G * x + S.
const auto gp = [&]() {
switch (_type) {
case Type::lpf:
return 1 - G; // 1 / (1 + g)
case Type::hpf:
return -G;
}
}();
const auto s0 = _s0 * gp;
const auto s1 = _s1 * gp;
// Also: https://www.youtube.com/watch?v=WatTtm8Py48
const auto u = (-G*G*k*x - G*k*s0 + G*k*x + k*s0 - k*s1) / (G*G*k - G*k + 1);
const auto xp = x + u;
const auto y0 = G * xp + s0;
const auto y1 = G * y0 + s1;
// In lpf mode, for example, we can also get:
// const auto y0h = xp - y0;
// const auto y1h = y0 - y1; // yb
// const auto yh = y0h - y1h; // yh
_s0 = next_state(y0, _s0);
_s1 = next_state(y1, _s1);
return y1;
}
private:
const Type _type{Type::lpf};
Params params{};
X sp = 1 / 48000.f;
X _g{};
X _G{};
X _s0{};
X _s1{};
auto next_state(X y, X s) -> X
{
switch (_type) {
case Type::lpf:
return 2 * y - s;
case Type::hpf:
return 2 * _g * y + s; // This is indeed 2 * ylp - s
}
}
};
// MARK: - Diode Ladder Filter
template<typename X>
struct Tpt_diode {
struct Params {
X fc = 1000;
X k = 0; // 0...16
};
auto reset(float sr) -> void
{
sp = 1 / sr;
_s1 = _s2 = _s3 = _s4 = 0;
prepare({});
}
auto prepare(const Params& params) -> void
{
this->params = params;
const auto g = std::tan(std::numbers::pi_v<X> * params.fc * sp);
_G = g / (1 + g);
}
auto process(X x) -> X
{
// See: https://www.discodsp.net/VAFilterDesign_2.1.2.pdf (p. 170)
const auto k = params.k;
const auto G = _G;
const auto gp = 1 - G; // 1 / (1 + g)
const auto s1 = _s1 * gp;
const auto s2 = _s2 * gp;
const auto s3 = _s3 * gp;
const auto s4 = _s4 * gp;
const auto gh = 0.5f * G;
const auto g34 = gh;
const auto g23 = gh / (1 - gh * gh);
const auto g12 = gh / (1 - gh * g23);
const auto g01 = 2 * gh / (1 - 2 * gh * g12);
const auto s34 = s4;
const auto s23 = (gh * s4 + s3) / (1 - gh * gh);
const auto s12 = (gh * s23 + s2) / (1 - gh * g23);
const auto s01 = (2 * gh * s12 + s1) / (1 - 2 * gh * g12);
const auto g04 = g01*g12*g23*g34;
const auto s04 = g12*g23*g34*s01 + g23*g34*s12 + g34*s23 + s34;
const auto u = (x - k * s04) / (1 + k * g04);
const auto y1 = g01 * u + s01;
const auto y2 = g12 * y1 + s12;
const auto y3 = g23 * y2 + s23;
const auto y4 = g34 * y3 + s34;
_s1 = 2 * y1 - _s1;
_s2 = 2 * y2 - _s2;
_s3 = 2 * y3 - _s3;
_s4 = 2 * y4 - _s4;
return y4;
}
private:
Params params{};
X sp = 1 / 48000.f;
X _G{};
X _s1{};
X _s2{};
X _s3{};
X _s4{};
};
#endif /* tpt_filters_h */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment