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 <aleloi@webrtc.org>
Reviewed-by: Sam Zackrisson <saza@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#22103}
This commit is contained in:
Alex Loiko 2018-02-20 15:58:36 +01:00 committed by Commit Bot
parent 1896cece01
commit a05ee82c4c
22 changed files with 1460 additions and 45 deletions

View file

@ -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",
]
}

View file

@ -11,28 +11,36 @@
#ifndef MODULES_AUDIO_PROCESSING_AGC2_AGC2_COMMON_H_
#define MODULES_AUDIO_PROCESSING_AGC2_AGC2_COMMON_H_
#include <cmath>
#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_

View file

@ -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<double> LinSpace(const double l,
const double r,
size_t num_points) {
RTC_CHECK(num_points >= 2);
std::vector<double> 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<double>(l) + i * step;
}
points[num_points - 1] = r;
return points;
}
} // namespace test
} // namespace webrtc

View file

@ -11,11 +11,24 @@
#ifndef MODULES_AUDIO_PROCESSING_AGC2_AGC2_TESTING_COMMON_H_
#define MODULES_AUDIO_PROCESSING_AGC2_AGC2_TESTING_COMMON_H_
#include <vector>
#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<double> LinSpace(const double l, const double r, size_t num_points);
} // namespace test
} // namespace webrtc
#endif // MODULES_AUDIO_PROCESSING_AGC2_AGC2_TESTING_COMMON_H_

View file

@ -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<double> points1 = test::LinSpace(-1.0, 2.0, 4);
const std::vector<double> expected_points1{{-1.0, 0.0, 1.0, 2.0}};
EXPECT_EQ(expected_points1, points1);
std::vector<double> points2 = test::LinSpace(0.0, 1.0, 4);
const std::vector<double> expected_points2{{0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0}};
EXPECT_EQ(points2, expected_points2);
}
} // namespace webrtc

View file

@ -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 <algorithm>
#include <cmath>
#include <queue>
#include <tuple>
#include <utility>
#include <vector>
#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<double, double> 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<double> 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<Interval, std::vector<Interval>> 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<double> 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<double> 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, &parameters);
PrecomputeBeyondKneeApproxParams(&limiter, &parameters);
return parameters;
}
} // namespace test
} // namespace webrtc

View file

@ -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 <array>
#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<float, kInterpolatedGainCurveTotalPoints>
computed_approximation_params_x;
std::array<float, kInterpolatedGainCurveTotalPoints>
computed_approximation_params_m;
std::array<float, kInterpolatedGainCurveTotalPoints>
computed_approximation_params_q;
};
InterpolatedParameters ComputeInterpolatedGainCurveApproximationParams();
} // namespace test
} // namespace webrtc
#endif // MODULES_AUDIO_PROCESSING_AGC2_COMPUTE_INTERPOLATED_GAIN_CURVE_H_

View file

@ -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 =

View file

@ -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<float> 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<float> signal) {
for (size_t k = 0; k < signal.num_channels(); ++k) {
rtc::ArrayView<float> channel_view = signal.channel(k);
for (auto& sample : channel_view) {
sample = rtc::SafeClamp(sample, kMinSampleValue, kMaxSampleValue);
sample = rtc::SafeClamp(sample, kMinFloatS16Value, kMaxFloatS16Value);
}
}
}

View file

@ -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<float> 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;
};

View file

@ -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<FixedGainController> CreateFixedGainController(
float gain_to_apply,
size_t rate,
bool enable_limiter) {
std::unique_ptr<FixedGainController> fgc =
rtc::MakeUnique<FixedGainController>(&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<FixedGainController> 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<FixedGainController> 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<FixedGainController> 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<FixedGainController> 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<FixedGainController> 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<FixedGainController> 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<FixedGainController> 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

View file

@ -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 <algorithm>
#include <array>
#include <cmath>
#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<float> 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<float, kSubFramesInFrame + 1>& scaling_factors,
size_t samples_per_channel,
rtc::ArrayView<float> 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<float>(
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<const float> per_sample_scaling_factors,
AudioFrameView<float> 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<float> 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<float>(
&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

View file

@ -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 <vector>
#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<float> 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<float, kSubFramesInFrame + 1> scaling_factors_ = {};
std::array<float, kMaximalNumberOfSamplesPerChannel>
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_

View file

@ -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<const float> channel =
vectors_with_float_frame.float_frame_view().channel(0);
for (const auto& sample : channel) {
EXPECT_LT(0.9f * kMaxAbsFloatS16Value, sample);
}
}
} // namespace webrtc

View file

@ -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<float, kInterpolatedGainCurveTotalPoints>
InterpolatedGainCurve::approximation_params_x_;
constexpr std::array<float, kInterpolatedGainCurveTotalPoints>
InterpolatedGainCurve::approximation_params_m_;
constexpr std::array<float, kInterpolatedGainCurveTotalPoints>
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<float, kInterpolatedGainCurveTotalPoints>::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

View file

@ -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 <array>
#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<float>(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<float, kInterpolatedGainCurveTotalPoints>
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<float, kInterpolatedGainCurveTotalPoints>
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<float, kInterpolatedGainCurveTotalPoints>
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_

View file

@ -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 <array>
#include <vector>
#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<double> 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

View file

@ -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 <cmath>
#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<double, 3> 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

View file

@ -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 <array>
#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<double, 3> 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_

View file

@ -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

View file

@ -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",
]
}

View file

@ -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<AudioProcessing> 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<AudioProcessing> 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<int>(fuzz_data->ReadOrDefaultValue<int8_t>(0), -50, 50);
apm_config.gain_controller2.fixed_gain_db = gain_db;
apm->ApplyConfig(apm_config);