| /* |
| * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. |
| * |
| * Use of this source code is governed by a BSD-style license |
| * that can be found in the LICENSE file in the root of the source |
| * tree. An additional intellectual property rights grant can be found |
| * in the file PATENTS. All contributing project authors may |
| * be found in the AUTHORS file in the root of the source tree. |
| */ |
| |
| #include "modules/audio_processing/agc/clipping_predictor.h" |
| |
| #include <tuple> |
| |
| #include "rtc_base/checks.h" |
| #include "test/gmock.h" |
| #include "test/gtest.h" |
| |
| namespace webrtc { |
| namespace { |
| |
| using ::testing::Eq; |
| using ::testing::Optional; |
| |
| constexpr int kSampleRateHz = 32000; |
| constexpr int kNumChannels = 1; |
| constexpr int kSamplesPerChannel = kSampleRateHz / 100; |
| constexpr int kWindowLength = 5; |
| constexpr int kReferenceWindowLength = 5; |
| constexpr int kReferenceWindowDelay = 5; |
| constexpr int kMaxMicLevel = 255; |
| constexpr int kMinMicLevel = 12; |
| constexpr int kDefaultClippedLevelStep = 15; |
| |
| using ClippingPredictorConfig = AudioProcessing::Config::GainController1:: |
| AnalogGainController::ClippingPredictor; |
| |
| void CallProcess(int num_calls, |
| const AudioFrameView<const float>& frame, |
| ClippingPredictor& predictor) { |
| for (int i = 0; i < num_calls; ++i) { |
| predictor.Process(frame); |
| } |
| } |
| |
| // Creates and processes an audio frame with a non-zero (approx. 4.15dB) crest |
| // factor. |
| void ProcessNonZeroCrestFactorAudio(int num_calls, |
| int num_channels, |
| float peak_ratio, |
| ClippingPredictor& predictor) { |
| RTC_DCHECK_GT(num_calls, 0); |
| RTC_DCHECK_GT(num_channels, 0); |
| RTC_DCHECK_LE(peak_ratio, 1.f); |
| std::vector<float*> audio(num_channels); |
| std::vector<float> audio_data(num_channels * kSamplesPerChannel, 0.f); |
| for (int channel = 0; channel < num_channels; ++channel) { |
| audio[channel] = &audio_data[channel * kSamplesPerChannel]; |
| for (int sample = 0; sample < kSamplesPerChannel; sample += 10) { |
| audio[channel][sample] = 0.1f * peak_ratio * 32767.f; |
| audio[channel][sample + 1] = 0.2f * peak_ratio * 32767.f; |
| audio[channel][sample + 2] = 0.3f * peak_ratio * 32767.f; |
| audio[channel][sample + 3] = 0.4f * peak_ratio * 32767.f; |
| audio[channel][sample + 4] = 0.5f * peak_ratio * 32767.f; |
| audio[channel][sample + 5] = 0.6f * peak_ratio * 32767.f; |
| audio[channel][sample + 6] = 0.7f * peak_ratio * 32767.f; |
| audio[channel][sample + 7] = 0.8f * peak_ratio * 32767.f; |
| audio[channel][sample + 8] = 0.9f * peak_ratio * 32767.f; |
| audio[channel][sample + 9] = 1.f * peak_ratio * 32767.f; |
| } |
| } |
| auto frame = AudioFrameView<const float>(audio.data(), num_channels, |
| kSamplesPerChannel); |
| CallProcess(num_calls, frame, predictor); |
| } |
| |
| void CheckChannelEstimatesWithValue(int num_channels, |
| int level, |
| int default_step, |
| int min_mic_level, |
| int max_mic_level, |
| const ClippingPredictor& predictor, |
| int expected) { |
| for (int i = 0; i < num_channels; ++i) { |
| EXPECT_THAT(predictor.EstimateClippedLevelStep( |
| i, level, default_step, min_mic_level, max_mic_level), |
| Optional(Eq(expected))); |
| } |
| } |
| |
| void CheckChannelEstimatesWithoutValue(int num_channels, |
| int level, |
| int default_step, |
| int min_mic_level, |
| int max_mic_level, |
| const ClippingPredictor& predictor) { |
| for (int i = 0; i < num_channels; ++i) { |
| EXPECT_EQ(predictor.EstimateClippedLevelStep(i, level, default_step, |
| min_mic_level, max_mic_level), |
| absl::nullopt); |
| } |
| } |
| |
| // Creates and processes an audio frame with a zero crest factor. |
| void ProcessZeroCrestFactorAudio(int num_calls, |
| int num_channels, |
| float peak_ratio, |
| ClippingPredictor& predictor) { |
| RTC_DCHECK_GT(num_calls, 0); |
| RTC_DCHECK_GT(num_channels, 0); |
| RTC_DCHECK_LE(peak_ratio, 1.f); |
| std::vector<float*> audio(num_channels); |
| std::vector<float> audio_data(num_channels * kSamplesPerChannel, 0.f); |
| for (int channel = 0; channel < num_channels; ++channel) { |
| audio[channel] = &audio_data[channel * kSamplesPerChannel]; |
| for (int sample = 0; sample < kSamplesPerChannel; ++sample) { |
| audio[channel][sample] = peak_ratio * 32767.f; |
| } |
| } |
| auto frame = AudioFrameView<const float>(audio.data(), num_channels, |
| kSamplesPerChannel); |
| CallProcess(num_calls, frame, predictor); |
| } |
| |
| class ClippingPredictorParameterization |
| : public ::testing::TestWithParam<std::tuple<int, int, int, int>> { |
| protected: |
| int num_channels() const { return std::get<0>(GetParam()); } |
| int window_length() const { return std::get<1>(GetParam()); } |
| int reference_window_length() const { return std::get<2>(GetParam()); } |
| int reference_window_delay() const { return std::get<3>(GetParam()); } |
| }; |
| |
| class ClippingEventPredictorParameterization |
| : public ::testing::TestWithParam<std::tuple<float, float>> { |
| protected: |
| float clipping_threshold() const { return std::get<0>(GetParam()); } |
| float crest_factor_margin() const { return std::get<1>(GetParam()); } |
| }; |
| |
| class ClippingPeakPredictorParameterization |
| : public ::testing::TestWithParam<std::tuple<bool, float>> { |
| protected: |
| float adaptive_step_estimation() const { return std::get<0>(GetParam()); } |
| float clipping_threshold() const { return std::get<1>(GetParam()); } |
| }; |
| |
| TEST_P(ClippingPredictorParameterization, |
| CheckClippingEventPredictorEstimateAfterCrestFactorDrop) { |
| if (reference_window_length() + reference_window_delay() > window_length()) { |
| ClippingPredictorConfig config; |
| config.window_length = window_length(); |
| config.reference_window_length = reference_window_length(); |
| config.reference_window_delay = reference_window_delay(); |
| config.clipping_threshold = -1.0f; |
| config.crest_factor_margin = 0.5f; |
| auto predictor = CreateClippingEventPredictor(num_channels(), config); |
| ProcessNonZeroCrestFactorAudio( |
| reference_window_length() + reference_window_delay() - window_length(), |
| num_channels(), /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| ProcessZeroCrestFactorAudio(window_length(), num_channels(), |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithValue( |
| num_channels(), /*level=*/255, kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor, kDefaultClippedLevelStep); |
| } |
| } |
| |
| TEST_P(ClippingPredictorParameterization, |
| CheckClippingEventPredictorNoEstimateAfterConstantCrestFactor) { |
| if (reference_window_length() + reference_window_delay() > window_length()) { |
| ClippingPredictorConfig config; |
| config.window_length = window_length(); |
| config.reference_window_length = reference_window_length(); |
| config.reference_window_delay = reference_window_delay(); |
| config.clipping_threshold = -1.0f; |
| config.crest_factor_margin = 0.5f; |
| auto predictor = CreateClippingEventPredictor(num_channels(), config); |
| ProcessNonZeroCrestFactorAudio( |
| reference_window_length() + reference_window_delay() - window_length(), |
| num_channels(), /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| ProcessNonZeroCrestFactorAudio(window_length(), num_channels(), |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| } |
| } |
| |
| TEST_P(ClippingPredictorParameterization, |
| CheckClippingPeakPredictorEstimateAfterHighCrestFactor) { |
| if (reference_window_length() + reference_window_delay() > window_length()) { |
| ClippingPredictorConfig config; |
| config.window_length = window_length(); |
| config.reference_window_length = reference_window_length(); |
| config.reference_window_delay = reference_window_delay(); |
| config.clipping_threshold = -1.0f; |
| auto predictor = |
| CreateAdaptiveStepClippingPeakPredictor(num_channels(), config); |
| ProcessNonZeroCrestFactorAudio( |
| reference_window_length() + reference_window_delay() - window_length(), |
| num_channels(), /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| ProcessNonZeroCrestFactorAudio(window_length(), num_channels(), |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithValue( |
| num_channels(), /*level=*/255, kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor, kDefaultClippedLevelStep); |
| } |
| } |
| |
| TEST_P(ClippingPredictorParameterization, |
| CheckClippingPeakPredictorNoEstimateAfterLowCrestFactor) { |
| if (reference_window_length() + reference_window_delay() > window_length()) { |
| ClippingPredictorConfig config; |
| config.window_length = window_length(); |
| config.reference_window_length = reference_window_length(); |
| config.reference_window_delay = reference_window_delay(); |
| config.clipping_threshold = -1.0f; |
| auto predictor = |
| CreateAdaptiveStepClippingPeakPredictor(num_channels(), config); |
| ProcessZeroCrestFactorAudio( |
| reference_window_length() + reference_window_delay() - window_length(), |
| num_channels(), /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| ProcessNonZeroCrestFactorAudio(window_length(), num_channels(), |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(num_channels(), /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(GainController1ClippingPredictor, |
| ClippingPredictorParameterization, |
| ::testing::Combine(::testing::Values(1, 5), |
| ::testing::Values(1, 5, 10), |
| ::testing::Values(1, 5), |
| ::testing::Values(0, 1, 5))); |
| |
| TEST_P(ClippingEventPredictorParameterization, |
| CheckEstimateAfterCrestFactorDrop) { |
| ClippingPredictorConfig config; |
| config.window_length = kWindowLength; |
| config.reference_window_length = kReferenceWindowLength; |
| config.reference_window_delay = kReferenceWindowDelay; |
| config.clipping_threshold = clipping_threshold(); |
| config.crest_factor_margin = crest_factor_margin(); |
| auto predictor = CreateClippingEventPredictor(kNumChannels, config); |
| ProcessNonZeroCrestFactorAudio(kReferenceWindowLength, kNumChannels, |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| ProcessZeroCrestFactorAudio(kWindowLength, kNumChannels, /*peak_ratio=*/0.99f, |
| *predictor); |
| if (clipping_threshold() < 20 * std::log10f(0.99f) && |
| crest_factor_margin() < 4.15f) { |
| CheckChannelEstimatesWithValue( |
| kNumChannels, /*level=*/255, kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor, kDefaultClippedLevelStep); |
| } else { |
| CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(GainController1ClippingPredictor, |
| ClippingEventPredictorParameterization, |
| ::testing::Combine(::testing::Values(-1.0f, 0.0f), |
| ::testing::Values(3.0f, 4.16f))); |
| |
| TEST_P(ClippingPeakPredictorParameterization, |
| CheckEstimateAfterHighCrestFactor) { |
| ClippingPredictorConfig config; |
| config.window_length = kWindowLength; |
| config.reference_window_length = kReferenceWindowLength; |
| config.reference_window_delay = kReferenceWindowDelay; |
| config.clipping_threshold = clipping_threshold(); |
| auto predictor = |
| adaptive_step_estimation() |
| ? CreateAdaptiveStepClippingPeakPredictor(kNumChannels, config) |
| : CreateFixedStepClippingPeakPredictor(kNumChannels, config); |
| ProcessNonZeroCrestFactorAudio(kReferenceWindowLength, kNumChannels, |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| ProcessZeroCrestFactorAudio(kWindowLength, kNumChannels, |
| /*peak_ratio=*/0.99f, *predictor); |
| if (clipping_threshold() < 20 * std::log10(0.99f)) { |
| if (adaptive_step_estimation()) { |
| CheckChannelEstimatesWithValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor, |
| /*expected=*/17); |
| } else { |
| CheckChannelEstimatesWithValue( |
| kNumChannels, /*level=*/255, kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor, kDefaultClippedLevelStep); |
| } |
| } else { |
| CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(GainController1ClippingPredictor, |
| ClippingPeakPredictorParameterization, |
| ::testing::Combine(::testing::Values(true, false), |
| ::testing::Values(-1.0f, 0.0f))); |
| |
| TEST(ClippingEventPredictorTest, CheckEstimateAfterReset) { |
| ClippingPredictorConfig config; |
| config.window_length = kWindowLength; |
| config.reference_window_length = kReferenceWindowLength; |
| config.reference_window_delay = kReferenceWindowDelay; |
| config.clipping_threshold = -1.0f; |
| config.crest_factor_margin = 3.0f; |
| auto predictor = CreateClippingEventPredictor(kNumChannels, config); |
| ProcessNonZeroCrestFactorAudio(kReferenceWindowLength, kNumChannels, |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| predictor->Reset(); |
| ProcessZeroCrestFactorAudio(kWindowLength, kNumChannels, |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| } |
| |
| TEST(ClippingPeakPredictorTest, CheckNoEstimateAfterReset) { |
| ClippingPredictorConfig config; |
| config.window_length = kWindowLength; |
| config.reference_window_length = kReferenceWindowLength; |
| config.reference_window_delay = kReferenceWindowDelay; |
| config.clipping_threshold = -1.0f; |
| config.crest_factor_margin = 3.0f; |
| auto predictor = |
| CreateAdaptiveStepClippingPeakPredictor(kNumChannels, config); |
| ProcessNonZeroCrestFactorAudio(kReferenceWindowLength, kNumChannels, |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| predictor->Reset(); |
| ProcessZeroCrestFactorAudio(kWindowLength, kNumChannels, |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| } |
| |
| TEST(ClippingPeakPredictorTest, CheckAdaptiveStepEstimate) { |
| ClippingPredictorConfig config; |
| config.window_length = kWindowLength; |
| config.reference_window_length = kReferenceWindowLength; |
| config.reference_window_delay = kReferenceWindowDelay; |
| config.clipping_threshold = -1.0f; |
| auto predictor = |
| CreateAdaptiveStepClippingPeakPredictor(kNumChannels, config); |
| ProcessNonZeroCrestFactorAudio(kReferenceWindowLength, kNumChannels, |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| ProcessZeroCrestFactorAudio(kWindowLength, kNumChannels, |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor, /*expected=*/17); |
| } |
| |
| TEST(ClippingPeakPredictorTest, CheckFixedStepEstimate) { |
| ClippingPredictorConfig config; |
| config.window_length = kWindowLength; |
| config.reference_window_length = kReferenceWindowLength; |
| config.reference_window_delay = kReferenceWindowDelay; |
| config.clipping_threshold = -1.0f; |
| auto predictor = CreateFixedStepClippingPeakPredictor(kNumChannels, config); |
| ProcessNonZeroCrestFactorAudio(kReferenceWindowLength, kNumChannels, |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithoutValue(kNumChannels, /*level=*/255, |
| kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor); |
| ProcessZeroCrestFactorAudio(kWindowLength, kNumChannels, |
| /*peak_ratio=*/0.99f, *predictor); |
| CheckChannelEstimatesWithValue( |
| kNumChannels, /*level=*/255, kDefaultClippedLevelStep, kMinMicLevel, |
| kMaxMicLevel, *predictor, kDefaultClippedLevelStep); |
| } |
| |
| } // namespace |
| } // namespace webrtc |