#include "biquad.h" #include "catch2/catch.hpp" #include "constants.h" #include "simd.h" #include #include #include #include #include #include static inline void lowpassCoefs(const int bufferSize, const float *__restrict alpha, const float *__restrict cosW0, float *__restrict b0, float *__restrict b1, float *__restrict b2, float *__restrict a1, float *__restrict a2) { #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { const auto a0 = 1.f + alpha[i]; b0[i] = (1.f - cosW0[i]) / (2.f * a0); b1[i] = (1.f - cosW0[i]) / a0; b2[i] = (1.f - cosW0[i]) / (2.f * a0); a1[i] = -2.f * cosW0[i] / a0; a2[i] = (1.f - alpha[i]) / a0; } } static inline void highpassCoefs(const int bufferSize, const float *__restrict alpha, const float *__restrict cosW0, float *__restrict b0, float *__restrict b1, float *__restrict b2, float *__restrict a1, float *__restrict a2) { #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { const auto a0 = 1.f + alpha[i]; b0[i] = (1.f + cosW0[i]) / (2.f * a0); b1[i] = -(1.f + cosW0[i]) / a0; b2[i] = (1.f + cosW0[i]) / (2.f * a0); a1[i] = -2.f * cosW0[i] / a0; a2[i] = (1.f - alpha[i]) / a0; } } static inline void bandpassCoefs(const int bufferSize, const float *__restrict q, const float *__restrict alpha, const float *__restrict cosW0, float *__restrict b0, float *__restrict b1, float *__restrict b2, float *__restrict a1, float *__restrict a2) { #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { const auto a0 = 1.f + alpha[i]; b0[i] = q[i] * alpha[i] / a0; b1[i] = 0.f; b2[i] = -q[i] * alpha[i] / a0; a1[i] = -2.f * cosW0[i] / a0; a2[i] = (1.f - alpha[i]) / a0; } } static inline void notchCoefs(const int bufferSize, const float *__restrict alpha, const float *__restrict cosW0, float *__restrict b0, float *__restrict b1, float *__restrict b2, float *__restrict a1, float *__restrict a2) { #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { const auto a0 = 1.f + alpha[i]; b0[i] = 1.f / a0; b1[i] = -2.f * cosW0[i] / a0; b2[i] = 1.f / a0; a1[i] = -2.f * cosW0[i] / a0; a2[i] = (1.f - alpha[i]) / a0; } } static inline void allpassCoefs(const int bufferSize, const float *__restrict alpha, const float *__restrict cosW0, float *__restrict b0, float *__restrict b1, float *__restrict b2, float *__restrict a1, float *__restrict a2) { #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { const auto a0 = 1.f + alpha[i]; b0[i] = (1.f - alpha[i]) / a0; b1[i] = -2.f * cosW0[i] / a0; b2[i] = (1.f + alpha[i]) / a0; a1[i] = -2.f * cosW0[i] / a0; a2[i] = (1.f - alpha[i]) / a0; } } static inline void eqBandCoefs(const int bufferSize, const float *__restrict dbGain, const float *__restrict alpha, const float *__restrict cosW0, float *__restrict b0, float *__restrict b1, float *__restrict b2, float *__restrict a1, float *__restrict a2) { // A = 10^(dbGain / 40) float A[bufferSize]; #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { A[i] = dbGain[i] / 40.f; } vfastpow10_array(A, A, bufferSize); #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { const auto a0 = 1.f + alpha[i] / A[i]; b0[i] = (1.f + alpha[i] * A[i]) / a0; b1[i] = -2.f * cosW0[i] / a0; b2[i] = (1.f - alpha[i] * A[i]) / a0; a1[i] = -2.f * cosW0[i] / a0; a2[i] = (1.f - alpha[i] / A[i]) / a0; } } static inline void lowShelfCoefs(const int bufferSize, const float *__restrict dbGain, const float *__restrict alpha, const float *__restrict cosW0, float *__restrict b0, float *__restrict b1, float *__restrict b2, float *__restrict a1, float *__restrict a2) { // A = 10^(dbGain / 40) float A[bufferSize]; float sqrtA[bufferSize]; #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { A[i] = dbGain[i] / 40.f; } vfastpow10_array(A, A, bufferSize); vfastnanzero_array(A, A, bufferSize); vfastclamp_array(A, 1e-6f, 1e6f, A, bufferSize); vfastsqrt_array(A, sqrtA, bufferSize); #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { const auto a0 = A[i] + 1.f + (A[i] - 1.f) * cosW0[i] + 2.f * sqrtA[i] * alpha[i]; b0[i] = A[i] * (A[i] + 1.f - (A[i] - 1.f) * cosW0[i] + 2.f * sqrtA[i] * alpha[i]) / a0; b1[i] = 2.f * A[i] * (A[i] - 1.f - (A[i] + 1.f) * cosW0[i]) / a0; b2[i] = A[i] * (A[i] + 1.f - (A[i] - 1.f) * cosW0[i] - 2.f * sqrtA[i] * alpha[i]) / a0; a1[i] = -2.f * (A[i] - 1.f + (A[i] + 1.f) * cosW0[i]) / a0; a2[i] = (A[i] + 1.f + (A[i] - 1.f) * cosW0[i] - 2.f * sqrtA[i] * alpha[i]) / a0; } } static inline void highShelfCoefs(const int bufferSize, const float *__restrict dbGain, const float *__restrict alpha, const float *__restrict cosW0, float *__restrict b0, float *__restrict b1, float *__restrict b2, float *__restrict a1, float *__restrict a2) { // A = 10^(dbGain / 40) float A[bufferSize]; float sqrtA[bufferSize]; #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { A[i] = dbGain[i] / 40.f; } vfastpow10_array(A, A, bufferSize); vfastnanzero_array(A, A, bufferSize); vfastclamp_array(A, 1e-6f, 1e6f, A, bufferSize); vfastsqrt_array(A, sqrtA, bufferSize); #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { const auto a0 = A[i] + 1.f - (A[i] - 1.f) * cosW0[i] + 2.f * sqrtA[i] * alpha[i]; b0[i] = A[i] * (A[i] + 1.f + (A[i] - 1.f) * cosW0[i] + 2.f * sqrtA[i] * alpha[i]) / a0; b1[i] = -2.f * A[i] * (A[i] - 1.f + (A[i] + 1.f) * cosW0[i]) / a0; b2[i] = A[i] * (A[i] + 1.f + (A[i] - 1.f) * cosW0[i] - 2.f * sqrtA[i] * alpha[i]) / a0; a1[i] = 2.f * (A[i] - 1.f - (A[i] + 1.f) * cosW0[i]) / a0; a2[i] = (A[i] + 1.f - (A[i] - 1.f) * cosW0[i] - 2.f * sqrtA[i] * alpha[i]) / a0; } } BiQuadFilter::BiQuadFilter(int sampleRate) : sampleRate(sampleRate), nyquistLimit((float)sampleRate / 2.f), w0Coef(2.f * M_PI / (float)sampleRate), xn2(0.f), xn1(0.f), yn2(0.f), yn1(0.f) {} void BiQuadFilter::processBuffer(BufferF32 frequencyInput, BufferF32 dbGainInput, BufferF32 qInput, BufferF32 input, BufferF32 output, Mode mode) { if (mode == Mode::BYPASS) { if (!input.isSameStorage(output)) { output.set(0, input); } reset(); return; } const auto bufferSize = input.getFrameCount(); assert(frequencyInput.getFrameCount() == bufferSize); assert(dbGainInput.getFrameCount() == bufferSize); assert(qInput.getFrameCount() == bufferSize); assert(output.getFrameCount() == bufferSize); assert(frequencyInput.getChannelCount() == 1); assert(dbGainInput.getChannelCount() == 1); assert(qInput.getChannelCount() == 1); assert(input.getChannelCount() == 1); assert(output.getChannelCount() == 1); const auto frequencyData = frequencyInput.getChannelData(0); const auto dbGainData = dbGainInput.getChannelData(0); const auto qData = qInput.getChannelData(0); const auto inputData = input.getChannelData(0); auto outputData = output.getChannelData(0); float w0[MAX_BUFFER_SIZE]; #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { // 0 <= w0 <= pi w0[i] = w0Coef * std::clamp(frequencyData[i], 20.f, nyquistLimit); } float alpha[MAX_BUFFER_SIZE]; vfastsin_0topi_array(w0, alpha, bufferSize); #pragma clang loop vectorize(enable) for (int i = 0; i < bufferSize; i++) { alpha[i] /= (2.f * qData[i]); } vfastnanzero_array(alpha, alpha, bufferSize); float cosW0[MAX_BUFFER_SIZE]; vfastcos_0topi_array(w0, cosW0, bufferSize); float b0[MAX_BUFFER_SIZE]; float b1[MAX_BUFFER_SIZE]; float b2[MAX_BUFFER_SIZE]; float a1[MAX_BUFFER_SIZE]; float a2[MAX_BUFFER_SIZE]; switch (mode) { case Mode::LOWPASS: lowpassCoefs(bufferSize, alpha, cosW0, b0, b1, b2, a1, a2); break; case Mode::HIGHPASS: highpassCoefs(bufferSize, alpha, cosW0, b0, b1, b2, a1, a2); break; case Mode::BANDPASS: bandpassCoefs(bufferSize, qData, alpha, cosW0, b0, b1, b2, a1, a2); break; case Mode::NOTCH: notchCoefs(bufferSize, alpha, cosW0, b0, b1, b2, a1, a2); break; case Mode::ALLPASS: allpassCoefs(bufferSize, alpha, cosW0, b0, b1, b2, a1, a2); break; case Mode::EQ_BAND: eqBandCoefs(bufferSize, dbGainData, alpha, cosW0, b0, b1, b2, a1, a2); break; case Mode::LOW_SHELF: lowShelfCoefs(bufferSize, dbGainData, alpha, cosW0, b0, b1, b2, a1, a2); break; case Mode::HIGH_SHELF: highShelfCoefs(bufferSize, dbGainData, alpha, cosW0, b0, b1, b2, a1, a2); break; default: assert(false); break; } auto xn2temp = xn2; auto xn1temp = xn1; auto yn2temp = yn2; auto yn1temp = yn1; for (int i = 0; i < bufferSize; i++) { const auto x = inputData[i]; const auto y = (b0[i] * x + b1[i] * xn1temp + b2[i] * xn2temp - a1[i] * yn1temp - a2[i] * yn2temp); outputData[i] = y; xn2temp = xn1temp; xn1temp = x; yn2temp = yn1temp; yn1temp = y; } vfastclamp_nanzero_array(outputData, -10.f, 10.f, outputData, bufferSize); xn2 = xn2temp; xn1 = xn1temp; yn2 = yn2temp; yn1 = yn1temp; // recover from bad situations if (!std::isfinite(xn2) || !std::isfinite(xn1) || !std::isfinite(yn2) || !std::isfinite(yn1)) { xn2 = xn1 = yn2 = yn1 = 0.f; } } void BiQuadFilter::processBufferConstant(float frequency, float dbGain, float q, BufferF32 input, BufferF32 output, Mode mode) { assert(!std::isnan(frequency)); assert(!std::isnan(dbGain)); assert(std::isfinite(q)); if (mode == Mode::BYPASS) { if (!input.isSameStorage(output)) { output.set(0, input); } reset(); return; } const auto bufferSize = input.getFrameCount(); assert(bufferSize == output.getFrameCount()); assert(input.getChannelCount() == 1); assert(output.getChannelCount() == 1); const auto inputData = input.getChannelData(0); auto outputData = output.getChannelData(0); float w0 = w0Coef * std::clamp(frequency, 20.f, nyquistLimit); float alpha = std::sin(w0) / (2.f * std::max(1e-6f, q)); float cosW0 = std::cos(w0); float b0, b1, b2, a1, a2; switch (mode) { case Mode::LOWPASS: lowpassCoefs(1, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2); break; case Mode::HIGHPASS: highpassCoefs(1, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2); break; case Mode::BANDPASS: bandpassCoefs(1, &q, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2); break; case Mode::NOTCH: notchCoefs(1, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2); break; case Mode::ALLPASS: allpassCoefs(1, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2); break; case Mode::EQ_BAND: eqBandCoefs(1, &dbGain, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2); break; case Mode::LOW_SHELF: lowShelfCoefs(1, &dbGain, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2); break; case Mode::HIGH_SHELF: highShelfCoefs(1, &dbGain, &alpha, &cosW0, &b0, &b1, &b2, &a1, &a2); break; default: assert(false); break; } auto xn2temp = xn2; auto xn1temp = xn1; auto yn2temp = yn2; auto yn1temp = yn1; for (int i = 0; i < bufferSize; i++) { const auto x = inputData[i]; const auto y = (b0 * x + b1 * xn1temp + b2 * xn2temp - a1 * yn1temp - a2 * yn2temp); outputData[i] = y; xn2temp = xn1temp; xn1temp = x; yn2temp = yn1temp; yn1temp = y; } vfastclamp_nanzero_array(outputData, -10.f, 10.f, outputData, bufferSize); xn2 = xn2temp; xn1 = xn1temp; yn2 = yn2temp; yn1 = yn1temp; } void BiQuadFilter::reset() { xn2 = xn1 = yn2 = yn1 = 0.f; } FilterChain::FilterChain(int sampleRate, int channelCount, int filterCount) : channelCount(channelCount) { filterConfigs.resize(filterCount); for (int i = 0; i < filterCount; i++) { filterConfigs[i].filters.resize(channelCount, BiQuadFilter(sampleRate)); filterConfigs[i].frequency = sampleRate / 2.f - 20.f; filterConfigs[i].dbGain = 0.f; filterConfigs[i].q = 0.707f; filterConfigs[i].mode = static_cast(BiQuadFilter::Mode::BYPASS); } } void FilterChain::processBuffer(BufferF32 buf) { for (int i = 0; i < filterConfigs.size(); i++) { const auto mode = static_cast( emscripten_atomic_load_u32(&filterConfigs[i].mode)); if (mode == BiQuadFilter::Mode::BYPASS) { for (int channel = 0; channel < channelCount; channel++) { filterConfigs[i].filters[channel].reset(); } continue; } for (int channel = 0; channel < channelCount; channel++) { auto channelBuf = buf.sliceChannel(channel); filterConfigs[i].filters[channel].processBufferConstant( emscripten_atomic_load_f32(&filterConfigs[i].frequency), emscripten_atomic_load_f32(&filterConfigs[i].dbGain), emscripten_atomic_load_f32(&filterConfigs[i].q), channelBuf, channelBuf, mode); } } } int FilterChain::getFilterCount() const { return filterConfigs.size(); } BiQuadFilter::Mode FilterChain::getMode(int idx) const { return static_cast( emscripten_atomic_load_u32(&filterConfigs[idx].mode)); } void FilterChain::setMode(int idx, BiQuadFilter::Mode mode) { emscripten_atomic_store_u32(&filterConfigs[idx].mode, static_cast(mode)); } float FilterChain::getFrequency(int idx) const { return emscripten_atomic_load_f32(&filterConfigs[idx].frequency); } void FilterChain::setFrequency(int idx, float frequency) { emscripten_atomic_store_f32(&filterConfigs[idx].frequency, frequency); } float FilterChain::getGain(int idx) const { return emscripten_atomic_load_f32(&filterConfigs[idx].dbGain); } void FilterChain::setGain(int idx, float dbGain) { emscripten_atomic_store_f32(&filterConfigs[idx].dbGain, dbGain); } float FilterChain::getQ(int idx) const { return emscripten_atomic_load_f32(&filterConfigs[idx].q); } void FilterChain::setQ(int idx, float q) { emscripten_atomic_store_f32(&filterConfigs[idx].q, q); } EMSCRIPTEN_BINDINGS(biquad) { using namespace emscripten; enum_("BiQuadFilterMode") .value("LOWPASS", BiQuadFilter::Mode::LOWPASS) .value("HIGHPASS", BiQuadFilter::Mode::HIGHPASS) .value("BANDPASS", BiQuadFilter::Mode::BANDPASS) .value("NOTCH", BiQuadFilter::Mode::NOTCH) .value("ALLPASS", BiQuadFilter::Mode::ALLPASS) .value("EQ_BAND", BiQuadFilter::Mode::EQ_BAND) .value("LOW_SHELF", BiQuadFilter::Mode::LOW_SHELF) .value("HIGH_SHELF", BiQuadFilter::Mode::HIGH_SHELF) .value("BYPASS", BiQuadFilter::Mode::BYPASS); class_("FilterChain") .smart_ptr>("FilterChain") .property("filterCount", &FilterChain::getFilterCount) .function("getMode(index)", &FilterChain::getMode) .function("setMode(index, mode)", &FilterChain::setMode) .function("getFrequency(index)", &FilterChain::getFrequency) .function("setFrequency(index, frequency)", &FilterChain::setFrequency) .function("getGain(index)", &FilterChain::getGain) .function("setGain(index, dbGain)", &FilterChain::setGain) .function("getQ(index)", &FilterChain::getQ) .function("setQ(index, q)", &FilterChain::setQ); }; TEST_CASE("Biquad filter", "[biquad]") { constexpr int BUFFER_SIZE = 1024; BufferF32 frequencyBuffer(1, BUFFER_SIZE); BufferF32 gainBuffer(1, BUFFER_SIZE); BufferF32 qBuffer(1, BUFFER_SIZE); BufferF32 inputBuffer(1, BUFFER_SIZE); BufferF32 outputBuffer(1, BUFFER_SIZE); frequencyBuffer.fill(440.f); gainBuffer.fill(0.f); qBuffer.fill(1.f); inputBuffer.noise(); outputBuffer.fill(0.f); for (int mode = 0; mode < 8; mode++) { auto biQuadFilter = std::make_shared(44100); biQuadFilter->processBuffer(frequencyBuffer, gainBuffer, qBuffer, inputBuffer, outputBuffer, static_cast(mode)); REQUIRE(!outputBuffer.hasNaN()); } for (int mode = 0; mode < 8; mode++) { auto biQuadFilter = std::make_shared(44100); biQuadFilter->processBufferConstant(440.f, 1.f, 1.f, inputBuffer, outputBuffer, static_cast(mode)); REQUIRE(!outputBuffer.hasNaN()); } } TEST_CASE("FilterChain basic properties", "[filterchain]") { constexpr int SAMPLE_RATE = 44100; constexpr int CHANNEL_COUNT = 2; constexpr int FILTER_COUNT = 4; auto filterChain = std::make_shared(SAMPLE_RATE, CHANNEL_COUNT, FILTER_COUNT); SECTION("Filter count is correct") { REQUIRE(filterChain->getFilterCount() == FILTER_COUNT); } SECTION("Default values are set correctly") { for (int i = 0; i < FILTER_COUNT; i++) { REQUIRE(filterChain->getMode(i) == BiQuadFilter::Mode::BYPASS); REQUIRE(filterChain->getFrequency(i) == Approx(SAMPLE_RATE / 2.f - 20.f)); REQUIRE(filterChain->getGain(i) == Approx(0.f)); REQUIRE(filterChain->getQ(i) == Approx(0.707f)); } } } TEST_CASE("FilterChain setters and getters", "[filterchain]") { constexpr int SAMPLE_RATE = 44100; constexpr int CHANNEL_COUNT = 2; constexpr int FILTER_COUNT = 3; auto filterChain = std::make_shared(SAMPLE_RATE, CHANNEL_COUNT, FILTER_COUNT); SECTION("Set and get mode") { filterChain->setMode(0, BiQuadFilter::Mode::HIGHPASS); filterChain->setMode(1, BiQuadFilter::Mode::BANDPASS); filterChain->setMode(2, BiQuadFilter::Mode::EQ_BAND); REQUIRE(filterChain->getMode(0) == BiQuadFilter::Mode::HIGHPASS); REQUIRE(filterChain->getMode(1) == BiQuadFilter::Mode::BANDPASS); REQUIRE(filterChain->getMode(2) == BiQuadFilter::Mode::EQ_BAND); } SECTION("Set and get frequency") { filterChain->setFrequency(0, 440.f); filterChain->setFrequency(1, 1000.f); filterChain->setFrequency(2, 5000.f); REQUIRE(filterChain->getFrequency(0) == Approx(440.f)); REQUIRE(filterChain->getFrequency(1) == Approx(1000.f)); REQUIRE(filterChain->getFrequency(2) == Approx(5000.f)); } SECTION("Set and get gain") { filterChain->setGain(0, 0.5f); filterChain->setGain(1, 2.0f); filterChain->setGain(2, 0.75f); REQUIRE(filterChain->getGain(0) == Approx(0.5f)); REQUIRE(filterChain->getGain(1) == Approx(2.0f)); REQUIRE(filterChain->getGain(2) == Approx(0.75f)); } SECTION("Set and get Q") { filterChain->setQ(0, 0.707f); filterChain->setQ(1, 2.0f); filterChain->setQ(2, 5.0f); REQUIRE(filterChain->getQ(0) == Approx(0.707f)); REQUIRE(filterChain->getQ(1) == Approx(2.0f)); REQUIRE(filterChain->getQ(2) == Approx(5.0f)); } } TEST_CASE("FilterChain buffer processing", "[filterchain]") { constexpr int SAMPLE_RATE = 44100; constexpr int BUFFER_SIZE = 1024; SECTION("Single channel, single filter") { auto filterChain = std::make_shared(SAMPLE_RATE, 1, 1); BufferF32 buffer(1, BUFFER_SIZE); buffer.noise(); filterChain->setMode(0, BiQuadFilter::Mode::LOWPASS); filterChain->setFrequency(0, 1000.f); filterChain->setQ(0, 0.707f); filterChain->processBuffer(buffer); REQUIRE(!buffer.hasNaN()); } SECTION("Stereo, multiple filters") { auto filterChain = std::make_shared(SAMPLE_RATE, 2, 3); BufferF32 buffer(2, BUFFER_SIZE); buffer.noise(); // Configure a typical EQ chain filterChain->setMode(0, BiQuadFilter::Mode::LOW_SHELF); filterChain->setFrequency(0, 100.f); filterChain->setGain(0, 3.f); filterChain->setQ(0, 0.707f); filterChain->setMode(1, BiQuadFilter::Mode::EQ_BAND); filterChain->setFrequency(1, 1000.f); filterChain->setGain(1, -2.f); filterChain->setQ(1, 1.5f); filterChain->setMode(2, BiQuadFilter::Mode::HIGH_SHELF); filterChain->setFrequency(2, 8000.f); filterChain->setGain(2, -20.f); filterChain->setQ(2, 0.707f); filterChain->processBuffer(buffer); REQUIRE(!buffer.hasNaN()); } SECTION("All filter modes") { for (int mode = 0; mode < 8; mode++) { auto filterChain = std::make_shared(SAMPLE_RATE, 1, 1); BufferF32 buffer(1, BUFFER_SIZE); buffer.noise(); filterChain->setMode(0, static_cast(mode)); filterChain->setFrequency(0, 440.f); filterChain->setGain(0, 1.5f); filterChain->setQ(0, 1.0f); filterChain->processBuffer(buffer); REQUIRE(!buffer.hasNaN()); } } SECTION("Multiple passes through chain") { auto filterChain = std::make_shared(SAMPLE_RATE, 1, 2); BufferF32 buffer(1, BUFFER_SIZE); buffer.noise(); filterChain->setMode(0, BiQuadFilter::Mode::HIGHPASS); filterChain->setFrequency(0, 100.f); filterChain->setMode(1, BiQuadFilter::Mode::LOWPASS); filterChain->setFrequency(1, 5000.f); // Process multiple times to test state persistence for (int i = 0; i < 5; i++) { filterChain->processBuffer(buffer); REQUIRE(!buffer.hasNaN()); } } SECTION("Many channels") { constexpr int MANY_CHANNELS = 8; auto filterChain = std::make_shared(SAMPLE_RATE, MANY_CHANNELS, 2); BufferF32 buffer(MANY_CHANNELS, BUFFER_SIZE); buffer.noise(); filterChain->setMode(0, BiQuadFilter::Mode::BANDPASS); filterChain->setFrequency(0, 1000.f); filterChain->setQ(0, 5.0f); filterChain->setMode(1, BiQuadFilter::Mode::NOTCH); filterChain->setFrequency(1, 2000.f); filterChain->setQ(1, 10.0f); filterChain->processBuffer(buffer); REQUIRE(!buffer.hasNaN()); } SECTION("Bypass mode") { auto filterChain = std::make_shared(SAMPLE_RATE, 1, 1); BufferF32 buffer(1, BUFFER_SIZE); BufferF32 checkBuffer(1, BUFFER_SIZE); buffer.noise(); checkBuffer.set(0, buffer); filterChain->setMode(0, BiQuadFilter::Mode::BYPASS); filterChain->processBuffer(buffer); bool allSame = true; for (int i = 0; i < BUFFER_SIZE; i++) { if (buffer[0][i] != checkBuffer[0][i]) { allSame = false; break; } } REQUIRE(allSame); } }