From a05ee82c4ccb2a69dc726eb75c5868a9d1151aad Mon Sep 17 00:00:00 2001 From: Alex Loiko Date: Tue, 20 Feb 2018 15:58:36 +0100 Subject: [PATCH] Fixed Digital mode of AGC2 implementation finished. This CL adds the GainCurveApplier (GCA). It owns a FixedDigitalLevelEstimator (LE) and an InterpolatedGainCurve (IGC). The GCA uses the LE to compute the input signal level, looks up a gain from IGC and applies it on the signal. The other IGC and LE submodules were added in previous CLs [1] and [2]. This CL also turns on AGC2 in the APM fuzzer. [1] https://webrtc-review.googlesource.com/c/src/+/51920 [2] https://webrtc-review.googlesource.com/c/src/+/52381 Bug: webrtc:7949 Change-Id: Idb10cc3ca9d6d2e4ac5824cc3391ed8aa680f6cd Reviewed-on: https://webrtc-review.googlesource.com/54361 Commit-Queue: Alex Loiko Reviewed-by: Sam Zackrisson Cr-Commit-Position: refs/heads/master@{#22103} --- modules/audio_processing/agc2/BUILD.gn | 16 ++ modules/audio_processing/agc2/agc2_common.h | 20 +- .../agc2/agc2_testing_common.cc | 33 +++ .../agc2/agc2_testing_common.h | 15 +- .../agc2/agc2_testing_common_unittest.cc | 26 ++ .../agc2/compute_interpolated_gain_curve.cc | 228 ++++++++++++++++++ .../agc2/compute_interpolated_gain_curve.h | 48 ++++ .../fixed_digital_level_estimator_unittest.cc | 6 +- .../agc2/fixed_gain_controller.cc | 23 +- .../agc2/fixed_gain_controller.h | 6 +- .../agc2/fixed_gain_controller_unittest.cc | 129 ++++++++-- .../agc2/gain_curve_applier.cc | 131 ++++++++++ .../agc2/gain_curve_applier.h | 56 +++++ .../agc2/gain_curve_applier_unittest.cc | 59 +++++ .../agc2/interpolated_gain_curve.cc | 105 ++++++++ .../agc2/interpolated_gain_curve.h | 122 ++++++++++ .../agc2/interpolated_gain_curve_unittest.cc | 202 ++++++++++++++++ modules/audio_processing/agc2/limiter.cc | 137 +++++++++++ modules/audio_processing/agc2/limiter.h | 74 ++++++ .../audio_processing/agc2/limiter_unittest.cc | 60 +++++ test/fuzzers/BUILD.gn | 1 + .../audio_processing_configs_fuzzer.cc | 8 + 22 files changed, 1460 insertions(+), 45 deletions(-) create mode 100644 modules/audio_processing/agc2/agc2_testing_common.cc create mode 100644 modules/audio_processing/agc2/agc2_testing_common_unittest.cc create mode 100644 modules/audio_processing/agc2/compute_interpolated_gain_curve.cc create mode 100644 modules/audio_processing/agc2/compute_interpolated_gain_curve.h create mode 100644 modules/audio_processing/agc2/gain_curve_applier.cc create mode 100644 modules/audio_processing/agc2/gain_curve_applier.h create mode 100644 modules/audio_processing/agc2/gain_curve_applier_unittest.cc create mode 100644 modules/audio_processing/agc2/interpolated_gain_curve.cc create mode 100644 modules/audio_processing/agc2/interpolated_gain_curve.h create mode 100644 modules/audio_processing/agc2/interpolated_gain_curve_unittest.cc create mode 100644 modules/audio_processing/agc2/limiter.cc create mode 100644 modules/audio_processing/agc2/limiter.h create mode 100644 modules/audio_processing/agc2/limiter_unittest.cc diff --git a/modules/audio_processing/agc2/BUILD.gn b/modules/audio_processing/agc2/BUILD.gn index e5890104a3..4aa212fdba 100644 --- a/modules/audio_processing/agc2/BUILD.gn +++ b/modules/audio_processing/agc2/BUILD.gn @@ -15,6 +15,10 @@ rtc_source_set("agc2") { "fixed_digital_level_estimator.h", "fixed_gain_controller.cc", "fixed_gain_controller.h", + "gain_curve_applier.cc", + "gain_curve_applier.h", + "interpolated_gain_curve.cc", + "interpolated_gain_curve.h", ] configs += [ "..:apm_debug_dump" ] @@ -25,6 +29,7 @@ rtc_source_set("agc2") { "../../../api:array_view", "../../../common_audio", "../../../rtc_base:checks", + "../../../rtc_base:gtest_prod", "../../../rtc_base:rtc_base_approved", ] } @@ -34,9 +39,18 @@ rtc_source_set("fixed_digital_unittests") { configs += [ "..:apm_debug_dump" ] sources = [ + "agc2_testing_common.cc", "agc2_testing_common.h", + "agc2_testing_common_unittest.cc", + "compute_interpolated_gain_curve.cc", + "compute_interpolated_gain_curve.h", "fixed_digital_level_estimator_unittest.cc", "fixed_gain_controller_unittest.cc", + "gain_curve_applier_unittest.cc", + "interpolated_gain_curve_unittest.cc", + "limiter.cc", + "limiter.h", + "limiter_unittest.cc", "vector_float_frame.cc", "vector_float_frame.h", ] @@ -46,6 +60,8 @@ rtc_source_set("fixed_digital_unittests") { "..:audio_frame_view", "../../../api:array_view", "../../../common_audio", + "../../../rtc_base:checks", + "../../../rtc_base:rtc_base_approved", "../../../rtc_base:rtc_base_tests_utils", ] } diff --git a/modules/audio_processing/agc2/agc2_common.h b/modules/audio_processing/agc2/agc2_common.h index af13dd093d..ad0ab4ea62 100644 --- a/modules/audio_processing/agc2/agc2_common.h +++ b/modules/audio_processing/agc2/agc2_common.h @@ -11,28 +11,36 @@ #ifndef MODULES_AUDIO_PROCESSING_AGC2_AGC2_COMMON_H_ #define MODULES_AUDIO_PROCESSING_AGC2_AGC2_COMMON_H_ +#include + #include "rtc_base/basictypes.h" namespace webrtc { -constexpr float kMinSampleValue = -32768.f; -constexpr float kMaxSampleValue = 32767.f; +constexpr float kMinFloatS16Value = -32768.f; +constexpr float kMaxFloatS16Value = 32767.f; +constexpr double kMaxAbsFloatS16Value = 32768.0; constexpr size_t kFrameDurationMs = 10; constexpr size_t kSubFramesInFrame = 20; +constexpr size_t kMaximalNumberOfSamplesPerChannel = 480; constexpr float kAttackFilterConstant = 0.f; -constexpr size_t kMaximalNumberOfSamplesPerChannel = 480; - // This is computed from kDecayMs by // 10 ** (-1/20 * subframe_duration / kDecayMs). // |subframe_duration| is |kFrameDurationMs / kSubFramesInFrame|. // kDecayMs is defined in agc2_testing_common.h constexpr float kDecayFilterConstant = 0.9998848773724686f; -// TODO(aleloi): add the other constants as more AGC2 components are -// added. +// Number of interpolation points for each region of the limiter. +// These values have been tuned to limit the interpolated gain curve error given +// the limiter parameters and allowing a maximum error of +/- 32768^-1. +constexpr size_t kInterpolatedGainCurveKneePoints = 22; +constexpr size_t kInterpolatedGainCurveBeyondKneePoints = 10; +constexpr size_t kInterpolatedGainCurveTotalPoints = + kInterpolatedGainCurveKneePoints + kInterpolatedGainCurveBeyondKneePoints; + } // namespace webrtc #endif // MODULES_AUDIO_PROCESSING_AGC2_AGC2_COMMON_H_ diff --git a/modules/audio_processing/agc2/agc2_testing_common.cc b/modules/audio_processing/agc2/agc2_testing_common.cc new file mode 100644 index 0000000000..6c22492e88 --- /dev/null +++ b/modules/audio_processing/agc2/agc2_testing_common.cc @@ -0,0 +1,33 @@ +/* + * 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/agc2_testing_common.h" + +#include "rtc_base/checks.h" + +namespace webrtc { + +namespace test { + +std::vector LinSpace(const double l, + const double r, + size_t num_points) { + RTC_CHECK(num_points >= 2); + std::vector points(num_points); + const double step = (r - l) / (num_points - 1.0); + points[0] = l; + for (size_t i = 1; i < num_points - 1; i++) { + points[i] = static_cast(l) + i * step; + } + points[num_points - 1] = r; + return points; +} +} // namespace test +} // namespace webrtc diff --git a/modules/audio_processing/agc2/agc2_testing_common.h b/modules/audio_processing/agc2/agc2_testing_common.h index 7e27a2425c..a176282ede 100644 --- a/modules/audio_processing/agc2/agc2_testing_common.h +++ b/modules/audio_processing/agc2/agc2_testing_common.h @@ -11,11 +11,24 @@ #ifndef MODULES_AUDIO_PROCESSING_AGC2_AGC2_TESTING_COMMON_H_ #define MODULES_AUDIO_PROCESSING_AGC2_AGC2_TESTING_COMMON_H_ +#include + +#include "rtc_base/basictypes.h" + namespace webrtc { -// Level Estimator test params. +namespace test { + +// Level Estimator test parameters. constexpr float kDecayMs = 500.f; +// Limiter parameters. +constexpr float kLimiterMaxInputLevelDbFs = 1.f; +constexpr float kLimiterKneeSmoothnessDb = 1.f; +constexpr float kLimiterCompressionRatio = 5.f; + +std::vector LinSpace(const double l, const double r, size_t num_points); +} // namespace test } // namespace webrtc #endif // MODULES_AUDIO_PROCESSING_AGC2_AGC2_TESTING_COMMON_H_ diff --git a/modules/audio_processing/agc2/agc2_testing_common_unittest.cc b/modules/audio_processing/agc2/agc2_testing_common_unittest.cc new file mode 100644 index 0000000000..b9f712617d --- /dev/null +++ b/modules/audio_processing/agc2/agc2_testing_common_unittest.cc @@ -0,0 +1,26 @@ +/* + * 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/agc2_testing_common.h" +#include "rtc_base/gunit.h" + +namespace webrtc { + +TEST(AutomaticGainController2Common, TestLinSpace) { + std::vector points1 = test::LinSpace(-1.0, 2.0, 4); + const std::vector expected_points1{{-1.0, 0.0, 1.0, 2.0}}; + EXPECT_EQ(expected_points1, points1); + + std::vector points2 = test::LinSpace(0.0, 1.0, 4); + const std::vector expected_points2{{0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0}}; + EXPECT_EQ(points2, expected_points2); +} + +} // namespace webrtc diff --git a/modules/audio_processing/agc2/compute_interpolated_gain_curve.cc b/modules/audio_processing/agc2/compute_interpolated_gain_curve.cc new file mode 100644 index 0000000000..f395bceffa --- /dev/null +++ b/modules/audio_processing/agc2/compute_interpolated_gain_curve.cc @@ -0,0 +1,228 @@ +/* + * 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/compute_interpolated_gain_curve.h" + +#include +#include +#include +#include +#include +#include + +#include "modules/audio_processing/agc2/agc2_common.h" +#include "modules/audio_processing/agc2/agc2_testing_common.h" +#include "modules/audio_processing/agc2/limiter.h" +#include "rtc_base/checks.h" + +namespace webrtc { +namespace { + +std::pair ComputeLinearApproximationParams( + const Limiter* limiter, + const double x) { + const double m = limiter->GetGainFirstDerivativeLinear(x); + const double q = limiter->GetGainLinear(x) - m * x; + return {m, q}; +} + +double ComputeAreaUnderPiecewiseLinearApproximation(const Limiter* limiter, + const double x0, + const double x1) { + RTC_CHECK_LT(x0, x1); + + // Linear approximation in x0 and x1. + double m0, q0, m1, q1; + std::tie(m0, q0) = ComputeLinearApproximationParams(limiter, x0); + std::tie(m1, q1) = ComputeLinearApproximationParams(limiter, x1); + + // Intersection point between two adjacent linear pieces. + RTC_CHECK_NE(m1, m0); + const double x_split = (q0 - q1) / (m1 - m0); + RTC_CHECK_LT(x0, x_split); + RTC_CHECK_LT(x_split, x1); + + auto area_under_linear_piece = [](double x_l, double x_r, double m, + double q) { + return x_r * (m * x_r / 2.0 + q) - x_l * (m * x_l / 2.0 + q); + }; + return area_under_linear_piece(x0, x_split, m0, q0) + + area_under_linear_piece(x_split, x1, m1, q1); +} + +// Computes the approximation error in the limiter region for a given interval. +// The error is computed as the difference between the areas beneath the limiter +// curve to approximate and its linear under-approximation. +double LimiterUnderApproximationNegativeError(const Limiter* limiter, + const double x0, + const double x1) { + const double area_limiter = limiter->GetGainIntegralLinear(x0, x1); + const double area_interpolated_curve = + ComputeAreaUnderPiecewiseLinearApproximation(limiter, x0, x1); + RTC_CHECK_GE(area_limiter, area_interpolated_curve); + return area_limiter - area_interpolated_curve; +} + +// Automatically finds where to sample the beyond-knee region of a limiter using +// a greedy optimization algorithm that iteratively decreases the approximation +// error. +// The solution is sub-optimal because the algorithm is greedy and the points +// are assigned by halving intervals (starting with the whole beyond-knee region +// as a single interval). However, even if sub-optimal, this algorithm works +// well in practice and it is efficiently implemented using priority queues. +std::vector SampleLimiterRegion(const Limiter* limiter) { + static_assert(kInterpolatedGainCurveBeyondKneePoints > 2, ""); + + struct Interval { + Interval() = default; // Ctor required by std::priority_queue. + Interval(double l, double r, double e) : x0(l), x1(r), error(e) { + RTC_CHECK(x0 < x1); + } + bool operator<(const Interval& other) const { return error < other.error; } + + double x0; + double x1; + double error; + }; + + std::priority_queue> q; + q.emplace(limiter->limiter_start_linear(), limiter->max_input_level_linear(), + LimiterUnderApproximationNegativeError( + limiter, limiter->limiter_start_linear(), + limiter->max_input_level_linear())); + + // Iteratively find points by halving the interval with greatest error. + while (q.size() < kInterpolatedGainCurveBeyondKneePoints) { + // Get the interval with highest error. + const auto interval = q.top(); + q.pop(); + + // Split |interval| and enqueue. + double x_split = (interval.x0 + interval.x1) / 2.0; + q.emplace(interval.x0, x_split, + LimiterUnderApproximationNegativeError(limiter, interval.x0, + x_split)); // Left. + q.emplace(x_split, interval.x1, + LimiterUnderApproximationNegativeError(limiter, x_split, + interval.x1)); // Right. + } + + // Copy x1 values and sort them. + RTC_CHECK_EQ(q.size(), kInterpolatedGainCurveBeyondKneePoints); + std::vector samples(kInterpolatedGainCurveBeyondKneePoints); + for (size_t i = 0; i < kInterpolatedGainCurveBeyondKneePoints; ++i) { + const auto interval = q.top(); + q.pop(); + samples[i] = interval.x1; + } + RTC_CHECK(q.empty()); + std::sort(samples.begin(), samples.end()); + + return samples; +} + +// Compute the parameters to over-approximate the knee region via linear +// interpolation. Over-approximating is saturation-safe since the knee region is +// convex. +void PrecomputeKneeApproxParams(const Limiter* limiter, + test::InterpolatedParameters* parameters) { + static_assert(kInterpolatedGainCurveKneePoints > 2, ""); + // Get |kInterpolatedGainCurveKneePoints| - 1 equally spaced points. + const std::vector points = test::LinSpace( + limiter->knee_start_linear(), limiter->limiter_start_linear(), + kInterpolatedGainCurveKneePoints - 1); + + // Set the first two points. The second is computed to help with the beginning + // of the knee region, which has high curvature. + parameters->computed_approximation_params_x[0] = points[0]; + parameters->computed_approximation_params_x[1] = + (points[0] + points[1]) / 2.0; + // Copy the remaining points. + std::copy(std::begin(points) + 1, std::end(points), + std::begin(parameters->computed_approximation_params_x) + 2); + + // Compute (m, q) pairs for each linear piece y = mx + q. + for (size_t i = 0; i < kInterpolatedGainCurveKneePoints - 1; ++i) { + const double x0 = parameters->computed_approximation_params_x[i]; + const double x1 = parameters->computed_approximation_params_x[i + 1]; + const double y0 = limiter->GetGainLinear(x0); + const double y1 = limiter->GetGainLinear(x1); + RTC_CHECK_NE(x1, x0); + parameters->computed_approximation_params_m[i] = (y1 - y0) / (x1 - x0); + parameters->computed_approximation_params_q[i] = + y0 - parameters->computed_approximation_params_m[i] * x0; + } +} + +// Compute the parameters to under-approximate the beyond-knee region via linear +// interpolation and greedy sampling. Under-approximating is saturation-safe +// since the beyond-knee region is concave. +void PrecomputeBeyondKneeApproxParams( + const Limiter* limiter, + test::InterpolatedParameters* parameters) { + // Find points on which the linear pieces are tangent to the gain curve. + const auto samples = SampleLimiterRegion(limiter); + + // Parametrize each linear piece. + double m, q; + std::tie(m, q) = ComputeLinearApproximationParams( + limiter, + parameters + ->computed_approximation_params_x[kInterpolatedGainCurveKneePoints - + 1]); + parameters + ->computed_approximation_params_m[kInterpolatedGainCurveKneePoints - 1] = + m; + parameters + ->computed_approximation_params_q[kInterpolatedGainCurveKneePoints - 1] = + q; + for (size_t i = 0; i < samples.size(); ++i) { + std::tie(m, q) = ComputeLinearApproximationParams(limiter, samples[i]); + parameters + ->computed_approximation_params_m[i + + kInterpolatedGainCurveKneePoints] = m; + parameters + ->computed_approximation_params_q[i + + kInterpolatedGainCurveKneePoints] = q; + } + + // Find the point of intersection between adjacent linear pieces. They will be + // used as boundaries between adjacent linear pieces. + for (size_t i = kInterpolatedGainCurveKneePoints; + i < kInterpolatedGainCurveKneePoints + + kInterpolatedGainCurveBeyondKneePoints; + ++i) { + RTC_CHECK_NE(parameters->computed_approximation_params_m[i], + parameters->computed_approximation_params_m[i - 1]); + parameters->computed_approximation_params_x[i] = + ( // Formula: (q0 - q1) / (m1 - m0). + parameters->computed_approximation_params_q[i - 1] - + parameters->computed_approximation_params_q[i]) / + (parameters->computed_approximation_params_m[i] - + parameters->computed_approximation_params_m[i - 1]); + } +} + +} // namespace + +namespace test { + +InterpolatedParameters ComputeInterpolatedGainCurveApproximationParams() { + InterpolatedParameters parameters; + Limiter limiter; + parameters.computed_approximation_params_x.fill(0.0f); + parameters.computed_approximation_params_m.fill(0.0f); + parameters.computed_approximation_params_q.fill(0.0f); + PrecomputeKneeApproxParams(&limiter, ¶meters); + PrecomputeBeyondKneeApproxParams(&limiter, ¶meters); + return parameters; +} +} // namespace test +} // namespace webrtc diff --git a/modules/audio_processing/agc2/compute_interpolated_gain_curve.h b/modules/audio_processing/agc2/compute_interpolated_gain_curve.h new file mode 100644 index 0000000000..5f52441ec3 --- /dev/null +++ b/modules/audio_processing/agc2/compute_interpolated_gain_curve.h @@ -0,0 +1,48 @@ +/* + * 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. + */ + +#ifndef MODULES_AUDIO_PROCESSING_AGC2_COMPUTE_INTERPOLATED_GAIN_CURVE_H_ +#define MODULES_AUDIO_PROCESSING_AGC2_COMPUTE_INTERPOLATED_GAIN_CURVE_H_ + +#include + +#include "modules/audio_processing/agc2/agc2_common.h" + +namespace webrtc { + +namespace test { + +// Parameters for interpolated gain curve using under-approximation to +// avoid saturation. +// +// The saturation gain is defined in order to let hard-clipping occur for +// those samples having a level that falls in the saturation region. It is an +// upper bound of the actual gain to apply - i.e., that returned by the +// limiter. + +// Knee and beyond-knee regions approximation parameters. +// The gain curve is approximated as a piece-wise linear function. +// |approx_params_x_| are the boundaries between adjacent linear pieces, +// |approx_params_m_| and |approx_params_q_| are the slope and the y-intercept +// values of each piece. +struct InterpolatedParameters { + std::array + computed_approximation_params_x; + std::array + computed_approximation_params_m; + std::array + computed_approximation_params_q; +}; + +InterpolatedParameters ComputeInterpolatedGainCurveApproximationParams(); +} // namespace test +} // namespace webrtc + +#endif // MODULES_AUDIO_PROCESSING_AGC2_COMPUTE_INTERPOLATED_GAIN_CURVE_H_ diff --git a/modules/audio_processing/agc2/fixed_digital_level_estimator_unittest.cc b/modules/audio_processing/agc2/fixed_digital_level_estimator_unittest.cc index d530ecc997..7547f8e2ed 100644 --- a/modules/audio_processing/agc2/fixed_digital_level_estimator_unittest.cc +++ b/modules/audio_processing/agc2/fixed_digital_level_estimator_unittest.cc @@ -122,7 +122,7 @@ TEST(AutomaticGainController2LevelEstimator, TEST(AutomaticGainController2LevelEstimator, TimeToDecreaseForLowLevel) { constexpr float kLevelReductionDb = 25; constexpr float kInitialLowLevel = -40; - constexpr float kExpectedTime = kLevelReductionDb * kDecayMs; + constexpr float kExpectedTime = kLevelReductionDb * test::kDecayMs; const float time_to_decrease = TimeMsToDecreaseLevel(22000, 1, kInitialLowLevel, kLevelReductionDb); @@ -133,7 +133,7 @@ TEST(AutomaticGainController2LevelEstimator, TimeToDecreaseForLowLevel) { TEST(AutomaticGainController2LevelEstimator, TimeToDecreaseForFullScaleLevel) { constexpr float kLevelReductionDb = 25; - constexpr float kExpectedTime = kLevelReductionDb * kDecayMs; + constexpr float kExpectedTime = kLevelReductionDb * test::kDecayMs; const float time_to_decrease = TimeMsToDecreaseLevel(26000, 1, 0, kLevelReductionDb); @@ -145,7 +145,7 @@ TEST(AutomaticGainController2LevelEstimator, TimeToDecreaseForFullScaleLevel) { TEST(AutomaticGainController2LevelEstimator, TimeToDecreaseForMultipleChannels) { constexpr float kLevelReductionDb = 25; - constexpr float kExpectedTime = kLevelReductionDb * kDecayMs; + constexpr float kExpectedTime = kLevelReductionDb * test::kDecayMs; constexpr size_t kNumChannels = 10; const float time_to_decrease = diff --git a/modules/audio_processing/agc2/fixed_gain_controller.cc b/modules/audio_processing/agc2/fixed_gain_controller.cc index a332365748..a565613d26 100644 --- a/modules/audio_processing/agc2/fixed_gain_controller.cc +++ b/modules/audio_processing/agc2/fixed_gain_controller.cc @@ -16,6 +16,7 @@ #include "api/array_view.h" #include "common_audio/include/audio_util.h" #include "modules/audio_processing/agc2/agc2_common.h" +#include "modules/audio_processing/agc2/interpolated_gain_curve.h" #include "modules/audio_processing/logging/apm_data_dumper.h" #include "rtc_base/checks.h" #include "rtc_base/logging.h" @@ -27,27 +28,30 @@ namespace { // Returns true when the gain factor is so close to 1 that it would // not affect int16 samples. bool CloseToOne(float gain_factor) { - return 1.f - 1.f / kMaxSampleValue <= gain_factor && - gain_factor <= 1.f + 1.f / kMaxSampleValue; + return 1.f - 1.f / kMaxFloatS16Value <= gain_factor && + gain_factor <= 1.f + 1.f / kMaxFloatS16Value; } } // namespace FixedGainController::FixedGainController(ApmDataDumper* apm_data_dumper) - : apm_data_dumper_(apm_data_dumper) { - RTC_DCHECK_LT(0.f, gain_to_apply_); - RTC_DLOG(LS_INFO) << "Gain to apply: " << gain_to_apply_; -} + : apm_data_dumper_(apm_data_dumper), + gain_curve_applier_(48000, apm_data_dumper_) {} void FixedGainController::SetGain(float gain_to_apply_db) { // Changes in gain_to_apply_ cause discontinuities. We assume // gain_to_apply_ is set in the beginning of the call. If it is // frequently changed, we should add interpolation between the // values. + // The gain + RTC_DCHECK_LE(-50.f, gain_to_apply_db); + RTC_DCHECK_LE(gain_to_apply_db, 50.f); gain_to_apply_ = DbToRatio(gain_to_apply_db); + RTC_DCHECK_LT(0.f, gain_to_apply_); + RTC_DLOG(LS_INFO) << "Gain to apply: " << gain_to_apply_db << " db."; } void FixedGainController::SetSampleRate(size_t sample_rate_hz) { - // TODO(aleloi): propagate the new sample rate to the GainCurveApplier. + gain_curve_applier_.SetSampleRate(sample_rate_hz); } void FixedGainController::EnableLimiter(bool enable_limiter) { @@ -70,8 +74,7 @@ void FixedGainController::Process(AudioFrameView signal) { // Use the limiter (if configured to). if (enable_limiter_) { - // TODO(aleloi): Process the signal with the - // GainCurveApplier. This will be done in the upcoming CLs. + gain_curve_applier_.Process(signal); // Dump data for debug. const auto channel_view = signal.channel(0); @@ -83,7 +86,7 @@ void FixedGainController::Process(AudioFrameView signal) { for (size_t k = 0; k < signal.num_channels(); ++k) { rtc::ArrayView channel_view = signal.channel(k); for (auto& sample : channel_view) { - sample = rtc::SafeClamp(sample, kMinSampleValue, kMaxSampleValue); + sample = rtc::SafeClamp(sample, kMinFloatS16Value, kMaxFloatS16Value); } } } diff --git a/modules/audio_processing/agc2/fixed_gain_controller.h b/modules/audio_processing/agc2/fixed_gain_controller.h index decc22e2b3..fd80348ea4 100644 --- a/modules/audio_processing/agc2/fixed_gain_controller.h +++ b/modules/audio_processing/agc2/fixed_gain_controller.h @@ -11,6 +11,7 @@ #ifndef MODULES_AUDIO_PROCESSING_AGC2_FIXED_GAIN_CONTROLLER_H_ #define MODULES_AUDIO_PROCESSING_AGC2_FIXED_GAIN_CONTROLLER_H_ +#include "modules/audio_processing/agc2/gain_curve_applier.h" #include "modules/audio_processing/include/audio_frame_view.h" namespace webrtc { @@ -22,13 +23,16 @@ class FixedGainController { void Process(AudioFrameView signal); + // Rate and gain may be changed at any time (but not concurrently + // with any other method call). void SetGain(float gain_to_apply_db); void SetSampleRate(size_t sample_rate_hz); void EnableLimiter(bool enable_limiter); private: float gain_to_apply_ = 1.f; - ApmDataDumper* apm_data_dumper_; + ApmDataDumper* apm_data_dumper_ = nullptr; + GainCurveApplier gain_curve_applier_; bool enable_limiter_ = true; }; diff --git a/modules/audio_processing/agc2/fixed_gain_controller_unittest.cc b/modules/audio_processing/agc2/fixed_gain_controller_unittest.cc index 9336fd768f..1d6c2ae8b1 100644 --- a/modules/audio_processing/agc2/fixed_gain_controller_unittest.cc +++ b/modules/audio_processing/agc2/fixed_gain_controller_unittest.cc @@ -11,9 +11,11 @@ #include "modules/audio_processing/agc2/fixed_gain_controller.h" #include "api/array_view.h" +#include "modules/audio_processing/agc2/agc2_testing_common.h" #include "modules/audio_processing/agc2/vector_float_frame.h" #include "modules/audio_processing/logging/apm_data_dumper.h" #include "rtc_base/gunit.h" +#include "rtc_base/ptr_util.h" namespace webrtc { namespace { @@ -45,13 +47,15 @@ float RunFixedGainControllerWithConstantInput(FixedGainController* fixed_gc, } ApmDataDumper test_data_dumper(0); -FixedGainController CreateFixedGainController(float gain_to_apply, - size_t rate, - bool enable_limiter) { - FixedGainController fgc(&test_data_dumper); - fgc.SetGain(gain_to_apply); - fgc.SetSampleRate(gain_to_apply); - fgc.EnableLimiter(enable_limiter); +std::unique_ptr CreateFixedGainController( + float gain_to_apply, + size_t rate, + bool enable_limiter) { + std::unique_ptr fgc = + rtc::MakeUnique(&test_data_dumper); + fgc->SetGain(gain_to_apply); + fgc->SetSampleRate(rate); + fgc->EnableLimiter(enable_limiter); return fgc; } @@ -59,51 +63,128 @@ FixedGainController CreateFixedGainController(float gain_to_apply, TEST(AutomaticGainController2FixedDigital, CreateUseWithoutLimiter) { const int kSampleRate = 48000; - FixedGainController fixed_gc = + std::unique_ptr fixed_gc = CreateFixedGainController(kGainToApplyDb, kSampleRate, false); VectorFloatFrame vectors_with_float_frame( 1, rtc::CheckedDivExact(kSampleRate, 100), kInputLevelLinear); auto float_frame = vectors_with_float_frame.float_frame_view(); - fixed_gc.Process(float_frame); + fixed_gc->Process(float_frame); const auto channel = float_frame.channel(0); EXPECT_LT(kInputLevelLinear, channel[0]); } TEST(AutomaticGainController2FixedDigital, CreateUseWithLimiter) { const int kSampleRate = 44000; - FixedGainController fixed_gc = + std::unique_ptr fixed_gc = CreateFixedGainController(kGainToApplyDb, kSampleRate, true); VectorFloatFrame vectors_with_float_frame( 1, rtc::CheckedDivExact(kSampleRate, 100), kInputLevelLinear); auto float_frame = vectors_with_float_frame.float_frame_view(); - fixed_gc.Process(float_frame); + fixed_gc->Process(float_frame); const auto channel = float_frame.channel(0); EXPECT_LT(kInputLevelLinear, channel[0]); } -TEST(AutomaticGainController2FixedDigital, GainShouldChangeOnSetGain) { - constexpr float input_level = 1000.f; - constexpr size_t num_frames = 5; - constexpr size_t kSampleRate = 8000; - constexpr float gain_db_no_change = 0.f; - constexpr float gain_db_factor_10 = 20.f; +TEST(AutomaticGainController2FixedDigital, CheckSaturationBehaviorWithLimiter) { + const float kInputLevel = 32767.f; + const size_t kNumFrames = 5; + const size_t kSampleRate = 42000; - FixedGainController fixed_gc_no_saturation = - CreateFixedGainController(gain_db_no_change, kSampleRate, false); + const auto gains_no_saturation = + test::LinSpace(0.1, test::kLimiterMaxInputLevelDbFs - 0.01, 10); + for (const auto gain_db : gains_no_saturation) { + // Since |test::kLimiterMaxInputLevelDbFs| > |gain_db|, the + // limiter will not saturate the signal. + std::unique_ptr fixed_gc_no_saturation = + CreateFixedGainController(gain_db, kSampleRate, true); + + // Saturation not expected. + SCOPED_TRACE(std::to_string(gain_db)); + EXPECT_LT( + RunFixedGainControllerWithConstantInput( + fixed_gc_no_saturation.get(), kInputLevel, kNumFrames, kSampleRate), + 32767.f); + } + + const auto gains_saturation = + test::LinSpace(test::kLimiterMaxInputLevelDbFs + 0.01, 10, 10); + for (const auto gain_db : gains_saturation) { + // Since |test::kLimiterMaxInputLevelDbFs| < |gain|, the limiter + // will saturate the signal. + std::unique_ptr fixed_gc_saturation = + CreateFixedGainController(gain_db, kSampleRate, true); + + // Saturation expected. + SCOPED_TRACE(std::to_string(gain_db)); + EXPECT_FLOAT_EQ( + RunFixedGainControllerWithConstantInput( + fixed_gc_saturation.get(), kInputLevel, kNumFrames, kSampleRate), + 32767.f); + } +} + +TEST(AutomaticGainController2FixedDigital, + CheckSaturationBehaviorWithLimiterSingleSample) { + const float kInputLevel = 32767.f; + const size_t kNumFrames = 5; + const size_t kSampleRate = 8000; + + const auto gains_no_saturation = + test::LinSpace(0.1, test::kLimiterMaxInputLevelDbFs - 0.01, 10); + for (const auto gain_db : gains_no_saturation) { + // Since |gain| > |test::kLimiterMaxInputLevelDbFs|, the limiter will + // not saturate the signal. + std::unique_ptr fixed_gc_no_saturation = + CreateFixedGainController(gain_db, kSampleRate, true); + + // Saturation not expected. + SCOPED_TRACE(std::to_string(gain_db)); + EXPECT_LT( + RunFixedGainControllerWithConstantInput( + fixed_gc_no_saturation.get(), kInputLevel, kNumFrames, kSampleRate), + 32767.f); + } + + const auto gains_saturation = + test::LinSpace(test::kLimiterMaxInputLevelDbFs + 0.01, 10, 10); + for (const auto gain_db : gains_saturation) { + // Singe |gain| < |test::kLimiterMaxInputLevelDbFs|, the limiter will + // saturate the signal. + std::unique_ptr fixed_gc_saturation = + CreateFixedGainController(gain_db, kSampleRate, true); + + // Saturation expected. + SCOPED_TRACE(std::to_string(gain_db)); + EXPECT_FLOAT_EQ( + RunFixedGainControllerWithConstantInput( + fixed_gc_saturation.get(), kInputLevel, kNumFrames, kSampleRate), + 32767.f); + } +} + +TEST(AutomaticGainController2FixedDigital, GainShouldChangeOnSetGain) { + constexpr float kInputLevel = 1000.f; + constexpr size_t kNumFrames = 5; + constexpr size_t kSampleRate = 8000; + constexpr float kGainDbNoChange = 0.f; + constexpr float kGainDbFactor10 = 20.f; + + std::unique_ptr fixed_gc_no_saturation = + CreateFixedGainController(kGainDbNoChange, kSampleRate, false); // Signal level is unchanged with 0 db gain. EXPECT_FLOAT_EQ( RunFixedGainControllerWithConstantInput( - &fixed_gc_no_saturation, input_level, num_frames, kSampleRate), - input_level); + fixed_gc_no_saturation.get(), kInputLevel, kNumFrames, kSampleRate), + kInputLevel); - fixed_gc_no_saturation.SetGain(gain_db_factor_10); + fixed_gc_no_saturation->SetGain(kGainDbFactor10); // +20db should increase signal by a factor of 10. EXPECT_FLOAT_EQ( RunFixedGainControllerWithConstantInput( - &fixed_gc_no_saturation, input_level, num_frames, kSampleRate), - input_level * 10); + fixed_gc_no_saturation.get(), kInputLevel, kNumFrames, kSampleRate), + kInputLevel * 10); } } // namespace webrtc diff --git a/modules/audio_processing/agc2/gain_curve_applier.cc b/modules/audio_processing/agc2/gain_curve_applier.cc new file mode 100644 index 0000000000..1610c4a19e --- /dev/null +++ b/modules/audio_processing/agc2/gain_curve_applier.cc @@ -0,0 +1,131 @@ +/* + * 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/gain_curve_applier.h" + +#include +#include +#include + +#include "api/array_view.h" +#include "rtc_base/checks.h" + +namespace webrtc { +namespace { + +// This constant affects the way scaling factors are interpolated for the first +// sub-frame of a frame. Only in the case in which the first sub-frame has an +// estimated level which is greater than the that of the previous analyzed +// sub-frame, linear interpolation is replaced with a power function which +// reduces the chances of over-shooting (and hence saturation), however reducing +// the fixed gain effectiveness. +constexpr float kAttackFirstSubframeInterpolationPower = 8.f; + +void InterpolateFirstSubframe(float last_factor, + float current_factor, + rtc::ArrayView subframe) { + const auto n = subframe.size(); + constexpr auto p = kAttackFirstSubframeInterpolationPower; + for (size_t i = 0; i < n; ++i) { + subframe[i] = std::pow(1.f - i / n, p) * (last_factor - current_factor) + + current_factor; + } +} + +void ComputePerSampleSubframeFactors( + const std::array& scaling_factors, + size_t samples_per_channel, + rtc::ArrayView per_sample_scaling_factors) { + const size_t num_subframes = scaling_factors.size() - 1; + const size_t subframe_size = + rtc::CheckedDivExact(samples_per_channel, num_subframes); + + // Handle first sub-frame differently in case of attack. + const bool is_attack = scaling_factors[0] > scaling_factors[1]; + if (is_attack) { + InterpolateFirstSubframe( + scaling_factors[0], scaling_factors[1], + rtc::ArrayView( + per_sample_scaling_factors.subview(0, subframe_size))); + } + + for (size_t i = is_attack ? 1 : 0; i < num_subframes; ++i) { + const size_t subframe_start = i * subframe_size; + const float scaling_start = scaling_factors[i]; + const float scaling_end = scaling_factors[i + 1]; + const float scaling_diff = (scaling_end - scaling_start) / subframe_size; + for (size_t j = 0; j < subframe_size; ++j) { + per_sample_scaling_factors[subframe_start + j] = + scaling_start + scaling_diff * j; + } + } +} + +void ScaleSamples(rtc::ArrayView per_sample_scaling_factors, + AudioFrameView signal) { + const size_t samples_per_channel = signal.samples_per_channel(); + RTC_DCHECK_EQ(samples_per_channel, per_sample_scaling_factors.size()); + for (size_t i = 0; i < signal.num_channels(); ++i) { + auto channel = signal.channel(i); + for (size_t j = 0; j < samples_per_channel; ++j) { + channel[j] *= per_sample_scaling_factors[j]; + } + } +} + +} // namespace + +GainCurveApplier::GainCurveApplier(size_t sample_rate_hz, + ApmDataDumper* apm_data_dumper) + : interp_gain_curve_(apm_data_dumper), + level_estimator_(sample_rate_hz, apm_data_dumper), + apm_data_dumper_(apm_data_dumper) {} + +GainCurveApplier::~GainCurveApplier() = default; + +void GainCurveApplier::Process(AudioFrameView signal) { + const auto level_estimate = level_estimator_.ComputeLevel(signal); + + RTC_DCHECK_EQ(level_estimate.size() + 1, scaling_factors_.size()); + scaling_factors_[0] = last_scaling_factor_; + std::transform(level_estimate.begin(), level_estimate.end(), + scaling_factors_.begin() + 1, [this](float x) { + return interp_gain_curve_.LookUpGainToApply(x); + }); + + const size_t samples_per_channel = signal.samples_per_channel(); + RTC_DCHECK_LE(samples_per_channel, kMaximalNumberOfSamplesPerChannel); + + auto per_sample_scaling_factors = rtc::ArrayView( + &per_sample_scaling_factors_[0], samples_per_channel); + ComputePerSampleSubframeFactors(scaling_factors_, samples_per_channel, + per_sample_scaling_factors); + ScaleSamples(per_sample_scaling_factors, signal); + + last_scaling_factor_ = scaling_factors_.back(); + + // Dump data for debug. + apm_data_dumper_->DumpRaw("agc2_gain_curve_applier_scaling_factors", + samples_per_channel, + per_sample_scaling_factors_.data()); +} + +InterpolatedGainCurve::Stats GainCurveApplier::GetGainCurveStats() const { + return interp_gain_curve_.get_stats(); +} + +void GainCurveApplier::SetSampleRate(size_t sample_rate_hz) { + level_estimator_.SetSampleRate(sample_rate_hz); + // Check that per_sample_scaling_factors_ is large enough. + RTC_DCHECK_LE(sample_rate_hz, + kMaximalNumberOfSamplesPerChannel * 1000 / kFrameDurationMs); +} + +} // namespace webrtc diff --git a/modules/audio_processing/agc2/gain_curve_applier.h b/modules/audio_processing/agc2/gain_curve_applier.h new file mode 100644 index 0000000000..86ca251948 --- /dev/null +++ b/modules/audio_processing/agc2/gain_curve_applier.h @@ -0,0 +1,56 @@ +/* + * 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. + */ + +#ifndef MODULES_AUDIO_PROCESSING_AGC2_GAIN_CURVE_APPLIER_H_ +#define MODULES_AUDIO_PROCESSING_AGC2_GAIN_CURVE_APPLIER_H_ + +#include + +#include "modules/audio_processing/agc2/fixed_digital_level_estimator.h" +#include "modules/audio_processing/agc2/interpolated_gain_curve.h" +#include "modules/audio_processing/include/audio_frame_view.h" +#include "rtc_base/constructormagic.h" + +namespace webrtc { +class ApmDataDumper; + +class GainCurveApplier { + public: + GainCurveApplier(size_t sample_rate_hz, ApmDataDumper* apm_data_dumper); + + ~GainCurveApplier(); + + void Process(AudioFrameView signal); + InterpolatedGainCurve::Stats GetGainCurveStats() const; + + // Supported rates must be + // * supported by FixedDigitalLevelEstimator + // * below kMaximalNumberOfSamplesPerChannel*1000/kFrameDurationMs + // so that samples_per_channel fit in the + // per_sample_scaling_factors_ array. + void SetSampleRate(size_t sample_rate_hz); + + private: + const InterpolatedGainCurve interp_gain_curve_; + FixedDigitalLevelEstimator level_estimator_; + ApmDataDumper* const apm_data_dumper_ = nullptr; + + // Work array containing the sub-frame scaling factors to be interpolated. + std::array scaling_factors_ = {}; + std::array + per_sample_scaling_factors_ = {}; + float last_scaling_factor_ = 1.f; + + RTC_DISALLOW_COPY_AND_ASSIGN(GainCurveApplier); +}; + +} // namespace webrtc + +#endif // MODULES_AUDIO_PROCESSING_AGC2_GAIN_CURVE_APPLIER_H_ diff --git a/modules/audio_processing/agc2/gain_curve_applier_unittest.cc b/modules/audio_processing/agc2/gain_curve_applier_unittest.cc new file mode 100644 index 0000000000..a7cb1b65df --- /dev/null +++ b/modules/audio_processing/agc2/gain_curve_applier_unittest.cc @@ -0,0 +1,59 @@ +/* + * 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/gain_curve_applier.h" + +#include "common_audio/include/audio_util.h" +#include "modules/audio_processing/agc2/agc2_common.h" +#include "modules/audio_processing/agc2/agc2_testing_common.h" +#include "modules/audio_processing/agc2/vector_float_frame.h" +#include "rtc_base/gunit.h" + +namespace webrtc { + +TEST(GainCurveApplier, GainCurveApplierShouldConstructAndRun) { + const int sample_rate_hz = 48000; + ApmDataDumper apm_data_dumper(0); + + GainCurveApplier gain_curve_applier(sample_rate_hz, &apm_data_dumper); + + VectorFloatFrame vectors_with_float_frame(1, sample_rate_hz / 100, + kMaxAbsFloatS16Value); + gain_curve_applier.Process(vectors_with_float_frame.float_frame_view()); +} + +TEST(GainCurveApplier, OutputVolumeAboveThreshold) { + const int sample_rate_hz = 48000; + const float input_level = + (kMaxAbsFloatS16Value + DbfsToFloatS16(test::kLimiterMaxInputLevelDbFs)) / + 2.f; + ApmDataDumper apm_data_dumper(0); + + GainCurveApplier gain_curve_applier(sample_rate_hz, &apm_data_dumper); + + // Give the level estimator time to adapt. + for (int i = 0; i < 5; ++i) { + VectorFloatFrame vectors_with_float_frame(1, sample_rate_hz / 100, + input_level); + gain_curve_applier.Process(vectors_with_float_frame.float_frame_view()); + } + + VectorFloatFrame vectors_with_float_frame(1, sample_rate_hz / 100, + input_level); + gain_curve_applier.Process(vectors_with_float_frame.float_frame_view()); + rtc::ArrayView channel = + vectors_with_float_frame.float_frame_view().channel(0); + + for (const auto& sample : channel) { + EXPECT_LT(0.9f * kMaxAbsFloatS16Value, sample); + } +} + +} // namespace webrtc diff --git a/modules/audio_processing/agc2/interpolated_gain_curve.cc b/modules/audio_processing/agc2/interpolated_gain_curve.cc new file mode 100644 index 0000000000..0cc8f9068d --- /dev/null +++ b/modules/audio_processing/agc2/interpolated_gain_curve.cc @@ -0,0 +1,105 @@ +/* + * 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/interpolated_gain_curve.h" + +#include "modules/audio_processing/agc2/agc2_common.h" +#include "modules/audio_processing/logging/apm_data_dumper.h" +#include "rtc_base/checks.h" +#include "rtc_base/logging.h" + +namespace webrtc { + +constexpr std::array + InterpolatedGainCurve::approximation_params_x_; + +constexpr std::array + InterpolatedGainCurve::approximation_params_m_; + +constexpr std::array + InterpolatedGainCurve::approximation_params_q_; + +InterpolatedGainCurve::InterpolatedGainCurve(ApmDataDumper* apm_data_dumper) + : apm_data_dumper_(apm_data_dumper) {} + +InterpolatedGainCurve::~InterpolatedGainCurve() { + if (stats_.available) { + // TODO(alessiob): We might want to add these stats as RTC metrics. + RTC_DCHECK(apm_data_dumper_); + apm_data_dumper_->DumpRaw("agc2_interp_gain_curve_lookups_identity", + stats_.look_ups_identity_region); + apm_data_dumper_->DumpRaw("agc2_interp_gain_curve_lookups_knee", + stats_.look_ups_knee_region); + apm_data_dumper_->DumpRaw("agc2_interp_gain_curve_lookups_limiter", + stats_.look_ups_limiter_region); + apm_data_dumper_->DumpRaw("agc2_interp_gain_curve_lookups_saturation", + stats_.look_ups_saturation_region); + } +} + +void InterpolatedGainCurve::UpdateStats(float input_level) const { + stats_.available = true; + + if (input_level < approximation_params_x_[0]) { + stats_.look_ups_identity_region++; + } else if (input_level < + approximation_params_x_[kInterpolatedGainCurveKneePoints - 1]) { + stats_.look_ups_knee_region++; + } else if (input_level < kMaxInputLevelLinear) { + stats_.look_ups_limiter_region++; + } else { + stats_.look_ups_saturation_region++; + } +} + +// Looks up a gain to apply given a non-negative input level. +// The cost of this operation depends on the region in which |input_level| +// falls. +// For the identity and the saturation regions the cost is O(1). +// For the other regions, namely knee and limiter, the cost is +// O(2 + log2(|LightkInterpolatedGainCurveTotalPoints|), plus O(1) for the +// linear interpolation (one product and one sum). +float InterpolatedGainCurve::LookUpGainToApply(float input_level) const { + UpdateStats(input_level); + + if (input_level <= approximation_params_x_[0]) { + // Identity region. + return 1.0f; + } + + if (input_level >= kMaxInputLevelLinear) { + // Saturating lower bound. The saturing samples exactly hit the clipping + // level. This method achieves has the lowest harmonic distorsion, but it + // may reduce the amplitude of the non-saturating samples too much. + return 32768.f / input_level; + } + + // Knee and limiter regions; find the linear piece index. Spelling + // out the complete type was the only way to silence both the clang + // plugin and the windows compilers. + std::array::const_iterator it = + std::lower_bound(approximation_params_x_.begin(), + approximation_params_x_.end(), input_level); + const size_t index = std::distance(approximation_params_x_.begin(), it) - 1; + RTC_DCHECK_LE(0, index); + RTC_DCHECK_LT(index, approximation_params_m_.size()); + RTC_DCHECK_LE(approximation_params_x_[index], input_level); + if (index < approximation_params_m_.size() - 1) { + RTC_DCHECK_LE(input_level, approximation_params_x_[index + 1]); + } + + // Piece-wise linear interploation. + const float gain = approximation_params_m_[index] * input_level + + approximation_params_q_[index]; + RTC_DCHECK_LE(0.f, gain); + return gain; +} + +} // namespace webrtc diff --git a/modules/audio_processing/agc2/interpolated_gain_curve.h b/modules/audio_processing/agc2/interpolated_gain_curve.h new file mode 100644 index 0000000000..de79c14958 --- /dev/null +++ b/modules/audio_processing/agc2/interpolated_gain_curve.h @@ -0,0 +1,122 @@ +/* + * 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. + */ + +#ifndef MODULES_AUDIO_PROCESSING_AGC2_INTERPOLATED_GAIN_CURVE_H_ +#define MODULES_AUDIO_PROCESSING_AGC2_INTERPOLATED_GAIN_CURVE_H_ + +#include + +#include "modules/audio_processing/agc2/agc2_common.h" +#include "modules/audio_processing/logging/apm_data_dumper.h" +#include "rtc_base/basictypes.h" +#include "rtc_base/gtest_prod_util.h" + +namespace webrtc { + +class ApmDataDumper; + +constexpr float kInputLevelScalingFactor = 32768.0f; + +// Defined as DbfsToLinear(kLimiterMaxInputLevelDbFs) +constexpr float kMaxInputLevelLinear = static_cast(36766.300710566735); + +// Interpolated gain curve using under-approximation to avoid saturation. +// +// The goal of this class is allowing fast look ups to get an accurate +// estimates of the gain to apply given an estimated input level. +class InterpolatedGainCurve { + public: + struct Stats { + // Region in which the output level equals the input one. + size_t look_ups_identity_region = 0; + // Smoothing between the identity and the limiter regions. + size_t look_ups_knee_region = 0; + // Limiter region in which the output and input levels are linearly related. + size_t look_ups_limiter_region = 0; + // Region in which saturation may occur since the input level is beyond the + // maximum expected by the limiter. + size_t look_ups_saturation_region = 0; + // True if stats have been populated. + bool available = false; + }; + + // InterpolatedGainCurve(InterpolatedGainCurve&&); + explicit InterpolatedGainCurve(ApmDataDumper* apm_data_dumper); + ~InterpolatedGainCurve(); + + Stats get_stats() const { return stats_; } + + // Given a non-negative input level (linear scale), a scalar factor to apply + // to a sub-frame is returned. + // Levels above kLimiterMaxInputLevelDbFs will be reduced to 0 dBFS + // after applying this gain + float LookUpGainToApply(float input_level) const; + + private: + // For comparing 'approximation_params_*_' with ones computed by + // ComputeInterpolatedGainCurve. + FRIEND_TEST_ALL_PREFIXES(AutomaticGainController2InterpolatedGainCurve, + CheckApproximationParams); + void UpdateStats(float input_level) const; + + ApmDataDumper* const apm_data_dumper_; + + static constexpr std::array + approximation_params_x_ = { + {30057.296875, 30148.986328125, 30240.67578125, 30424.052734375, + 30607.4296875, 30790.806640625, 30974.18359375, 31157.560546875, + 31340.939453125, 31524.31640625, 31707.693359375, 31891.0703125, + 32074.447265625, 32257.82421875, 32441.201171875, 32624.580078125, + 32807.95703125, 32991.33203125, 33174.7109375, 33358.08984375, + 33541.46484375, 33724.84375, 33819.53515625, 34009.5390625, + 34200.05859375, 34389.81640625, 34674.48828125, 35054.375, + 35434.86328125, 35814.81640625, 36195.16796875, 36575.03125}}; + static constexpr std::array + approximation_params_m_ = { + {-3.515235675877192989e-07, -1.050251626111275982e-06, + -2.085213736791047268e-06, -3.443004743530764244e-06, + -4.773849468620028347e-06, -6.077375928725814447e-06, + -7.353257842623861507e-06, -8.601219633419532329e-06, + -9.821013009059242904e-06, -1.101243378798244521e-05, + -1.217532644659513608e-05, -1.330956911260727793e-05, + -1.441507538402220234e-05, -1.549179251014720649e-05, + -1.653970684856176376e-05, -1.755882840370759368e-05, + -1.854918446042574942e-05, -1.951086778717581183e-05, + -2.044398024736437947e-05, -2.1348627342376858e-05, + -2.222496914328075945e-05, -2.265374678245279938e-05, + -2.242570917587727308e-05, -2.220122041762806475e-05, + -2.19802095671184361e-05, -2.176260204578284174e-05, + -2.133731686626560986e-05, -2.092481918225530535e-05, + -2.052459603874012828e-05, -2.013615448959171772e-05, + -1.975903069251216948e-05, -1.939277899509761482e-05}}; + + static constexpr std::array + approximation_params_q_ = { + {1.010565876960754395, 1.031631827354431152, 1.062929749488830566, + 1.104239225387573242, 1.144973039627075195, 1.185109615325927734, + 1.224629044532775879, 1.263512492179870605, 1.301741957664489746, + 1.339300632476806641, 1.376173257827758789, 1.412345528602600098, + 1.447803974151611328, 1.482536554336547852, 1.516532182693481445, + 1.549780607223510742, 1.582272171974182129, 1.613999366760253906, + 1.644955039024353027, 1.675132393836975098, 1.704526185989379883, + 1.718986630439758301, 1.711274504661560059, 1.703639745712280273, + 1.696081161499023438, 1.688597679138183594, 1.673851132392883301, + 1.659391283988952637, 1.645209431648254395, 1.631297469139099121, + 1.617647409439086914, 1.604251742362976074}}; + + // Stats. + mutable Stats stats_; + + // RTC_DISALLOW_COPY_AND_ASSIGN(InterpolatedGainCurve); +}; + +} // namespace webrtc + +#endif // MODULES_AUDIO_PROCESSING_AGC2_INTERPOLATED_GAIN_CURVE_H_ diff --git a/modules/audio_processing/agc2/interpolated_gain_curve_unittest.cc b/modules/audio_processing/agc2/interpolated_gain_curve_unittest.cc new file mode 100644 index 0000000000..d75ddbd7f7 --- /dev/null +++ b/modules/audio_processing/agc2/interpolated_gain_curve_unittest.cc @@ -0,0 +1,202 @@ +/* + * 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 +#include + +#include "api/array_view.h" +#include "common_audio/include/audio_util.h" +#include "modules/audio_processing/agc2/agc2_common.h" +#include "modules/audio_processing/agc2/compute_interpolated_gain_curve.h" +#include "modules/audio_processing/agc2/interpolated_gain_curve.h" +#include "modules/audio_processing/agc2/limiter.h" +#include "modules/audio_processing/logging/apm_data_dumper.h" +#include "rtc_base/checks.h" +#include "rtc_base/gunit.h" + +namespace webrtc { +namespace { + +constexpr double kLevelEpsilon = 1e-2 * kMaxAbsFloatS16Value; +constexpr float kInterpolatedGainCurveTolerance = 1.f / 32768.f; +ApmDataDumper apm_data_dumper(0); +const Limiter limiter; + +} // namespace + +TEST(AutomaticGainController2InterpolatedGainCurve, CreateUse) { + InterpolatedGainCurve igc(&apm_data_dumper); + + const auto levels = test::LinSpace( + kLevelEpsilon, DbfsToFloatS16(limiter.max_input_level_db() + 1), 500); + for (const auto level : levels) { + EXPECT_GE(igc.LookUpGainToApply(level), 0.0f); + } +} + +TEST(AutomaticGainController2InterpolatedGainCurve, CheckValidOutput) { + InterpolatedGainCurve igc(&apm_data_dumper); + + const auto levels = test::LinSpace( + kLevelEpsilon, limiter.max_input_level_linear() * 2.0, 500); + for (const auto level : levels) { + SCOPED_TRACE(std::to_string(level)); + const float gain = igc.LookUpGainToApply(level); + EXPECT_LE(0.0f, gain); + EXPECT_LE(gain, 1.0f); + } +} + +TEST(AutomaticGainController2InterpolatedGainCurve, CheckMonotonicity) { + InterpolatedGainCurve igc(&apm_data_dumper); + + const auto levels = test::LinSpace( + kLevelEpsilon, limiter.max_input_level_linear() + kLevelEpsilon + 0.5, + 500); + float prev_gain = igc.LookUpGainToApply(0.0f); + for (const auto level : levels) { + const float gain = igc.LookUpGainToApply(level); + EXPECT_GE(prev_gain, gain); + prev_gain = gain; + } +} + +TEST(AutomaticGainController2InterpolatedGainCurve, CheckApproximation) { + InterpolatedGainCurve igc(&apm_data_dumper); + + const auto levels = test::LinSpace( + kLevelEpsilon, limiter.max_input_level_linear() - kLevelEpsilon, 500); + for (const auto level : levels) { + SCOPED_TRACE(std::to_string(level)); + EXPECT_LT( + std::fabs(limiter.GetGainLinear(level) - igc.LookUpGainToApply(level)), + kInterpolatedGainCurveTolerance); + } +} + +TEST(AutomaticGainController2InterpolatedGainCurve, CheckRegionBoundaries) { + InterpolatedGainCurve igc(&apm_data_dumper); + + const std::vector levels{ + {kLevelEpsilon, limiter.knee_start_linear() + kLevelEpsilon, + limiter.limiter_start_linear() + kLevelEpsilon, + limiter.max_input_level_linear() + kLevelEpsilon}}; + for (const auto level : levels) { + igc.LookUpGainToApply(level); + } + + const auto stats = igc.get_stats(); + EXPECT_EQ(1ul, stats.look_ups_identity_region); + EXPECT_EQ(1ul, stats.look_ups_knee_region); + EXPECT_EQ(1ul, stats.look_ups_limiter_region); + EXPECT_EQ(1ul, stats.look_ups_saturation_region); +} + +TEST(AutomaticGainController2InterpolatedGainCurve, CheckIdentityRegion) { + constexpr size_t kNumSteps = 10; + InterpolatedGainCurve igc(&apm_data_dumper); + + const auto levels = + test::LinSpace(kLevelEpsilon, limiter.knee_start_linear(), kNumSteps); + for (const auto level : levels) { + SCOPED_TRACE(std::to_string(level)); + EXPECT_EQ(1.0f, igc.LookUpGainToApply(level)); + } + + const auto stats = igc.get_stats(); + EXPECT_EQ(kNumSteps - 1, stats.look_ups_identity_region); + EXPECT_EQ(1ul, stats.look_ups_knee_region); + EXPECT_EQ(0ul, stats.look_ups_limiter_region); + EXPECT_EQ(0ul, stats.look_ups_saturation_region); +} + +TEST(AutomaticGainController2InterpolatedGainCurve, + CheckNoOverApproximationKnee) { + constexpr size_t kNumSteps = 10; + InterpolatedGainCurve igc(&apm_data_dumper); + + const auto levels = + test::LinSpace(limiter.knee_start_linear() + kLevelEpsilon, + limiter.limiter_start_linear(), kNumSteps); + for (const auto level : levels) { + SCOPED_TRACE(std::to_string(level)); + // Small tolerance added (needed because comparing a float with a double). + EXPECT_LE(igc.LookUpGainToApply(level), + limiter.GetGainLinear(level) + 1e-7); + } + + const auto stats = igc.get_stats(); + EXPECT_EQ(0ul, stats.look_ups_identity_region); + EXPECT_EQ(kNumSteps - 1, stats.look_ups_knee_region); + EXPECT_EQ(1ul, stats.look_ups_limiter_region); + EXPECT_EQ(0ul, stats.look_ups_saturation_region); +} + +TEST(AutomaticGainController2InterpolatedGainCurve, + CheckNoOverApproximationBeyondKnee) { + constexpr size_t kNumSteps = 10; + InterpolatedGainCurve igc(&apm_data_dumper); + + const auto levels = test::LinSpace( + limiter.limiter_start_linear() + kLevelEpsilon, + limiter.max_input_level_linear() - kLevelEpsilon, kNumSteps); + for (const auto level : levels) { + SCOPED_TRACE(std::to_string(level)); + // Small tolerance added (needed because comparing a float with a double). + EXPECT_LE(igc.LookUpGainToApply(level), + limiter.GetGainLinear(level) + 1e-7); + } + + const auto stats = igc.get_stats(); + EXPECT_EQ(0ul, stats.look_ups_identity_region); + EXPECT_EQ(0ul, stats.look_ups_knee_region); + EXPECT_EQ(kNumSteps, stats.look_ups_limiter_region); + EXPECT_EQ(0ul, stats.look_ups_saturation_region); +} + +TEST(AutomaticGainController2InterpolatedGainCurve, + CheckNoOverApproximationWithSaturation) { + constexpr size_t kNumSteps = 3; + InterpolatedGainCurve igc(&apm_data_dumper); + + const auto levels = test::LinSpace( + limiter.max_input_level_linear() + kLevelEpsilon, + limiter.max_input_level_linear() + kLevelEpsilon + 0.5, kNumSteps); + for (const auto level : levels) { + SCOPED_TRACE(std::to_string(level)); + EXPECT_LE(igc.LookUpGainToApply(level), limiter.GetGainLinear(level)); + } + + const auto stats = igc.get_stats(); + EXPECT_EQ(0ul, stats.look_ups_identity_region); + EXPECT_EQ(0ul, stats.look_ups_knee_region); + EXPECT_EQ(0ul, stats.look_ups_limiter_region); + EXPECT_EQ(kNumSteps, stats.look_ups_saturation_region); +} + +TEST(AutomaticGainController2InterpolatedGainCurve, CheckApproximationParams) { + test::InterpolatedParameters parameters = + test::ComputeInterpolatedGainCurveApproximationParams(); + + InterpolatedGainCurve igc(&apm_data_dumper); + + for (size_t i = 0; i < kInterpolatedGainCurveTotalPoints; ++i) { + // The tolerance levels are chosen to account for deviations due + // to computing with single precision floating point numbers. + EXPECT_NEAR(igc.approximation_params_x_[i], + parameters.computed_approximation_params_x[i], 0.9f); + EXPECT_NEAR(igc.approximation_params_m_[i], + parameters.computed_approximation_params_m[i], 0.00001f); + EXPECT_NEAR(igc.approximation_params_q_[i], + parameters.computed_approximation_params_q[i], 0.001f); + } +} + +} // namespace webrtc diff --git a/modules/audio_processing/agc2/limiter.cc b/modules/audio_processing/agc2/limiter.cc new file mode 100644 index 0000000000..d2b9877be1 --- /dev/null +++ b/modules/audio_processing/agc2/limiter.cc @@ -0,0 +1,137 @@ +/* + * 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/limiter.h" + +#include + +#include "common_audio/include/audio_util.h" +#include "modules/audio_processing/agc2/agc2_common.h" +#include "rtc_base/checks.h" + +namespace webrtc { +namespace { + +double ComputeKneeStart(double max_input_level_db, + double knee_smoothness_db, + double compression_ratio) { + RTC_CHECK_LT((compression_ratio - 1.0) * knee_smoothness_db / + (2.0 * compression_ratio), + max_input_level_db); + return -knee_smoothness_db / 2.0 - + max_input_level_db / (compression_ratio - 1.0); +} + +std::array ComputeKneeRegionPolynomial(double knee_start_dbfs, + double knee_smoothness_db, + double compression_ratio) { + const double a = (1.0 - compression_ratio) / + (2.0 * knee_smoothness_db * compression_ratio); + const double b = 1.0 - 2.0 * a * knee_start_dbfs; + const double c = a * knee_start_dbfs * knee_start_dbfs; + return {{a, b, c}}; +} + +double ComputeLimiterD1(double max_input_level_db, double compression_ratio) { + return (std::pow(10.0, -max_input_level_db / (20.0 * compression_ratio)) * + (1.0 - compression_ratio) / compression_ratio) / + kMaxAbsFloatS16Value; +} + +constexpr double ComputeLimiterD2(double compression_ratio) { + return (1.0 - 2.0 * compression_ratio) / compression_ratio; +} + +double ComputeLimiterI2(double max_input_level_db, + double compression_ratio, + double gain_curve_limiter_i1) { + RTC_CHECK_NE(gain_curve_limiter_i1, 0.f); + return std::pow(10.0, -max_input_level_db / (20.0 * compression_ratio)) / + gain_curve_limiter_i1 / + std::pow(kMaxAbsFloatS16Value, gain_curve_limiter_i1 - 1); +} + +} // namespace + +Limiter::Limiter() + : max_input_level_linear_(DbfsToFloatS16(max_input_level_db_)), + knee_start_dbfs_(ComputeKneeStart(max_input_level_db_, + knee_smoothness_db_, + compression_ratio_)), + knee_start_linear_(DbfsToFloatS16(knee_start_dbfs_)), + limiter_start_dbfs_(knee_start_dbfs_ + knee_smoothness_db_), + limiter_start_linear_(DbfsToFloatS16(limiter_start_dbfs_)), + knee_region_polynomial_(ComputeKneeRegionPolynomial(knee_start_dbfs_, + knee_smoothness_db_, + compression_ratio_)), + gain_curve_limiter_d1_( + ComputeLimiterD1(max_input_level_db_, compression_ratio_)), + gain_curve_limiter_d2_(ComputeLimiterD2(compression_ratio_)), + gain_curve_limiter_i1_(1.0 / compression_ratio_), + gain_curve_limiter_i2_(ComputeLimiterI2(max_input_level_db_, + compression_ratio_, + gain_curve_limiter_i1_)) { + static_assert(knee_smoothness_db_ > 0.0f, ""); + static_assert(compression_ratio_ > 1.0f, ""); + RTC_CHECK_GE(max_input_level_db_, knee_start_dbfs_ + knee_smoothness_db_); +} + +constexpr double Limiter::max_input_level_db_; +constexpr double Limiter::knee_smoothness_db_; +constexpr double Limiter::compression_ratio_; + +double Limiter::GetOutputLevelDbfs(double input_level_dbfs) const { + if (input_level_dbfs < knee_start_dbfs_) { + return input_level_dbfs; + } else if (input_level_dbfs < limiter_start_dbfs_) { + return GetKneeRegionOutputLevelDbfs(input_level_dbfs); + } + return GetCompressorRegionOutputLevelDbfs(input_level_dbfs); +} + +double Limiter::GetGainLinear(double input_level_linear) const { + if (input_level_linear < knee_start_linear_) { + return 1.0; + } + return DbfsToFloatS16( + GetOutputLevelDbfs(FloatS16ToDbfs(input_level_linear))) / + input_level_linear; +} + +// Computes the first derivative of GetGainLinear() in |x|. +double Limiter::GetGainFirstDerivativeLinear(double x) const { + // Beyond-knee region only. + RTC_CHECK_GE(x, limiter_start_linear_ - 1e-7 * kMaxAbsFloatS16Value); + return gain_curve_limiter_d1_ * + std::pow(x / kMaxAbsFloatS16Value, gain_curve_limiter_d2_); +} + +// Computes the integral of GetGainLinear() in the range [x0, x1]. +double Limiter::GetGainIntegralLinear(double x0, double x1) const { + RTC_CHECK_LE(x0, x1); // Valid interval. + RTC_CHECK_GE(x0, limiter_start_linear_); // Beyond-knee region only. + auto limiter_integral = [this](const double& x) { + return gain_curve_limiter_i2_ * std::pow(x, gain_curve_limiter_i1_); + }; + return limiter_integral(x1) - limiter_integral(x0); +} + +double Limiter::GetKneeRegionOutputLevelDbfs(double input_level_dbfs) const { + return knee_region_polynomial_[0] * input_level_dbfs * input_level_dbfs + + knee_region_polynomial_[1] * input_level_dbfs + + knee_region_polynomial_[2]; +} + +double Limiter::GetCompressorRegionOutputLevelDbfs( + double input_level_dbfs) const { + return (input_level_dbfs - max_input_level_db_) / compression_ratio_; +} + +} // namespace webrtc diff --git a/modules/audio_processing/agc2/limiter.h b/modules/audio_processing/agc2/limiter.h new file mode 100644 index 0000000000..f350baeef1 --- /dev/null +++ b/modules/audio_processing/agc2/limiter.h @@ -0,0 +1,74 @@ +/* + * 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. + */ + +#ifndef MODULES_AUDIO_PROCESSING_AGC2_LIMITER_H_ +#define MODULES_AUDIO_PROCESSING_AGC2_LIMITER_H_ + +#include + +#include "modules/audio_processing/agc2/agc2_testing_common.h" + +namespace webrtc { + +// A class for computing gain curve parameters. The gain curve is +// defined by constants kLimiterMaxInputLevelDbFs, kLimiterKneeSmoothnessDb, +// kLimiterCompressionRatio. The curve consints of one linear part, +// one quadratic polynomial part and another linear part. The +// constants define the parameters of the parts. +class Limiter { + public: + Limiter(); + + double max_input_level_db() const { return max_input_level_db_; } + double max_input_level_linear() const { return max_input_level_linear_; } + double knee_start_linear() const { return knee_start_linear_; } + double limiter_start_linear() const { return limiter_start_linear_; } + + // These methods can be marked 'constexpr' in C++ 14. + double GetOutputLevelDbfs(double input_level_dbfs) const; + double GetGainLinear(double input_level_linear) const; + double GetGainFirstDerivativeLinear(double x) const; + double GetGainIntegralLinear(double x0, double x1) const; + + private: + double GetKneeRegionOutputLevelDbfs(double input_level_dbfs) const; + double GetCompressorRegionOutputLevelDbfs(double input_level_dbfs) const; + + static constexpr double max_input_level_db_ = test::kLimiterMaxInputLevelDbFs; + static constexpr double knee_smoothness_db_ = test::kLimiterKneeSmoothnessDb; + static constexpr double compression_ratio_ = test::kLimiterCompressionRatio; + + const double max_input_level_linear_; + + // Do not modify signal with level <= knee_start_dbfs_. + const double knee_start_dbfs_; + const double knee_start_linear_; + + // The upper end of the knee region, which is between knee_start_dbfs_ and + // limiter_start_dbfs_. + const double limiter_start_dbfs_; + const double limiter_start_linear_; + + // Coefficients {a, b, c} of the knee region polynomial + // ax^2 + bx + c in the DB scale. + const std::array knee_region_polynomial_; + + // Parameters for the computation of the first derivative of GetGainLinear(). + const double gain_curve_limiter_d1_; + const double gain_curve_limiter_d2_; + + // Parameters for the computation of the integral of GetGainLinear(). + const double gain_curve_limiter_i1_; + const double gain_curve_limiter_i2_; +}; + +} // namespace webrtc + +#endif // MODULES_AUDIO_PROCESSING_AGC2_LIMITER_H_ diff --git a/modules/audio_processing/agc2/limiter_unittest.cc b/modules/audio_processing/agc2/limiter_unittest.cc new file mode 100644 index 0000000000..7079812634 --- /dev/null +++ b/modules/audio_processing/agc2/limiter_unittest.cc @@ -0,0 +1,60 @@ +/* + * 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/limiter.h" + +#include "rtc_base/gunit.h" + +namespace webrtc { + +TEST(FixedDigitalGainController2Limiter, ConstructDestruct) { + Limiter l; +} + +TEST(FixedDigitalGainController2Limiter, GainCurveShouldBeMonotone) { + Limiter l; + float last_output_level = 0.f; + bool has_last_output_level = false; + for (float level = -90.f; level <= l.max_input_level_db(); level += 0.5f) { + const float current_output_level = l.GetOutputLevelDbfs(level); + if (!has_last_output_level) { + last_output_level = current_output_level; + has_last_output_level = true; + } + EXPECT_LE(last_output_level, current_output_level); + last_output_level = current_output_level; + } +} + +TEST(FixedDigitalGainController2Limiter, GainCurveShouldBeContinuous) { + Limiter l; + float last_output_level = 0.f; + bool has_last_output_level = false; + constexpr float kMaxDelta = 0.5f; + for (float level = -90.f; level <= l.max_input_level_db(); level += 0.5f) { + const float current_output_level = l.GetOutputLevelDbfs(level); + if (!has_last_output_level) { + last_output_level = current_output_level; + has_last_output_level = true; + } + EXPECT_LE(current_output_level, last_output_level + kMaxDelta); + last_output_level = current_output_level; + } +} + +TEST(FixedDigitalGainController2Limiter, OutputGainShouldBeLessThanFullScale) { + Limiter l; + for (float level = -90.f; level <= l.max_input_level_db(); level += 0.5f) { + const float current_output_level = l.GetOutputLevelDbfs(level); + EXPECT_LE(current_output_level, 0.f); + } +} + +} // namespace webrtc diff --git a/test/fuzzers/BUILD.gn b/test/fuzzers/BUILD.gn index d2716aa02b..fad746dd91 100644 --- a/test/fuzzers/BUILD.gn +++ b/test/fuzzers/BUILD.gn @@ -445,6 +445,7 @@ webrtc_fuzzer_test("audio_processing_fuzzer") { ":audio_processing_fuzzer_helper", ":fuzz_data_helper", "../../modules/audio_processing", + "../../rtc_base:rtc_base_approved", ] } diff --git a/test/fuzzers/audio_processing_configs_fuzzer.cc b/test/fuzzers/audio_processing_configs_fuzzer.cc index 81baab1ac6..18913d8d92 100644 --- a/test/fuzzers/audio_processing_configs_fuzzer.cc +++ b/test/fuzzers/audio_processing_configs_fuzzer.cc @@ -9,6 +9,7 @@ */ #include "modules/audio_processing/include/audio_processing.h" +#include "rtc_base/numerics/safe_minmax.h" #include "test/fuzzers/audio_processing_fuzzer_helper.h" #include "test/fuzzers/fuzz_data_helper.h" @@ -36,6 +37,7 @@ std::unique_ptr CreateApm(test::FuzzDataHelper* fuzz_data) { bool use_le = fuzz_data->ReadOrDefaultValue(true); bool use_vad = fuzz_data->ReadOrDefaultValue(true); bool use_agc_limiter = fuzz_data->ReadOrDefaultValue(true); + bool use_agc2_limiter = fuzz_data->ReadOrDefaultValue(true); // Filter out incompatible settings that lead to CHECK failures. if (use_aecm && use_aec) { @@ -70,6 +72,12 @@ std::unique_ptr CreateApm(test::FuzzDataHelper* fuzz_data) { apm_config.residual_echo_detector.enabled = red; apm_config.level_controller.enabled = lc; apm_config.high_pass_filter.enabled = hpf; + apm_config.gain_controller2.enabled = use_agc2_limiter; + + // Read an int8 value, but don't let it be too large or small. + const float gain_db = + rtc::SafeClamp(fuzz_data->ReadOrDefaultValue(0), -50, 50); + apm_config.gain_controller2.fixed_gain_db = gain_db; apm->ApplyConfig(apm_config);