BiQuadFilter: API improvements

Bug: webrtc:7494
Change-Id: If0270cddeb46fa53c0fbb385c85e48f28f9e1a5c
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/236342
Reviewed-by: Hanna Silen <silen@webrtc.org>
Commit-Queue: Alessio Bazzica <alessiob@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#35274}
This commit is contained in:
Alessio Bazzica 2021-10-26 13:29:37 +02:00 committed by WebRTC LUCI CQ
parent 32c4ecb3db
commit 2bf6d45f14
4 changed files with 113 additions and 72 deletions

View File

@ -10,26 +10,37 @@
#include "modules/audio_processing/agc2/biquad_filter.h"
#include <stddef.h>
#include "rtc_base/arraysize.h"
namespace webrtc {
// Transposed direct form I implementation of a bi-quad filter applied to an
// input signal `x` to produce an output signal `y`.
BiQuadFilter::BiQuadFilter(const Config& config)
: config_(config), state_({}) {}
BiQuadFilter::~BiQuadFilter() = default;
void BiQuadFilter::SetConfig(const Config& config) {
config_ = config;
state_ = {};
}
void BiQuadFilter::Reset() {
state_ = {};
}
void BiQuadFilter::Process(rtc::ArrayView<const float> x,
rtc::ArrayView<float> y) {
RTC_DCHECK_EQ(x.size(), y.size());
for (size_t k = 0; k < x.size(); ++k) {
// Use temporary variable for x[k] to allow in-place function call
// (that x and y refer to the same array).
// Use a temporary variable for `x[k]` to allow in-place processing.
const float tmp = x[k];
y[k] = coefficients_.b[0] * tmp + coefficients_.b[1] * biquad_state_.b[0] +
coefficients_.b[2] * biquad_state_.b[1] -
coefficients_.a[0] * biquad_state_.a[0] -
coefficients_.a[1] * biquad_state_.a[1];
biquad_state_.b[1] = biquad_state_.b[0];
biquad_state_.b[0] = tmp;
biquad_state_.a[1] = biquad_state_.a[0];
biquad_state_.a[0] = y[k];
y[k] = config_.b[0] * tmp + config_.b[1] * state_.b[0] +
config_.b[2] * state_.b[1] - config_.a[0] * state_.a[0] -
config_.a[1] * state_.a[1];
state_.b[1] = state_.b[0];
state_.b[0] = tmp;
state_.a[1] = state_.a[0];
state_.a[0] = y[k];
}
}

View File

@ -11,54 +11,44 @@
#ifndef MODULES_AUDIO_PROCESSING_AGC2_BIQUAD_FILTER_H_
#define MODULES_AUDIO_PROCESSING_AGC2_BIQUAD_FILTER_H_
#include <algorithm>
#include "api/array_view.h"
#include "rtc_base/arraysize.h"
#include "rtc_base/constructor_magic.h"
namespace webrtc {
// Transposed direct form I implementation of a bi-quad filter.
// b[0] + b[1] • z^(-1) + b[2] • z^(-2)
// H(z) = ------------------------------------
// 1 + a[1] • z^(-1) + a[2] • z^(-2)
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];
// Computed as `[b, a] = scipy.signal.butter(N=2, Wn, btype)`.
struct Config {
float b[3]; // b[0], b[1], b[2].
float a[2]; // a[1], a[2].
};
BiQuadFilter() = default;
explicit BiQuadFilter(const Config& config);
BiQuadFilter(const BiQuadFilter&) = delete;
BiQuadFilter& operator=(const BiQuadFilter&) = delete;
~BiQuadFilter();
void Initialize(const BiQuadCoefficients& coefficients) {
coefficients_ = coefficients;
}
// Sets the filter configuration and resets the internal state.
void SetConfig(const Config& config);
void Reset() { biquad_state_.Reset(); }
// Zeroes the filter state.
void Reset();
// Produces a filtered output y of the input x. Both x and y need to
// have the same length. In-place modification is allowed.
// Filters `x` and writes the output in `y`, which must have the same length
// of `x`. In-place processing is supported.
void Process(rtc::ArrayView<const float> x, rtc::ArrayView<float> y);
private:
struct BiQuadState {
BiQuadState() { Reset(); }
void Reset() {
std::fill(b, b + arraysize(b), 0.f);
std::fill(a, a + arraysize(a), 0.f);
}
Config config_;
struct State {
float b[2];
float a[2];
};
BiQuadState biquad_state_;
BiQuadCoefficients coefficients_;
RTC_DISALLOW_COPY_AND_ASSIGN(BiQuadFilter);
} state_;
};
} // namespace webrtc

