diff --git a/modules/audio_processing/BUILD.gn b/modules/audio_processing/BUILD.gn index 38ff7a9cba..1d7d723b7a 100644 --- a/modules/audio_processing/BUILD.gn +++ b/modules/audio_processing/BUILD.gn @@ -543,6 +543,7 @@ if (rtc_include_tests) { "../audio_coding:neteq_input_audio_tools", "aec_dump:mock_aec_dump_unittests", "agc2:adaptive_digital_unittests", + "agc2:biquad_filter_unittests", "agc2:fixed_digital_unittests", "agc2:noise_estimator_unittests", "agc2/rnn_vad:unittests", diff --git a/modules/audio_processing/agc2/BUILD.gn b/modules/audio_processing/agc2/BUILD.gn index 6f92f848cc..9ea1d44a60 100644 --- a/modules/audio_processing/agc2/BUILD.gn +++ b/modules/audio_processing/agc2/BUILD.gn @@ -47,6 +47,27 @@ rtc_source_set("adaptive_digital") { ] } +rtc_source_set("biquad_filter") { + visibility = [ ":*" ] + sources = [ + "biquad_filter.cc", + "biquad_filter.h", + ] + deps = [ + "../../../api:array_view", + "../../../rtc_base:rtc_base_approved", + ] +} + +rtc_source_set("common") { + sources = [ + "agc2_common.h", + ] + deps = [ + "../../../rtc_base:rtc_base_approved", + ] +} + rtc_source_set("fixed_digital") { sources = [ "fixed_digital_level_estimator.cc", @@ -75,41 +96,6 @@ rtc_source_set("fixed_digital") { ] } -rtc_source_set("common") { - sources = [ - "agc2_common.h", - ] - deps = [ - "../../../rtc_base:rtc_base_approved", - ] -} - -rtc_source_set("noise_level_estimator") { - sources = [ - "biquad_filter.cc", - "biquad_filter.h", - "down_sampler.cc", - "down_sampler.h", - "noise_level_estimator.cc", - "noise_level_estimator.h", - "noise_spectrum_estimator.cc", - "noise_spectrum_estimator.h", - "signal_classifier.cc", - "signal_classifier.h", - ] - deps = [ - "..:aec_core", - "..:apm_logging", - "..:audio_frame_view", - "../../../api:array_view", - "../../../common_audio", - "../../../rtc_base:checks", - "../../../rtc_base:macromagic", - ] - - configs += [ "..:apm_debug_dump" ] -} - rtc_source_set("gain_applier") { sources = [ "gain_applier.cc", @@ -122,19 +108,65 @@ rtc_source_set("gain_applier") { ] } -rtc_source_set("test_utils") { - testonly = true - visibility = [ ":*" ] +rtc_source_set("noise_level_estimator") { sources = [ - "agc2_testing_common.cc", - "agc2_testing_common.h", - "vector_float_frame.cc", - "vector_float_frame.h", + "down_sampler.cc", + "down_sampler.h", + "noise_level_estimator.cc", + "noise_level_estimator.h", + "noise_spectrum_estimator.cc", + "noise_spectrum_estimator.h", + "signal_classifier.cc", + "signal_classifier.h", ] deps = [ + ":biquad_filter", + "..:aec_core", + "..:apm_logging", "..:audio_frame_view", + "../../../api:array_view", + "../../../common_audio", + "../../../rtc_base:checks", + "../../../rtc_base:macromagic", + ] + + configs += [ "..:apm_debug_dump" ] +} + +rtc_source_set("adaptive_digital_unittests") { + testonly = true + configs += [ "..:apm_debug_dump" ] + + sources = [ + "adaptive_digital_gain_applier_unittest.cc", + "adaptive_mode_level_estimator_unittest.cc", + "gain_applier_unittest.cc", + "saturation_protector_unittest.cc", + ] + deps = [ + ":adaptive_digital", + ":common", + ":gain_applier", + ":test_utils", + "..:apm_logging", + "..:audio_frame_view", + "../../../api:array_view", + "../../../common_audio", "../../../rtc_base:checks", "../../../rtc_base:rtc_base_approved", + "../../../rtc_base:rtc_base_tests_utils", + "../vad:vad_with_level", + ] +} + +rtc_source_set("biquad_filter_unittests") { + testonly = true + sources = [ + "biquad_filter_unittest.cc", + ] + deps = [ + ":biquad_filter", + "../../../rtc_base:rtc_base_tests_utils", ] } @@ -168,32 +200,6 @@ rtc_source_set("fixed_digital_unittests") { ] } -rtc_source_set("adaptive_digital_unittests") { - testonly = true - configs += [ "..:apm_debug_dump" ] - - sources = [ - "adaptive_digital_gain_applier_unittest.cc", - "adaptive_mode_level_estimator_unittest.cc", - "gain_applier_unittest.cc", - "saturation_protector_unittest.cc", - ] - deps = [ - ":adaptive_digital", - ":common", - ":gain_applier", - ":test_utils", - "..:apm_logging", - "..:audio_frame_view", - "../../../api:array_view", - "../../../common_audio", - "../../../rtc_base:checks", - "../../../rtc_base:rtc_base_approved", - "../../../rtc_base:rtc_base_tests_utils", - "../vad:vad_with_level", - ] -} - rtc_source_set("noise_estimator_unittests") { testonly = true configs += [ "..:apm_debug_dump" ] @@ -213,3 +219,19 @@ rtc_source_set("noise_estimator_unittests") { "../../../rtc_base:rtc_base_tests_utils", ] } + +rtc_source_set("test_utils") { + testonly = true + visibility = [ ":*" ] + sources = [ + "agc2_testing_common.cc", + "agc2_testing_common.h", + "vector_float_frame.cc", + "vector_float_frame.h", + ] + deps = [ + "..:audio_frame_view", + "../../../rtc_base:checks", + "../../../rtc_base:rtc_base_approved", + ] +} diff --git a/modules/audio_processing/agc2/biquad_filter.cc b/modules/audio_processing/agc2/biquad_filter.cc index c15c6449ad..9858d50584 100644 --- a/modules/audio_processing/agc2/biquad_filter.cc +++ b/modules/audio_processing/agc2/biquad_filter.cc @@ -12,9 +12,8 @@ namespace webrtc { -// This method applies a biquad filter to an input signal x to produce an -// output signal y. The biquad coefficients are specified at the construction -// of the object. +// Transposed direct form I implementation of a bi-quad filter applied to an +// input signal |x| to produce an output signal |y|. void BiQuadFilter::Process(rtc::ArrayView x, rtc::ArrayView y) { for (size_t k = 0; k < x.size(); ++k) { diff --git a/modules/audio_processing/agc2/biquad_filter.h b/modules/audio_processing/agc2/biquad_filter.h index 4fd5e2e392..523d5822d3 100644 --- a/modules/audio_processing/agc2/biquad_filter.h +++ b/modules/audio_processing/agc2/biquad_filter.h @@ -19,6 +19,10 @@ namespace webrtc { class BiQuadFilter { public: + // Normalized filter coefficients. + // b_0 + b_1 • z^(-1) + b_2 • z^(-2) + // H(z) = --------------------------------- + // 1 + a_1 • z^(-1) + a_2 • z^(-2) struct BiQuadCoefficients { float b[3]; float a[2]; @@ -31,7 +35,7 @@ class BiQuadFilter { } // Produces a filtered output y of the input x. Both x and y need to - // have the same length. + // have the same length. In-place modification is allowed. void Process(rtc::ArrayView x, rtc::ArrayView y); private: diff --git a/modules/audio_processing/agc2/biquad_filter_unittest.cc b/modules/audio_processing/agc2/biquad_filter_unittest.cc new file mode 100644 index 0000000000..75c3b59961 --- /dev/null +++ b/modules/audio_processing/agc2/biquad_filter_unittest.cc @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "modules/audio_processing/agc2/biquad_filter.h" + +#include +#include +#include + +// TODO(bugs.webrtc.org/8948): Add when the issue is fixed. +// #include "test/fpe_observer.h" +#include "rtc_base/gunit.h" + +namespace webrtc { +namespace test { +namespace { + +constexpr size_t kFrameSize = 8; +constexpr size_t kNumFrames = 4; +using FloatArraySequence = + std::array, kNumFrames>; + +constexpr FloatArraySequence kBiQuadInputSeq = { + {{-87.166290f, -8.029022f, 101.619583f, -0.294296f, -5.825764f, -8.890625f, + 10.310432f, 54.845333f}, + {-64.647644f, -6.883945f, 11.059189f, -95.242538f, -108.870834f, + 11.024944f, 63.044102f, -52.709583f}, + {-32.350529f, -18.108028f, -74.022339f, -8.986874f, -1.525581f, + 103.705513f, 6.346226f, -14.319557f}, + {22.645832f, -64.597153f, 55.462521f, -109.393188f, 10.117825f, + -40.019642f, -98.612228f, -8.330326f}}}; + +// Generated via "B, A = scipy.signal.butter(2, 30/12000, btype='highpass')" +const BiQuadFilter::BiQuadCoefficients kBiQuadConfig = { + {0.99446179f, -1.98892358f, 0.99446179f}, + {-1.98889291f, 0.98895425f}}; + +// Comparing to scipy. The expected output is generated as follows: +// zi = np.float32([0, 0]) +// for i in range(4): +// yn, zi = scipy.signal.lfilter(B, A, x[i], zi=zi) +// print(yn) +constexpr FloatArraySequence kBiQuadOutputSeq = { + {{-86.68354497f, -7.02175351f, 102.10290352f, -0.37487333f, -5.87205847f, + -8.85521608f, 10.33772563f, 54.51157181f}, + {-64.92531604f, -6.76395978f, 11.15534507f, -94.68073341f, -107.18177856f, + 13.24642474f, 64.84288941f, -50.97822629f}, + {-30.1579652f, -15.64850899f, -71.06662821f, -5.5883229f, 1.91175353f, + 106.5572003f, 8.57183046f, -12.06298473f}, + {24.84286614f, -62.18094158f, 57.91488056f, -106.65685933f, 13.38760103f, + -36.60367134f, -94.44880104f, -3.59920354f}}}; + +// Fail for every pair from two equally sized rtc::ArrayView views such +// that their relative error is above a given threshold. If the expected value +// of a pair is 0, the tolerance is used to check the absolute error. +void ExpectNearRelative(rtc::ArrayView expected, + rtc::ArrayView computed, + const float tolerance) { + // The relative error is undefined when the expected value is 0. + // When that happens, check the absolute error instead. |safe_den| is used + // below to implement such logic. + auto safe_den = [](float x) { return (x == 0.f) ? 1.f : std::fabs(x); }; + ASSERT_EQ(expected.size(), computed.size()); + for (size_t i = 0; i < expected.size(); ++i) { + const float abs_diff = std::fabs(expected[i] - computed[i]); + // No failure when the values are equal. + if (abs_diff == 0.f) + continue; + SCOPED_TRACE(i); + SCOPED_TRACE(expected[i]); + SCOPED_TRACE(computed[i]); + EXPECT_LE(abs_diff / safe_den(expected[i]), tolerance); + } +} + +} // namespace + +TEST(BiQuadFilterTest, FilterNotInPlace) { + BiQuadFilter filter; + filter.Initialize(kBiQuadConfig); + std::array samples; + + // TODO(https://bugs.webrtc.org/8948): Add when the issue is fixed. + // FloatingPointExceptionObserver fpe_observer; + + for (size_t i = 0; i < kNumFrames; ++i) { + SCOPED_TRACE(i); + filter.Process(kBiQuadInputSeq[i], samples); + ExpectNearRelative(kBiQuadOutputSeq[i], samples, 2e-4f); + } +} + +TEST(BiQuadFilterTest, FilterInPlace) { + BiQuadFilter filter; + filter.Initialize(kBiQuadConfig); + std::array samples; + + // TODO(https://bugs.webrtc.org/8948): Add when the issue is fixed. + // FloatingPointExceptionObserver fpe_observer; + + for (size_t i = 0; i < kNumFrames; ++i) { + SCOPED_TRACE(i); + std::copy(kBiQuadInputSeq[i].begin(), kBiQuadInputSeq[i].end(), + samples.begin()); + filter.Process({samples}, {samples}); + ExpectNearRelative(kBiQuadOutputSeq[i], samples, 2e-4f); + } +} +} // namespace test +} // namespace webrtc