summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/modules/audio_processing/agc2/input_volume_controller_unittest.cc
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/libwebrtc/modules/audio_processing/agc2/input_volume_controller_unittest.cc')
-rw-r--r--third_party/libwebrtc/modules/audio_processing/agc2/input_volume_controller_unittest.cc1857
1 files changed, 1857 insertions, 0 deletions
diff --git a/third_party/libwebrtc/modules/audio_processing/agc2/input_volume_controller_unittest.cc b/third_party/libwebrtc/modules/audio_processing/agc2/input_volume_controller_unittest.cc
new file mode 100644
index 0000000000..d1bdcf25a5
--- /dev/null
+++ b/third_party/libwebrtc/modules/audio_processing/agc2/input_volume_controller_unittest.cc
@@ -0,0 +1,1857 @@
+/*
+ * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "modules/audio_processing/agc2/input_volume_controller.h"
+
+#include <algorithm>
+#include <fstream>
+#include <limits>
+#include <string>
+#include <vector>
+
+#include "rtc_base/numerics/safe_minmax.h"
+#include "rtc_base/strings/string_builder.h"
+#include "system_wrappers/include/metrics.h"
+#include "test/field_trial.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+#include "test/testsupport/file_utils.h"
+
+using ::testing::_;
+using ::testing::AtLeast;
+using ::testing::DoAll;
+using ::testing::Return;
+using ::testing::SetArgPointee;
+
+namespace webrtc {
+namespace {
+
+constexpr int kSampleRateHz = 32000;
+constexpr int kNumChannels = 1;
+constexpr int kInitialInputVolume = 128;
+constexpr int kClippedMin = 165; // Arbitrary, but different from the default.
+constexpr float kAboveClippedThreshold = 0.2f;
+constexpr int kMinMicLevel = 20;
+constexpr int kClippedLevelStep = 15;
+constexpr float kClippedRatioThreshold = 0.1f;
+constexpr int kClippedWaitFrames = 300;
+constexpr float kHighSpeechProbability = 0.7f;
+constexpr float kLowSpeechProbability = 0.1f;
+constexpr float kSpeechLevel = -25.0f;
+constexpr float kSpeechProbabilityThreshold = 0.5f;
+constexpr float kSpeechRatioThreshold = 0.8f;
+
+constexpr float kMinSample = std::numeric_limits<int16_t>::min();
+constexpr float kMaxSample = std::numeric_limits<int16_t>::max();
+
+using ClippingPredictorConfig = AudioProcessing::Config::GainController1::
+ AnalogGainController::ClippingPredictor;
+
+using InputVolumeControllerConfig = InputVolumeController::Config;
+
+constexpr ClippingPredictorConfig kDefaultClippingPredictorConfig{};
+
+std::unique_ptr<InputVolumeController> CreateInputVolumeController(
+ int clipped_level_step = kClippedLevelStep,
+ float clipped_ratio_threshold = kClippedRatioThreshold,
+ int clipped_wait_frames = kClippedWaitFrames,
+ bool enable_clipping_predictor = false,
+ int update_input_volume_wait_frames = 0) {
+ InputVolumeControllerConfig config{
+ .min_input_volume = kMinMicLevel,
+ .clipped_level_min = kClippedMin,
+ .clipped_level_step = clipped_level_step,
+ .clipped_ratio_threshold = clipped_ratio_threshold,
+ .clipped_wait_frames = clipped_wait_frames,
+ .enable_clipping_predictor = enable_clipping_predictor,
+ .target_range_max_dbfs = -18,
+ .target_range_min_dbfs = -30,
+ .update_input_volume_wait_frames = update_input_volume_wait_frames,
+ .speech_probability_threshold = kSpeechProbabilityThreshold,
+ .speech_ratio_threshold = kSpeechRatioThreshold,
+ };
+
+ return std::make_unique<InputVolumeController>(/*num_capture_channels=*/1,
+ config);
+}
+
+// (Over)writes `samples_value` for the samples in `audio_buffer`.
+// When `clipped_ratio`, a value in [0, 1], is greater than 0, the corresponding
+// fraction of the frame is set to a full scale value to simulate clipping.
+void WriteAudioBufferSamples(float samples_value,
+ float clipped_ratio,
+ AudioBuffer& audio_buffer) {
+ RTC_DCHECK_GE(samples_value, kMinSample);
+ RTC_DCHECK_LE(samples_value, kMaxSample);
+ RTC_DCHECK_GE(clipped_ratio, 0.0f);
+ RTC_DCHECK_LE(clipped_ratio, 1.0f);
+ int num_channels = audio_buffer.num_channels();
+ int num_samples = audio_buffer.num_frames();
+ int num_clipping_samples = clipped_ratio * num_samples;
+ for (int ch = 0; ch < num_channels; ++ch) {
+ int i = 0;
+ for (; i < num_clipping_samples; ++i) {
+ audio_buffer.channels()[ch][i] = 32767.0f;
+ }
+ for (; i < num_samples; ++i) {
+ audio_buffer.channels()[ch][i] = samples_value;
+ }
+ }
+}
+
+// (Over)writes samples in `audio_buffer`. Alternates samples `samples_value`
+// and zero.
+void WriteAlternatingAudioBufferSamples(float samples_value,
+ AudioBuffer& audio_buffer) {
+ RTC_DCHECK_GE(samples_value, kMinSample);
+ RTC_DCHECK_LE(samples_value, kMaxSample);
+ 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] = samples_value;
+ audio_buffer.channels()[ch][i + 1] = 0.0f;
+ }
+ }
+}
+
+// Reads a given number of 10 ms chunks from a PCM file and feeds them to
+// `InputVolumeController`.
+class SpeechSamplesReader {
+ private:
+ // Recording properties.
+ static constexpr int kPcmSampleRateHz = 16000;
+ static constexpr int kPcmNumChannels = 1;
+ static constexpr int kPcmBytesPerSamples = sizeof(int16_t);
+
+ public:
+ SpeechSamplesReader()
+ : is_(test::ResourcePath("audio_processing/agc/agc_audio", "pcm"),
+ std::ios::binary | std::ios::ate),
+ audio_buffer_(kPcmSampleRateHz,
+ kPcmNumChannels,
+ kPcmSampleRateHz,
+ kPcmNumChannels,
+ kPcmSampleRateHz,
+ kPcmNumChannels),
+ buffer_(audio_buffer_.num_frames()),
+ buffer_num_bytes_(buffer_.size() * kPcmBytesPerSamples) {
+ RTC_CHECK(is_);
+ }
+
+ // Reads `num_frames` 10 ms frames from the beginning of the PCM file, applies
+ // `gain_db` and feeds the frames into `controller` by calling
+ // `AnalyzeInputAudio()` and `RecommendInputVolume()` for each frame. Reads
+ // the number of 10 ms frames available in the PCM file if `num_frames` is too
+ // large - i.e., does not loop. `speech_probability` and `speech_level_dbfs`
+ // are passed to `RecommendInputVolume()`.
+ int Feed(int num_frames,
+ int applied_input_volume,
+ int gain_db,
+ float speech_probability,
+ absl::optional<float> speech_level_dbfs,
+ InputVolumeController& controller) {
+ RTC_DCHECK(controller.capture_output_used());
+
+ float gain = std::pow(10.0f, gain_db / 20.0f); // From dB to linear gain.
+ is_.seekg(0, is_.beg); // Start from the beginning of the PCM file.
+
+ // Read and feed frames.
+ for (int i = 0; i < num_frames; ++i) {
+ is_.read(reinterpret_cast<char*>(buffer_.data()), buffer_num_bytes_);
+ if (is_.gcount() < buffer_num_bytes_) {
+ // EOF reached. Stop.
+ break;
+ }
+ // Apply gain and copy samples into `audio_buffer_`.
+ std::transform(buffer_.begin(), buffer_.end(),
+ audio_buffer_.channels()[0], [gain](int16_t v) -> float {
+ return rtc::SafeClamp(static_cast<float>(v) * gain,
+ kMinSample, kMaxSample);
+ });
+ controller.AnalyzeInputAudio(applied_input_volume, audio_buffer_);
+ const auto recommended_input_volume = controller.RecommendInputVolume(
+ speech_probability, speech_level_dbfs);
+
+ // Expect no errors: Applied volume set for every frame;
+ // `RecommendInputVolume()` returns a non-empty value.
+ EXPECT_TRUE(recommended_input_volume.has_value());
+
+ applied_input_volume = *recommended_input_volume;
+ }
+ return applied_input_volume;
+ }
+
+ private:
+ std::ifstream is_;
+ AudioBuffer audio_buffer_;
+ std::vector<int16_t> buffer_;
+ const std::streamsize buffer_num_bytes_;
+};
+
+// Runs the MonoInputVolumeControl processing sequence following the API
+// contract. Returns the updated recommended input volume.
+float UpdateRecommendedInputVolume(MonoInputVolumeController& mono_controller,
+ int applied_input_volume,
+ float speech_probability,
+ absl::optional<float> rms_error_dbfs) {
+ mono_controller.set_stream_analog_level(applied_input_volume);
+ EXPECT_EQ(mono_controller.recommended_analog_level(), applied_input_volume);
+ mono_controller.Process(rms_error_dbfs, speech_probability);
+ return mono_controller.recommended_analog_level();
+}
+
+} // namespace
+
+// TODO(bugs.webrtc.org/12874): Use constexpr struct with designated
+// initializers once fixed.
+constexpr InputVolumeControllerConfig GetInputVolumeControllerTestConfig() {
+ InputVolumeControllerConfig config{
+ .clipped_level_min = kClippedMin,
+ .clipped_level_step = kClippedLevelStep,
+ .clipped_ratio_threshold = kClippedRatioThreshold,
+ .clipped_wait_frames = kClippedWaitFrames,
+ .enable_clipping_predictor = kDefaultClippingPredictorConfig.enabled,
+ .target_range_max_dbfs = -18,
+ .target_range_min_dbfs = -30,
+ .update_input_volume_wait_frames = 0,
+ .speech_probability_threshold = 0.5f,
+ .speech_ratio_threshold = 1.0f,
+ };
+ return config;
+}
+
+// Helper class that provides an `InputVolumeController` instance with an
+// `AudioBuffer` instance and `CallAgcSequence()`, a helper method that runs the
+// `InputVolumeController` instance on the `AudioBuffer` one by sticking to the
+// API contract.
+class InputVolumeControllerTestHelper {
+ public:
+ // Ctor. Initializes `audio_buffer` with zeros.
+ // TODO(bugs.webrtc.org/7494): Remove the default argument.
+ InputVolumeControllerTestHelper(const InputVolumeController::Config& config =
+ GetInputVolumeControllerTestConfig())
+ : audio_buffer(kSampleRateHz,
+ kNumChannels,
+ kSampleRateHz,
+ kNumChannels,
+ kSampleRateHz,
+ kNumChannels),
+ controller(/*num_capture_channels=*/1, config) {
+ controller.Initialize();
+ WriteAudioBufferSamples(/*samples_value=*/0.0f, /*clipped_ratio=*/0.0f,
+ audio_buffer);
+ }
+
+ // Calls the sequence of `InputVolumeController` methods according to the API
+ // contract, namely:
+ // - Sets the applied input volume;
+ // - Uses `audio_buffer` to call `AnalyzeInputAudio()` and
+ // `RecommendInputVolume()`;
+ // Returns the recommended input volume.
+ absl::optional<int> CallAgcSequence(int applied_input_volume,
+ float speech_probability,
+ absl::optional<float> speech_level_dbfs,
+ int num_calls = 1) {
+ RTC_DCHECK_GE(num_calls, 1);
+ absl::optional<int> volume = applied_input_volume;
+ for (int i = 0; i < num_calls; ++i) {
+ // Repeat the initial volume if `RecommendInputVolume()` doesn't return a
+ // value.
+ controller.AnalyzeInputAudio(volume.value_or(applied_input_volume),
+ audio_buffer);
+ volume = controller.RecommendInputVolume(speech_probability,
+ speech_level_dbfs);
+
+ // Allow deviation from the API contract: `RecommendInputVolume()` doesn't
+ // return a recommended input volume.
+ if (volume.has_value()) {
+ EXPECT_EQ(*volume, controller.recommended_input_volume());
+ }
+ }
+ return volume;
+ }
+
+ // Deprecated.
+ // TODO(bugs.webrtc.org/7494): Let the caller write `audio_buffer` and use
+ // `CallAgcSequence()`.
+ int CallRecommendInputVolume(int num_calls,
+ int initial_volume,
+ float speech_probability,
+ absl::optional<float> speech_level_dbfs) {
+ RTC_DCHECK(controller.capture_output_used());
+
+ // Create non-clipping audio for `AnalyzeInputAudio()`.
+ WriteAlternatingAudioBufferSamples(0.1f * kMaxSample, audio_buffer);
+ int volume = initial_volume;
+ for (int i = 0; i < num_calls; ++i) {
+ controller.AnalyzeInputAudio(volume, audio_buffer);
+ const auto recommended_input_volume = controller.RecommendInputVolume(
+ speech_probability, speech_level_dbfs);
+
+ // Expect no errors: Applied volume set for every frame;
+ // `RecommendInputVolume()` returns a non-empty value.
+ EXPECT_TRUE(recommended_input_volume.has_value());
+
+ volume = *recommended_input_volume;
+ }
+ return volume;
+ }
+
+ // Deprecated.
+ // TODO(bugs.webrtc.org/7494): Let the caller write `audio_buffer` and use
+ // `CallAgcSequence()`.
+ void CallAnalyzeInputAudio(int num_calls, float clipped_ratio) {
+ RTC_DCHECK(controller.capture_output_used());
+
+ RTC_DCHECK_GE(clipped_ratio, 0.0f);
+ RTC_DCHECK_LE(clipped_ratio, 1.0f);
+ WriteAudioBufferSamples(/*samples_value=*/0.0f, clipped_ratio,
+ audio_buffer);
+ for (int i = 0; i < num_calls; ++i) {
+ controller.AnalyzeInputAudio(controller.recommended_input_volume(),
+ audio_buffer);
+ }
+ }
+
+ AudioBuffer audio_buffer;
+ InputVolumeController controller;
+};
+
+class InputVolumeControllerChannelSampleRateTest
+ : public ::testing::TestWithParam<std::tuple<int, int>> {
+ protected:
+ int GetNumChannels() const { return std::get<0>(GetParam()); }
+ int GetSampleRateHz() const { return std::get<1>(GetParam()); }
+};
+
+TEST_P(InputVolumeControllerChannelSampleRateTest, CheckIsAlive) {
+ const int num_channels = GetNumChannels();
+ const int sample_rate_hz = GetSampleRateHz();
+
+ constexpr InputVolumeController::Config kConfig{.enable_clipping_predictor =
+ true};
+ InputVolumeController controller(num_channels, kConfig);
+ controller.Initialize();
+ AudioBuffer buffer(sample_rate_hz, num_channels, sample_rate_hz, num_channels,
+ sample_rate_hz, num_channels);
+
+ constexpr int kStartupVolume = 100;
+ int applied_initial_volume = kStartupVolume;
+
+ // Trigger a downward adaptation with clipping.
+ constexpr int kLevelWithinTargetDbfs =
+ (kConfig.target_range_min_dbfs + kConfig.target_range_max_dbfs) / 2;
+ WriteAlternatingAudioBufferSamples(/*samples_value=*/kMaxSample, buffer);
+ const int initial_volume1 = applied_initial_volume;
+ for (int i = 0; i < 400; ++i) {
+ controller.AnalyzeInputAudio(applied_initial_volume, buffer);
+ auto recommended_input_volume = controller.RecommendInputVolume(
+ kLowSpeechProbability,
+ /*speech_level_dbfs=*/kLevelWithinTargetDbfs);
+ ASSERT_TRUE(recommended_input_volume.has_value());
+ applied_initial_volume = *recommended_input_volume;
+ }
+ ASSERT_LT(controller.recommended_input_volume(), initial_volume1);
+
+ // Fill in audio that does not clip.
+ WriteAlternatingAudioBufferSamples(/*samples_value=*/1234.5f, buffer);
+
+ // Trigger an upward adaptation.
+ const int initial_volume2 = controller.recommended_input_volume();
+ for (int i = 0; i < kConfig.clipped_wait_frames; ++i) {
+ controller.AnalyzeInputAudio(applied_initial_volume, buffer);
+ auto recommended_input_volume = controller.RecommendInputVolume(
+ kHighSpeechProbability,
+ /*speech_level_dbfs=*/kConfig.target_range_min_dbfs - 5);
+ ASSERT_TRUE(recommended_input_volume.has_value());
+ applied_initial_volume = *recommended_input_volume;
+ }
+ EXPECT_GT(controller.recommended_input_volume(), initial_volume2);
+
+ // Trigger a downward adaptation.
+ const int initial_volume = controller.recommended_input_volume();
+ for (int i = 0; i < kConfig.update_input_volume_wait_frames; ++i) {
+ controller.AnalyzeInputAudio(applied_initial_volume, buffer);
+ auto recommended_input_volume = controller.RecommendInputVolume(
+ kHighSpeechProbability,
+ /*speech_level_dbfs=*/kConfig.target_range_max_dbfs + 5);
+ ASSERT_TRUE(recommended_input_volume.has_value());
+ applied_initial_volume = *recommended_input_volume;
+ }
+ EXPECT_LT(controller.recommended_input_volume(), initial_volume);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ ,
+ InputVolumeControllerChannelSampleRateTest,
+ ::testing::Combine(::testing::Values(1, 2, 3, 6),
+ ::testing::Values(8000, 16000, 32000, 48000)));
+
+class InputVolumeControllerParametrizedTest
+ : public ::testing::TestWithParam<int> {};
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ StartupMinVolumeConfigurationRespectedWhenAppliedInputVolumeAboveMin) {
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = GetParam()});
+
+ EXPECT_EQ(*helper.CallAgcSequence(/*applied_input_volume=*/128,
+ /*speech_probability=*/0.9f,
+ /*speech_level_dbfs=*/-80),
+ 128);
+}
+
+TEST_P(
+ InputVolumeControllerParametrizedTest,
+ StartupMinVolumeConfigurationRespectedWhenAppliedInputVolumeMaybeBelowMin) {
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = GetParam()});
+
+ EXPECT_GE(*helper.CallAgcSequence(/*applied_input_volume=*/10,
+ /*speech_probability=*/0.9f,
+ /*speech_level_dbfs=*/-80),
+ 10);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ StartupMinVolumeRespectedWhenAppliedVolumeNonZero) {
+ const int kMinInputVolume = GetParam();
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = kMinInputVolume,
+ .target_range_min_dbfs = -30,
+ .update_input_volume_wait_frames = 1,
+ .speech_probability_threshold = 0.5f,
+ .speech_ratio_threshold = 0.5f});
+
+ // Volume change possible; speech level below the digital gain window.
+ int volume = *helper.CallAgcSequence(/*applied_input_volume=*/1,
+ /*speech_probability=*/0.9f,
+ /*speech_level_dbfs=*/-80);
+
+ EXPECT_EQ(volume, kMinInputVolume);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ MinVolumeRepeatedlyRespectedWhenAppliedVolumeNonZero) {
+ const int kMinInputVolume = GetParam();
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = kMinInputVolume,
+ .target_range_min_dbfs = -30,
+ .update_input_volume_wait_frames = 1,
+ .speech_probability_threshold = 0.5f,
+ .speech_ratio_threshold = 0.5f});
+
+ // Volume change possible; speech level below the digital gain window.
+ for (int i = 0; i < 100; ++i) {
+ const int volume = *helper.CallAgcSequence(/*applied_input_volume=*/1,
+ /*speech_probability=*/0.9f,
+ /*speech_level_dbfs=*/-80);
+ EXPECT_GE(volume, kMinInputVolume);
+ }
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ StartupMinVolumeRespectedOnceWhenAppliedVolumeZero) {
+ const int kMinInputVolume = GetParam();
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = kMinInputVolume,
+ .target_range_min_dbfs = -30,
+ .update_input_volume_wait_frames = 1,
+ .speech_probability_threshold = 0.5f,
+ .speech_ratio_threshold = 0.5f});
+
+ int volume = *helper.CallAgcSequence(/*applied_input_volume=*/0,
+ /*speech_probability=*/0.9f,
+ /*speech_level_dbfs=*/-80);
+
+ EXPECT_EQ(volume, kMinInputVolume);
+
+ // No change of volume regardless of a speech level below the digital gain
+ // window; applied volume is zero.
+ volume = *helper.CallAgcSequence(/*applied_input_volume=*/0,
+ /*speech_probability=*/0.9f,
+ /*speech_level_dbfs=*/-80);
+
+ EXPECT_EQ(volume, 0);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest, MicVolumeResponseToRmsError) {
+ InputVolumeControllerConfig config = GetInputVolumeControllerTestConfig();
+ config.min_input_volume = GetParam();
+ InputVolumeControllerTestHelper helper(config);
+ int volume = *helper.CallAgcSequence(kInitialInputVolume,
+ kHighSpeechProbability, kSpeechLevel);
+
+ // Inside the digital gain's window; no change of volume.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -23.0f);
+
+ // Inside the digital gain's window; no change of volume.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -28.0f);
+
+ // Above the digital gain's window; volume should be increased.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -29.0f);
+ EXPECT_EQ(volume, 128);
+
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -38.0f);
+ EXPECT_EQ(volume, 156);
+
+ // Inside the digital gain's window; no change of volume.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -23.0f);
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -18.0f);
+
+ // Below the digial gain's window; volume should be decreased.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -17.0f);
+ EXPECT_EQ(volume, 155);
+
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -17.0f);
+ EXPECT_EQ(volume, 151);
+
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -9.0f);
+ EXPECT_EQ(volume, 119);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest, MicVolumeIsLimited) {
+ InputVolumeControllerConfig config = GetInputVolumeControllerTestConfig();
+ const int min_input_volume = GetParam();
+ config.min_input_volume = min_input_volume;
+ InputVolumeControllerTestHelper helper(config);
+ int volume = *helper.CallAgcSequence(kInitialInputVolume,
+ kHighSpeechProbability, kSpeechLevel);
+
+ // Maximum upwards change is limited.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -48.0f);
+ EXPECT_EQ(volume, 183);
+
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -48.0f);
+ EXPECT_EQ(volume, 243);
+
+ // Won't go higher than the maximum.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -48.0f);
+ EXPECT_EQ(volume, 255);
+
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -17.0f);
+ EXPECT_EQ(volume, 254);
+
+ // Maximum downwards change is limited.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, 22.0f);
+ EXPECT_EQ(volume, 194);
+
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, 22.0f);
+ EXPECT_EQ(volume, 137);
+
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, 22.0f);
+ EXPECT_EQ(volume, 88);
+
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, 22.0f);
+ EXPECT_EQ(volume, 54);
+
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, 22.0f);
+ EXPECT_EQ(volume, 33);
+
+ // Won't go lower than the minimum.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, 22.0f);
+ EXPECT_EQ(volume, std::max(18, min_input_volume));
+
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, 22.0f);
+ EXPECT_EQ(volume, std::max(12, min_input_volume));
+}
+
+TEST_P(InputVolumeControllerParametrizedTest, NoActionWhileMuted) {
+ InputVolumeControllerTestHelper helper_1(
+ /*config=*/{.min_input_volume = GetParam()});
+ InputVolumeControllerTestHelper helper_2(
+ /*config=*/{.min_input_volume = GetParam()});
+
+ int volume_1 = *helper_1.CallAgcSequence(/*applied_input_volume=*/255,
+ kHighSpeechProbability, kSpeechLevel,
+ /*num_calls=*/1);
+ int volume_2 = *helper_2.CallAgcSequence(/*applied_input_volume=*/255,
+ kHighSpeechProbability, kSpeechLevel,
+ /*num_calls=*/1);
+
+ EXPECT_EQ(volume_1, 255);
+ EXPECT_EQ(volume_2, 255);
+
+ helper_2.controller.HandleCaptureOutputUsedChange(false);
+
+ WriteAlternatingAudioBufferSamples(kMaxSample, helper_1.audio_buffer);
+ WriteAlternatingAudioBufferSamples(kMaxSample, helper_2.audio_buffer);
+
+ volume_1 =
+ *helper_1.CallAgcSequence(volume_1, kHighSpeechProbability, kSpeechLevel,
+ /*num_calls=*/1);
+ volume_2 =
+ *helper_2.CallAgcSequence(volume_2, kHighSpeechProbability, kSpeechLevel,
+ /*num_calls=*/1);
+
+ EXPECT_LT(volume_1, 255);
+ EXPECT_EQ(volume_2, 255);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ UnmutingChecksVolumeWithoutRaising) {
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = GetParam()});
+ helper.CallAgcSequence(kInitialInputVolume, kHighSpeechProbability,
+ kSpeechLevel);
+
+ helper.controller.HandleCaptureOutputUsedChange(false);
+ helper.controller.HandleCaptureOutputUsedChange(true);
+
+ constexpr int kInputVolume = 127;
+
+ // SetMicVolume should not be called.
+ EXPECT_EQ(
+ helper.CallRecommendInputVolume(/*num_calls=*/1, kInputVolume,
+ kHighSpeechProbability, kSpeechLevel),
+ kInputVolume);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest, UnmutingRaisesTooLowVolume) {
+ const int min_input_volume = GetParam();
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = min_input_volume});
+ helper.CallAgcSequence(kInitialInputVolume, kHighSpeechProbability,
+ kSpeechLevel);
+
+ helper.controller.HandleCaptureOutputUsedChange(false);
+ helper.controller.HandleCaptureOutputUsedChange(true);
+
+ constexpr int kInputVolume = 11;
+
+ EXPECT_EQ(
+ helper.CallRecommendInputVolume(/*num_calls=*/1, kInputVolume,
+ kHighSpeechProbability, kSpeechLevel),
+ min_input_volume);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ ManualLevelChangeResultsInNoSetMicCall) {
+ InputVolumeControllerConfig config = GetInputVolumeControllerTestConfig();
+ config.min_input_volume = GetParam();
+ InputVolumeControllerTestHelper helper(config);
+ int volume = *helper.CallAgcSequence(kInitialInputVolume,
+ kHighSpeechProbability, kSpeechLevel);
+
+ // GetMicVolume returns a value outside of the quantization slack, indicating
+ // a manual volume change.
+ ASSERT_NE(volume, 154);
+ volume = helper.CallRecommendInputVolume(
+ /*num_calls=*/1, /*initial_volume=*/154, kHighSpeechProbability, -29.0f);
+ EXPECT_EQ(volume, 154);
+
+ // Do the same thing, except downwards now.
+ volume = helper.CallRecommendInputVolume(
+ /*num_calls=*/1, /*initial_volume=*/100, kHighSpeechProbability, -17.0f);
+ EXPECT_EQ(volume, 100);
+
+ // And finally verify the AGC continues working without a manual change.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -17.0f);
+ EXPECT_EQ(volume, 99);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ RecoveryAfterManualLevelChangeFromMax) {
+ InputVolumeControllerConfig config = GetInputVolumeControllerTestConfig();
+ config.min_input_volume = GetParam();
+ InputVolumeControllerTestHelper helper(config);
+ int volume = *helper.CallAgcSequence(kInitialInputVolume,
+ kHighSpeechProbability, kSpeechLevel);
+
+ // Force the mic up to max volume. Takes a few steps due to the residual
+ // gain limitation.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -48.0f);
+ EXPECT_EQ(volume, 183);
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -48.0f);
+ EXPECT_EQ(volume, 243);
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -48.0f);
+ EXPECT_EQ(volume, 255);
+
+ // Manual change does not result in SetMicVolume call.
+ volume = helper.CallRecommendInputVolume(
+ /*num_calls=*/1, /*initial_volume=*/50, kHighSpeechProbability, -17.0f);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 50);
+
+ // Continues working as usual afterwards.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -38.0f);
+
+ EXPECT_EQ(volume, 65);
+}
+
+// Checks that the minimum input volume is enforced during the upward adjustment
+// of the input volume.
+TEST_P(InputVolumeControllerParametrizedTest,
+ EnforceMinInputVolumeDuringUpwardsAdjustment) {
+ const int min_input_volume = GetParam();
+ InputVolumeControllerConfig config = GetInputVolumeControllerTestConfig();
+ config.min_input_volume = min_input_volume;
+ InputVolumeControllerTestHelper helper(config);
+ int volume = *helper.CallAgcSequence(kInitialInputVolume,
+ kHighSpeechProbability, kSpeechLevel);
+
+ // Manual change below min, but strictly positive, otherwise no action will be
+ // taken.
+ volume = helper.CallRecommendInputVolume(
+ /*num_calls=*/1, /*initial_volume=*/1, kHighSpeechProbability, -17.0f);
+
+ // Trigger an upward adjustment of the input volume.
+ EXPECT_EQ(volume, min_input_volume);
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -29.0f);
+ EXPECT_EQ(volume, min_input_volume);
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -30.0f);
+ EXPECT_EQ(volume, min_input_volume);
+
+ // After a number of consistently low speech level observations, the input
+ // volume is eventually raised above the minimum.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/10, volume,
+ kHighSpeechProbability, -38.0f);
+ EXPECT_GT(volume, min_input_volume);
+}
+
+// Checks that, when the min mic level override is specified, AGC immediately
+// applies the minimum mic level after the mic level is manually set below the
+// minimum gain to enforce.
+TEST_P(InputVolumeControllerParametrizedTest,
+ RecoveryAfterManualLevelChangeBelowMin) {
+ const int min_input_volume = GetParam();
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = min_input_volume});
+ int volume = *helper.CallAgcSequence(kInitialInputVolume,
+ kHighSpeechProbability, kSpeechLevel);
+
+ // Manual change below min, but strictly positive, otherwise
+ // AGC won't take any action.
+ volume = helper.CallRecommendInputVolume(
+ /*num_calls=*/1, /*initial_volume=*/1, kHighSpeechProbability, -17.0f);
+ EXPECT_EQ(volume, min_input_volume);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest, NoClippingHasNoImpact) {
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = GetParam()});
+ helper.CallAgcSequence(kInitialInputVolume, kHighSpeechProbability,
+ kSpeechLevel);
+
+ helper.CallAnalyzeInputAudio(/*num_calls=*/100, /*clipped_ratio=*/0);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 128);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ ClippingUnderThresholdHasNoImpact) {
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = GetParam()});
+ helper.CallAgcSequence(kInitialInputVolume, kHighSpeechProbability,
+ kSpeechLevel);
+
+ helper.CallAnalyzeInputAudio(/*num_calls=*/1, /*clipped_ratio=*/0.099);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 128);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest, ClippingLowersVolume) {
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = GetParam()});
+ helper.CallAgcSequence(/*applied_input_volume=*/255, kHighSpeechProbability,
+ kSpeechLevel);
+
+ helper.CallAnalyzeInputAudio(/*num_calls=*/1, /*clipped_ratio=*/0.2);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 240);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ WaitingPeriodBetweenClippingChecks) {
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = GetParam()});
+ helper.CallAgcSequence(/*applied_input_volume=*/255, kHighSpeechProbability,
+ kSpeechLevel);
+
+ helper.CallAnalyzeInputAudio(/*num_calls=*/1,
+ /*clipped_ratio=*/kAboveClippedThreshold);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 240);
+
+ helper.CallAnalyzeInputAudio(/*num_calls=*/300,
+ /*clipped_ratio=*/kAboveClippedThreshold);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 240);
+
+ helper.CallAnalyzeInputAudio(/*num_calls=*/1,
+ /*clipped_ratio=*/kAboveClippedThreshold);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 225);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest, ClippingLoweringIsLimited) {
+ InputVolumeControllerConfig config = GetInputVolumeControllerTestConfig();
+ config.min_input_volume = GetParam();
+ InputVolumeControllerTestHelper helper(config);
+ helper.CallAgcSequence(/*applied_input_volume=*/180, kHighSpeechProbability,
+ kSpeechLevel);
+
+ helper.CallAnalyzeInputAudio(/*num_calls=*/1,
+ /*clipped_ratio=*/kAboveClippedThreshold);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), kClippedMin);
+
+ helper.CallAnalyzeInputAudio(/*num_calls=*/1000,
+ /*clipped_ratio=*/kAboveClippedThreshold);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), kClippedMin);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ ClippingMaxIsRespectedWhenEqualToLevel) {
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = GetParam()});
+ helper.CallAgcSequence(/*applied_input_volume=*/255, kHighSpeechProbability,
+ kSpeechLevel);
+
+ helper.CallAnalyzeInputAudio(/*num_calls=*/1,
+ /*clipped_ratio=*/kAboveClippedThreshold);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 240);
+
+ helper.CallRecommendInputVolume(/*num_calls=*/10, /*initial_volume=*/240,
+ kHighSpeechProbability, -48.0f);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 240);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ ClippingMaxIsRespectedWhenHigherThanLevel) {
+ InputVolumeControllerConfig config = GetInputVolumeControllerTestConfig();
+ config.min_input_volume = GetParam();
+ InputVolumeControllerTestHelper helper(config);
+ helper.CallAgcSequence(/*applied_input_volume=*/200, kHighSpeechProbability,
+ kSpeechLevel);
+
+ helper.CallAnalyzeInputAudio(/*num_calls=*/1,
+ /*clipped_ratio=*/kAboveClippedThreshold);
+ int volume = helper.controller.recommended_input_volume();
+ EXPECT_EQ(volume, 185);
+
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -58.0f);
+ EXPECT_EQ(volume, 240);
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/10, volume,
+ kHighSpeechProbability, -58.0f);
+ EXPECT_EQ(volume, 240);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest, UserCanRaiseVolumeAfterClipping) {
+ InputVolumeControllerConfig config = GetInputVolumeControllerTestConfig();
+ config.min_input_volume = GetParam();
+ InputVolumeControllerTestHelper helper(config);
+ helper.CallAgcSequence(/*applied_input_volume=*/225, kHighSpeechProbability,
+ kSpeechLevel);
+
+ helper.CallAnalyzeInputAudio(/*num_calls=*/1,
+ /*clipped_ratio=*/kAboveClippedThreshold);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 210);
+
+ // User changed the volume.
+ int volume = helper.CallRecommendInputVolume(
+ /*num_calls=*/1, /*initial_volume-*/ 250, kHighSpeechProbability, -32.0f);
+ EXPECT_EQ(volume, 250);
+
+ // Move down...
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -8.0f);
+ EXPECT_EQ(volume, 210);
+ // And back up to the new max established by the user.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -58.0f);
+ EXPECT_EQ(volume, 250);
+ // Will not move above new maximum.
+ volume = helper.CallRecommendInputVolume(/*num_calls=*/1, volume,
+ kHighSpeechProbability, -48.0f);
+ EXPECT_EQ(volume, 250);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ ClippingDoesNotPullLowVolumeBackUp) {
+ InputVolumeControllerConfig config = GetInputVolumeControllerTestConfig();
+ config.min_input_volume = GetParam();
+ InputVolumeControllerTestHelper helper(config);
+ helper.CallAgcSequence(/*applied_input_volume=*/80, kHighSpeechProbability,
+ kSpeechLevel);
+
+ int initial_volume = helper.controller.recommended_input_volume();
+ helper.CallAnalyzeInputAudio(/*num_calls=*/1,
+ /*clipped_ratio=*/kAboveClippedThreshold);
+ EXPECT_EQ(helper.controller.recommended_input_volume(), initial_volume);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest, TakesNoActionOnZeroMicVolume) {
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = GetParam()});
+ helper.CallAgcSequence(kInitialInputVolume, kHighSpeechProbability,
+ kSpeechLevel);
+
+ EXPECT_EQ(
+ helper.CallRecommendInputVolume(/*num_calls=*/10, /*initial_volume=*/0,
+ kHighSpeechProbability, -48.0f),
+ 0);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest, ClippingDetectionLowersVolume) {
+ InputVolumeControllerConfig config = GetInputVolumeControllerTestConfig();
+ config.min_input_volume = GetParam();
+ InputVolumeControllerTestHelper helper(config);
+ int volume = *helper.CallAgcSequence(/*applied_input_volume=*/255,
+ kHighSpeechProbability, kSpeechLevel,
+ /*num_calls=*/1);
+
+ EXPECT_EQ(volume, 255);
+
+ WriteAlternatingAudioBufferSamples(0.99f * kMaxSample, helper.audio_buffer);
+ volume = *helper.CallAgcSequence(volume, kHighSpeechProbability, kSpeechLevel,
+ /*num_calls=*/100);
+
+ EXPECT_EQ(volume, 255);
+
+ WriteAlternatingAudioBufferSamples(kMaxSample, helper.audio_buffer);
+ volume = *helper.CallAgcSequence(volume, kHighSpeechProbability, kSpeechLevel,
+ /*num_calls=*/100);
+
+ EXPECT_EQ(volume, 240);
+}
+
+// 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_P(InputVolumeControllerParametrizedTest, ClippingParametersVerified) {
+ std::unique_ptr<InputVolumeController> controller =
+ CreateInputVolumeController(kClippedLevelStep, kClippedRatioThreshold,
+ kClippedWaitFrames);
+ controller->Initialize();
+ EXPECT_EQ(controller->clipped_level_step_, kClippedLevelStep);
+ EXPECT_EQ(controller->clipped_ratio_threshold_, kClippedRatioThreshold);
+ EXPECT_EQ(controller->clipped_wait_frames_, kClippedWaitFrames);
+ std::unique_ptr<InputVolumeController> controller_custom =
+ CreateInputVolumeController(/*clipped_level_step=*/10,
+ /*clipped_ratio_threshold=*/0.2f,
+ /*clipped_wait_frames=*/50);
+ controller_custom->Initialize();
+ EXPECT_EQ(controller_custom->clipped_level_step_, 10);
+ EXPECT_EQ(controller_custom->clipped_ratio_threshold_, 0.2f);
+ EXPECT_EQ(controller_custom->clipped_wait_frames_, 50);
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ DisableClippingPredictorDisablesClippingPredictor) {
+ std::unique_ptr<InputVolumeController> controller =
+ CreateInputVolumeController(kClippedLevelStep, kClippedRatioThreshold,
+ kClippedWaitFrames,
+ /*enable_clipping_predictor=*/false);
+ controller->Initialize();
+
+ EXPECT_FALSE(controller->clipping_predictor_enabled());
+ EXPECT_FALSE(controller->use_clipping_predictor_step());
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ EnableClippingPredictorEnablesClippingPredictor) {
+ std::unique_ptr<InputVolumeController> controller =
+ CreateInputVolumeController(kClippedLevelStep, kClippedRatioThreshold,
+ kClippedWaitFrames,
+ /*enable_clipping_predictor=*/true);
+ controller->Initialize();
+
+ EXPECT_TRUE(controller->clipping_predictor_enabled());
+ EXPECT_TRUE(controller->use_clipping_predictor_step());
+}
+
+TEST_P(InputVolumeControllerParametrizedTest,
+ DisableClippingPredictorDoesNotLowerVolume) {
+ int volume = 255;
+ InputVolumeControllerConfig config = GetInputVolumeControllerTestConfig();
+ config.enable_clipping_predictor = false;
+ auto helper = InputVolumeControllerTestHelper(config);
+ helper.controller.Initialize();
+
+ EXPECT_FALSE(helper.controller.clipping_predictor_enabled());
+ EXPECT_FALSE(helper.controller.use_clipping_predictor_step());
+
+ // Expect no change if clipping prediction is enabled.
+ for (int j = 0; j < 31; ++j) {
+ WriteAlternatingAudioBufferSamples(0.99f * kMaxSample, helper.audio_buffer);
+ volume =
+ *helper.CallAgcSequence(volume, kLowSpeechProbability, kSpeechLevel,
+ /*num_calls=*/5);
+
+ WriteAudioBufferSamples(0.99f * kMaxSample, /*clipped_ratio=*/0.0f,
+ helper.audio_buffer);
+ volume =
+ *helper.CallAgcSequence(volume, kLowSpeechProbability, kSpeechLevel,
+ /*num_calls=*/5);
+
+ EXPECT_EQ(volume, 255);
+ }
+}
+
+// TODO(bugs.webrtc.org/7494): Split into several smaller tests.
+TEST_P(InputVolumeControllerParametrizedTest,
+ UsedClippingPredictionsProduceLowerAnalogLevels) {
+ constexpr int kInitialLevel = 255;
+ constexpr float kCloseToClippingPeakRatio = 0.99f;
+ int volume_1 = kInitialLevel;
+ int volume_2 = kInitialLevel;
+
+ // Create two helpers, one with clipping prediction and one without.
+ auto config_1 = GetInputVolumeControllerTestConfig();
+ auto config_2 = GetInputVolumeControllerTestConfig();
+ config_1.enable_clipping_predictor = true;
+ config_2.enable_clipping_predictor = false;
+ auto helper_1 = InputVolumeControllerTestHelper(config_1);
+ auto helper_2 = InputVolumeControllerTestHelper(config_2);
+ helper_1.controller.Initialize();
+ helper_2.controller.Initialize();
+
+ EXPECT_TRUE(helper_1.controller.clipping_predictor_enabled());
+ EXPECT_FALSE(helper_2.controller.clipping_predictor_enabled());
+ EXPECT_TRUE(helper_1.controller.use_clipping_predictor_step());
+
+ // Expect a change if clipping prediction is enabled.
+ WriteAlternatingAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ helper_1.audio_buffer);
+ WriteAlternatingAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 5);
+
+ WriteAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ /*clipped_ratio=*/0.0f, helper_1.audio_buffer);
+ WriteAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ /*clipped_ratio=*/0.0f, helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 5);
+
+ EXPECT_EQ(volume_1, kInitialLevel - kClippedLevelStep);
+ EXPECT_EQ(volume_2, kInitialLevel);
+
+ // Expect no change during waiting.
+ for (int i = 0; i < kClippedWaitFrames / 10; ++i) {
+ WriteAlternatingAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ helper_1.audio_buffer);
+ WriteAlternatingAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 5);
+
+ WriteAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ /*clipped_ratio=*/0.0f, helper_1.audio_buffer);
+ WriteAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ /*clipped_ratio=*/0.0f, helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 5);
+
+ EXPECT_EQ(volume_1, kInitialLevel - kClippedLevelStep);
+ EXPECT_EQ(volume_2, kInitialLevel);
+ }
+
+ // Expect a change when the prediction step is used.
+ WriteAlternatingAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ helper_1.audio_buffer);
+ WriteAlternatingAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 5);
+
+ WriteAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ /*clipped_ratio=*/0.0f, helper_1.audio_buffer);
+ WriteAudioBufferSamples(kCloseToClippingPeakRatio * kMaxSample,
+ /*clipped_ratio=*/0.0f, helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 5);
+
+ EXPECT_EQ(volume_1, kInitialLevel - 2 * kClippedLevelStep);
+ EXPECT_EQ(volume_2, kInitialLevel);
+
+ // Expect no change when clipping is not detected or predicted.
+ for (int i = 0; i < 2 * kClippedWaitFrames / 10; ++i) {
+ WriteAlternatingAudioBufferSamples(/*samples_value=*/0.0f,
+ helper_1.audio_buffer);
+ WriteAlternatingAudioBufferSamples(/*samples_value=*/0.0f,
+ helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 5);
+
+ WriteAudioBufferSamples(/*samples_value=*/0.0f, /*clipped_ratio=*/0.0f,
+ helper_1.audio_buffer);
+ WriteAudioBufferSamples(/*samples_value=*/0.0f, /*clipped_ratio=*/0.0f,
+ helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ }
+
+ EXPECT_EQ(volume_1, kInitialLevel - 2 * kClippedLevelStep);
+ EXPECT_EQ(volume_2, kInitialLevel);
+
+ // Expect a change for clipping frames.
+ WriteAlternatingAudioBufferSamples(kMaxSample, helper_1.audio_buffer);
+ WriteAlternatingAudioBufferSamples(kMaxSample, helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 1);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 1);
+
+ EXPECT_EQ(volume_1, kInitialLevel - 3 * kClippedLevelStep);
+ EXPECT_EQ(volume_2, kInitialLevel - kClippedLevelStep);
+
+ // Expect no change during waiting.
+ for (int i = 0; i < kClippedWaitFrames / 10; ++i) {
+ WriteAlternatingAudioBufferSamples(kMaxSample, helper_1.audio_buffer);
+ WriteAlternatingAudioBufferSamples(kMaxSample, helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 5);
+
+ WriteAudioBufferSamples(kMaxSample, /*clipped_ratio=*/1.0f,
+ helper_1.audio_buffer);
+ WriteAudioBufferSamples(kMaxSample, /*clipped_ratio=*/1.0f,
+ helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 5);
+ }
+
+ EXPECT_EQ(volume_1, kInitialLevel - 3 * kClippedLevelStep);
+ EXPECT_EQ(volume_2, kInitialLevel - kClippedLevelStep);
+
+ // Expect a change for clipping frames.
+ WriteAlternatingAudioBufferSamples(kMaxSample, helper_1.audio_buffer);
+ WriteAlternatingAudioBufferSamples(kMaxSample, helper_2.audio_buffer);
+ volume_1 = *helper_1.CallAgcSequence(volume_1, kLowSpeechProbability,
+ kSpeechLevel, 1);
+ volume_2 = *helper_2.CallAgcSequence(volume_2, kLowSpeechProbability,
+ kSpeechLevel, 1);
+
+ EXPECT_EQ(volume_1, kInitialLevel - 4 * kClippedLevelStep);
+ EXPECT_EQ(volume_2, kInitialLevel - 2 * kClippedLevelStep);
+}
+
+// Checks that passing an empty speech level has no effect on the input volume.
+TEST_P(InputVolumeControllerParametrizedTest, EmptyRmsErrorHasNoEffect) {
+ InputVolumeController controller(kNumChannels,
+ GetInputVolumeControllerTestConfig());
+ controller.Initialize();
+
+ // Feed speech with low energy that would trigger an upward adapation of
+ // the analog level if an speech level was not low and the RMS level empty.
+ constexpr int kNumFrames = 125;
+ constexpr int kGainDb = -20;
+ SpeechSamplesReader reader;
+ int volume = reader.Feed(kNumFrames, kInitialInputVolume, kGainDb,
+ kLowSpeechProbability, absl::nullopt, controller);
+
+ // Check that no adaptation occurs.
+ ASSERT_EQ(volume, kInitialInputVolume);
+}
+
+// Checks that the recommended input volume is not updated unless enough
+// frames have been processed after the previous update.
+TEST(InputVolumeControllerTest, UpdateInputVolumeWaitFramesIsEffective) {
+ constexpr int kInputVolume = kInitialInputVolume;
+ std::unique_ptr<InputVolumeController> controller_wait_0 =
+ CreateInputVolumeController(kClippedLevelStep, kClippedRatioThreshold,
+ kClippedWaitFrames,
+ /*enable_clipping_predictor=*/false,
+ /*update_input_volume_wait_frames=*/0);
+ std::unique_ptr<InputVolumeController> controller_wait_100 =
+ CreateInputVolumeController(kClippedLevelStep, kClippedRatioThreshold,
+ kClippedWaitFrames,
+ /*enable_clipping_predictor=*/false,
+ /*update_input_volume_wait_frames=*/100);
+ controller_wait_0->Initialize();
+ controller_wait_100->Initialize();
+
+ SpeechSamplesReader reader_1;
+ SpeechSamplesReader reader_2;
+ int volume_wait_0 = reader_1.Feed(
+ /*num_frames=*/99, kInputVolume, /*gain_db=*/0, kHighSpeechProbability,
+ /*speech_level_dbfs=*/-42.0f, *controller_wait_0);
+ int volume_wait_100 = reader_2.Feed(
+ /*num_frames=*/99, kInputVolume, /*gain_db=*/0, kHighSpeechProbability,
+ /*speech_level_dbfs=*/-42.0f, *controller_wait_100);
+
+ // Check that adaptation only occurs if enough frames have been processed.
+ ASSERT_GT(volume_wait_0, kInputVolume);
+ ASSERT_EQ(volume_wait_100, kInputVolume);
+
+ volume_wait_0 =
+ reader_1.Feed(/*num_frames=*/1, volume_wait_0,
+ /*gain_db=*/0, kHighSpeechProbability,
+ /*speech_level_dbfs=*/-42.0f, *controller_wait_0);
+ volume_wait_100 =
+ reader_2.Feed(/*num_frames=*/1, volume_wait_100,
+ /*gain_db=*/0, kHighSpeechProbability,
+ /*speech_level_dbfs=*/-42.0f, *controller_wait_100);
+
+ // Check that adaptation only occurs when enough frames have been processed.
+ ASSERT_GT(volume_wait_0, kInputVolume);
+ ASSERT_GT(volume_wait_100, kInputVolume);
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+ InputVolumeControllerParametrizedTest,
+ ::testing::Values(12, 20));
+
+TEST(InputVolumeControllerTest,
+ MinInputVolumeEnforcedWithClippingWhenAboveClippedLevelMin) {
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = 80, .clipped_level_min = 70});
+
+ // Trigger a downward adjustment caused by clipping input. Use a low speech
+ // probability to limit the volume changes to clipping handling.
+ WriteAudioBufferSamples(/*samples_value=*/4000.0f, /*clipped_ratio=*/0.8f,
+ helper.audio_buffer);
+ constexpr int kNumCalls = 800;
+ helper.CallAgcSequence(/*applied_input_volume=*/100, kLowSpeechProbability,
+ /*speech_level_dbfs=*/-18.0f, kNumCalls);
+
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 80);
+}
+
+TEST(InputVolumeControllerTest,
+ ClippedlevelMinEnforcedWithClippingWhenAboveMinInputVolume) {
+ InputVolumeControllerTestHelper helper(
+ /*config=*/{.min_input_volume = 70, .clipped_level_min = 80});
+
+ // Trigger a downward adjustment caused by clipping input. Use a low speech
+ // probability to limit the volume changes to clipping handling.
+ WriteAudioBufferSamples(/*samples_value=*/4000.0f, /*clipped_ratio=*/0.8f,
+ helper.audio_buffer);
+ constexpr int kNumCalls = 800;
+ helper.CallAgcSequence(/*applied_input_volume=*/100, kLowSpeechProbability,
+ /*speech_level_dbfs=*/-18.0f, kNumCalls);
+
+ EXPECT_EQ(helper.controller.recommended_input_volume(), 80);
+}
+
+TEST(InputVolumeControllerTest, SpeechRatioThresholdIsEffective) {
+ constexpr int kInputVolume = kInitialInputVolume;
+ // Create two input volume controllers with 10 frames between volume updates
+ // and the minimum speech ratio of 0.8 and speech probability threshold 0.5.
+ std::unique_ptr<InputVolumeController> controller_1 =
+ CreateInputVolumeController(kClippedLevelStep, kClippedRatioThreshold,
+ kClippedWaitFrames,
+ /*enable_clipping_predictor=*/false,
+ /*update_input_volume_wait_frames=*/10);
+ std::unique_ptr<InputVolumeController> controller_2 =
+ CreateInputVolumeController(kClippedLevelStep, kClippedRatioThreshold,
+ kClippedWaitFrames,
+ /*enable_clipping_predictor=*/false,
+ /*update_input_volume_wait_frames=*/10);
+ controller_1->Initialize();
+ controller_2->Initialize();
+
+ SpeechSamplesReader reader_1;
+ SpeechSamplesReader reader_2;
+
+ int volume_1 = reader_1.Feed(/*num_frames=*/1, kInputVolume, /*gain_db=*/0,
+ /*speech_probability=*/0.7f,
+ /*speech_level_dbfs=*/-42.0f, *controller_1);
+ int volume_2 = reader_2.Feed(/*num_frames=*/1, kInputVolume, /*gain_db=*/0,
+ /*speech_probability=*/0.4f,
+ /*speech_level_dbfs=*/-42.0f, *controller_2);
+
+ ASSERT_EQ(volume_1, kInputVolume);
+ ASSERT_EQ(volume_2, kInputVolume);
+
+ volume_1 = reader_1.Feed(/*num_frames=*/2, volume_1, /*gain_db=*/0,
+ /*speech_probability=*/0.4f,
+ /*speech_level_dbfs=*/-42.0f, *controller_1);
+ volume_2 = reader_2.Feed(/*num_frames=*/2, volume_2, /*gain_db=*/0,
+ /*speech_probability=*/0.4f,
+ /*speech_level_dbfs=*/-42.0f, *controller_2);
+
+ ASSERT_EQ(volume_1, kInputVolume);
+ ASSERT_EQ(volume_2, kInputVolume);
+
+ volume_1 = reader_1.Feed(
+ /*num_frames=*/7, volume_1, /*gain_db=*/0,
+ /*speech_probability=*/0.7f, /*speech_level_dbfs=*/-42.0f, *controller_1);
+ volume_2 = reader_2.Feed(
+ /*num_frames=*/7, volume_2, /*gain_db=*/0,
+ /*speech_probability=*/0.7f, /*speech_level_dbfs=*/-42.0f, *controller_2);
+
+ ASSERT_GT(volume_1, kInputVolume);
+ ASSERT_EQ(volume_2, kInputVolume);
+}
+
+TEST(InputVolumeControllerTest, SpeechProbabilityThresholdIsEffective) {
+ constexpr int kInputVolume = kInitialInputVolume;
+ // Create two input volume controllers with the exact same settings and
+ // 10 frames between volume updates.
+ std::unique_ptr<InputVolumeController> controller_1 =
+ CreateInputVolumeController(kClippedLevelStep, kClippedRatioThreshold,
+ kClippedWaitFrames,
+ /*enable_clipping_predictor=*/false,
+ /*update_input_volume_wait_frames=*/10);
+ std::unique_ptr<InputVolumeController> controller_2 =
+ CreateInputVolumeController(kClippedLevelStep, kClippedRatioThreshold,
+ kClippedWaitFrames,
+ /*enable_clipping_predictor=*/false,
+ /*update_input_volume_wait_frames=*/10);
+ controller_1->Initialize();
+ controller_2->Initialize();
+
+ SpeechSamplesReader reader_1;
+ SpeechSamplesReader reader_2;
+
+ // Process with two sets of inputs: Use `reader_1` to process inputs
+ // that make the volume to be adjusted after enough frames have been
+ // processsed and `reader_2` to process inputs that won't make the volume
+ // to be adjusted.
+ int volume_1 = reader_1.Feed(/*num_frames=*/1, kInputVolume, /*gain_db=*/0,
+ /*speech_probability=*/0.5f,
+ /*speech_level_dbfs=*/-42.0f, *controller_1);
+ int volume_2 = reader_2.Feed(/*num_frames=*/1, kInputVolume, /*gain_db=*/0,
+ /*speech_probability=*/0.49f,
+ /*speech_level_dbfs=*/-42.0f, *controller_2);
+
+ ASSERT_EQ(volume_1, kInputVolume);
+ ASSERT_EQ(volume_2, kInputVolume);
+
+ reader_1.Feed(/*num_frames=*/2, volume_1, /*gain_db=*/0,
+ /*speech_probability=*/0.49f, /*speech_level_dbfs=*/-42.0f,
+ *controller_1);
+ reader_2.Feed(/*num_frames=*/2, volume_2, /*gain_db=*/0,
+ /*speech_probability=*/0.49f, /*speech_level_dbfs=*/-42.0f,
+ *controller_2);
+
+ ASSERT_EQ(volume_1, kInputVolume);
+ ASSERT_EQ(volume_2, kInputVolume);
+
+ volume_1 = reader_1.Feed(
+ /*num_frames=*/7, volume_1, /*gain_db=*/0,
+ /*speech_probability=*/0.5f, /*speech_level_dbfs=*/-42.0f, *controller_1);
+ volume_2 = reader_2.Feed(
+ /*num_frames=*/7, volume_2, /*gain_db=*/0,
+ /*speech_probability=*/0.5f, /*speech_level_dbfs=*/-42.0f, *controller_2);
+
+ ASSERT_GT(volume_1, kInputVolume);
+ ASSERT_EQ(volume_2, kInputVolume);
+}
+
+TEST(InputVolumeControllerTest,
+ DoNotLogRecommendedInputVolumeOnChangeToMatchTarget) {
+ metrics::Reset();
+
+ SpeechSamplesReader reader;
+ auto controller = CreateInputVolumeController();
+ controller->Initialize();
+ // Trigger a downward volume change by inputting audio that clips. Pass a
+ // speech level that falls in the target range to make sure that the
+ // adaptation is not made to match the target range.
+ constexpr int kStartupVolume = 255;
+ const int volume = reader.Feed(/*num_frames=*/14, kStartupVolume,
+ /*gain_db=*/50, kHighSpeechProbability,
+ /*speech_level_dbfs=*/-20.0f, *controller);
+ ASSERT_LT(volume, kStartupVolume);
+ EXPECT_METRIC_THAT(
+ metrics::Samples(
+ "WebRTC.Audio.Apm.RecommendedInputVolume.OnChangeToMatchTarget"),
+ ::testing::IsEmpty());
+}
+
+TEST(InputVolumeControllerTest,
+ LogRecommendedInputVolumeOnUpwardChangeToMatchTarget) {
+ metrics::Reset();
+
+ SpeechSamplesReader reader;
+ auto controller = CreateInputVolumeController();
+ controller->Initialize();
+ constexpr int kStartupVolume = 100;
+ // Trigger an upward volume change by inputting audio that does not clip and
+ // by passing a speech level below the target range.
+ const int volume = reader.Feed(/*num_frames=*/14, kStartupVolume,
+ /*gain_db=*/-6, kHighSpeechProbability,
+ /*speech_level_dbfs=*/-50.0f, *controller);
+ ASSERT_GT(volume, kStartupVolume);
+ EXPECT_METRIC_THAT(
+ metrics::Samples(
+ "WebRTC.Audio.Apm.RecommendedInputVolume.OnChangeToMatchTarget"),
+ ::testing::Not(::testing::IsEmpty()));
+}
+
+TEST(InputVolumeControllerTest,
+ LogRecommendedInputVolumeOnDownwardChangeToMatchTarget) {
+ metrics::Reset();
+
+ SpeechSamplesReader reader;
+ auto controller = CreateInputVolumeController();
+ controller->Initialize();
+ constexpr int kStartupVolume = 100;
+ // Trigger a downward volume change by inputting audio that does not clip and
+ // by passing a speech level above the target range.
+ const int volume = reader.Feed(/*num_frames=*/14, kStartupVolume,
+ /*gain_db=*/-6, kHighSpeechProbability,
+ /*speech_level_dbfs=*/-5.0f, *controller);
+ ASSERT_LT(volume, kStartupVolume);
+ EXPECT_METRIC_THAT(
+ metrics::Samples(
+ "WebRTC.Audio.Apm.RecommendedInputVolume.OnChangeToMatchTarget"),
+ ::testing::Not(::testing::IsEmpty()));
+}
+
+TEST(MonoInputVolumeControllerTest, CheckHandleClippingLowersVolume) {
+ constexpr int kInitialInputVolume = 100;
+ constexpr int kInputVolumeStep = 29;
+ MonoInputVolumeController mono_controller(
+ /*clipped_level_min=*/70,
+ /*min_mic_level=*/32,
+ /*update_input_volume_wait_frames=*/3, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ mono_controller.Initialize();
+
+ UpdateRecommendedInputVolume(mono_controller, kInitialInputVolume,
+ kLowSpeechProbability,
+ /*rms_error_dbfs*/ -10.0f);
+
+ mono_controller.HandleClipping(kInputVolumeStep);
+
+ EXPECT_EQ(mono_controller.recommended_analog_level(),
+ kInitialInputVolume - kInputVolumeStep);
+}
+
+TEST(MonoInputVolumeControllerTest,
+ CheckProcessNegativeRmsErrorDecreasesInputVolume) {
+ constexpr int kInitialInputVolume = 100;
+ MonoInputVolumeController mono_controller(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/32,
+ /*update_input_volume_wait_frames=*/3, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ mono_controller.Initialize();
+
+ int volume = UpdateRecommendedInputVolume(
+ mono_controller, kInitialInputVolume, kHighSpeechProbability, -10.0f);
+ volume = UpdateRecommendedInputVolume(mono_controller, volume,
+ kHighSpeechProbability, -10.0f);
+ volume = UpdateRecommendedInputVolume(mono_controller, volume,
+ kHighSpeechProbability, -10.0f);
+
+ EXPECT_LT(volume, kInitialInputVolume);
+}
+
+TEST(MonoInputVolumeControllerTest,
+ CheckProcessPositiveRmsErrorIncreasesInputVolume) {
+ constexpr int kInitialInputVolume = 100;
+ MonoInputVolumeController mono_controller(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/32,
+ /*update_input_volume_wait_frames=*/3, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ mono_controller.Initialize();
+
+ int volume = UpdateRecommendedInputVolume(
+ mono_controller, kInitialInputVolume, kHighSpeechProbability, 10.0f);
+ volume = UpdateRecommendedInputVolume(mono_controller, volume,
+ kHighSpeechProbability, 10.0f);
+ volume = UpdateRecommendedInputVolume(mono_controller, volume,
+ kHighSpeechProbability, 10.0f);
+
+ EXPECT_GT(volume, kInitialInputVolume);
+}
+
+TEST(MonoInputVolumeControllerTest,
+ CheckProcessNegativeRmsErrorDecreasesInputVolumeWithLimit) {
+ constexpr int kInitialInputVolume = 100;
+ MonoInputVolumeController mono_controller_1(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/32,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ MonoInputVolumeController mono_controller_2(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/32,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ MonoInputVolumeController mono_controller_3(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/32,
+ /*update_input_volume_wait_frames=*/2,
+ /*speech_probability_threshold=*/0.7,
+ /*speech_ratio_threshold=*/0.8);
+ mono_controller_1.Initialize();
+ mono_controller_2.Initialize();
+ mono_controller_3.Initialize();
+
+ // Process RMS errors in the range
+ // [`-kMaxResidualGainChange`, `kMaxResidualGainChange`].
+ int volume_1 = UpdateRecommendedInputVolume(
+ mono_controller_1, kInitialInputVolume, kHighSpeechProbability, -14.0f);
+ volume_1 = UpdateRecommendedInputVolume(mono_controller_1, volume_1,
+ kHighSpeechProbability, -14.0f);
+ // Process RMS errors outside the range
+ // [`-kMaxResidualGainChange`, `kMaxResidualGainChange`].
+ int volume_2 = UpdateRecommendedInputVolume(
+ mono_controller_2, kInitialInputVolume, kHighSpeechProbability, -15.0f);
+ int volume_3 = UpdateRecommendedInputVolume(
+ mono_controller_3, kInitialInputVolume, kHighSpeechProbability, -30.0f);
+ volume_2 = UpdateRecommendedInputVolume(mono_controller_2, volume_2,
+ kHighSpeechProbability, -15.0f);
+ volume_3 = UpdateRecommendedInputVolume(mono_controller_3, volume_3,
+ kHighSpeechProbability, -30.0f);
+
+ EXPECT_LT(volume_1, kInitialInputVolume);
+ EXPECT_LT(volume_2, volume_1);
+ EXPECT_EQ(volume_2, volume_3);
+}
+
+TEST(MonoInputVolumeControllerTest,
+ CheckProcessPositiveRmsErrorIncreasesInputVolumeWithLimit) {
+ constexpr int kInitialInputVolume = 100;
+ MonoInputVolumeController mono_controller_1(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/32,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ MonoInputVolumeController mono_controller_2(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/32,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ MonoInputVolumeController mono_controller_3(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/32,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ mono_controller_1.Initialize();
+ mono_controller_2.Initialize();
+ mono_controller_3.Initialize();
+
+ // Process RMS errors in the range
+ // [`-kMaxResidualGainChange`, `kMaxResidualGainChange`].
+ int volume_1 = UpdateRecommendedInputVolume(
+ mono_controller_1, kInitialInputVolume, kHighSpeechProbability, 14.0f);
+ volume_1 = UpdateRecommendedInputVolume(mono_controller_1, volume_1,
+ kHighSpeechProbability, 14.0f);
+ // Process RMS errors outside the range
+ // [`-kMaxResidualGainChange`, `kMaxResidualGainChange`].
+ int volume_2 = UpdateRecommendedInputVolume(
+ mono_controller_2, kInitialInputVolume, kHighSpeechProbability, 15.0f);
+ int volume_3 = UpdateRecommendedInputVolume(
+ mono_controller_3, kInitialInputVolume, kHighSpeechProbability, 30.0f);
+ volume_2 = UpdateRecommendedInputVolume(mono_controller_2, volume_2,
+ kHighSpeechProbability, 15.0f);
+ volume_3 = UpdateRecommendedInputVolume(mono_controller_3, volume_3,
+ kHighSpeechProbability, 30.0f);
+
+ EXPECT_GT(volume_1, kInitialInputVolume);
+ EXPECT_GT(volume_2, volume_1);
+ EXPECT_EQ(volume_2, volume_3);
+}
+
+TEST(MonoInputVolumeControllerTest,
+ CheckProcessRmsErrorDecreasesInputVolumeRepeatedly) {
+ constexpr int kInitialInputVolume = 100;
+ MonoInputVolumeController mono_controller(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/32,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ mono_controller.Initialize();
+
+ int volume_before = UpdateRecommendedInputVolume(
+ mono_controller, kInitialInputVolume, kHighSpeechProbability, -10.0f);
+ volume_before = UpdateRecommendedInputVolume(mono_controller, volume_before,
+ kHighSpeechProbability, -10.0f);
+
+ EXPECT_LT(volume_before, kInitialInputVolume);
+
+ int volume_after = UpdateRecommendedInputVolume(
+ mono_controller, volume_before, kHighSpeechProbability, -10.0f);
+ volume_after = UpdateRecommendedInputVolume(mono_controller, volume_after,
+ kHighSpeechProbability, -10.0f);
+
+ EXPECT_LT(volume_after, volume_before);
+}
+
+TEST(MonoInputVolumeControllerTest,
+ CheckProcessPositiveRmsErrorIncreasesInputVolumeRepeatedly) {
+ constexpr int kInitialInputVolume = 100;
+ MonoInputVolumeController mono_controller(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/32,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ mono_controller.Initialize();
+
+ int volume_before = UpdateRecommendedInputVolume(
+ mono_controller, kInitialInputVolume, kHighSpeechProbability, 10.0f);
+ volume_before = UpdateRecommendedInputVolume(mono_controller, volume_before,
+ kHighSpeechProbability, 10.0f);
+
+ EXPECT_GT(volume_before, kInitialInputVolume);
+
+ int volume_after = UpdateRecommendedInputVolume(
+ mono_controller, volume_before, kHighSpeechProbability, 10.0f);
+ volume_after = UpdateRecommendedInputVolume(mono_controller, volume_after,
+ kHighSpeechProbability, 10.0f);
+
+ EXPECT_GT(volume_after, volume_before);
+}
+
+TEST(MonoInputVolumeControllerTest, CheckClippedLevelMinIsEffective) {
+ constexpr int kInitialInputVolume = 100;
+ constexpr int kClippedLevelMin = 70;
+ MonoInputVolumeController mono_controller_1(
+ kClippedLevelMin,
+ /*min_mic_level=*/84,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ MonoInputVolumeController mono_controller_2(
+ kClippedLevelMin,
+ /*min_mic_level=*/84,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ mono_controller_1.Initialize();
+ mono_controller_2.Initialize();
+
+ // Process one frame to reset the state for `HandleClipping()`.
+ EXPECT_EQ(UpdateRecommendedInputVolume(mono_controller_1, kInitialInputVolume,
+ kLowSpeechProbability, -10.0f),
+ kInitialInputVolume);
+ EXPECT_EQ(UpdateRecommendedInputVolume(mono_controller_2, kInitialInputVolume,
+ kLowSpeechProbability, -10.0f),
+ kInitialInputVolume);
+
+ mono_controller_1.HandleClipping(29);
+ mono_controller_2.HandleClipping(31);
+
+ EXPECT_EQ(mono_controller_2.recommended_analog_level(), kClippedLevelMin);
+ EXPECT_LT(mono_controller_2.recommended_analog_level(),
+ mono_controller_1.recommended_analog_level());
+}
+
+TEST(MonoInputVolumeControllerTest, CheckMinMicLevelIsEffective) {
+ constexpr int kInitialInputVolume = 100;
+ constexpr int kMinMicLevel = 64;
+ MonoInputVolumeController mono_controller_1(
+ /*clipped_level_min=*/64, kMinMicLevel,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ MonoInputVolumeController mono_controller_2(
+ /*clipped_level_min=*/64, kMinMicLevel,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ mono_controller_1.Initialize();
+ mono_controller_2.Initialize();
+
+ int volume_1 = UpdateRecommendedInputVolume(
+ mono_controller_1, kInitialInputVolume, kHighSpeechProbability, -10.0f);
+ int volume_2 = UpdateRecommendedInputVolume(
+ mono_controller_2, kInitialInputVolume, kHighSpeechProbability, -10.0f);
+
+ EXPECT_EQ(volume_1, kInitialInputVolume);
+ EXPECT_EQ(volume_2, kInitialInputVolume);
+
+ volume_1 = UpdateRecommendedInputVolume(mono_controller_1, volume_1,
+ kHighSpeechProbability, -10.0f);
+ volume_2 = UpdateRecommendedInputVolume(mono_controller_2, volume_2,
+ kHighSpeechProbability, -30.0f);
+
+ EXPECT_LT(volume_1, kInitialInputVolume);
+ EXPECT_LT(volume_2, volume_1);
+ EXPECT_EQ(volume_2, kMinMicLevel);
+}
+
+TEST(MonoInputVolumeControllerTest,
+ CheckUpdateInputVolumeWaitFramesIsEffective) {
+ constexpr int kInitialInputVolume = 100;
+ MonoInputVolumeController mono_controller_1(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/84,
+ /*update_input_volume_wait_frames=*/1, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ MonoInputVolumeController mono_controller_2(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/84,
+ /*update_input_volume_wait_frames=*/3, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ mono_controller_1.Initialize();
+ mono_controller_2.Initialize();
+
+ int volume_1 = UpdateRecommendedInputVolume(
+ mono_controller_1, kInitialInputVolume, kHighSpeechProbability, -10.0f);
+ int volume_2 = UpdateRecommendedInputVolume(
+ mono_controller_2, kInitialInputVolume, kHighSpeechProbability, -10.0f);
+
+ EXPECT_EQ(volume_1, kInitialInputVolume);
+ EXPECT_EQ(volume_2, kInitialInputVolume);
+
+ volume_1 = UpdateRecommendedInputVolume(mono_controller_1, volume_1,
+ kHighSpeechProbability, -10.0f);
+ volume_2 = UpdateRecommendedInputVolume(mono_controller_2, volume_2,
+ kHighSpeechProbability, -10.0f);
+
+ EXPECT_LT(volume_1, kInitialInputVolume);
+ EXPECT_EQ(volume_2, kInitialInputVolume);
+
+ volume_2 = UpdateRecommendedInputVolume(mono_controller_2, volume_2,
+ kHighSpeechProbability, -10.0f);
+
+ EXPECT_LT(volume_2, kInitialInputVolume);
+}
+
+TEST(MonoInputVolumeControllerTest,
+ CheckSpeechProbabilityThresholdIsEffective) {
+ constexpr int kInitialInputVolume = 100;
+ constexpr float kSpeechProbabilityThreshold = 0.8f;
+ MonoInputVolumeController mono_controller_1(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/84,
+ /*update_input_volume_wait_frames=*/2, kSpeechProbabilityThreshold,
+ kSpeechRatioThreshold);
+ MonoInputVolumeController mono_controller_2(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/84,
+ /*update_input_volume_wait_frames=*/2, kSpeechProbabilityThreshold,
+ kSpeechRatioThreshold);
+ mono_controller_1.Initialize();
+ mono_controller_2.Initialize();
+
+ int volume_1 =
+ UpdateRecommendedInputVolume(mono_controller_1, kInitialInputVolume,
+ kSpeechProbabilityThreshold, -10.0f);
+ int volume_2 =
+ UpdateRecommendedInputVolume(mono_controller_2, kInitialInputVolume,
+ kSpeechProbabilityThreshold, -10.0f);
+
+ EXPECT_EQ(volume_1, kInitialInputVolume);
+ EXPECT_EQ(volume_2, kInitialInputVolume);
+
+ volume_1 = UpdateRecommendedInputVolume(
+ mono_controller_1, volume_1, kSpeechProbabilityThreshold - 0.1f, -10.0f);
+ volume_2 = UpdateRecommendedInputVolume(mono_controller_2, volume_2,
+ kSpeechProbabilityThreshold, -10.0f);
+
+ EXPECT_EQ(volume_1, kInitialInputVolume);
+ EXPECT_LT(volume_2, volume_1);
+}
+
+TEST(MonoInputVolumeControllerTest, CheckSpeechRatioThresholdIsEffective) {
+ constexpr int kInitialInputVolume = 100;
+ MonoInputVolumeController mono_controller_1(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/84,
+ /*update_input_volume_wait_frames=*/4, kHighSpeechProbability,
+ /*speech_ratio_threshold=*/0.75f);
+ MonoInputVolumeController mono_controller_2(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/84,
+ /*update_input_volume_wait_frames=*/4, kHighSpeechProbability,
+ /*speech_ratio_threshold=*/0.75f);
+ mono_controller_1.Initialize();
+ mono_controller_2.Initialize();
+
+ int volume_1 = UpdateRecommendedInputVolume(
+ mono_controller_1, kInitialInputVolume, kHighSpeechProbability, -10.0f);
+ int volume_2 = UpdateRecommendedInputVolume(
+ mono_controller_2, kInitialInputVolume, kHighSpeechProbability, -10.0f);
+
+ volume_1 = UpdateRecommendedInputVolume(mono_controller_1, volume_1,
+ kHighSpeechProbability, -10.0f);
+ volume_2 = UpdateRecommendedInputVolume(mono_controller_2, volume_2,
+ kHighSpeechProbability, -10.0f);
+
+ volume_1 = UpdateRecommendedInputVolume(mono_controller_1, volume_1,
+ kLowSpeechProbability, -10.0f);
+ volume_2 = UpdateRecommendedInputVolume(mono_controller_2, volume_2,
+ kLowSpeechProbability, -10.0f);
+
+ EXPECT_EQ(volume_1, kInitialInputVolume);
+ EXPECT_EQ(volume_2, kInitialInputVolume);
+
+ volume_1 = UpdateRecommendedInputVolume(mono_controller_1, volume_1,
+ kLowSpeechProbability, -10.0f);
+ volume_2 = UpdateRecommendedInputVolume(mono_controller_2, volume_2,
+ kHighSpeechProbability, -10.0f);
+
+ EXPECT_EQ(volume_1, kInitialInputVolume);
+ EXPECT_LT(volume_2, volume_1);
+}
+
+TEST(MonoInputVolumeControllerTest,
+ CheckProcessEmptyRmsErrorDoesNotLowerVolume) {
+ constexpr int kInitialInputVolume = 100;
+ MonoInputVolumeController mono_controller_1(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/84,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ MonoInputVolumeController mono_controller_2(
+ /*clipped_level_min=*/64,
+ /*min_mic_level=*/84,
+ /*update_input_volume_wait_frames=*/2, kHighSpeechProbability,
+ kSpeechRatioThreshold);
+ mono_controller_1.Initialize();
+ mono_controller_2.Initialize();
+
+ int volume_1 = UpdateRecommendedInputVolume(
+ mono_controller_1, kInitialInputVolume, kHighSpeechProbability, -10.0f);
+ int volume_2 = UpdateRecommendedInputVolume(
+ mono_controller_2, kInitialInputVolume, kHighSpeechProbability, -10.0f);
+
+ EXPECT_EQ(volume_1, kInitialInputVolume);
+ EXPECT_EQ(volume_2, kInitialInputVolume);
+
+ volume_1 = UpdateRecommendedInputVolume(
+ mono_controller_1, volume_1, kHighSpeechProbability, absl::nullopt);
+ volume_2 = UpdateRecommendedInputVolume(mono_controller_2, volume_2,
+ kHighSpeechProbability, -10.0f);
+
+ EXPECT_EQ(volume_1, kInitialInputVolume);
+ EXPECT_LT(volume_2, volume_1);
+}
+
+} // namespace webrtc