aboutsummaryrefslogtreecommitdiff
path: root/modules/audio_processing/agc
diff options
context:
space:
mode:
Diffstat (limited to 'modules/audio_processing/agc')
-rw-r--r--modules/audio_processing/agc/BUILD.gn56
-rw-r--r--modules/audio_processing/agc/agc_manager_direct.cc182
-rw-r--r--modules/audio_processing/agc/agc_manager_direct.h67
-rw-r--r--modules/audio_processing/agc/agc_manager_direct_unittest.cc256
-rw-r--r--modules/audio_processing/agc/clipping_predictor.cc383
-rw-r--r--modules/audio_processing/agc/clipping_predictor.h63
-rw-r--r--modules/audio_processing/agc/clipping_predictor_evaluator.cc175
-rw-r--r--modules/audio_processing/agc/clipping_predictor_evaluator.h102
-rw-r--r--modules/audio_processing/agc/clipping_predictor_evaluator_unittest.cc568
-rw-r--r--modules/audio_processing/agc/clipping_predictor_level_buffer.cc77
-rw-r--r--modules/audio_processing/agc/clipping_predictor_level_buffer.h71
-rw-r--r--modules/audio_processing/agc/clipping_predictor_level_buffer_unittest.cc131
-rw-r--r--modules/audio_processing/agc/clipping_predictor_unittest.cc491
13 files changed, 2555 insertions, 67 deletions
diff --git a/modules/audio_processing/agc/BUILD.gn b/modules/audio_processing/agc/BUILD.gn
index 5ad6644323..4bb8c5494b 100644
--- a/modules/audio_processing/agc/BUILD.gn
+++ b/modules/audio_processing/agc/BUILD.gn
@@ -19,11 +19,14 @@ rtc_library("agc") {
]
configs += [ "..:apm_debug_dump" ]
deps = [
+ ":clipping_predictor",
+ ":clipping_predictor_evaluator",
":gain_control_interface",
":gain_map",
":level_estimation",
"..:apm_logging",
"..:audio_buffer",
+ "..:audio_frame_view",
"../../../common_audio",
"../../../common_audio:common_audio_c",
"../../../rtc_base:checks",
@@ -38,6 +41,49 @@ rtc_library("agc") {
absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
}
+rtc_library("clipping_predictor") {
+ sources = [
+ "clipping_predictor.cc",
+ "clipping_predictor.h",
+ ]
+ deps = [
+ ":clipping_predictor_level_buffer",
+ ":gain_map",
+ "..:api",
+ "..:audio_frame_view",
+ "../../../common_audio",
+ "../../../rtc_base:checks",
+ "../../../rtc_base:logging",
+ "../../../rtc_base:safe_minmax",
+ ]
+ absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
+}
+
+rtc_library("clipping_predictor_evaluator") {
+ sources = [
+ "clipping_predictor_evaluator.cc",
+ "clipping_predictor_evaluator.h",
+ ]
+ deps = [
+ "../../../rtc_base:checks",
+ "../../../rtc_base:logging",
+ ]
+ absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
+}
+
+rtc_library("clipping_predictor_level_buffer") {
+ sources = [
+ "clipping_predictor_level_buffer.cc",
+ "clipping_predictor_level_buffer.h",
+ ]
+ deps = [
+ "../../../rtc_base:checks",
+ "../../../rtc_base:logging",
+ "../../../rtc_base:rtc_base_approved",
+ ]
+ absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
+}
+
rtc_library("level_estimation") {
sources = [
"agc.cc",
@@ -96,6 +142,9 @@ if (rtc_include_tests) {
testonly = true
sources = [
"agc_manager_direct_unittest.cc",
+ "clipping_predictor_evaluator_unittest.cc",
+ "clipping_predictor_level_buffer_unittest.cc",
+ "clipping_predictor_unittest.cc",
"loudness_histogram_unittest.cc",
"mock_agc.h",
]
@@ -103,13 +152,20 @@ if (rtc_include_tests) {
deps = [
":agc",
+ ":clipping_predictor",
+ ":clipping_predictor_evaluator",
+ ":clipping_predictor_level_buffer",
":gain_control_interface",
":level_estimation",
"..:mocks",
+ "../../../rtc_base:checks",
+ "../../../rtc_base:rtc_base_approved",
+ "../../../rtc_base:safe_conversions",
"../../../test:field_trial",
"../../../test:fileutils",
"../../../test:test_support",
"//testing/gtest",
]
+ absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
}
}
diff --git a/modules/audio_processing/agc/agc_manager_direct.cc b/modules/audio_processing/agc/agc_manager_direct.cc
index 2454d1bbb1..817678801e 100644
--- a/modules/audio_processing/agc/agc_manager_direct.cc
+++ b/modules/audio_processing/agc/agc_manager_direct.cc
@@ -16,6 +16,7 @@
#include "common_audio/include/audio_util.h"
#include "modules/audio_processing/agc/gain_control.h"
#include "modules/audio_processing/agc/gain_map_internal.h"
+#include "modules/audio_processing/include/audio_frame_view.h"
#include "rtc_base/atomic_ops.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
@@ -27,33 +28,33 @@ namespace webrtc {
namespace {
-// Amount the microphone level is lowered with every clipping event.
-const int kClippedLevelStep = 15;
-// Proportion of clipped samples required to declare a clipping event.
-const float kClippedRatioThreshold = 0.1f;
-// Time in frames to wait after a clipping event before checking again.
-const int kClippedWaitFrames = 300;
-
// Amount of error we tolerate in the microphone level (presumably due to OS
// quantization) before we assume the user has manually adjusted the microphone.
-const int kLevelQuantizationSlack = 25;
+constexpr int kLevelQuantizationSlack = 25;
-const int kDefaultCompressionGain = 7;
-const int kMaxCompressionGain = 12;
-const int kMinCompressionGain = 2;
+constexpr int kDefaultCompressionGain = 7;
+constexpr int kMaxCompressionGain = 12;
+constexpr int kMinCompressionGain = 2;
// Controls the rate of compression changes towards the target.
-const float kCompressionGainStep = 0.05f;
+constexpr float kCompressionGainStep = 0.05f;
-const int kMaxMicLevel = 255;
+constexpr int kMaxMicLevel = 255;
static_assert(kGainMapSize > kMaxMicLevel, "gain map too small");
-const int kMinMicLevel = 12;
+constexpr int kMinMicLevel = 12;
// Prevent very large microphone level changes.
-const int kMaxResidualGainChange = 15;
+constexpr int kMaxResidualGainChange = 15;
// Maximum additional gain allowed to compensate for microphone level
// restrictions from clipping events.
-const int kSurplusCompressionGain = 6;
+constexpr int kSurplusCompressionGain = 6;
+
+// History size for the clipping predictor evaluator (unit: number of 10 ms
+// frames).
+constexpr int kClippingPredictorEvaluatorHistorySize = 32;
+
+using ClippingPredictorConfig = AudioProcessing::Config::GainController1::
+ AnalogGainController::ClippingPredictor;
// Returns whether a fall-back solution to choose the maximum level should be
// chosen.
@@ -132,6 +133,33 @@ float ComputeClippedRatio(const float* const* audio,
return static_cast<float>(num_clipped) / (samples_per_channel);
}
+void LogClippingPredictorMetrics(const ClippingPredictorEvaluator& evaluator) {
+ RTC_LOG(LS_INFO) << "Clipping predictor metrics: TP "
+ << evaluator.true_positives() << " TN "
+ << evaluator.true_negatives() << " FP "
+ << evaluator.false_positives() << " FN "
+ << evaluator.false_negatives();
+ const float precision_denominator =
+ evaluator.true_positives() + evaluator.false_positives();
+ const float recall_denominator =
+ evaluator.true_positives() + evaluator.false_negatives();
+ if (precision_denominator > 0 && recall_denominator > 0) {
+ const float precision = evaluator.true_positives() / precision_denominator;
+ const float recall = evaluator.true_positives() / recall_denominator;
+ RTC_LOG(LS_INFO) << "Clipping predictor metrics: P " << precision << " R "
+ << recall;
+ const float f1_score_denominator = precision + recall;
+ if (f1_score_denominator > 0.0f) {
+ const float f1_score = 2 * precision * recall / f1_score_denominator;
+ RTC_LOG(LS_INFO) << "Clipping predictor metrics: F1 " << f1_score;
+ RTC_HISTOGRAM_COUNTS_LINEAR("WebRTC.Audio.Agc.ClippingPredictor.F1Score",
+ std::round(f1_score * 100.0f), /*min=*/0,
+ /*max=*/100,
+ /*bucket_count=*/50);
+ }
+ }
+}
+
} // namespace
MonoAgc::MonoAgc(ApmDataDumper* data_dumper,
@@ -182,19 +210,19 @@ void MonoAgc::Process(const int16_t* audio,
}
}
-void MonoAgc::HandleClipping() {
+void MonoAgc::HandleClipping(int clipped_level_step) {
// Always decrease the maximum level, even if the current level is below
// threshold.
- SetMaxLevel(std::max(clipped_level_min_, max_level_ - kClippedLevelStep));
+ SetMaxLevel(std::max(clipped_level_min_, max_level_ - clipped_level_step));
if (log_to_histograms_) {
RTC_HISTOGRAM_BOOLEAN("WebRTC.Audio.AgcClippingAdjustmentAllowed",
- level_ - kClippedLevelStep >= clipped_level_min_);
+ level_ - clipped_level_step >= clipped_level_min_);
}
if (level_ > clipped_level_min_) {
// Don't try to adjust the level if we're already below the limit. As
// a consequence, if the user has brought the level above the limit, we
// will still not react until the postproc updates the level.
- SetLevel(std::max(clipped_level_min_, level_ - kClippedLevelStep));
+ SetLevel(std::max(clipped_level_min_, level_ - clipped_level_step));
// Reset the AGCs for all channels since the level has changed.
agc_->Reset();
}
@@ -401,35 +429,58 @@ void MonoAgc::UpdateCompressor() {
int AgcManagerDirect::instance_counter_ = 0;
-AgcManagerDirect::AgcManagerDirect(Agc* agc,
- int startup_min_level,
- int clipped_level_min,
- int sample_rate_hz)
+AgcManagerDirect::AgcManagerDirect(
+ Agc* agc,
+ int startup_min_level,
+ int clipped_level_min,
+ int sample_rate_hz,
+ int clipped_level_step,
+ float clipped_ratio_threshold,
+ int clipped_wait_frames,
+ const ClippingPredictorConfig& clipping_config)
: AgcManagerDirect(/*num_capture_channels*/ 1,
startup_min_level,
clipped_level_min,
/*disable_digital_adaptive*/ false,
- sample_rate_hz) {
+ sample_rate_hz,
+ clipped_level_step,
+ clipped_ratio_threshold,
+ clipped_wait_frames,
+ clipping_config) {
RTC_DCHECK(channel_agcs_[0]);
RTC_DCHECK(agc);
channel_agcs_[0]->set_agc(agc);
}
-AgcManagerDirect::AgcManagerDirect(int num_capture_channels,
- int startup_min_level,
- int clipped_level_min,
- bool disable_digital_adaptive,
- int sample_rate_hz)
+AgcManagerDirect::AgcManagerDirect(
+ int num_capture_channels,
+ int startup_min_level,
+ int clipped_level_min,
+ bool disable_digital_adaptive,
+ int sample_rate_hz,
+ int clipped_level_step,
+ float clipped_ratio_threshold,
+ int clipped_wait_frames,
+ const ClippingPredictorConfig& clipping_config)
: data_dumper_(
new ApmDataDumper(rtc::AtomicOps::Increment(&instance_counter_))),
use_min_channel_level_(!UseMaxAnalogChannelLevel()),
sample_rate_hz_(sample_rate_hz),
num_capture_channels_(num_capture_channels),
disable_digital_adaptive_(disable_digital_adaptive),
- frames_since_clipped_(kClippedWaitFrames),
+ frames_since_clipped_(clipped_wait_frames),
capture_output_used_(true),
+ clipped_level_step_(clipped_level_step),
+ clipped_ratio_threshold_(clipped_ratio_threshold),
+ clipped_wait_frames_(clipped_wait_frames),
channel_agcs_(num_capture_channels),
- new_compressions_to_set_(num_capture_channels) {
+ new_compressions_to_set_(num_capture_channels),
+ clipping_predictor_(
+ CreateClippingPredictor(num_capture_channels, clipping_config)),
+ use_clipping_predictor_step_(!!clipping_predictor_ &&
+ clipping_config.use_predicted_step),
+ clipping_predictor_evaluator_(kClippingPredictorEvaluatorHistorySize),
+ clipping_predictor_log_counter_(0) {
const int min_mic_level = GetMinMicLevel();
for (size_t ch = 0; ch < channel_agcs_.size(); ++ch) {
ApmDataDumper* data_dumper_ch = ch == 0 ? data_dumper_.get() : nullptr;
@@ -438,7 +489,12 @@ AgcManagerDirect::AgcManagerDirect(int num_capture_channels,
data_dumper_ch, startup_min_level, clipped_level_min,
disable_digital_adaptive_, min_mic_level);
}
- RTC_DCHECK_LT(0, channel_agcs_.size());
+ RTC_DCHECK(!channel_agcs_.empty());
+ RTC_DCHECK_GT(clipped_level_step, 0);
+ RTC_DCHECK_LE(clipped_level_step, 255);
+ RTC_DCHECK_GT(clipped_ratio_threshold, 0.f);
+ RTC_DCHECK_LT(clipped_ratio_threshold, 1.f);
+ RTC_DCHECK_GT(clipped_wait_frames, 0);
channel_agcs_[0]->ActivateLogging();
}
@@ -453,6 +509,8 @@ void AgcManagerDirect::Initialize() {
capture_output_used_ = true;
AggregateChannelLevels();
+ clipping_predictor_evaluator_.Reset();
+ clipping_predictor_log_counter_ = 0;
}
void AgcManagerDirect::SetupDigitalGainControl(
@@ -489,7 +547,13 @@ void AgcManagerDirect::AnalyzePreProcess(const float* const* audio,
return;
}
- if (frames_since_clipped_ < kClippedWaitFrames) {
+ if (!!clipping_predictor_) {
+ AudioFrameView<const float> frame = AudioFrameView<const float>(
+ audio, num_capture_channels_, static_cast<int>(samples_per_channel));
+ clipping_predictor_->Analyze(frame);
+ }
+
+ if (frames_since_clipped_ < clipped_wait_frames_) {
++frames_since_clipped_;
return;
}
@@ -505,14 +569,54 @@ void AgcManagerDirect::AnalyzePreProcess(const float* const* audio,
// gain is increased, through SetMaxLevel().
float clipped_ratio =
ComputeClippedRatio(audio, num_capture_channels_, samples_per_channel);
-
- if (clipped_ratio > kClippedRatioThreshold) {
- RTC_DLOG(LS_INFO) << "[agc] Clipping detected. clipped_ratio="
- << clipped_ratio;
+ const bool clipping_detected = clipped_ratio > clipped_ratio_threshold_;
+ bool clipping_predicted = false;
+ int predicted_step = 0;
+ if (!!clipping_predictor_) {
+ for (int channel = 0; channel < num_capture_channels_; ++channel) {
+ const auto step = clipping_predictor_->EstimateClippedLevelStep(
+ channel, stream_analog_level_, clipped_level_step_,
+ channel_agcs_[channel]->min_mic_level(), kMaxMicLevel);
+ if (use_clipping_predictor_step_ && step.has_value()) {
+ predicted_step = std::max(predicted_step, step.value());
+ clipping_predicted = true;
+ }
+ }
+ // Clipping prediction evaluation.
+ absl::optional<int> prediction_interval =
+ clipping_predictor_evaluator_.Observe(clipping_detected,
+ clipping_predicted);
+ if (prediction_interval.has_value()) {
+ RTC_HISTOGRAM_COUNTS_LINEAR(
+ "WebRTC.Audio.Agc.ClippingPredictor.PredictionInterval",
+ prediction_interval.value(), /*min=*/0,
+ /*max=*/49, /*bucket_count=*/50);
+ }
+ constexpr int kNumFramesIn30Seconds = 3000;
+ clipping_predictor_log_counter_++;
+ if (clipping_predictor_log_counter_ == kNumFramesIn30Seconds) {
+ LogClippingPredictorMetrics(clipping_predictor_evaluator_);
+ clipping_predictor_log_counter_ = 0;
+ }
+ }
+ if (clipping_detected || clipping_predicted) {
+ int step = clipped_level_step_;
+ if (clipping_detected) {
+ RTC_DLOG(LS_INFO) << "[agc] Clipping detected. clipped_ratio="
+ << clipped_ratio;
+ }
+ if (clipping_predicted) {
+ step = std::max(predicted_step, clipped_level_step_);
+ RTC_DLOG(LS_INFO) << "[agc] Clipping predicted. step=" << step;
+ }
for (auto& state_ch : channel_agcs_) {
- state_ch->HandleClipping();
+ state_ch->HandleClipping(step);
}
frames_since_clipped_ = 0;
+ if (!!clipping_predictor_) {
+ clipping_predictor_->Reset();
+ clipping_predictor_evaluator_.Reset();
+ }
}
AggregateChannelLevels();
}
diff --git a/modules/audio_processing/agc/agc_manager_direct.h b/modules/audio_processing/agc/agc_manager_direct.h
index f9417cffff..7ac96a661c 100644
--- a/modules/audio_processing/agc/agc_manager_direct.h
+++ b/modules/audio_processing/agc/agc_manager_direct.h
@@ -15,6 +15,8 @@
#include "absl/types/optional.h"
#include "modules/audio_processing/agc/agc.h"
+#include "modules/audio_processing/agc/clipping_predictor.h"
+#include "modules/audio_processing/agc/clipping_predictor_evaluator.h"
#include "modules/audio_processing/audio_buffer.h"
#include "modules/audio_processing/logging/apm_data_dumper.h"
#include "rtc_base/gtest_prod_util.h"
@@ -34,12 +36,23 @@ class AgcManagerDirect final {
// AgcManagerDirect will configure GainControl internally. The user is
// responsible for processing the audio using it after the call to Process.
// The operating range of startup_min_level is [12, 255] and any input value
- // outside that range will be clamped.
- AgcManagerDirect(int num_capture_channels,
- int startup_min_level,
- int clipped_level_min,
- bool disable_digital_adaptive,
- int sample_rate_hz);
+ // outside that range will be clamped. `clipped_level_step` is the amount
+ // the microphone level is lowered with every clipping event, limited to
+ // (0, 255]. `clipped_ratio_threshold` is the proportion of clipped
+ // samples required to declare a clipping event, limited to (0.f, 1.f).
+ // `clipped_wait_frames` is the time in frames to wait after a clipping event
+ // before checking again, limited to values higher than 0.
+ AgcManagerDirect(
+ int num_capture_channels,
+ int startup_min_level,
+ int clipped_level_min,
+ bool disable_digital_adaptive,
+ int sample_rate_hz,
+ int clipped_level_step,
+ float clipped_ratio_threshold,
+ int clipped_wait_frames,
+ const AudioProcessing::Config::GainController1::AnalogGainController::
+ ClippingPredictor& clipping_config);
~AgcManagerDirect();
AgcManagerDirect(const AgcManagerDirect&) = delete;
@@ -64,6 +77,14 @@ class AgcManagerDirect final {
// If available, returns a new compression gain for the digital gain control.
absl::optional<int> GetDigitalComressionGain();
+ // Returns true if clipping prediction is enabled.
+ bool clipping_predictor_enabled() const { return !!clipping_predictor_; }
+
+ // Returns true if clipping prediction is used to adjust the analog gain.
+ bool use_clipping_predictor_step() const {
+ return use_clipping_predictor_step_;
+ }
+
private:
friend class AgcManagerDirectTest;
@@ -81,13 +102,28 @@ class AgcManagerDirect final {
AgcMinMicLevelExperimentEnabled50);
FRIEND_TEST_ALL_PREFIXES(AgcManagerDirectStandaloneTest,
AgcMinMicLevelExperimentEnabledAboveStartupLevel);
+ FRIEND_TEST_ALL_PREFIXES(AgcManagerDirectStandaloneTest,
+ ClippingParametersVerified);
+ FRIEND_TEST_ALL_PREFIXES(AgcManagerDirectStandaloneTest,
+ DisableClippingPredictorDoesNotLowerVolume);
+ FRIEND_TEST_ALL_PREFIXES(
+ AgcManagerDirectStandaloneTest,
+ EnableClippingPredictorWithUnusedPredictedStepDoesNotLowerVolume);
+ FRIEND_TEST_ALL_PREFIXES(AgcManagerDirectStandaloneTest,
+ EnableClippingPredictorLowersVolume);
// Dependency injection for testing. Don't delete |agc| as the memory is owned
// by the manager.
- AgcManagerDirect(Agc* agc,
- int startup_min_level,
- int clipped_level_min,
- int sample_rate_hz);
+ AgcManagerDirect(
+ Agc* agc,
+ int startup_min_level,
+ int clipped_level_min,
+ int sample_rate_hz,
+ int clipped_level_step,
+ float clipped_ratio_threshold,
+ int clipped_wait_frames,
+ const AudioProcessing::Config::GainController1::AnalogGainController::
+ ClippingPredictor& clipping_config);
void AnalyzePreProcess(const float* const* audio, size_t samples_per_channel);
@@ -105,8 +141,17 @@ class AgcManagerDirect final {
bool capture_output_used_;
int channel_controlling_gain_ = 0;
+ const int clipped_level_step_;
+ const float clipped_ratio_threshold_;
+ const int clipped_wait_frames_;
+
std::vector<std::unique_ptr<MonoAgc>> channel_agcs_;
std::vector<absl::optional<int>> new_compressions_to_set_;
+
+ const std::unique_ptr<ClippingPredictor> clipping_predictor_;
+ const bool use_clipping_predictor_step_;
+ ClippingPredictorEvaluator clipping_predictor_evaluator_;
+ int clipping_predictor_log_counter_;
};
class MonoAgc {
@@ -123,7 +168,7 @@ class MonoAgc {
void Initialize();
void HandleCaptureOutputUsedChange(bool capture_output_used);
- void HandleClipping();
+ void HandleClipping(int clipped_level_step);
void Process(const int16_t* audio,
size_t samples_per_channel,
diff --git a/modules/audio_processing/agc/agc_manager_direct_unittest.cc b/modules/audio_processing/agc/agc_manager_direct_unittest.cc
index 1954ed4b21..bb284f9abc 100644
--- a/modules/audio_processing/agc/agc_manager_direct_unittest.cc
+++ b/modules/audio_processing/agc/agc_manager_direct_unittest.cc
@@ -26,13 +26,19 @@ using ::testing::SetArgPointee;
namespace webrtc {
namespace {
-const int kSampleRateHz = 32000;
-const int kNumChannels = 1;
-const int kSamplesPerChannel = kSampleRateHz / 100;
-const int kInitialVolume = 128;
+constexpr int kSampleRateHz = 32000;
+constexpr int kNumChannels = 1;
+constexpr int kSamplesPerChannel = kSampleRateHz / 100;
+constexpr int kInitialVolume = 128;
constexpr int kClippedMin = 165; // Arbitrary, but different from the default.
-const float kAboveClippedThreshold = 0.2f;
-const int kMinMicLevel = 12;
+constexpr float kAboveClippedThreshold = 0.2f;
+constexpr int kMinMicLevel = 12;
+constexpr int kClippedLevelStep = 15;
+constexpr float kClippedRatioThreshold = 0.1f;
+constexpr int kClippedWaitFrames = 300;
+
+using ClippingPredictorConfig = AudioProcessing::Config::GainController1::
+ AnalogGainController::ClippingPredictor;
class MockGainControl : public GainControl {
public:
@@ -57,10 +63,53 @@ class MockGainControl : public GainControl {
};
std::unique_ptr<AgcManagerDirect> CreateAgcManagerDirect(
- int startup_min_level) {
+ int startup_min_level,
+ int clipped_level_step,
+ float clipped_ratio_threshold,
+ int clipped_wait_frames) {
return std::make_unique<AgcManagerDirect>(
/*num_capture_channels=*/1, startup_min_level, kClippedMin,
- /*disable_digital_adaptive=*/true, kSampleRateHz);
+ /*disable_digital_adaptive=*/true, kSampleRateHz, clipped_level_step,
+ clipped_ratio_threshold, clipped_wait_frames, ClippingPredictorConfig());
+}
+
+std::unique_ptr<AgcManagerDirect> CreateAgcManagerDirect(
+ int startup_min_level,
+ int clipped_level_step,
+ float clipped_ratio_threshold,
+ int clipped_wait_frames,
+ const ClippingPredictorConfig& clipping_cfg) {
+ return std::make_unique<AgcManagerDirect>(
+ /*num_capture_channels=*/1, startup_min_level, kClippedMin,
+ /*disable_digital_adaptive=*/true, kSampleRateHz, clipped_level_step,
+ clipped_ratio_threshold, clipped_wait_frames, clipping_cfg);
+}
+
+void CallPreProcessAudioBuffer(int num_calls,
+ float peak_ratio,
+ AgcManagerDirect& manager) {
+ RTC_DCHECK_GE(1.f, peak_ratio);
+ AudioBuffer audio_buffer(kSampleRateHz, 1, kSampleRateHz, 1, kSampleRateHz,
+ 1);
+ const int num_channels = audio_buffer.num_channels();
+ const int num_frames = audio_buffer.num_frames();
+ for (int ch = 0; ch < num_channels; ++ch) {
+ for (int i = 0; i < num_frames; i += 2) {
+ audio_buffer.channels()[ch][i] = peak_ratio * 32767.f;
+ audio_buffer.channels()[ch][i + 1] = 0.0f;
+ }
+ }
+ for (int n = 0; n < num_calls / 2; ++n) {
+ manager.AnalyzePreProcess(&audio_buffer);
+ }
+ for (int ch = 0; ch < num_channels; ++ch) {
+ for (int i = 0; i < num_frames; ++i) {
+ audio_buffer.channels()[ch][i] = peak_ratio * 32767.f;
+ }
+ }
+ for (int n = 0; n < num_calls - num_calls / 2; ++n) {
+ manager.AnalyzePreProcess(&audio_buffer);
+ }
}
} // namespace
@@ -69,7 +118,14 @@ class AgcManagerDirectTest : public ::testing::Test {
protected:
AgcManagerDirectTest()
: agc_(new MockAgc),
- manager_(agc_, kInitialVolume, kClippedMin, kSampleRateHz),
+ manager_(agc_,
+ kInitialVolume,
+ kClippedMin,
+ kSampleRateHz,
+ kClippedLevelStep,
+ kClippedRatioThreshold,
+ kClippedWaitFrames,
+ ClippingPredictorConfig()),
audio(kNumChannels),
audio_data(kNumChannels * kSamplesPerChannel, 0.f) {
ExpectInitialize();
@@ -124,12 +180,32 @@ class AgcManagerDirectTest : public ::testing::Test {
audio[ch][k] = 32767.f;
}
}
-
for (int i = 0; i < num_calls; ++i) {
manager_.AnalyzePreProcess(audio.data(), kSamplesPerChannel);
}
}
+ void CallPreProcForChangingAudio(int num_calls, float peak_ratio) {
+ RTC_DCHECK_GE(1.f, peak_ratio);
+ std::fill(audio_data.begin(), audio_data.end(), 0.f);
+ for (size_t ch = 0; ch < kNumChannels; ++ch) {
+ for (size_t k = 0; k < kSamplesPerChannel; k += 2) {
+ audio[ch][k] = peak_ratio * 32767.f;
+ }
+ }
+ for (int i = 0; i < num_calls / 2; ++i) {
+ manager_.AnalyzePreProcess(audio.data(), kSamplesPerChannel);
+ }
+ for (size_t ch = 0; ch < kNumChannels; ++ch) {
+ for (size_t k = 0; k < kSamplesPerChannel; ++k) {
+ audio[ch][k] = peak_ratio * 32767.f;
+ }
+ }
+ for (int i = 0; i < num_calls - num_calls / 2; ++i) {
+ manager_.AnalyzePreProcess(audio.data(), kSamplesPerChannel);
+ }
+ }
+
MockAgc* agc_;
MockGainControl gctrl_;
AgcManagerDirect manager_;
@@ -696,6 +772,25 @@ TEST_F(AgcManagerDirectTest, TakesNoActionOnZeroMicVolume) {
EXPECT_EQ(0, manager_.stream_analog_level());
}
+TEST_F(AgcManagerDirectTest, ClippingDetectionLowersVolume) {
+ SetVolumeAndProcess(255);
+ EXPECT_EQ(255, manager_.stream_analog_level());
+ CallPreProcForChangingAudio(/*num_calls=*/100, /*peak_ratio=*/0.99f);
+ EXPECT_EQ(255, manager_.stream_analog_level());
+ CallPreProcForChangingAudio(/*num_calls=*/100, /*peak_ratio=*/1.0f);
+ EXPECT_EQ(240, manager_.stream_analog_level());
+}
+
+TEST_F(AgcManagerDirectTest, DisabledClippingPredictorDoesNotLowerVolume) {
+ SetVolumeAndProcess(255);
+ EXPECT_FALSE(manager_.clipping_predictor_enabled());
+ EXPECT_EQ(255, manager_.stream_analog_level());
+ CallPreProcForChangingAudio(/*num_calls=*/100, /*peak_ratio=*/0.99f);
+ EXPECT_EQ(255, manager_.stream_analog_level());
+ CallPreProcForChangingAudio(/*num_calls=*/100, /*peak_ratio=*/0.99f);
+ EXPECT_EQ(255, manager_.stream_analog_level());
+}
+
TEST(AgcManagerDirectStandaloneTest, DisableDigitalDisablesDigital) {
auto agc = std::unique_ptr<Agc>(new ::testing::NiceMock<MockAgc>());
MockGainControl gctrl;
@@ -705,14 +800,16 @@ TEST(AgcManagerDirectStandaloneTest, DisableDigitalDisablesDigital) {
EXPECT_CALL(gctrl, enable_limiter(false));
std::unique_ptr<AgcManagerDirect> manager =
- CreateAgcManagerDirect(kInitialVolume);
+ CreateAgcManagerDirect(kInitialVolume, kClippedLevelStep,
+ kClippedRatioThreshold, kClippedWaitFrames);
manager->Initialize();
manager->SetupDigitalGainControl(&gctrl);
}
TEST(AgcManagerDirectStandaloneTest, AgcMinMicLevelExperiment) {
std::unique_ptr<AgcManagerDirect> manager =
- CreateAgcManagerDirect(kInitialVolume);
+ CreateAgcManagerDirect(kInitialVolume, kClippedLevelStep,
+ kClippedRatioThreshold, kClippedWaitFrames);
EXPECT_EQ(manager->channel_agcs_[0]->min_mic_level(), kMinMicLevel);
EXPECT_EQ(manager->channel_agcs_[0]->startup_min_level(), kInitialVolume);
}
@@ -721,7 +818,8 @@ TEST(AgcManagerDirectStandaloneTest, AgcMinMicLevelExperimentDisabled) {
test::ScopedFieldTrials field_trial(
"WebRTC-Audio-AgcMinMicLevelExperiment/Disabled/");
std::unique_ptr<AgcManagerDirect> manager =
- CreateAgcManagerDirect(kInitialVolume);
+ CreateAgcManagerDirect(kInitialVolume, kClippedLevelStep,
+ kClippedRatioThreshold, kClippedWaitFrames);
EXPECT_EQ(manager->channel_agcs_[0]->min_mic_level(), kMinMicLevel);
EXPECT_EQ(manager->channel_agcs_[0]->startup_min_level(), kInitialVolume);
}
@@ -732,7 +830,8 @@ TEST(AgcManagerDirectStandaloneTest, AgcMinMicLevelExperimentOutOfRangeAbove) {
test::ScopedFieldTrials field_trial(
"WebRTC-Audio-AgcMinMicLevelExperiment/Enabled-256/");
std::unique_ptr<AgcManagerDirect> manager =
- CreateAgcManagerDirect(kInitialVolume);
+ CreateAgcManagerDirect(kInitialVolume, kClippedLevelStep,
+ kClippedRatioThreshold, kClippedWaitFrames);
EXPECT_EQ(manager->channel_agcs_[0]->min_mic_level(), kMinMicLevel);
EXPECT_EQ(manager->channel_agcs_[0]->startup_min_level(), kInitialVolume);
}
@@ -743,7 +842,8 @@ TEST(AgcManagerDirectStandaloneTest, AgcMinMicLevelExperimentOutOfRangeBelow) {
test::ScopedFieldTrials field_trial(
"WebRTC-Audio-AgcMinMicLevelExperiment/Enabled--1/");
std::unique_ptr<AgcManagerDirect> manager =
- CreateAgcManagerDirect(kInitialVolume);
+ CreateAgcManagerDirect(kInitialVolume, kClippedLevelStep,
+ kClippedRatioThreshold, kClippedWaitFrames);
EXPECT_EQ(manager->channel_agcs_[0]->min_mic_level(), kMinMicLevel);
EXPECT_EQ(manager->channel_agcs_[0]->startup_min_level(), kInitialVolume);
}
@@ -755,7 +855,8 @@ TEST(AgcManagerDirectStandaloneTest, AgcMinMicLevelExperimentEnabled50) {
test::ScopedFieldTrials field_trial(
"WebRTC-Audio-AgcMinMicLevelExperiment/Enabled-50/");
std::unique_ptr<AgcManagerDirect> manager =
- CreateAgcManagerDirect(kInitialVolume);
+ CreateAgcManagerDirect(kInitialVolume, kClippedLevelStep,
+ kClippedRatioThreshold, kClippedWaitFrames);
EXPECT_EQ(manager->channel_agcs_[0]->min_mic_level(), 50);
EXPECT_EQ(manager->channel_agcs_[0]->startup_min_level(), kInitialVolume);
}
@@ -768,9 +869,130 @@ TEST(AgcManagerDirectStandaloneTest,
test::ScopedFieldTrials field_trial(
"WebRTC-Audio-AgcMinMicLevelExperiment/Enabled-50/");
std::unique_ptr<AgcManagerDirect> manager =
- CreateAgcManagerDirect(/*startup_min_level=*/30);
+ CreateAgcManagerDirect(/*startup_min_level=*/30, kClippedLevelStep,
+ kClippedRatioThreshold, kClippedWaitFrames);
EXPECT_EQ(manager->channel_agcs_[0]->min_mic_level(), 50);
EXPECT_EQ(manager->channel_agcs_[0]->startup_min_level(), 50);
}
+// TODO(bugs.webrtc.org/12774): Test the bahavior of `clipped_level_step`.
+// TODO(bugs.webrtc.org/12774): Test the bahavior of `clipped_ratio_threshold`.
+// TODO(bugs.webrtc.org/12774): Test the bahavior of `clipped_wait_frames`.
+// Verifies that configurable clipping parameters are initialized as intended.
+TEST(AgcManagerDirectStandaloneTest, ClippingParametersVerified) {
+ std::unique_ptr<AgcManagerDirect> manager =
+ CreateAgcManagerDirect(kInitialVolume, kClippedLevelStep,
+ kClippedRatioThreshold, kClippedWaitFrames);
+ manager->Initialize();
+ EXPECT_EQ(manager->clipped_level_step_, kClippedLevelStep);
+ EXPECT_EQ(manager->clipped_ratio_threshold_, kClippedRatioThreshold);
+ EXPECT_EQ(manager->clipped_wait_frames_, kClippedWaitFrames);
+ std::unique_ptr<AgcManagerDirect> manager_custom =
+ CreateAgcManagerDirect(kInitialVolume,
+ /*clipped_level_step=*/10,
+ /*clipped_ratio_threshold=*/0.2f,
+ /*clipped_wait_frames=*/50);
+ manager_custom->Initialize();
+ EXPECT_EQ(manager_custom->clipped_level_step_, 10);
+ EXPECT_EQ(manager_custom->clipped_ratio_threshold_, 0.2f);
+ EXPECT_EQ(manager_custom->clipped_wait_frames_, 50);
+}
+
+TEST(AgcManagerDirectStandaloneTest,
+ DisableClippingPredictorDisablesClippingPredictor) {
+ ClippingPredictorConfig default_config;
+ EXPECT_FALSE(default_config.enabled);
+ std::unique_ptr<AgcManagerDirect> manager = CreateAgcManagerDirect(
+ kInitialVolume, kClippedLevelStep, kClippedRatioThreshold,
+ kClippedWaitFrames, default_config);
+ manager->Initialize();
+ EXPECT_FALSE(manager->clipping_predictor_enabled());
+ EXPECT_FALSE(manager->use_clipping_predictor_step());
+}
+
+TEST(AgcManagerDirectStandaloneTest, ClippingPredictorDisabledByDefault) {
+ constexpr ClippingPredictorConfig kDefaultConfig;
+ EXPECT_FALSE(kDefaultConfig.enabled);
+}
+
+TEST(AgcManagerDirectStandaloneTest,
+ EnableClippingPredictorEnablesClippingPredictor) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ ClippingPredictorConfig config;
+ config.enabled = true;
+ config.use_predicted_step = true;
+ std::unique_ptr<AgcManagerDirect> manager = CreateAgcManagerDirect(
+ kInitialVolume, kClippedLevelStep, kClippedRatioThreshold,
+ kClippedWaitFrames, config);
+ manager->Initialize();
+ EXPECT_TRUE(manager->clipping_predictor_enabled());
+ EXPECT_TRUE(manager->use_clipping_predictor_step());
+}
+
+TEST(AgcManagerDirectStandaloneTest,
+ DisableClippingPredictorDoesNotLowerVolume) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ constexpr ClippingPredictorConfig kConfig{/*enabled=*/false};
+ AgcManagerDirect manager(new ::testing::NiceMock<MockAgc>(), kInitialVolume,
+ kClippedMin, kSampleRateHz, kClippedLevelStep,
+ kClippedRatioThreshold, kClippedWaitFrames, kConfig);
+ manager.Initialize();
+ manager.set_stream_analog_level(/*level=*/255);
+ EXPECT_FALSE(manager.clipping_predictor_enabled());
+ EXPECT_FALSE(manager.use_clipping_predictor_step());
+ EXPECT_EQ(manager.stream_analog_level(), 255);
+ manager.Process(nullptr);
+ CallPreProcessAudioBuffer(/*num_calls=*/10, /*peak_ratio=*/0.99f, manager);
+ EXPECT_EQ(manager.stream_analog_level(), 255);
+ CallPreProcessAudioBuffer(/*num_calls=*/300, /*peak_ratio=*/0.99f, manager);
+ EXPECT_EQ(manager.stream_analog_level(), 255);
+ CallPreProcessAudioBuffer(/*num_calls=*/10, /*peak_ratio=*/0.99f, manager);
+ EXPECT_EQ(manager.stream_analog_level(), 255);
+}
+
+TEST(AgcManagerDirectStandaloneTest,
+ EnableClippingPredictorWithUnusedPredictedStepDoesNotLowerVolume) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ ClippingPredictorConfig config;
+ config.enabled = true;
+ config.use_predicted_step = false;
+ AgcManagerDirect manager(new ::testing::NiceMock<MockAgc>(), kInitialVolume,
+ kClippedMin, kSampleRateHz, kClippedLevelStep,
+ kClippedRatioThreshold, kClippedWaitFrames, config);
+ manager.Initialize();
+ manager.set_stream_analog_level(/*level=*/255);
+ EXPECT_TRUE(manager.clipping_predictor_enabled());
+ EXPECT_FALSE(manager.use_clipping_predictor_step());
+ EXPECT_EQ(manager.stream_analog_level(), 255);
+ manager.Process(nullptr);
+ CallPreProcessAudioBuffer(/*num_calls=*/10, /*peak_ratio=*/0.99f, manager);
+ EXPECT_EQ(manager.stream_analog_level(), 255);
+ CallPreProcessAudioBuffer(/*num_calls=*/300, /*peak_ratio=*/0.99f, manager);
+ EXPECT_EQ(manager.stream_analog_level(), 255);
+ CallPreProcessAudioBuffer(/*num_calls=*/10, /*peak_ratio=*/0.99f, manager);
+ EXPECT_EQ(manager.stream_analog_level(), 255);
+}
+
+TEST(AgcManagerDirectStandaloneTest, EnableClippingPredictorLowersVolume) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ ClippingPredictorConfig config;
+ config.enabled = true;
+ config.use_predicted_step = true;
+ AgcManagerDirect manager(new ::testing::NiceMock<MockAgc>(), kInitialVolume,
+ kClippedMin, kSampleRateHz, kClippedLevelStep,
+ kClippedRatioThreshold, kClippedWaitFrames, config);
+ manager.Initialize();
+ manager.set_stream_analog_level(/*level=*/255);
+ EXPECT_TRUE(manager.clipping_predictor_enabled());
+ EXPECT_TRUE(manager.use_clipping_predictor_step());
+ EXPECT_EQ(manager.stream_analog_level(), 255);
+ manager.Process(nullptr);
+ CallPreProcessAudioBuffer(/*num_calls=*/10, /*peak_ratio=*/0.99f, manager);
+ EXPECT_EQ(manager.stream_analog_level(), 240);
+ CallPreProcessAudioBuffer(/*num_calls=*/300, /*peak_ratio=*/0.99f, manager);
+ EXPECT_EQ(manager.stream_analog_level(), 240);
+ CallPreProcessAudioBuffer(/*num_calls=*/10, /*peak_ratio=*/0.99f, manager);
+ EXPECT_EQ(manager.stream_analog_level(), 225);
+}
+
} // namespace webrtc
diff --git a/modules/audio_processing/agc/clipping_predictor.cc b/modules/audio_processing/agc/clipping_predictor.cc
new file mode 100644
index 0000000000..982bbca2ee
--- /dev/null
+++ b/modules/audio_processing/agc/clipping_predictor.cc
@@ -0,0 +1,383 @@
+/*
+ * Copyright (c) 2021 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/agc/clipping_predictor.h"
+
+#include <algorithm>
+#include <memory>
+
+#include "common_audio/include/audio_util.h"
+#include "modules/audio_processing/agc/clipping_predictor_level_buffer.h"
+#include "modules/audio_processing/agc/gain_map_internal.h"
+#include "rtc_base/checks.h"
+#include "rtc_base/logging.h"
+#include "rtc_base/numerics/safe_minmax.h"
+
+namespace webrtc {
+namespace {
+
+constexpr int kClippingPredictorMaxGainChange = 15;
+
+// Estimates the new level from the gain error; a copy of the function
+// `LevelFromGainError` in agc_manager_direct.cc.
+int LevelFromGainError(int gain_error,
+ int level,
+ int min_mic_level,
+ int max_mic_level) {
+ RTC_DCHECK_GE(level, 0);
+ RTC_DCHECK_LE(level, max_mic_level);
+ if (gain_error == 0) {
+ return level;
+ }
+ int new_level = level;
+ if (gain_error > 0) {
+ while (kGainMap[new_level] - kGainMap[level] < gain_error &&
+ new_level < max_mic_level) {
+ ++new_level;
+ }
+ } else {
+ while (kGainMap[new_level] - kGainMap[level] > gain_error &&
+ new_level > min_mic_level) {
+ --new_level;
+ }
+ }
+ return new_level;
+}
+
+float ComputeCrestFactor(const ClippingPredictorLevelBuffer::Level& level) {
+ const float crest_factor =
+ FloatS16ToDbfs(level.max) - FloatS16ToDbfs(std::sqrt(level.average));
+ return crest_factor;
+}
+
+// Crest factor-based clipping prediction and clipped level step estimation.
+class ClippingEventPredictor : public ClippingPredictor {
+ public:
+ // ClippingEventPredictor with `num_channels` channels (limited to values
+ // higher than zero); window size `window_length` and reference window size
+ // `reference_window_length` (both referring to the number of frames in the
+ // respective sliding windows and limited to values higher than zero);
+ // reference window delay `reference_window_delay` (delay in frames, limited
+ // to values zero and higher with an additional requirement of
+ // `window_length` < `reference_window_length` + reference_window_delay`);
+ // and an estimation peak threshold `clipping_threshold` and a crest factor
+ // drop threshold `crest_factor_margin` (both in dB).
+ ClippingEventPredictor(int num_channels,
+ int window_length,
+ int reference_window_length,
+ int reference_window_delay,
+ float clipping_threshold,
+ float crest_factor_margin)
+ : window_length_(window_length),
+ reference_window_length_(reference_window_length),
+ reference_window_delay_(reference_window_delay),
+ clipping_threshold_(clipping_threshold),
+ crest_factor_margin_(crest_factor_margin) {
+ RTC_DCHECK_GT(num_channels, 0);
+ RTC_DCHECK_GT(window_length, 0);
+ RTC_DCHECK_GT(reference_window_length, 0);
+ RTC_DCHECK_GE(reference_window_delay, 0);
+ RTC_DCHECK_GT(reference_window_length + reference_window_delay,
+ window_length);
+ const int buffer_length = GetMinFramesProcessed();
+ RTC_DCHECK_GT(buffer_length, 0);
+ for (int i = 0; i < num_channels; ++i) {
+ ch_buffers_.push_back(
+ std::make_unique<ClippingPredictorLevelBuffer>(buffer_length));
+ }
+ }
+
+ ClippingEventPredictor(const ClippingEventPredictor&) = delete;
+ ClippingEventPredictor& operator=(const ClippingEventPredictor&) = delete;
+ ~ClippingEventPredictor() {}
+
+ void Reset() {
+ const int num_channels = ch_buffers_.size();
+ for (int i = 0; i < num_channels; ++i) {
+ ch_buffers_[i]->Reset();
+ }
+ }
+
+ // Analyzes a frame of audio and stores the framewise metrics in
+ // `ch_buffers_`.
+ void Analyze(const AudioFrameView<const float>& frame) {
+ const int num_channels = frame.num_channels();
+ RTC_DCHECK_EQ(num_channels, ch_buffers_.size());
+ const int samples_per_channel = frame.samples_per_channel();
+ RTC_DCHECK_GT(samples_per_channel, 0);
+ for (int channel = 0; channel < num_channels; ++channel) {
+ float sum_squares = 0.0f;
+ float peak = 0.0f;
+ for (const auto& sample : frame.channel(channel)) {
+ sum_squares += sample * sample;
+ peak = std::max(std::fabs(sample), peak);
+ }
+ ch_buffers_[channel]->Push(
+ {sum_squares / static_cast<float>(samples_per_channel), peak});
+ }
+ }
+
+ // Estimates the analog gain adjustment for channel `channel` using a
+ // sliding window over the frame-wise metrics in `ch_buffers_`. Returns an
+ // estimate for the clipped level step equal to `default_clipped_level_step_`
+ // if at least `GetMinFramesProcessed()` frames have been processed since the
+ // last reset and a clipping event is predicted. `level`, `min_mic_level`, and
+ // `max_mic_level` are limited to [0, 255] and `default_step` to [1, 255].
+ absl::optional<int> EstimateClippedLevelStep(int channel,
+ int level,
+ int default_step,
+ int min_mic_level,
+ int max_mic_level) const {
+ RTC_CHECK_GE(channel, 0);
+ RTC_CHECK_LT(channel, ch_buffers_.size());
+ RTC_DCHECK_GE(level, 0);
+ RTC_DCHECK_LE(level, 255);
+ RTC_DCHECK_GT(default_step, 0);
+ RTC_DCHECK_LE(default_step, 255);
+ RTC_DCHECK_GE(min_mic_level, 0);
+ RTC_DCHECK_LE(min_mic_level, 255);
+ RTC_DCHECK_GE(max_mic_level, 0);
+ RTC_DCHECK_LE(max_mic_level, 255);
+ if (level <= min_mic_level) {
+ return absl::nullopt;
+ }
+ if (PredictClippingEvent(channel)) {
+ const int new_level =
+ rtc::SafeClamp(level - default_step, min_mic_level, max_mic_level);
+ const int step = level - new_level;
+ if (step > 0) {
+ return step;
+ }
+ }
+ return absl::nullopt;
+ }
+
+ private:
+ int GetMinFramesProcessed() const {
+ return reference_window_delay_ + reference_window_length_;
+ }
+
+ // Predicts clipping events based on the processed audio frames. Returns
+ // true if a clipping event is likely.
+ bool PredictClippingEvent(int channel) const {
+ const auto metrics =
+ ch_buffers_[channel]->ComputePartialMetrics(0, window_length_);
+ if (!metrics.has_value() ||
+ !(FloatS16ToDbfs(metrics.value().max) > clipping_threshold_)) {
+ return false;
+ }
+ const auto reference_metrics = ch_buffers_[channel]->ComputePartialMetrics(
+ reference_window_delay_, reference_window_length_);
+ if (!reference_metrics.has_value()) {
+ return false;
+ }
+ const float crest_factor = ComputeCrestFactor(metrics.value());
+ const float reference_crest_factor =
+ ComputeCrestFactor(reference_metrics.value());
+ if (crest_factor < reference_crest_factor - crest_factor_margin_) {
+ return true;
+ }
+ return false;
+ }
+
+ std::vector<std::unique_ptr<ClippingPredictorLevelBuffer>> ch_buffers_;
+ const int window_length_;
+ const int reference_window_length_;
+ const int reference_window_delay_;
+ const float clipping_threshold_;
+ const float crest_factor_margin_;
+};
+
+// Performs crest factor-based clipping peak prediction.
+class ClippingPeakPredictor : public ClippingPredictor {
+ public:
+ // Ctor. ClippingPeakPredictor with `num_channels` channels (limited to values
+ // higher than zero); window size `window_length` and reference window size
+ // `reference_window_length` (both referring to the number of frames in the
+ // respective sliding windows and limited to values higher than zero);
+ // reference window delay `reference_window_delay` (delay in frames, limited
+ // to values zero and higher with an additional requirement of
+ // `window_length` < `reference_window_length` + reference_window_delay`);
+ // and a clipping prediction threshold `clipping_threshold` (in dB). Adaptive
+ // clipped level step estimation is used if `adaptive_step_estimation` is
+ // true.
+ explicit ClippingPeakPredictor(int num_channels,
+ int window_length,
+ int reference_window_length,
+ int reference_window_delay,
+ int clipping_threshold,
+ bool adaptive_step_estimation)
+ : window_length_(window_length),
+ reference_window_length_(reference_window_length),
+ reference_window_delay_(reference_window_delay),
+ clipping_threshold_(clipping_threshold),
+ adaptive_step_estimation_(adaptive_step_estimation) {
+ RTC_DCHECK_GT(num_channels, 0);
+ RTC_DCHECK_GT(window_length, 0);
+ RTC_DCHECK_GT(reference_window_length, 0);
+ RTC_DCHECK_GE(reference_window_delay, 0);
+ RTC_DCHECK_GT(reference_window_length + reference_window_delay,
+ window_length);
+ const int buffer_length = GetMinFramesProcessed();
+ RTC_DCHECK_GT(buffer_length, 0);
+ for (int i = 0; i < num_channels; ++i) {
+ ch_buffers_.push_back(
+ std::make_unique<ClippingPredictorLevelBuffer>(buffer_length));
+ }
+ }
+
+ ClippingPeakPredictor(const ClippingPeakPredictor&) = delete;
+ ClippingPeakPredictor& operator=(const ClippingPeakPredictor&) = delete;
+ ~ClippingPeakPredictor() {}
+
+ void Reset() {
+ const int num_channels = ch_buffers_.size();
+ for (int i = 0; i < num_channels; ++i) {
+ ch_buffers_[i]->Reset();
+ }
+ }
+
+ // Analyzes a frame of audio and stores the framewise metrics in
+ // `ch_buffers_`.
+ void Analyze(const AudioFrameView<const float>& frame) {
+ const int num_channels = frame.num_channels();
+ RTC_DCHECK_EQ(num_channels, ch_buffers_.size());
+ const int samples_per_channel = frame.samples_per_channel();
+ RTC_DCHECK_GT(samples_per_channel, 0);
+ for (int channel = 0; channel < num_channels; ++channel) {
+ float sum_squares = 0.0f;
+ float peak = 0.0f;
+ for (const auto& sample : frame.channel(channel)) {
+ sum_squares += sample * sample;
+ peak = std::max(std::fabs(sample), peak);
+ }
+ ch_buffers_[channel]->Push(
+ {sum_squares / static_cast<float>(samples_per_channel), peak});
+ }
+ }
+
+ // Estimates the analog gain adjustment for channel `channel` using a
+ // sliding window over the frame-wise metrics in `ch_buffers_`. Returns an
+ // estimate for the clipped level step (equal to
+ // `default_clipped_level_step_` if `adaptive_estimation_` is false) if at
+ // least `GetMinFramesProcessed()` frames have been processed since the last
+ // reset and a clipping event is predicted. `level`, `min_mic_level`, and
+ // `max_mic_level` are limited to [0, 255] and `default_step` to [1, 255].
+ absl::optional<int> EstimateClippedLevelStep(int channel,
+ int level,
+ int default_step,
+ int min_mic_level,
+ int max_mic_level) const {
+ RTC_DCHECK_GE(channel, 0);
+ RTC_DCHECK_LT(channel, ch_buffers_.size());
+ RTC_DCHECK_GE(level, 0);
+ RTC_DCHECK_LE(level, 255);
+ RTC_DCHECK_GT(default_step, 0);
+ RTC_DCHECK_LE(default_step, 255);
+ RTC_DCHECK_GE(min_mic_level, 0);
+ RTC_DCHECK_LE(min_mic_level, 255);
+ RTC_DCHECK_GE(max_mic_level, 0);
+ RTC_DCHECK_LE(max_mic_level, 255);
+ if (level <= min_mic_level) {
+ return absl::nullopt;
+ }
+ absl::optional<float> estimate_db = EstimatePeakValue(channel);
+ if (estimate_db.has_value() && estimate_db.value() > clipping_threshold_) {
+ int step = 0;
+ if (!adaptive_step_estimation_) {
+ step = default_step;
+ } else {
+ const int estimated_gain_change =
+ rtc::SafeClamp(-static_cast<int>(std::ceil(estimate_db.value())),
+ -kClippingPredictorMaxGainChange, 0);
+ step =
+ std::max(level - LevelFromGainError(estimated_gain_change, level,
+ min_mic_level, max_mic_level),
+ default_step);
+ }
+ const int new_level =
+ rtc::SafeClamp(level - step, min_mic_level, max_mic_level);
+ if (level > new_level) {
+ return level - new_level;
+ }
+ }
+ return absl::nullopt;
+ }
+
+ private:
+ int GetMinFramesProcessed() {
+ return reference_window_delay_ + reference_window_length_;
+ }
+
+ // Predicts clipping sample peaks based on the processed audio frames.
+ // Returns the estimated peak value if clipping is predicted. Otherwise
+ // returns absl::nullopt.
+ absl::optional<float> EstimatePeakValue(int channel) const {
+ const auto reference_metrics = ch_buffers_[channel]->ComputePartialMetrics(
+ reference_window_delay_, reference_window_length_);
+ if (!reference_metrics.has_value()) {
+ return absl::nullopt;
+ }
+ const auto metrics =
+ ch_buffers_[channel]->ComputePartialMetrics(0, window_length_);
+ if (!metrics.has_value() ||
+ !(FloatS16ToDbfs(metrics.value().max) > clipping_threshold_)) {
+ return absl::nullopt;
+ }
+ const float reference_crest_factor =
+ ComputeCrestFactor(reference_metrics.value());
+ const float& mean_squares = metrics.value().average;
+ const float projected_peak =
+ reference_crest_factor + FloatS16ToDbfs(std::sqrt(mean_squares));
+ return projected_peak;
+ }
+
+ std::vector<std::unique_ptr<ClippingPredictorLevelBuffer>> ch_buffers_;
+ const int window_length_;
+ const int reference_window_length_;
+ const int reference_window_delay_;
+ const int clipping_threshold_;
+ const bool adaptive_step_estimation_;
+};
+
+} // namespace
+
+std::unique_ptr<ClippingPredictor> CreateClippingPredictor(
+ int num_channels,
+ const AudioProcessing::Config::GainController1::AnalogGainController::
+ ClippingPredictor& config) {
+ if (!config.enabled) {
+ RTC_LOG(LS_INFO) << "[agc] Clipping prediction disabled.";
+ return nullptr;
+ }
+ RTC_LOG(LS_INFO) << "[agc] Clipping prediction enabled.";
+ using ClippingPredictorMode = AudioProcessing::Config::GainController1::
+ AnalogGainController::ClippingPredictor::Mode;
+ switch (config.mode) {
+ case ClippingPredictorMode::kClippingEventPrediction:
+ return std::make_unique<ClippingEventPredictor>(
+ num_channels, config.window_length, config.reference_window_length,
+ config.reference_window_delay, config.clipping_threshold,
+ config.crest_factor_margin);
+ case ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction:
+ return std::make_unique<ClippingPeakPredictor>(
+ num_channels, config.window_length, config.reference_window_length,
+ config.reference_window_delay, config.clipping_threshold,
+ /*adaptive_step_estimation=*/true);
+ case ClippingPredictorMode::kFixedStepClippingPeakPrediction:
+ return std::make_unique<ClippingPeakPredictor>(
+ num_channels, config.window_length, config.reference_window_length,
+ config.reference_window_delay, config.clipping_threshold,
+ /*adaptive_step_estimation=*/false);
+ }
+ RTC_NOTREACHED();
+}
+
+} // namespace webrtc
diff --git a/modules/audio_processing/agc/clipping_predictor.h b/modules/audio_processing/agc/clipping_predictor.h
new file mode 100644
index 0000000000..ee2b6ef1e7
--- /dev/null
+++ b/modules/audio_processing/agc/clipping_predictor.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2021 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_AGC_CLIPPING_PREDICTOR_H_
+#define MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_H_
+
+#include <memory>
+#include <vector>
+
+#include "absl/types/optional.h"
+#include "modules/audio_processing/include/audio_frame_view.h"
+#include "modules/audio_processing/include/audio_processing.h"
+
+namespace webrtc {
+
+// Frame-wise clipping prediction and clipped level step estimation. Analyzes
+// 10 ms multi-channel frames and estimates an analog mic level decrease step
+// to possibly avoid clipping when predicted. `Analyze()` and
+// `EstimateClippedLevelStep()` can be called in any order.
+class ClippingPredictor {
+ public:
+ virtual ~ClippingPredictor() = default;
+
+ virtual void Reset() = 0;
+
+ // Analyzes a 10 ms multi-channel audio frame.
+ virtual void Analyze(const AudioFrameView<const float>& frame) = 0;
+
+ // Predicts if clipping is going to occur for the specified `channel` in the
+ // near-future and, if so, it returns a recommended analog mic level decrease
+ // step. Returns absl::nullopt if clipping is not predicted.
+ // `level` is the current analog mic level, `default_step` is the amount the
+ // mic level is lowered by the analog controller with every clipping event and
+ // `min_mic_level` and `max_mic_level` is the range of allowed analog mic
+ // levels.
+ virtual absl::optional<int> EstimateClippedLevelStep(
+ int channel,
+ int level,
+ int default_step,
+ int min_mic_level,
+ int max_mic_level) const = 0;
+
+};
+
+// Creates a ClippingPredictor based on the provided `config`. When enabled,
+// the following must hold for `config`:
+// `window_length < reference_window_length + reference_window_delay`.
+// Returns `nullptr` if `config.enabled` is false.
+std::unique_ptr<ClippingPredictor> CreateClippingPredictor(
+ int num_channels,
+ const AudioProcessing::Config::GainController1::AnalogGainController::
+ ClippingPredictor& config);
+
+} // namespace webrtc
+
+#endif // MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_H_
diff --git a/modules/audio_processing/agc/clipping_predictor_evaluator.cc b/modules/audio_processing/agc/clipping_predictor_evaluator.cc
new file mode 100644
index 0000000000..2a4ea922cf
--- /dev/null
+++ b/modules/audio_processing/agc/clipping_predictor_evaluator.cc
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2021 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/agc/clipping_predictor_evaluator.h"
+
+#include <algorithm>
+
+#include "rtc_base/checks.h"
+#include "rtc_base/logging.h"
+
+namespace webrtc {
+namespace {
+
+// Returns the index of the oldest item in the ring buffer for a non-empty
+// ring buffer with give `size`, `tail` index and `capacity`.
+int OldestExpectedDetectionIndex(int size, int tail, int capacity) {
+ RTC_DCHECK_GT(size, 0);
+ return tail - size + (tail < size ? capacity : 0);
+}
+
+} // namespace
+
+ClippingPredictorEvaluator::ClippingPredictorEvaluator(int history_size)
+ : history_size_(history_size),
+ ring_buffer_capacity_(history_size + 1),
+ ring_buffer_(ring_buffer_capacity_),
+ true_positives_(0),
+ true_negatives_(0),
+ false_positives_(0),
+ false_negatives_(0) {
+ RTC_DCHECK_GT(history_size_, 0);
+ Reset();
+}
+
+ClippingPredictorEvaluator::~ClippingPredictorEvaluator() = default;
+
+absl::optional<int> ClippingPredictorEvaluator::Observe(
+ bool clipping_detected,
+ bool clipping_predicted) {
+ RTC_DCHECK_GE(ring_buffer_size_, 0);
+ RTC_DCHECK_LE(ring_buffer_size_, ring_buffer_capacity_);
+ RTC_DCHECK_GE(ring_buffer_tail_, 0);
+ RTC_DCHECK_LT(ring_buffer_tail_, ring_buffer_capacity_);
+
+ DecreaseTimesToLive();
+ if (clipping_predicted) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ Push(/*expected_detection=*/{/*ttl=*/history_size_, /*detected=*/false});
+ }
+ // Clipping is expected if there are expected detections regardless of
+ // whether all the expected detections have been previously matched - i.e.,
+ // `ExpectedDetection::detected` is true.
+ const bool clipping_expected = ring_buffer_size_ > 0;
+
+ absl::optional<int> prediction_interval;
+ if (clipping_expected && clipping_detected) {
+ prediction_interval = FindEarliestPredictionInterval();
+ // Add a true positive for each unexpired expected detection.
+ const int num_modified_items = MarkExpectedDetectionAsDetected();
+ true_positives_ += num_modified_items;
+ RTC_DCHECK(prediction_interval.has_value() || num_modified_items == 0);
+ RTC_DCHECK(!prediction_interval.has_value() || num_modified_items > 0);
+ } else if (clipping_expected && !clipping_detected) {
+ // Add a false positive if there is one expected detection that has expired
+ // and that has never been matched before. Note that there is at most one
+ // unmatched expired detection.
+ if (HasExpiredUnmatchedExpectedDetection()) {
+ false_positives_++;
+ }
+ } else if (!clipping_expected && clipping_detected) {
+ false_negatives_++;
+ } else {
+ RTC_DCHECK(!clipping_expected && !clipping_detected);
+ true_negatives_++;
+ }
+ return prediction_interval;
+}
+
+void ClippingPredictorEvaluator::Reset() {
+ // Empty the ring buffer of expected detections.
+ ring_buffer_tail_ = 0;
+ ring_buffer_size_ = 0;
+}
+
+// Cost: O(1).
+void ClippingPredictorEvaluator::Push(ExpectedDetection value) {
+ ring_buffer_[ring_buffer_tail_] = value;
+ ring_buffer_tail_++;
+ if (ring_buffer_tail_ == ring_buffer_capacity_) {
+ ring_buffer_tail_ = 0;
+ }
+ ring_buffer_size_ = std::min(ring_buffer_capacity_, ring_buffer_size_ + 1);
+}
+
+// Cost: O(N).
+void ClippingPredictorEvaluator::DecreaseTimesToLive() {
+ bool expired_found = false;
+ for (int i = ring_buffer_tail_ - ring_buffer_size_; i < ring_buffer_tail_;
+ ++i) {
+ int index = i >= 0 ? i : ring_buffer_capacity_ + i;
+ RTC_DCHECK_GE(index, 0);
+ RTC_DCHECK_LT(index, ring_buffer_.size());
+ RTC_DCHECK_GE(ring_buffer_[index].ttl, 0);
+ if (ring_buffer_[index].ttl == 0) {
+ RTC_DCHECK(!expired_found)
+ << "There must be at most one expired item in the ring buffer.";
+ expired_found = true;
+ RTC_DCHECK_EQ(index, OldestExpectedDetectionIndex(ring_buffer_size_,
+ ring_buffer_tail_,
+ ring_buffer_capacity_))
+ << "The expired item must be the oldest in the ring buffer.";
+ }
+ ring_buffer_[index].ttl--;
+ }
+ if (expired_found) {
+ ring_buffer_size_--;
+ }
+}
+
+// Cost: O(N).
+absl::optional<int> ClippingPredictorEvaluator::FindEarliestPredictionInterval()
+ const {
+ absl::optional<int> prediction_interval;
+ for (int i = ring_buffer_tail_ - ring_buffer_size_; i < ring_buffer_tail_;
+ ++i) {
+ int index = i >= 0 ? i : ring_buffer_capacity_ + i;
+ RTC_DCHECK_GE(index, 0);
+ RTC_DCHECK_LT(index, ring_buffer_.size());
+ if (!ring_buffer_[index].detected) {
+ prediction_interval = std::max(prediction_interval.value_or(0),
+ history_size_ - ring_buffer_[index].ttl);
+ }
+ }
+ return prediction_interval;
+}
+
+// Cost: O(N).
+int ClippingPredictorEvaluator::MarkExpectedDetectionAsDetected() {
+ int num_modified_items = 0;
+ for (int i = ring_buffer_tail_ - ring_buffer_size_; i < ring_buffer_tail_;
+ ++i) {
+ int index = i >= 0 ? i : ring_buffer_capacity_ + i;
+ RTC_DCHECK_GE(index, 0);
+ RTC_DCHECK_LT(index, ring_buffer_.size());
+ if (!ring_buffer_[index].detected) {
+ num_modified_items++;
+ }
+ ring_buffer_[index].detected = true;
+ }
+ return num_modified_items;
+}
+
+// Cost: O(1).
+bool ClippingPredictorEvaluator::HasExpiredUnmatchedExpectedDetection() const {
+ if (ring_buffer_size_ == 0) {
+ return false;
+ }
+ // If an expired item, that is `ttl` equal to 0, exists, it must be the
+ // oldest.
+ const int oldest_index = OldestExpectedDetectionIndex(
+ ring_buffer_size_, ring_buffer_tail_, ring_buffer_capacity_);
+ RTC_DCHECK_GE(oldest_index, 0);
+ RTC_DCHECK_LT(oldest_index, ring_buffer_.size());
+ return ring_buffer_[oldest_index].ttl == 0 &&
+ !ring_buffer_[oldest_index].detected;
+}
+
+} // namespace webrtc
diff --git a/modules/audio_processing/agc/clipping_predictor_evaluator.h b/modules/audio_processing/agc/clipping_predictor_evaluator.h
new file mode 100644
index 0000000000..e76f25d5e1
--- /dev/null
+++ b/modules/audio_processing/agc/clipping_predictor_evaluator.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2021 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_AGC_CLIPPING_PREDICTOR_EVALUATOR_H_
+#define MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_EVALUATOR_H_
+
+#include <vector>
+
+#include "absl/types/optional.h"
+
+namespace webrtc {
+
+// Counts true/false positives/negatives while observing sequences of flag pairs
+// that indicate whether clipping has been detected and/or if clipping is
+// predicted. When a true positive is found measures the time interval between
+// prediction and detection events.
+// From the time a prediction is observed and for a period equal to
+// `history_size` calls to `Observe()`, one or more detections are expected. If
+// the expectation is met, a true positives is added and the time interval
+// between the earliest prediction and the detection is recorded; otherwise,
+// when the deadline is reached, a false positive is added. Note that one
+// detection matches all the expected detections that have not expired - i.e.,
+// one detection counts as multiple true positives.
+// If a detection is observed, but no prediction has been observed over the past
+// `history_size` calls to `Observe()`, then a false negative is added;
+// otherwise, a true negative is added.
+class ClippingPredictorEvaluator {
+ public:
+ // Ctor. `history_size` indicates how long to wait for a call to `Observe()`
+ // having `clipping_detected` set to true from the time clipping is predicted.
+ explicit ClippingPredictorEvaluator(int history_size);
+ ClippingPredictorEvaluator(const ClippingPredictorEvaluator&) = delete;
+ ClippingPredictorEvaluator& operator=(const ClippingPredictorEvaluator&) =
+ delete;
+ ~ClippingPredictorEvaluator();
+
+ // Observes whether clipping has been detected and/or if clipping is
+ // predicted. When predicted one or more detections are expected in the next
+ // `history_size_` calls of `Observe()`. When true positives are found returns
+ // the prediction interval between the earliest prediction and the detection.
+ absl::optional<int> Observe(bool clipping_detected, bool clipping_predicted);
+
+ // Removes any expectation recently set after a call to `Observe()` having
+ // `clipping_predicted` set to true.
+ void Reset();
+
+ // Metrics getters.
+ int true_positives() const { return true_positives_; }
+ int true_negatives() const { return true_negatives_; }
+ int false_positives() const { return false_positives_; }
+ int false_negatives() const { return false_negatives_; }
+
+ private:
+ const int history_size_;
+
+ // State of a detection expected to be observed after a prediction.
+ struct ExpectedDetection {
+ // Time to live (TTL); remaining number of `Observe()` calls to match a call
+ // having `clipping_detected` set to true.
+ int ttl;
+ // True if an `Observe()` call having `clipping_detected` set to true has
+ // been observed.
+ bool detected;
+ };
+ // Ring buffer of expected detections.
+ const int ring_buffer_capacity_;
+ std::vector<ExpectedDetection> ring_buffer_;
+ int ring_buffer_tail_;
+ int ring_buffer_size_;
+
+ // Pushes `expected_detection` into `expected_matches_ring_buffer_`.
+ void Push(ExpectedDetection expected_detection);
+ // Decreased the TTLs in `expected_matches_ring_buffer_` and removes expired
+ // items.
+ void DecreaseTimesToLive();
+ // Returns the prediction interval for the earliest unexpired expected
+ // detection if any.
+ absl::optional<int> FindEarliestPredictionInterval() const;
+ // Marks all the items in `expected_matches_ring_buffer_` as `detected` and
+ // returns the number of updated items.
+ int MarkExpectedDetectionAsDetected();
+ // Returns true if `expected_matches_ring_buffer_` has an item having `ttl`
+ // equal to 0 (expired) and `detected` equal to false (unmatched).
+ bool HasExpiredUnmatchedExpectedDetection() const;
+
+ // Metrics.
+ int true_positives_;
+ int true_negatives_;
+ int false_positives_;
+ int false_negatives_;
+};
+
+} // namespace webrtc
+
+#endif // MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_EVALUATOR_H_
diff --git a/modules/audio_processing/agc/clipping_predictor_evaluator_unittest.cc b/modules/audio_processing/agc/clipping_predictor_evaluator_unittest.cc
new file mode 100644
index 0000000000..1eb83eae61
--- /dev/null
+++ b/modules/audio_processing/agc/clipping_predictor_evaluator_unittest.cc
@@ -0,0 +1,568 @@
+/*
+ * Copyright (c) 2021 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/agc/clipping_predictor_evaluator.h"
+
+#include <cstdint>
+#include <memory>
+#include <tuple>
+#include <vector>
+
+#include "absl/types/optional.h"
+#include "rtc_base/numerics/safe_conversions.h"
+#include "rtc_base/random.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace {
+
+using testing::Eq;
+using testing::Optional;
+
+constexpr bool kDetected = true;
+constexpr bool kNotDetected = false;
+
+constexpr bool kPredicted = true;
+constexpr bool kNotPredicted = false;
+
+int SumTrueFalsePositivesNegatives(
+ const ClippingPredictorEvaluator& evaluator) {
+ return evaluator.true_positives() + evaluator.true_negatives() +
+ evaluator.false_positives() + evaluator.false_negatives();
+}
+
+// Checks the metrics after init - i.e., no call to `Observe()`.
+TEST(ClippingPredictorEvaluatorTest, Init) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ EXPECT_EQ(evaluator.true_positives(), 0);
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+class ClippingPredictorEvaluatorParameterization
+ : public ::testing::TestWithParam<std::tuple<int, int>> {
+ protected:
+ uint64_t seed() const {
+ return rtc::checked_cast<uint64_t>(std::get<0>(GetParam()));
+ }
+ int history_size() const { return std::get<1>(GetParam()); }
+};
+
+// Checks that after each call to `Observe()` at most one metric changes.
+TEST_P(ClippingPredictorEvaluatorParameterization, AtMostOneMetricChanges) {
+ constexpr int kNumCalls = 123;
+ Random random_generator(seed());
+ ClippingPredictorEvaluator evaluator(history_size());
+
+ for (int i = 0; i < kNumCalls; ++i) {
+ SCOPED_TRACE(i);
+ // Read metrics before `Observe()` is called.
+ const int last_tp = evaluator.true_positives();
+ const int last_tn = evaluator.true_negatives();
+ const int last_fp = evaluator.false_positives();
+ const int last_fn = evaluator.false_negatives();
+ // `Observe()` a random observation.
+ bool clipping_detected = random_generator.Rand<bool>();
+ bool clipping_predicted = random_generator.Rand<bool>();
+ evaluator.Observe(clipping_detected, clipping_predicted);
+
+ // Check that at most one metric has changed.
+ int num_changes = 0;
+ num_changes += last_tp == evaluator.true_positives() ? 0 : 1;
+ num_changes += last_tn == evaluator.true_negatives() ? 0 : 1;
+ num_changes += last_fp == evaluator.false_positives() ? 0 : 1;
+ num_changes += last_fn == evaluator.false_negatives() ? 0 : 1;
+ EXPECT_GE(num_changes, 0);
+ EXPECT_LE(num_changes, 1);
+ }
+}
+
+// Checks that after each call to `Observe()` each metric either remains
+// unchanged or grows.
+TEST_P(ClippingPredictorEvaluatorParameterization, MetricsAreWeaklyMonotonic) {
+ constexpr int kNumCalls = 123;
+ Random random_generator(seed());
+ ClippingPredictorEvaluator evaluator(history_size());
+
+ for (int i = 0; i < kNumCalls; ++i) {
+ SCOPED_TRACE(i);
+ // Read metrics before `Observe()` is called.
+ const int last_tp = evaluator.true_positives();
+ const int last_tn = evaluator.true_negatives();
+ const int last_fp = evaluator.false_positives();
+ const int last_fn = evaluator.false_negatives();
+ // `Observe()` a random observation.
+ bool clipping_detected = random_generator.Rand<bool>();
+ bool clipping_predicted = random_generator.Rand<bool>();
+ evaluator.Observe(clipping_detected, clipping_predicted);
+
+ // Check that metrics are weakly monotonic.
+ EXPECT_GE(evaluator.true_positives(), last_tp);
+ EXPECT_GE(evaluator.true_negatives(), last_tn);
+ EXPECT_GE(evaluator.false_positives(), last_fp);
+ EXPECT_GE(evaluator.false_negatives(), last_fn);
+ }
+}
+
+// Checks that after each call to `Observe()` the growth speed of each metrics
+// is bounded.
+TEST_P(ClippingPredictorEvaluatorParameterization, BoundedMetricsGrowth) {
+ constexpr int kNumCalls = 123;
+ Random random_generator(seed());
+ ClippingPredictorEvaluator evaluator(history_size());
+
+ for (int i = 0; i < kNumCalls; ++i) {
+ SCOPED_TRACE(i);
+ // Read metrics before `Observe()` is called.
+ const int last_tp = evaluator.true_positives();
+ const int last_tn = evaluator.true_negatives();
+ const int last_fp = evaluator.false_positives();
+ const int last_fn = evaluator.false_negatives();
+ // `Observe()` a random observation.
+ bool clipping_detected = random_generator.Rand<bool>();
+ bool clipping_predicted = random_generator.Rand<bool>();
+ evaluator.Observe(clipping_detected, clipping_predicted);
+
+ // Check that TPs grow by at most `history_size() + 1`. Such an upper bound
+ // is reached when multiple predictions are matched by a single detection.
+ EXPECT_LE(evaluator.true_positives() - last_tp, history_size() + 1);
+ // Check that TNs, FPs and FNs grow by at most one. `max_growth`.
+ EXPECT_LE(evaluator.true_negatives() - last_tn, 1);
+ EXPECT_LE(evaluator.false_positives() - last_fp, 1);
+ EXPECT_LE(evaluator.false_negatives() - last_fn, 1);
+ }
+}
+
+// Checks that `Observe()` returns a prediction interval if and only if one or
+// more true positives are found.
+TEST_P(ClippingPredictorEvaluatorParameterization,
+ PredictionIntervalIfAndOnlyIfTruePositives) {
+ constexpr int kNumCalls = 123;
+ Random random_generator(seed());
+ ClippingPredictorEvaluator evaluator(history_size());
+
+ for (int i = 0; i < kNumCalls; ++i) {
+ SCOPED_TRACE(i);
+ // Read true positives before `Observe()` is called.
+ const int last_tp = evaluator.true_positives();
+ // `Observe()` a random observation.
+ bool clipping_detected = random_generator.Rand<bool>();
+ bool clipping_predicted = random_generator.Rand<bool>();
+ absl::optional<int> prediction_interval =
+ evaluator.Observe(clipping_detected, clipping_predicted);
+
+ // Check that the prediction interval is returned when a true positive is
+ // found.
+ if (evaluator.true_positives() == last_tp) {
+ EXPECT_FALSE(prediction_interval.has_value());
+ } else {
+ EXPECT_TRUE(prediction_interval.has_value());
+ }
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ ClippingPredictorEvaluatorTest,
+ ClippingPredictorEvaluatorParameterization,
+ ::testing::Combine(::testing::Values(4, 8, 15, 16, 23, 42),
+ ::testing::Values(1, 10, 21)));
+
+// Checks that, observing a detection and a prediction after init, produces a
+// true positive.
+TEST(ClippingPredictorEvaluatorTest, OneTruePositiveAfterInit) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kDetected, kPredicted);
+ EXPECT_EQ(evaluator.true_positives(), 1);
+
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that, observing a detection but no prediction after init, produces a
+// false negative.
+TEST(ClippingPredictorEvaluatorTest, OneFalseNegativeAfterInit) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.false_negatives(), 1);
+
+ EXPECT_EQ(evaluator.true_positives(), 0);
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+}
+
+// Checks that, observing no detection but a prediction after init, produces a
+// false positive after expiration.
+TEST(ClippingPredictorEvaluatorTest, OneFalsePositiveAfterInit) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 1);
+
+ EXPECT_EQ(evaluator.true_positives(), 0);
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that, observing no detection and no prediction after init, produces a
+// true negative.
+TEST(ClippingPredictorEvaluatorTest, OneTrueNegativeAfterInit) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.true_negatives(), 1);
+
+ EXPECT_EQ(evaluator.true_positives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that the evaluator detects true negatives when clipping is neither
+// predicted nor detected.
+TEST(ClippingPredictorEvaluatorTest, NeverDetectedAndNotPredicted) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.true_negatives(), 4);
+
+ EXPECT_EQ(evaluator.true_positives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that the evaluator detects a false negative when clipping is detected
+// but not predicted.
+TEST(ClippingPredictorEvaluatorTest, DetectedButNotPredicted) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.false_negatives(), 1);
+
+ EXPECT_EQ(evaluator.true_positives(), 0);
+ EXPECT_EQ(evaluator.true_negatives(), 3);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+}
+
+// Checks that the evaluator does not detect a false positive when clipping is
+// predicted but not detected until the observation period expires.
+TEST(ClippingPredictorEvaluatorTest,
+ PredictedOnceAndNeverDetectedBeforeDeadline) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 1);
+
+ EXPECT_EQ(evaluator.true_positives(), 0);
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that the evaluator detects a false positive when clipping is predicted
+// but detected after the observation period expires.
+TEST(ClippingPredictorEvaluatorTest, PredictedOnceButDetectedAfterDeadline) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 1);
+
+ EXPECT_EQ(evaluator.true_positives(), 0);
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 1);
+}
+
+// Checks that a prediction followed by a detection counts as true positive.
+TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndThenImmediatelyDetected) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ evaluator.Observe(kDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.true_positives(), 1);
+
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that a prediction followed by a delayed detection counts as true
+// positive if the delay is within the observation period.
+TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndDetectedBeforeDeadline) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ evaluator.Observe(kDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.true_positives(), 1);
+
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that a prediction followed by a delayed detection counts as true
+// positive if the delay equals the observation period.
+TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndDetectedAtDeadline) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ evaluator.Observe(kDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.true_positives(), 1);
+
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that a prediction followed by a multiple adjacent detections within
+// the deadline counts as a single true positive and that, after the deadline,
+// a detection counts as a false negative.
+TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndDetectedMultipleTimes) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ // Multiple detections.
+ evaluator.Observe(kDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.true_positives(), 1);
+ evaluator.Observe(kDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.true_positives(), 1);
+ // A detection outside of the observation period counts as false negative.
+ evaluator.Observe(kDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.false_negatives(), 1);
+ EXPECT_EQ(SumTrueFalsePositivesNegatives(evaluator), 2);
+
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+}
+
+// Checks that a false positive is added when clipping is detected after a too
+// early prediction.
+TEST(ClippingPredictorEvaluatorTest,
+ PredictedMultipleTimesAndDetectedOnceAfterDeadline) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted); // ---+
+ evaluator.Observe(kNotDetected, kPredicted); // |
+ evaluator.Observe(kNotDetected, kPredicted); // |
+ evaluator.Observe(kNotDetected, kPredicted); // <--+ Not matched.
+ // The time to match a detection after the first prediction expired.
+ EXPECT_EQ(evaluator.false_positives(), 1);
+ evaluator.Observe(kDetected, kNotPredicted);
+ // The detection above does not match the first prediction because it happened
+ // after the deadline of the 1st prediction.
+ EXPECT_EQ(evaluator.false_positives(), 1);
+
+ EXPECT_EQ(evaluator.true_positives(), 3);
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that multiple consecutive predictions match the first detection
+// observed before the expected detection deadline expires.
+TEST(ClippingPredictorEvaluatorTest, PredictedMultipleTimesAndDetectedOnce) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted); // --+
+ evaluator.Observe(kNotDetected, kPredicted); // | --+
+ evaluator.Observe(kNotDetected, kPredicted); // | | --+
+ evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ <-+
+ EXPECT_EQ(evaluator.true_positives(), 3);
+ // The following observations do not generate any true negatives as they
+ // belong to the observation period of the last prediction - for which a
+ // detection has already been matched.
+ const int true_negatives = evaluator.true_negatives();
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.true_negatives(), true_negatives);
+
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that multiple consecutive predictions match the multiple detections
+// observed before the expected detection deadline expires.
+TEST(ClippingPredictorEvaluatorTest,
+ PredictedMultipleTimesAndDetectedMultipleTimes) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted); // --+
+ evaluator.Observe(kNotDetected, kPredicted); // | --+
+ evaluator.Observe(kNotDetected, kPredicted); // | | --+
+ evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ <-+
+ evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+
+ EXPECT_EQ(evaluator.true_positives(), 3);
+ // The following observation does not generate a true negative as it belongs
+ // to the observation period of the last prediction - for which two detections
+ // have already been matched.
+ const int true_negatives = evaluator.true_negatives();
+ evaluator.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(evaluator.true_negatives(), true_negatives);
+
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that multiple consecutive predictions match all the detections
+// observed before the expected detection deadline expires.
+TEST(ClippingPredictorEvaluatorTest, PredictedMultipleTimesAndAllDetected) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted); // --+
+ evaluator.Observe(kNotDetected, kPredicted); // | --+
+ evaluator.Observe(kNotDetected, kPredicted); // | | --+
+ evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ <-+
+ evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+
+ evaluator.Observe(kDetected, kNotPredicted); // <-+
+ EXPECT_EQ(evaluator.true_positives(), 3);
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+// Checks that multiple non-consecutive predictions match all the detections
+// observed before the expected detection deadline expires.
+TEST(ClippingPredictorEvaluatorTest,
+ PredictedMultipleTimesWithGapAndAllDetected) {
+ ClippingPredictorEvaluator evaluator(/*history_size=*/3);
+ evaluator.Observe(kNotDetected, kPredicted); // --+
+ evaluator.Observe(kNotDetected, kNotPredicted); // |
+ evaluator.Observe(kNotDetected, kPredicted); // | --+
+ evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+
+ evaluator.Observe(kDetected, kNotPredicted); // <-+
+ evaluator.Observe(kDetected, kNotPredicted); // <-+
+ EXPECT_EQ(evaluator.true_positives(), 2);
+ EXPECT_EQ(evaluator.true_negatives(), 0);
+ EXPECT_EQ(evaluator.false_positives(), 0);
+ EXPECT_EQ(evaluator.false_negatives(), 0);
+}
+
+class ClippingPredictorEvaluatorPredictionIntervalParameterization
+ : public ::testing::TestWithParam<std::tuple<int, int>> {
+ protected:
+ int num_extra_observe_calls() const { return std::get<0>(GetParam()); }
+ int history_size() const { return std::get<1>(GetParam()); }
+};
+
+// Checks that the minimum prediction interval is returned if clipping is
+// correctly predicted as soon as detected - i.e., no anticipation.
+TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization,
+ MinimumPredictionInterval) {
+ ClippingPredictorEvaluator evaluator(history_size());
+ for (int i = 0; i < num_extra_observe_calls(); ++i) {
+ EXPECT_EQ(evaluator.Observe(kNotDetected, kNotPredicted), absl::nullopt);
+ }
+ absl::optional<int> prediction_interval =
+ evaluator.Observe(kDetected, kPredicted);
+ EXPECT_THAT(prediction_interval, Optional(Eq(0)));
+}
+
+// Checks that a prediction interval between the minimum and the maximum is
+// returned if clipping is correctly predicted before it is detected but not as
+// early as possible.
+TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization,
+ IntermediatePredictionInterval) {
+ ClippingPredictorEvaluator evaluator(history_size());
+ for (int i = 0; i < num_extra_observe_calls(); ++i) {
+ EXPECT_EQ(evaluator.Observe(kNotDetected, kNotPredicted), absl::nullopt);
+ }
+ EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt);
+ EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt);
+ EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt);
+ absl::optional<int> prediction_interval =
+ evaluator.Observe(kDetected, kPredicted);
+ EXPECT_THAT(prediction_interval, Optional(Eq(3)));
+}
+
+// Checks that the maximum prediction interval is returned if clipping is
+// correctly predicted as early as possible.
+TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization,
+ MaximumPredictionInterval) {
+ ClippingPredictorEvaluator evaluator(history_size());
+ for (int i = 0; i < num_extra_observe_calls(); ++i) {
+ EXPECT_EQ(evaluator.Observe(kNotDetected, kNotPredicted), absl::nullopt);
+ }
+ for (int i = 0; i < history_size(); ++i) {
+ EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt);
+ }
+ absl::optional<int> prediction_interval =
+ evaluator.Observe(kDetected, kPredicted);
+ EXPECT_THAT(prediction_interval, Optional(Eq(history_size())));
+}
+
+// Checks that `Observe()` returns the prediction interval as soon as a true
+// positive is found and never again while ongoing detections are matched to a
+// previously observed prediction.
+TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization,
+ PredictionIntervalReturnedOnce) {
+ ASSERT_LT(num_extra_observe_calls(), history_size());
+ ClippingPredictorEvaluator evaluator(history_size());
+ // Observe predictions before detection.
+ for (int i = 0; i < num_extra_observe_calls(); ++i) {
+ EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt);
+ }
+ // Observe a detection.
+ absl::optional<int> prediction_interval =
+ evaluator.Observe(kDetected, kPredicted);
+ EXPECT_TRUE(prediction_interval.has_value());
+ // `Observe()` does not return a prediction interval anymore during ongoing
+ // detections observed while a detection is still expected.
+ for (int i = 0; i < history_size(); ++i) {
+ EXPECT_EQ(evaluator.Observe(kDetected, kNotPredicted), absl::nullopt);
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ ClippingPredictorEvaluatorTest,
+ ClippingPredictorEvaluatorPredictionIntervalParameterization,
+ ::testing::Combine(::testing::Values(0, 3, 5), ::testing::Values(7, 11)));
+
+// Checks that, when a detection is expected, the expectation is removed if and
+// only if `Reset()` is called after a prediction is observed.
+TEST(ClippingPredictorEvaluatorTest, NoFalsePositivesAfterReset) {
+ constexpr int kHistorySize = 2;
+
+ ClippingPredictorEvaluator with_reset(kHistorySize);
+ with_reset.Observe(kNotDetected, kPredicted);
+ with_reset.Reset();
+ with_reset.Observe(kNotDetected, kNotPredicted);
+ with_reset.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(with_reset.true_positives(), 0);
+ EXPECT_EQ(with_reset.true_negatives(), 2);
+ EXPECT_EQ(with_reset.false_positives(), 0);
+ EXPECT_EQ(with_reset.false_negatives(), 0);
+
+ ClippingPredictorEvaluator no_reset(kHistorySize);
+ no_reset.Observe(kNotDetected, kPredicted);
+ no_reset.Observe(kNotDetected, kNotPredicted);
+ no_reset.Observe(kNotDetected, kNotPredicted);
+ EXPECT_EQ(no_reset.true_positives(), 0);
+ EXPECT_EQ(no_reset.true_negatives(), 0);
+ EXPECT_EQ(no_reset.false_positives(), 1);
+ EXPECT_EQ(no_reset.false_negatives(), 0);
+}
+
+} // namespace
+} // namespace webrtc
diff --git a/modules/audio_processing/agc/clipping_predictor_level_buffer.cc b/modules/audio_processing/agc/clipping_predictor_level_buffer.cc
new file mode 100644
index 0000000000..bc33cda040
--- /dev/null
+++ b/modules/audio_processing/agc/clipping_predictor_level_buffer.cc
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2021 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/agc/clipping_predictor_level_buffer.h"
+
+#include <algorithm>
+#include <cmath>
+
+#include "rtc_base/checks.h"
+#include "rtc_base/logging.h"
+
+namespace webrtc {
+
+bool ClippingPredictorLevelBuffer::Level::operator==(const Level& level) const {
+ constexpr float kEpsilon = 1e-6f;
+ return std::fabs(average - level.average) < kEpsilon &&
+ std::fabs(max - level.max) < kEpsilon;
+}
+
+ClippingPredictorLevelBuffer::ClippingPredictorLevelBuffer(int capacity)
+ : tail_(-1), size_(0), data_(std::max(1, capacity)) {
+ if (capacity > kMaxCapacity) {
+ RTC_LOG(LS_WARNING) << "[agc]: ClippingPredictorLevelBuffer exceeds the "
+ << "maximum allowed capacity. Capacity: " << capacity;
+ }
+ RTC_DCHECK(!data_.empty());
+}
+
+void ClippingPredictorLevelBuffer::Reset() {
+ tail_ = -1;
+ size_ = 0;
+}
+
+void ClippingPredictorLevelBuffer::Push(Level level) {
+ ++tail_;
+ if (tail_ == Capacity()) {
+ tail_ = 0;
+ }
+ if (size_ < Capacity()) {
+ size_++;
+ }
+ data_[tail_] = level;
+}
+
+// TODO(bugs.webrtc.org/12774): Optimize partial computation for long buffers.
+absl::optional<ClippingPredictorLevelBuffer::Level>
+ClippingPredictorLevelBuffer::ComputePartialMetrics(int delay,
+ int num_items) const {
+ RTC_DCHECK_GE(delay, 0);
+ RTC_DCHECK_LT(delay, Capacity());
+ RTC_DCHECK_GT(num_items, 0);
+ RTC_DCHECK_LE(num_items, Capacity());
+ RTC_DCHECK_LE(delay + num_items, Capacity());
+ if (delay + num_items > Size()) {
+ return absl::nullopt;
+ }
+ float sum = 0.0f;
+ float max = 0.0f;
+ for (int i = 0; i < num_items && i < Size(); ++i) {
+ int idx = tail_ - delay - i;
+ if (idx < 0) {
+ idx += Capacity();
+ }
+ sum += data_[idx].average;
+ max = std::fmax(data_[idx].max, max);
+ }
+ return absl::optional<Level>({sum / static_cast<float>(num_items), max});
+}
+
+} // namespace webrtc
diff --git a/modules/audio_processing/agc/clipping_predictor_level_buffer.h b/modules/audio_processing/agc/clipping_predictor_level_buffer.h
new file mode 100644
index 0000000000..f3e8368194
--- /dev/null
+++ b/modules/audio_processing/agc/clipping_predictor_level_buffer.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2021 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_AGC_CLIPPING_PREDICTOR_LEVEL_BUFFER_H_
+#define MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_LEVEL_BUFFER_H_
+
+#include <memory>
+#include <vector>
+
+#include "absl/types/optional.h"
+
+namespace webrtc {
+
+// A circular buffer to store frame-wise `Level` items for clipping prediction.
+// The current implementation is not optimized for large buffer lengths.
+class ClippingPredictorLevelBuffer {
+ public:
+ struct Level {
+ float average;
+ float max;
+ bool operator==(const Level& level) const;
+ };
+
+ // Recommended maximum capacity. It is possible to create a buffer with a
+ // larger capacity, but the implementation is not optimized for large values.
+ static constexpr int kMaxCapacity = 100;
+
+ // Ctor. Sets the buffer capacity to max(1, `capacity`) and logs a warning
+ // message if the capacity is greater than `kMaxCapacity`.
+ explicit ClippingPredictorLevelBuffer(int capacity);
+ ~ClippingPredictorLevelBuffer() {}
+ ClippingPredictorLevelBuffer(const ClippingPredictorLevelBuffer&) = delete;
+ ClippingPredictorLevelBuffer& operator=(const ClippingPredictorLevelBuffer&) =
+ delete;
+
+ void Reset();
+
+ // Returns the current number of items stored in the buffer.
+ int Size() const { return size_; }
+
+ // Returns the capacity of the buffer.
+ int Capacity() const { return data_.size(); }
+
+ // Adds a `level` item into the circular buffer `data_`. Stores at most
+ // `Capacity()` items. If more items are pushed, the new item replaces the
+ // least recently pushed item.
+ void Push(Level level);
+
+ // If at least `num_items` + `delay` items have been pushed, returns the
+ // average and maximum value for the `num_items` most recently pushed items
+ // from `delay` to `delay` - `num_items` (a delay equal to zero corresponds
+ // to the most recently pushed item). The value of `delay` is limited to
+ // [0, N] and `num_items` to [1, M] where N + M is the capacity of the buffer.
+ absl::optional<Level> ComputePartialMetrics(int delay, int num_items) const;
+
+ private:
+ int tail_;
+ int size_;
+ std::vector<Level> data_;
+};
+
+} // namespace webrtc
+
+#endif // MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_LEVEL_BUFFER_H_
diff --git a/modules/audio_processing/agc/clipping_predictor_level_buffer_unittest.cc b/modules/audio_processing/agc/clipping_predictor_level_buffer_unittest.cc
new file mode 100644
index 0000000000..7e594a1eca
--- /dev/null
+++ b/modules/audio_processing/agc/clipping_predictor_level_buffer_unittest.cc
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2021 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/agc/clipping_predictor_level_buffer.h"
+
+#include <algorithm>
+
+#include "test/gmock.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace {
+
+using ::testing::Eq;
+using ::testing::Optional;
+
+class ClippingPredictorLevelBufferParametrization
+ : public ::testing::TestWithParam<int> {
+ protected:
+ int capacity() const { return GetParam(); }
+};
+
+TEST_P(ClippingPredictorLevelBufferParametrization, CheckEmptyBufferSize) {
+ ClippingPredictorLevelBuffer buffer(capacity());
+ EXPECT_EQ(buffer.Capacity(), std::max(capacity(), 1));
+ EXPECT_EQ(buffer.Size(), 0);
+}
+
+TEST_P(ClippingPredictorLevelBufferParametrization, CheckHalfEmptyBufferSize) {
+ ClippingPredictorLevelBuffer buffer(capacity());
+ for (int i = 0; i < buffer.Capacity() / 2; ++i) {
+ buffer.Push({2, 4});
+ }
+ EXPECT_EQ(buffer.Capacity(), std::max(capacity(), 1));
+ EXPECT_EQ(buffer.Size(), std::max(capacity(), 1) / 2);
+}
+
+TEST_P(ClippingPredictorLevelBufferParametrization, CheckFullBufferSize) {
+ ClippingPredictorLevelBuffer buffer(capacity());
+ for (int i = 0; i < buffer.Capacity(); ++i) {
+ buffer.Push({2, 4});
+ }
+ EXPECT_EQ(buffer.Capacity(), std::max(capacity(), 1));
+ EXPECT_EQ(buffer.Size(), std::max(capacity(), 1));
+}
+
+TEST_P(ClippingPredictorLevelBufferParametrization, CheckLargeBufferSize) {
+ ClippingPredictorLevelBuffer buffer(capacity());
+ for (int i = 0; i < 2 * buffer.Capacity(); ++i) {
+ buffer.Push({2, 4});
+ }
+ EXPECT_EQ(buffer.Capacity(), std::max(capacity(), 1));
+ EXPECT_EQ(buffer.Size(), std::max(capacity(), 1));
+}
+
+TEST_P(ClippingPredictorLevelBufferParametrization, CheckSizeAfterReset) {
+ ClippingPredictorLevelBuffer buffer(capacity());
+ buffer.Push({1, 1});
+ buffer.Push({1, 1});
+ buffer.Reset();
+ EXPECT_EQ(buffer.Capacity(), std::max(capacity(), 1));
+ EXPECT_EQ(buffer.Size(), 0);
+ buffer.Push({1, 1});
+ EXPECT_EQ(buffer.Capacity(), std::max(capacity(), 1));
+ EXPECT_EQ(buffer.Size(), 1);
+}
+
+INSTANTIATE_TEST_SUITE_P(ClippingPredictorLevelBufferTest,
+ ClippingPredictorLevelBufferParametrization,
+ ::testing::Values(-1, 0, 1, 123));
+
+TEST(ClippingPredictorLevelBufferTest, CheckMetricsAfterFullBuffer) {
+ ClippingPredictorLevelBuffer buffer(/*capacity=*/2);
+ buffer.Push({1, 2});
+ buffer.Push({3, 6});
+ EXPECT_THAT(buffer.ComputePartialMetrics(/*delay=*/0, /*num_items=*/1),
+ Optional(Eq(ClippingPredictorLevelBuffer::Level{3, 6})));
+ EXPECT_THAT(buffer.ComputePartialMetrics(/*delay=*/1, /*num_items=*/1),
+ Optional(Eq(ClippingPredictorLevelBuffer::Level{1, 2})));
+ EXPECT_THAT(buffer.ComputePartialMetrics(/*delay=*/0, /*num_items=*/2),
+ Optional(Eq(ClippingPredictorLevelBuffer::Level{2, 6})));
+}
+
+TEST(ClippingPredictorLevelBufferTest, CheckMetricsAfterPushBeyondCapacity) {
+ ClippingPredictorLevelBuffer buffer(/*capacity=*/2);
+ buffer.Push({1, 1});
+ buffer.Push({3, 6});
+ buffer.Push({5, 10});
+ buffer.Push({7, 14});
+ buffer.Push({6, 12});
+ EXPECT_THAT(buffer.ComputePartialMetrics(/*delay=*/0, /*num_items=*/1),
+ Optional(Eq(ClippingPredictorLevelBuffer::Level{6, 12})));
+ EXPECT_THAT(buffer.ComputePartialMetrics(/*delay=*/1, /*num_items=*/1),
+ Optional(Eq(ClippingPredictorLevelBuffer::Level{7, 14})));
+ EXPECT_THAT(buffer.ComputePartialMetrics(/*delay=*/0, /*num_items=*/2),
+ Optional(Eq(ClippingPredictorLevelBuffer::Level{6.5f, 14})));
+}
+
+TEST(ClippingPredictorLevelBufferTest, CheckMetricsAfterTooFewItems) {
+ ClippingPredictorLevelBuffer buffer(/*capacity=*/4);
+ buffer.Push({1, 2});
+ buffer.Push({3, 6});
+ EXPECT_EQ(buffer.ComputePartialMetrics(/*delay=*/0, /*num_items=*/3),
+ absl::nullopt);
+ EXPECT_EQ(buffer.ComputePartialMetrics(/*delay=*/2, /*num_items=*/1),
+ absl::nullopt);
+}
+
+TEST(ClippingPredictorLevelBufferTest, CheckMetricsAfterReset) {
+ ClippingPredictorLevelBuffer buffer(/*capacity=*/2);
+ buffer.Push({1, 2});
+ buffer.Reset();
+ buffer.Push({5, 10});
+ buffer.Push({7, 14});
+ EXPECT_THAT(buffer.ComputePartialMetrics(/*delay=*/0, /*num_items=*/1),
+ Optional(Eq(ClippingPredictorLevelBuffer::Level{7, 14})));
+ EXPECT_THAT(buffer.ComputePartialMetrics(/*delay=*/0, /*num_items=*/2),
+ Optional(Eq(ClippingPredictorLevelBuffer::Level{6, 14})));
+ EXPECT_THAT(buffer.ComputePartialMetrics(/*delay=*/1, /*num_items=*/1),
+ Optional(Eq(ClippingPredictorLevelBuffer::Level{5, 10})));
+}
+
+} // namespace
+} // namespace webrtc
diff --git a/modules/audio_processing/agc/clipping_predictor_unittest.cc b/modules/audio_processing/agc/clipping_predictor_unittest.cc
new file mode 100644
index 0000000000..e848e1a724
--- /dev/null
+++ b/modules/audio_processing/agc/clipping_predictor_unittest.cc
@@ -0,0 +1,491 @@
+/*
+ * Copyright (c) 2021 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/agc/clipping_predictor.h"
+
+#include <cstdint>
+#include <limits>
+#include <tuple>
+
+#include "rtc_base/checks.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace {
+
+using ::testing::Eq;
+using ::testing::Optional;
+using ClippingPredictorConfig = AudioProcessing::Config::GainController1::
+ AnalogGainController::ClippingPredictor;
+using ClippingPredictorMode = AudioProcessing::Config::GainController1::
+ AnalogGainController::ClippingPredictor::Mode;
+
+constexpr int kSampleRateHz = 32000;
+constexpr int kNumChannels = 1;
+constexpr int kSamplesPerChannel = kSampleRateHz / 100;
+constexpr int kMaxMicLevel = 255;
+constexpr int kMinMicLevel = 12;
+constexpr int kDefaultClippedLevelStep = 15;
+constexpr float kMaxSampleS16 =
+ static_cast<float>(std::numeric_limits<int16_t>::max());
+
+// Threshold in dB corresponding to a signal with an amplitude equal to 99% of
+// the dynamic range - i.e., computed as `20*log10(0.99)`.
+constexpr float kClippingThresholdDb = -0.08729610804900176f;
+
+void CallAnalyze(int num_calls,
+ const AudioFrameView<const float>& frame,
+ ClippingPredictor& predictor) {
+ for (int i = 0; i < num_calls; ++i) {
+ predictor.Analyze(frame);
+ }
+}
+
+// Creates and analyzes an audio frame with a non-zero (approx. 4.15dB) crest
+// factor.
+void AnalyzeNonZeroCrestFactorAudio(int num_calls,
+ int num_channels,
+ float peak_ratio,
+ ClippingPredictor& predictor) {
+ RTC_DCHECK_GT(num_calls, 0);
+ RTC_DCHECK_GT(num_channels, 0);
+ RTC_DCHECK_LE(peak_ratio, 1.0f);
+ std::vector<float*> audio(num_channels);
+ std::vector<float> audio_data(num_channels * kSamplesPerChannel, 0.0f);
+ for (int channel = 0; channel < num_channels; ++channel) {
+ audio[channel] = &audio_data[channel * kSamplesPerChannel];
+ for (int sample = 0; sample < kSamplesPerChannel; sample += 10) {
+ audio[channel][sample] = 0.1f * peak_ratio * kMaxSampleS16;
+ audio[channel][sample + 1] = 0.2f * peak_ratio * kMaxSampleS16;
+ audio[channel][sample + 2] = 0.3f * peak_ratio * kMaxSampleS16;
+ audio[channel][sample + 3] = 0.4f * peak_ratio * kMaxSampleS16;
+ audio[channel][sample + 4] = 0.5f * peak_ratio * kMaxSampleS16;
+ audio[channel][sample + 5] = 0.6f * peak_ratio * kMaxSampleS16;
+ audio[channel][sample + 6] = 0.7f * peak_ratio * kMaxSampleS16;
+ audio[channel][sample + 7] = 0.8f * peak_ratio * kMaxSampleS16;
+ audio[channel][sample + 8] = 0.9f * peak_ratio * kMaxSampleS16;
+ audio[channel][sample + 9] = 1.0f * peak_ratio * kMaxSampleS16;
+ }
+ }
+ AudioFrameView<const float> frame(audio.data(), num_channels,
+ kSamplesPerChannel);
+ CallAnalyze(num_calls, frame, predictor);
+}
+
+void CheckChannelEstimatesWithValue(int num_channels,
+ int level,
+ int default_step,
+ int min_mic_level,
+ int max_mic_level,
+ const ClippingPredictor& predictor,
+ int expected) {
+ for (int i = 0; i < num_channels; ++i) {
+ SCOPED_TRACE(i);
+ EXPECT_THAT(predictor.EstimateClippedLevelStep(
+ i, level, default_step, min_mic_level, max_mic_level),
+ Optional(Eq(expected)));
+ }
+}
+
+void CheckChannelEstimatesWithoutValue(int num_channels,
+ int level,
+ int default_step,
+ int min_mic_level,
+ int max_mic_level,
+ const ClippingPredictor& predictor) {
+ for (int i = 0; i < num_channels; ++i) {
+ SCOPED_TRACE(i);
+ EXPECT_EQ(predictor.EstimateClippedLevelStep(i, level, default_step,
+ min_mic_level, max_mic_level),
+ absl::nullopt);
+ }
+}
+
+// Creates and analyzes an audio frame with a zero crest factor.
+void AnalyzeZeroCrestFactorAudio(int num_calls,
+ int num_channels,
+ float peak_ratio,
+ ClippingPredictor& predictor) {
+ RTC_DCHECK_GT(num_calls, 0);
+ RTC_DCHECK_GT(num_channels, 0);
+ RTC_DCHECK_LE(peak_ratio, 1.f);
+ std::vector<float*> audio(num_channels);
+ std::vector<float> audio_data(num_channels * kSamplesPerChannel, 0.f);
+ for (int channel = 0; channel < num_channels; ++channel) {
+ audio[channel] = &audio_data[channel * kSamplesPerChannel];
+ for (int sample = 0; sample < kSamplesPerChannel; ++sample) {
+ audio[channel][sample] = peak_ratio * kMaxSampleS16;
+ }
+ }
+ auto frame = AudioFrameView<const float>(audio.data(), num_channels,
+ kSamplesPerChannel);
+ CallAnalyze(num_calls, frame, predictor);
+}
+
+TEST(ClippingPeakPredictorTest, NoPredictorCreated) {
+ auto predictor =
+ CreateClippingPredictor(kNumChannels, /*config=*/{/*enabled=*/false});
+ EXPECT_FALSE(predictor);
+}
+
+TEST(ClippingPeakPredictorTest, ClippingEventPredictionCreated) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ auto predictor = CreateClippingPredictor(
+ kNumChannels,
+ /*config=*/{/*enabled=*/true,
+ /*mode=*/ClippingPredictorMode::kClippingEventPrediction});
+ EXPECT_TRUE(predictor);
+}
+
+TEST(ClippingPeakPredictorTest, AdaptiveStepClippingPeakPredictionCreated) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ auto predictor = CreateClippingPredictor(
+ kNumChannels, /*config=*/{
+ /*enabled=*/true,
+ /*mode=*/ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction});
+ EXPECT_TRUE(predictor);
+}
+
+TEST(ClippingPeakPredictorTest, FixedStepClippingPeakPredictionCreated) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ auto predictor = CreateClippingPredictor(
+ kNumChannels, /*config=*/{
+ /*enabled=*/true,
+ /*mode=*/ClippingPredictorMode::kFixedStepClippingPeakPrediction});
+ EXPECT_TRUE(predictor);
+}
+
+class ClippingPredictorParameterization
+ : public ::testing::TestWithParam<std::tuple<int, int, int, int>> {
+ protected:
+ int num_channels() const { return std::get<0>(GetParam()); }
+ ClippingPredictorConfig GetConfig(ClippingPredictorMode mode) const {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ return {/*enabled=*/true,
+ /*mode=*/mode,
+ /*window_length=*/std::get<1>(GetParam()),
+ /*reference_window_length=*/std::get<2>(GetParam()),
+ /*reference_window_delay=*/std::get<3>(GetParam()),
+ /*clipping_threshold=*/-1.0f,
+ /*crest_factor_margin=*/0.5f};
+ }
+};
+
+TEST_P(ClippingPredictorParameterization,
+ CheckClippingEventPredictorEstimateAfterCrestFactorDrop) {
+ const ClippingPredictorConfig config =
+ GetConfig(ClippingPredictorMode::kClippingEventPrediction);
+ if (config.reference_window_length + config.reference_window_delay <=
+ config.window_length) {
+ return;
+ }
+ auto predictor = CreateClippingPredictor(num_channels(), config);
+ AnalyzeNonZeroCrestFactorAudio(
+ /*num_calls=*/config.reference_window_length +
+ config.reference_window_delay - config.window_length,
+ num_channels(), /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ AnalyzeZeroCrestFactorAudio(config.window_length, num_channels(),
+ /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithValue(
+ num_channels(), /*level=*/255, kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor, kDefaultClippedLevelStep);
+}
+
+TEST_P(ClippingPredictorParameterization,
+ CheckClippingEventPredictorNoEstimateAfterConstantCrestFactor) {
+ const ClippingPredictorConfig config =
+ GetConfig(ClippingPredictorMode::kClippingEventPrediction);
+ if (config.reference_window_length + config.reference_window_delay <=
+ config.window_length) {
+ return;
+ }
+ auto predictor = CreateClippingPredictor(num_channels(), config);
+ AnalyzeNonZeroCrestFactorAudio(
+ /*num_calls=*/config.reference_window_length +
+ config.reference_window_delay - config.window_length,
+ num_channels(), /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ AnalyzeNonZeroCrestFactorAudio(/*num_calls=*/config.window_length,
+ num_channels(),
+ /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+}
+
+TEST_P(ClippingPredictorParameterization,
+ CheckClippingPeakPredictorEstimateAfterHighCrestFactor) {
+ const ClippingPredictorConfig config =
+ GetConfig(ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction);
+ if (config.reference_window_length + config.reference_window_delay <=
+ config.window_length) {
+ return;
+ }
+ auto predictor = CreateClippingPredictor(num_channels(), config);
+ AnalyzeNonZeroCrestFactorAudio(
+ /*num_calls=*/config.reference_window_length +
+ config.reference_window_delay - config.window_length,
+ num_channels(), /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ AnalyzeNonZeroCrestFactorAudio(/*num_calls=*/config.window_length,
+ num_channels(),
+ /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithValue(
+ num_channels(), /*level=*/255, kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor, kDefaultClippedLevelStep);
+}
+
+TEST_P(ClippingPredictorParameterization,
+ CheckClippingPeakPredictorNoEstimateAfterLowCrestFactor) {
+ const ClippingPredictorConfig config =
+ GetConfig(ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction);
+ if (config.reference_window_length + config.reference_window_delay <=
+ config.window_length) {
+ return;
+ }
+ auto predictor = CreateClippingPredictor(num_channels(), config);
+ AnalyzeZeroCrestFactorAudio(
+ /*num_calls=*/config.reference_window_length +
+ config.reference_window_delay - config.window_length,
+ num_channels(), /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ AnalyzeNonZeroCrestFactorAudio(/*num_calls=*/config.window_length,
+ num_channels(),
+ /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+}
+
+INSTANTIATE_TEST_SUITE_P(GainController1ClippingPredictor,
+ ClippingPredictorParameterization,
+ ::testing::Combine(::testing::Values(1, 5),
+ ::testing::Values(1, 5, 10),
+ ::testing::Values(1, 5),
+ ::testing::Values(0, 1, 5)));
+
+class ClippingEventPredictorParameterization
+ : public ::testing::TestWithParam<std::tuple<float, float>> {
+ protected:
+ ClippingPredictorConfig GetConfig() const {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ return {/*enabled=*/true,
+ /*mode=*/ClippingPredictorMode::kClippingEventPrediction,
+ /*window_length=*/5,
+ /*reference_window_length=*/5,
+ /*reference_window_delay=*/5,
+ /*clipping_threshold=*/std::get<0>(GetParam()),
+ /*crest_factor_margin=*/std::get<1>(GetParam())};
+ }
+};
+
+TEST_P(ClippingEventPredictorParameterization,
+ CheckEstimateAfterCrestFactorDrop) {
+ const ClippingPredictorConfig config = GetConfig();
+ auto predictor = CreateClippingPredictor(kNumChannels, config);
+ AnalyzeNonZeroCrestFactorAudio(/*num_calls=*/config.reference_window_length,
+ kNumChannels, /*peak_ratio=*/0.99f,
+ *predictor);
+ CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ AnalyzeZeroCrestFactorAudio(config.window_length, kNumChannels,
+ /*peak_ratio=*/0.99f, *predictor);
+ // TODO(bugs.webrtc.org/12774): Add clarifying comment.
+ // TODO(bugs.webrtc.org/12774): Remove 4.15f threshold and split tests.
+ if (config.clipping_threshold < kClippingThresholdDb &&
+ config.crest_factor_margin < 4.15f) {
+ CheckChannelEstimatesWithValue(
+ kNumChannels, /*level=*/255, kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor, kDefaultClippedLevelStep);
+ } else {
+ CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(GainController1ClippingPredictor,
+ ClippingEventPredictorParameterization,
+ ::testing::Combine(::testing::Values(-1.0f, 0.0f),
+ ::testing::Values(3.0f, 4.16f)));
+
+class ClippingPredictorModeParameterization
+ : public ::testing::TestWithParam<ClippingPredictorMode> {
+ protected:
+ ClippingPredictorConfig GetConfig(float clipping_threshold_dbfs) const {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ return {/*enabled=*/true,
+ /*mode=*/GetParam(),
+ /*window_length=*/5,
+ /*reference_window_length=*/5,
+ /*reference_window_delay=*/5,
+ /*clipping_threshold=*/clipping_threshold_dbfs,
+ /*crest_factor_margin=*/3.0f};
+ }
+};
+
+TEST_P(ClippingPredictorModeParameterization,
+ CheckEstimateAfterHighCrestFactorWithNoClippingMargin) {
+ const ClippingPredictorConfig config = GetConfig(
+ /*clipping_threshold_dbfs=*/0.0f);
+ auto predictor = CreateClippingPredictor(kNumChannels, config);
+ AnalyzeNonZeroCrestFactorAudio(/*num_calls=*/config.reference_window_length,
+ kNumChannels, /*peak_ratio=*/0.99f,
+ *predictor);
+ CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ AnalyzeZeroCrestFactorAudio(config.window_length, kNumChannels,
+ /*peak_ratio=*/0.99f, *predictor);
+ // Since the clipping threshold is set to 0 dBFS, `EstimateClippedLevelStep()`
+ // is expected to return an unavailable value.
+ CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+}
+
+TEST_P(ClippingPredictorModeParameterization,
+ CheckEstimateAfterHighCrestFactorWithClippingMargin) {
+ const ClippingPredictorConfig config =
+ GetConfig(/*clipping_threshold_dbfs=*/-1.0f);
+ auto predictor = CreateClippingPredictor(kNumChannels, config);
+ AnalyzeNonZeroCrestFactorAudio(/*num_calls=*/config.reference_window_length,
+ kNumChannels,
+ /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ AnalyzeZeroCrestFactorAudio(config.window_length, kNumChannels,
+ /*peak_ratio=*/0.99f, *predictor);
+ // TODO(bugs.webrtc.org/12774): Add clarifying comment.
+ const float expected_step =
+ config.mode == ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction
+ ? 17
+ : kDefaultClippedLevelStep;
+ CheckChannelEstimatesWithValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor, expected_step);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ GainController1ClippingPredictor,
+ ClippingPredictorModeParameterization,
+ ::testing::Values(
+ ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction,
+ ClippingPredictorMode::kFixedStepClippingPeakPrediction));
+
+TEST(ClippingEventPredictorTest, CheckEstimateAfterReset) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ constexpr ClippingPredictorConfig kConfig{
+ /*enabled=*/true,
+ /*mode=*/ClippingPredictorMode::kClippingEventPrediction,
+ /*window_length=*/5,
+ /*reference_window_length=*/5,
+ /*reference_window_delay=*/5,
+ /*clipping_threshold=*/-1.0f,
+ /*crest_factor_margin=*/3.0f};
+ auto predictor = CreateClippingPredictor(kNumChannels, kConfig);
+ AnalyzeNonZeroCrestFactorAudio(/*num_calls=*/kConfig.reference_window_length,
+ kNumChannels,
+ /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ predictor->Reset();
+ AnalyzeZeroCrestFactorAudio(kConfig.window_length, kNumChannels,
+ /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+}
+
+TEST(ClippingPeakPredictorTest, CheckNoEstimateAfterReset) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ constexpr ClippingPredictorConfig kConfig{
+ /*enabled=*/true,
+ /*mode=*/ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction,
+ /*window_length=*/5,
+ /*reference_window_length=*/5,
+ /*reference_window_delay=*/5,
+ /*clipping_threshold=*/-1.0f};
+ auto predictor = CreateClippingPredictor(kNumChannels, kConfig);
+ AnalyzeNonZeroCrestFactorAudio(/*num_calls=*/kConfig.reference_window_length,
+ kNumChannels,
+ /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ predictor->Reset();
+ AnalyzeZeroCrestFactorAudio(kConfig.window_length, kNumChannels,
+ /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+}
+
+TEST(ClippingPeakPredictorTest, CheckAdaptiveStepEstimate) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ constexpr ClippingPredictorConfig kConfig{
+ /*enabled=*/true,
+ /*mode=*/ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction,
+ /*window_length=*/5,
+ /*reference_window_length=*/5,
+ /*reference_window_delay=*/5,
+ /*clipping_threshold=*/-1.0f};
+ auto predictor = CreateClippingPredictor(kNumChannels, kConfig);
+ AnalyzeNonZeroCrestFactorAudio(/*num_calls=*/kConfig.reference_window_length,
+ kNumChannels, /*peak_ratio=*/0.99f,
+ *predictor);
+ CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ AnalyzeZeroCrestFactorAudio(kConfig.window_length, kNumChannels,
+ /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor, /*expected=*/17);
+}
+
+TEST(ClippingPeakPredictorTest, CheckFixedStepEstimate) {
+ // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed.
+ constexpr ClippingPredictorConfig kConfig{
+ /*enabled=*/true,
+ /*mode=*/ClippingPredictorMode::kFixedStepClippingPeakPrediction,
+ /*window_length=*/5,
+ /*reference_window_length=*/5,
+ /*reference_window_delay=*/5,
+ /*clipping_threshold=*/-1.0f};
+ auto predictor = CreateClippingPredictor(kNumChannels, kConfig);
+ AnalyzeNonZeroCrestFactorAudio(/*num_calls=*/kConfig.reference_window_length,
+ kNumChannels, /*peak_ratio=*/0.99f,
+ *predictor);
+ CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255,
+ kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor);
+ AnalyzeZeroCrestFactorAudio(kConfig.window_length, kNumChannels,
+ /*peak_ratio=*/0.99f, *predictor);
+ CheckChannelEstimatesWithValue(
+ kNumChannels, /*level=*/255, kDefaultClippedLevelStep, kMinMicLevel,
+ kMaxMicLevel, *predictor, kDefaultClippedLevelStep);
+}
+
+} // namespace
+} // namespace webrtc