diff --git a/CMakeLists.txt b/CMakeLists.txt index 80e4da4..2cbb830 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,8 @@ if(NOT CMAKE_CROSSCOMPILING) "lib/grit/audio/envelope/envelope_follower_test.cpp" + "lib/grit/audio/filter/biquad_test.cpp" + "lib/grit/audio/music/note_test.cpp" "lib/grit/audio/noise/dither_test.cpp" diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 2f17ee3..6e7a9af 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -39,6 +39,7 @@ target_sources(gritwave-grit INTERFACE "grit/audio/envelope/envelope_follower.hpp" "grit/audio/filter.hpp" + "grit/audio/filter/biquad.hpp" "grit/audio/filter/dynamic_smoothing.hpp" "grit/audio/mix.hpp" diff --git a/lib/grit/audio/filter.hpp b/lib/grit/audio/filter.hpp index 66b87be..fcd0d78 100644 --- a/lib/grit/audio/filter.hpp +++ b/lib/grit/audio/filter.hpp @@ -3,4 +3,5 @@ /// \defgroup grit-audio-filter Filter /// \ingroup grit-audio +#include #include diff --git a/lib/grit/audio/filter/biquad.hpp b/lib/grit/audio/filter/biquad.hpp new file mode 100644 index 0000000..849c01b --- /dev/null +++ b/lib/grit/audio/filter/biquad.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include + +namespace grit { + +/// Calculates coefficents for 2nd order biquad filters +/// \see BiquadTDF2 +/// \ingroup grit-audio-filter +template +struct BiquadCoefficients +{ + using value_type = Float; + using SampleType = Float; + + [[nodiscard]] static constexpr auto makeBypass() -> etl::array + { + return {Float(1), Float(0), Float(0), Float(1), Float(0), Float(0)}; + } +}; + +/// \brief 2nd order IIR filter using the transpose direct form 2 structure. +/// \ingroup grit-audio-filter +template +struct BiquadTDF2 +{ + using value_type = Float; + using SampleType = Float; + using Coefficients = BiquadCoefficients; + + constexpr BiquadTDF2() = default; + + constexpr auto setCoefficients(etl::span coefficients) -> void + { + etl::copy(coefficients.begin(), coefficients.end(), _coefficients.begin()); + } + + constexpr auto reset() -> void + { + _z[0] = Float(0); + _z[1] = Float(0); + } + + [[nodiscard]] constexpr auto operator()(Float x) -> Float + { + auto const b0 = _coefficients[Index::B0]; + auto const b1 = _coefficients[Index::B1]; + auto const b2 = _coefficients[Index::B2]; + auto const a1 = _coefficients[Index::A1]; + auto const a2 = _coefficients[Index::A2]; + + auto const y = b0 * x + _z[0]; + _z[0] = b1 * x - a1 * y + _z[1]; + _z[1] = b2 * x - a2 * y; + return y; + } + +private: + enum Index + { + B0, + B1, + B2, + A0, + A1, + A2, + NumCoefficients, + }; + + etl::array _coefficients{Coefficients::makeBypass()}; + etl::array _z{}; +}; + +} // namespace grit diff --git a/lib/grit/audio/filter/biquad_test.cpp b/lib/grit/audio/filter/biquad_test.cpp new file mode 100644 index 0000000..8a1e2b0 --- /dev/null +++ b/lib/grit/audio/filter/biquad_test.cpp @@ -0,0 +1,53 @@ +#include "biquad.hpp" + +#include + +#include +#include +#include + +TEMPLATE_TEST_CASE("audio/filter: BiquadCoefficients::makeBypass", "", float, double) +{ + using Float = TestType; + using Coefficients = grit::BiquadCoefficients; + + static constexpr auto bypass = Coefficients::makeBypass(); + STATIC_REQUIRE(bypass.size() == 6U); + + REQUIRE_THAT(bypass[0], Catch::Matchers::WithinAbs(1.0, 1e-6)); + REQUIRE_THAT(bypass[1], Catch::Matchers::WithinAbs(0.0, 1e-6)); + REQUIRE_THAT(bypass[2], Catch::Matchers::WithinAbs(0.0, 1e-6)); + REQUIRE_THAT(bypass[3], Catch::Matchers::WithinAbs(1.0, 1e-6)); + REQUIRE_THAT(bypass[4], Catch::Matchers::WithinAbs(0.0, 1e-6)); + REQUIRE_THAT(bypass[5], Catch::Matchers::WithinAbs(0.0, 1e-6)); +} + +TEMPLATE_TEST_CASE("audio/filter: BiquadTDF2", "", float, double) +{ + using Float = TestType; + using Filter = grit::BiquadTDF2; + + static constexpr auto iterations = 1'000; + + auto rng = etl::xoshiro128plusplus{Catch::getSeed()}; + auto dist = etl::uniform_real_distribution{Float(-1), Float(+1)}; + + SECTION("default coefficents are bypass") + { + auto filter = Filter{}; + for (auto i{0}; i < iterations; ++i) { + auto const x = dist(rng); + auto const y = filter(x); + REQUIRE_THAT(y, Catch::Matchers::WithinAbs(x, 1e-6)); + } + + filter.setCoefficients(Filter::Coefficients::makeBypass()); + filter.reset(); + + for (auto i{0}; i < iterations; ++i) { + auto const x = dist(rng); + auto const y = filter(x); + REQUIRE_THAT(y, Catch::Matchers::WithinAbs(x, 1e-6)); + } + } +}