/* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ #include "modules/audio_processing/agc2/saturation_protector.h" #include "modules/audio_processing/agc2/agc2_common.h" #include "modules/audio_processing/logging/apm_data_dumper.h" #include "rtc_base/gunit.h" namespace webrtc { namespace { constexpr float kInitialHeadroomDb = 20.0f; constexpr int kNoAdjacentSpeechFramesRequired = 1; constexpr float kMaxSpeechProbability = 1.0f; // Calls `Analyze(speech_probability, peak_dbfs, speech_level_dbfs)` // `num_iterations` times on `saturation_protector` and return the largest // headroom difference between two consecutive calls. float RunOnConstantLevel(int num_iterations, float speech_probability, float peak_dbfs, float speech_level_dbfs, SaturationProtector& saturation_protector) { float last_headroom = saturation_protector.HeadroomDb(); float max_difference = 0.0f; for (int i = 0; i < num_iterations; ++i) { saturation_protector.Analyze(speech_probability, peak_dbfs, speech_level_dbfs); const float new_headroom = saturation_protector.HeadroomDb(); max_difference = std::max(max_difference, std::fabs(new_headroom - last_headroom)); last_headroom = new_headroom; } return max_difference; } // Checks that the returned headroom value is correctly reset. TEST(GainController2SaturationProtector, Reset) { ApmDataDumper apm_data_dumper(0); auto saturation_protector = CreateSaturationProtector( kInitialHeadroomDb, kNoAdjacentSpeechFramesRequired, &apm_data_dumper); const float initial_headroom_db = saturation_protector->HeadroomDb(); RunOnConstantLevel(/*num_iterations=*/10, kMaxSpeechProbability, /*peak_dbfs=*/0.0f, /*speech_level_dbfs=*/-10.0f, *saturation_protector); // Make sure that there are side-effects. ASSERT_NE(initial_headroom_db, saturation_protector->HeadroomDb()); saturation_protector->Reset(); EXPECT_EQ(initial_headroom_db, saturation_protector->HeadroomDb()); } // Checks that the estimate converges to the ratio between peaks and level // estimator values after a while. TEST(GainController2SaturationProtector, EstimatesCrestRatio) { constexpr int kNumIterations = 2000; constexpr float kPeakLevelDbfs = -20.0f; constexpr float kCrestFactorDb = kInitialHeadroomDb + 1.0f; constexpr float kSpeechLevelDbfs = kPeakLevelDbfs - kCrestFactorDb; const float kMaxDifferenceDb = 0.5f * std::fabs(kInitialHeadroomDb - kCrestFactorDb); ApmDataDumper apm_data_dumper(0); auto saturation_protector = CreateSaturationProtector( kInitialHeadroomDb, kNoAdjacentSpeechFramesRequired, &apm_data_dumper); RunOnConstantLevel(kNumIterations, kMaxSpeechProbability, kPeakLevelDbfs, kSpeechLevelDbfs, *saturation_protector); EXPECT_NEAR(saturation_protector->HeadroomDb(), kCrestFactorDb, kMaxDifferenceDb); } // Checks that the headroom does not change too quickly. TEST(GainController2SaturationProtector, ChangeSlowly) { constexpr int kNumIterations = 1000; constexpr float kPeakLevelDbfs = -20.f; constexpr float kCrestFactorDb = kInitialHeadroomDb - 5.f; constexpr float kOtherCrestFactorDb = kInitialHeadroomDb; constexpr float kSpeechLevelDbfs = kPeakLevelDbfs - kCrestFactorDb; constexpr float kOtherSpeechLevelDbfs = kPeakLevelDbfs - kOtherCrestFactorDb; ApmDataDumper apm_data_dumper(0); auto saturation_protector = CreateSaturationProtector( kInitialHeadroomDb, kNoAdjacentSpeechFramesRequired, &apm_data_dumper); float max_difference_db = RunOnConstantLevel(kNumIterations, kMaxSpeechProbability, kPeakLevelDbfs, kSpeechLevelDbfs, *saturation_protector); max_difference_db = std::max( RunOnConstantLevel(kNumIterations, kMaxSpeechProbability, kPeakLevelDbfs, kOtherSpeechLevelDbfs, *saturation_protector), max_difference_db); constexpr float kMaxChangeSpeedDbPerSecond = 0.5f; // 1 db / 2 seconds. EXPECT_LE(max_difference_db, kMaxChangeSpeedDbPerSecond / 1000 * kFrameDurationMs); } class SaturationProtectorParametrization : public ::testing::TestWithParam { protected: int adjacent_speech_frames_threshold() const { return GetParam(); } }; TEST_P(SaturationProtectorParametrization, DoNotAdaptToShortSpeechSegments) { ApmDataDumper apm_data_dumper(0); auto saturation_protector = CreateSaturationProtector( kInitialHeadroomDb, adjacent_speech_frames_threshold(), &apm_data_dumper); const float initial_headroom_db = saturation_protector->HeadroomDb(); RunOnConstantLevel(/*num_iterations=*/adjacent_speech_frames_threshold() - 1, kMaxSpeechProbability, /*peak_dbfs=*/0.0f, /*speech_level_dbfs=*/-10.0f, *saturation_protector); // No adaptation expected. EXPECT_EQ(initial_headroom_db, saturation_protector->HeadroomDb()); } TEST_P(SaturationProtectorParametrization, AdaptToEnoughSpeechSegments) { ApmDataDumper apm_data_dumper(0); auto saturation_protector = CreateSaturationProtector( kInitialHeadroomDb, adjacent_speech_frames_threshold(), &apm_data_dumper); const float initial_headroom_db = saturation_protector->HeadroomDb(); RunOnConstantLevel(/*num_iterations=*/adjacent_speech_frames_threshold() + 1, kMaxSpeechProbability, /*peak_dbfs=*/0.0f, /*speech_level_dbfs=*/-10.0f, *saturation_protector); // Adaptation expected. EXPECT_NE(initial_headroom_db, saturation_protector->HeadroomDb()); } INSTANTIATE_TEST_SUITE_P(GainController2, SaturationProtectorParametrization, ::testing::Values(2, 9, 17)); } // namespace } // namespace webrtc