| /* | |
| * 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 <cmath> | |
| #include <cstdint> | |
| #include <fstream> | |
| #include <ios> | |
| #include <limits> | |
| #include <memory> | |
| #include <optional> | |
| #include <tuple> | |
| #include <vector> | |
| #include "api/audio/audio_processing.h" | |
| #include "api/environment/environment_factory.h" | |
| #include "modules/audio_processing/audio_buffer.h" | |
| #include "rtc_base/checks.h" | |
| #include "rtc_base/numerics/safe_minmax.h" | |
| #include "system_wrappers/include/metrics.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 kDefaultInitialInputVolume = 128; | |
| constexpr int kClippedMin = 165; // Arbitrary, but different from the default. | |
| constexpr float kAboveClippedThreshold = 0.2f; | |
| 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 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 = 20, | |
| .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 = 0.5f, | |
| .speech_ratio_threshold = kSpeechRatioThreshold, | |
| }; | |
| return std::make_unique<InputVolumeController>( | |
| /*num_capture_channels=*/1, config, CreateEnvironment().field_trials()); | |
| } | |
| // (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, | |
| std::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, | |
| std::ifstream::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 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, | |
| std::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, | |
| CreateEnvironment().field_trials()) { | |
| 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. | |
| std::optional<int> CallAgcSequence(int applied_input_volume, | |
| float speech_probability, | |
| std::optional<float> speech_level_dbfs, | |
| int num_calls = 1) { | |
| RTC_DCHECK_GE(num_calls, 1); | |
| std::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, | |
| std::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, | |
| CreateEnvironment().field_trials()); | |
| 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(kDefaultInitialInputVolume, | |
| 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(kDefaultInitialInputVolume, | |
| 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(kDefaultInitialInputVolume, 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(kDefaultInitialInputVolume, 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(kDefaultInitialInputVolume, | |
| 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(kDefaultInitialInputVolume, | |
| 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(kDefaultInitialInputVolume, | |
| 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(kDefaultInitialInputVolume, | |
| 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(kDefaultInitialInputVolume, 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(kDefaultInitialInputVolume, 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(kDefaultInitialInputVolume, 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(), | |
| CreateEnvironment().field_trials()); | |
| 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, kDefaultInitialInputVolume, kGainDb, | |
| kLowSpeechProbability, std::nullopt, controller); | |
| // Check that no adaptation occurs. | |
| ASSERT_EQ(volume, kDefaultInitialInputVolume); | |
| } | |
| // 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 = kDefaultInitialInputVolume; | |
| 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 = kDefaultInitialInputVolume; | |
| // 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 = kDefaultInitialInputVolume; | |
| // 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, std::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 |