View File

@ -19,11 +19,10 @@
#include "rtc_base/gunit.h"
namespace webrtc {
namespace test {
namespace {
constexpr size_t kFrameSize = 8;
constexpr size_t kNumFrames = 4;
constexpr int kFrameSize = 8;
constexpr int kNumFrames = 4;
using FloatArraySequence =
std::array<std::array<float, kFrameSize>, kNumFrames>;
@ -37,8 +36,8 @@ constexpr FloatArraySequence kBiQuadInputSeq = {
{{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 = {
// Computed as `scipy.signal.butter(N=2, Wn=60/24000, btype='highpass')`.
constexpr BiQuadFilter::Config kBiQuadConfig{
{0.99446179f, -1.98892358f, 0.99446179f},
{-1.98889291f, 0.98895425f}};
@ -57,22 +56,23 @@ constexpr FloatArraySequence kBiQuadOutputSeq = {
{{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<float> views such
// Fails for every pair from two equally sized rtc::ArrayView<float> 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.
// of a pair is 0, `tolerance` is used to check the absolute error.
void ExpectNearRelative(rtc::ArrayView<const float> expected,
rtc::ArrayView<const float> 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); };
auto safe_den = [](float x) { return (x == 0.0f) ? 1.0f : 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)
if (abs_diff == 0.0f) {
continue;
}
SCOPED_TRACE(i);
SCOPED_TRACE(expected[i]);
SCOPED_TRACE(computed[i]);
@ -80,32 +80,32 @@ void ExpectNearRelative(rtc::ArrayView<const float> expected,
}
}
} // namespace
// Checks that filtering works when different containers are used both as input
// and as output.
TEST(BiQuadFilterTest, FilterNotInPlace) {
BiQuadFilter filter;
filter.Initialize(kBiQuadConfig);
BiQuadFilter filter(kBiQuadConfig);
std::array<float, kFrameSize> samples;
// TODO(https://bugs.webrtc.org/8948): Add when the issue is fixed.
// FloatingPointExceptionObserver fpe_observer;
for (size_t i = 0; i < kNumFrames; ++i) {
for (int i = 0; i < kNumFrames; ++i) {
SCOPED_TRACE(i);
filter.Process(kBiQuadInputSeq[i], samples);
ExpectNearRelative(kBiQuadOutputSeq[i], samples, 2e-4f);
}
}
// Checks that filtering works when the same container is used both as input and
// as output.
TEST(BiQuadFilterTest, FilterInPlace) {
BiQuadFilter filter;
filter.Initialize(kBiQuadConfig);
BiQuadFilter filter(kBiQuadConfig);
std::array<float, kFrameSize> samples;
// TODO(https://bugs.webrtc.org/8948): Add when the issue is fixed.
// FloatingPointExceptionObserver fpe_observer;
for (size_t i = 0; i < kNumFrames; ++i) {
for (int i = 0; i < kNumFrames; ++i) {
SCOPED_TRACE(i);
std::copy(kBiQuadInputSeq[i].begin(), kBiQuadInputSeq[i].end(),
samples.begin());
@ -114,23 +114,62 @@ TEST(BiQuadFilterTest, FilterInPlace) {
}
}
TEST(BiQuadFilterTest, Reset) {
BiQuadFilter filter;
filter.Initialize(kBiQuadConfig);
// Checks that different configurations produce different outputs.
TEST(BiQuadFilterTest, SetConfigDifferentOutput) {
BiQuadFilter filter(/*config=*/{{0.97803048f, -1.95606096f, 0.97803048f},
{-1.95557824f, 0.95654368f}});
std::array<float, kFrameSize> samples1;
for (size_t i = 0; i < kNumFrames; ++i) {
for (int i = 0; i < kNumFrames; ++i) {
filter.Process(kBiQuadInputSeq[i], samples1);
}
filter.Reset();
filter.SetConfig(
{{0.09763107f, 0.19526215f, 0.09763107f}, {-0.94280904f, 0.33333333f}});
std::array<float, kFrameSize> samples2;
for (size_t i = 0; i < kNumFrames; ++i) {
for (int i = 0; i < kNumFrames; ++i) {
filter.Process(kBiQuadInputSeq[i], samples2);
}
EXPECT_NE(samples1, samples2);
}
// Checks that when `SetConfig()` is called but the filter coefficients are the
// same the filter state is reset.
TEST(BiQuadFilterTest, SetConfigResetsState) {
BiQuadFilter filter(kBiQuadConfig);
std::array<float, kFrameSize> samples1;
for (int i = 0; i < kNumFrames; ++i) {
filter.Process(kBiQuadInputSeq[i], samples1);
}
filter.SetConfig(kBiQuadConfig);
std::array<float, kFrameSize> samples2;
for (int i = 0; i < kNumFrames; ++i) {
filter.Process(kBiQuadInputSeq[i], samples2);
}
EXPECT_EQ(samples1, samples2);
}
} // namespace test
// Checks that when `Reset()` is called the filter state is reset.
TEST(BiQuadFilterTest, Reset) {
BiQuadFilter filter(kBiQuadConfig);
std::array<float, kFrameSize> samples1;
for (int i = 0; i < kNumFrames; ++i) {
filter.Process(kBiQuadInputSeq[i], samples1);
}
filter.Reset();
std::array<float, kFrameSize> samples2;
for (int i = 0; i < kNumFrames; ++i) {
filter.Process(kBiQuadInputSeq[i], samples2);
}
EXPECT_EQ(samples1, samples2);
}
} // namespace
} // namespace webrtc

View File

@ -19,8 +19,8 @@ namespace webrtc {
namespace rnn_vad {
namespace {
// Generated via "B, A = scipy.signal.butter(2, 30/12000, btype='highpass')"
const BiQuadFilter::BiQuadCoefficients kHpfConfig24k = {
// Computed as `scipy.signal.butter(N=2, Wn=60/24000, btype='highpass')`.
constexpr BiQuadFilter::Config kHpfConfig24k{
{0.99446179f, -1.98892358f, 0.99446179f},
{-1.98889291f, 0.98895425f}};
@ -28,6 +28,7 @@ const BiQuadFilter::BiQuadCoefficients kHpfConfig24k = {
FeaturesExtractor::FeaturesExtractor(const AvailableCpuFeatures& cpu_features)
: use_high_pass_filter_(false),
hpf_(kHpfConfig24k),
pitch_buf_24kHz_(),
pitch_buf_24kHz_view_(pitch_buf_24kHz_.GetBufferView()),
lp_residual_(kBufSize24kHz),
@ -35,7 +36,6 @@ FeaturesExtractor::FeaturesExtractor(const AvailableCpuFeatures& cpu_features)
pitch_estimator_(cpu_features),
reference_frame_view_(pitch_buf_24kHz_.GetMostRecentValuesView()) {
RTC_DCHECK_EQ(kBufSize24kHz, lp_residual_.size());
hpf_.Initialize(kHpfConfig24k);
Reset();
}
@ -44,8 +44,9 @@ FeaturesExtractor::~FeaturesExtractor() = default;
void FeaturesExtractor::Reset() {
pitch_buf_24kHz_.Reset();
spectral_features_extractor_.Reset();
if (use_high_pass_filter_)
if (use_high_pass_filter_) {
hpf_.Reset();
}
}
bool FeaturesExtractor::CheckSilenceComputeFeatures(