diff --git a/modules/audio_processing/agc2/BUILD.gn b/modules/audio_processing/agc2/BUILD.gn index 15f730491c..cc145fb43f 100644 --- a/modules/audio_processing/agc2/BUILD.gn +++ b/modules/audio_processing/agc2/BUILD.gn @@ -31,6 +31,7 @@ rtc_source_set("adaptive_digital") { deps = [ ":common", + ":gain_applier", ":noise_level_estimator", "..:aec_core", "..:apm_logging", @@ -108,6 +109,18 @@ rtc_source_set("noise_level_estimator") { configs += [ "..:apm_debug_dump" ] } +rtc_source_set("gain_applier") { + sources = [ + "gain_applier.cc", + "gain_applier.h", + ] + deps = [ + ":common", + "..:audio_frame_view", + "../../../rtc_base:safe_minmax", + ] +} + rtc_source_set("test_utils") { testonly = true visibility = [ ":*" ] @@ -161,11 +174,13 @@ rtc_source_set("adaptive_digital_unittests") { sources = [ "adaptive_digital_gain_applier_unittest.cc", "adaptive_mode_level_estimator_unittest.cc", + "gain_applier_unittest.cc", "saturation_protector_unittest.cc", ] deps = [ ":adaptive_digital", ":common", + ":gain_applier", ":test_utils", "..:apm_logging", "..:audio_frame_view", diff --git a/modules/audio_processing/agc2/adaptive_digital_gain_applier.cc b/modules/audio_processing/agc2/adaptive_digital_gain_applier.cc index af37d24060..74facc6dd2 100644 --- a/modules/audio_processing/agc2/adaptive_digital_gain_applier.cc +++ b/modules/audio_processing/agc2/adaptive_digital_gain_applier.cc @@ -64,52 +64,13 @@ float ComputeGainChangeThisFrameDb(float target_gain_db, return rtc::SafeClamp(target_gain_difference_db, -kMaxGainChangePerFrameDb, kMaxGainChangePerFrameDb); } - -// Returns true when the gain factor is so close to 1 that it would -// not affect int16 samples. -bool GainCloseToOne(float gain_factor) { - return 1.f - 1.f / kMaxFloatS16Value <= gain_factor && - gain_factor <= 1.f + 1.f / kMaxFloatS16Value; -} - -void ApplyGainWithRamping(float last_gain_linear, - float gain_at_end_of_frame_linear, - AudioFrameView float_frame) { - // Do not modify the signal when input is loud. - if (last_gain_linear == gain_at_end_of_frame_linear && - GainCloseToOne(gain_at_end_of_frame_linear)) { - return; - } - - // A typical case: gain is constant and different from 1. - if (last_gain_linear == gain_at_end_of_frame_linear) { - for (size_t k = 0; k < float_frame.num_channels(); ++k) { - rtc::ArrayView channel_view = float_frame.channel(k); - for (auto& sample : channel_view) { - sample *= gain_at_end_of_frame_linear; - } - } - return; - } - - // The gain changes. We have to change slowly to avoid discontinuities. - const size_t samples = float_frame.samples_per_channel(); - RTC_DCHECK_GT(samples, 0); - const float increment = - (gain_at_end_of_frame_linear - last_gain_linear) / samples; - float gain = last_gain_linear; - for (size_t i = 0; i < samples; ++i) { - for (size_t ch = 0; ch < float_frame.num_channels(); ++ch) { - float_frame.channel(ch)[i] *= gain; - } - gain += increment; - } -} } // namespace AdaptiveDigitalGainApplier::AdaptiveDigitalGainApplier( ApmDataDumper* apm_data_dumper) - : apm_data_dumper_(apm_data_dumper) {} + : gain_applier_(false, 1.f), // Initial gain is 1, and we do not + // clip after gain. + apm_data_dumper_(apm_data_dumper) {} void AdaptiveDigitalGainApplier::Process( float input_level_dbfs, @@ -151,15 +112,13 @@ void AdaptiveDigitalGainApplier::Process( // Optimization: avoid calling math functions if gain does not // change. - const float gain_at_end_of_frame = - gain_change_this_frame_db == 0.f - ? last_gain_linear_ - : DbToRatio(last_gain_db_ + gain_change_this_frame_db); - - ApplyGainWithRamping(last_gain_linear_, gain_at_end_of_frame, float_frame); + if (gain_change_this_frame_db != 0.f) { + gain_applier_.SetGainFactor( + DbToRatio(last_gain_db_ + gain_change_this_frame_db)); + } + gain_applier_.ApplyGain(float_frame); // Remember that the gain has changed for the next iteration. - last_gain_linear_ = gain_at_end_of_frame; last_gain_db_ = last_gain_db_ + gain_change_this_frame_db; apm_data_dumper_->DumpRaw("agc2_applied_gain_db", last_gain_db_); } diff --git a/modules/audio_processing/agc2/adaptive_digital_gain_applier.h b/modules/audio_processing/agc2/adaptive_digital_gain_applier.h index 748ab02633..b33ccbac66 100644 --- a/modules/audio_processing/agc2/adaptive_digital_gain_applier.h +++ b/modules/audio_processing/agc2/adaptive_digital_gain_applier.h @@ -11,6 +11,7 @@ #ifndef MODULES_AUDIO_PROCESSING_AGC2_ADAPTIVE_DIGITAL_GAIN_APPLIER_H_ #define MODULES_AUDIO_PROCESSING_AGC2_ADAPTIVE_DIGITAL_GAIN_APPLIER_H_ +#include "modules/audio_processing/agc2/gain_applier.h" #include "modules/audio_processing/include/audio_frame_view.h" #include "modules/audio_processing/vad/vad_with_level.h" @@ -29,11 +30,8 @@ class AdaptiveDigitalGainApplier { AudioFrameView float_frame); private: - // Keep track of current gain for ramping up and down and - // logging. This member variable is redundant together with - // last_gain_db_. Both are kept as an optimization. - float last_gain_linear_ = 1.f; float last_gain_db_ = 0.f; + GainApplier gain_applier_; // For some combinations of noise and speech probability, increasing // the level is not allowed. Since we may get VAD results in bursts, diff --git a/modules/audio_processing/agc2/fixed_gain_controller.cc b/modules/audio_processing/agc2/fixed_gain_controller.cc index ceaa10b9c6..e59ae346fe 100644 --- a/modules/audio_processing/agc2/fixed_gain_controller.cc +++ b/modules/audio_processing/agc2/fixed_gain_controller.cc @@ -55,7 +55,7 @@ void FixedGainController::SetSampleRate(size_t sample_rate_hz) { } void FixedGainController::Process(AudioFrameView signal) { - // Apply fixed digital gain; interpolate if necessary. One of the + // Apply fixed digital gain. One of the // planned usages of the FGC is to only use the limiter. In that // case, the gain would be 1.0. Not doing the multiplications speeds // it up considerably. Hence the check. diff --git a/modules/audio_processing/agc2/gain_applier.cc b/modules/audio_processing/agc2/gain_applier.cc new file mode 100644 index 0000000000..38eb1de03f --- /dev/null +++ b/modules/audio_processing/agc2/gain_applier.cc @@ -0,0 +1,101 @@ +/* + * 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/gain_applier.h" + +#include "modules/audio_processing/agc2/agc2_common.h" +#include "rtc_base/numerics/safe_minmax.h" + +namespace webrtc { +namespace { + +// Returns true when the gain factor is so close to 1 that it would +// not affect int16 samples. +bool GainCloseToOne(float gain_factor) { + return 1.f - 1.f / kMaxFloatS16Value <= gain_factor && + gain_factor <= 1.f + 1.f / kMaxFloatS16Value; +} + +void ClipSignal(AudioFrameView signal) { + for (size_t k = 0; k < signal.num_channels(); ++k) { + rtc::ArrayView channel_view = signal.channel(k); + for (auto& sample : channel_view) { + sample = rtc::SafeClamp(sample, kMinFloatS16Value, kMaxFloatS16Value); + } + } +} + +void ApplyGainWithRamping(float last_gain_linear, + float gain_at_end_of_frame_linear, + float inverse_samples_per_channel, + AudioFrameView float_frame) { + // Do not modify the signal. + if (last_gain_linear == gain_at_end_of_frame_linear && + GainCloseToOne(gain_at_end_of_frame_linear)) { + return; + } + + // Gain is constant and different from 1. + if (last_gain_linear == gain_at_end_of_frame_linear) { + for (size_t k = 0; k < float_frame.num_channels(); ++k) { + rtc::ArrayView channel_view = float_frame.channel(k); + for (auto& sample : channel_view) { + sample *= gain_at_end_of_frame_linear; + } + } + return; + } + + // The gain changes. We have to change slowly to avoid discontinuities. + const float increment = (gain_at_end_of_frame_linear - last_gain_linear) * + inverse_samples_per_channel; + float gain = last_gain_linear; + for (size_t i = 0; i < float_frame.samples_per_channel(); ++i) { + for (size_t ch = 0; ch < float_frame.num_channels(); ++ch) { + float_frame.channel(ch)[i] *= gain; + } + gain += increment; + } +} + +} // namespace + +GainApplier::GainApplier(bool hard_clip_samples, float initial_gain_factor) + : hard_clip_samples_(hard_clip_samples), + last_gain_factor_(initial_gain_factor), + current_gain_factor_(initial_gain_factor) {} + +void GainApplier::ApplyGain(AudioFrameView signal) { + if (static_cast(signal.samples_per_channel()) != samples_per_channel_) { + Initialize(signal.samples_per_channel()); + } + + ApplyGainWithRamping(last_gain_factor_, current_gain_factor_, + inverse_samples_per_channel_, signal); + + last_gain_factor_ = current_gain_factor_; + + if (hard_clip_samples_) { + ClipSignal(signal); + } +} + +void GainApplier::SetGainFactor(float gain_factor) { + RTC_DCHECK_GT(gain_factor, 0.f); + current_gain_factor_ = gain_factor; +} + +void GainApplier::Initialize(size_t samples_per_channel) { + RTC_DCHECK_GT(samples_per_channel, 0); + samples_per_channel_ = static_cast(samples_per_channel); + inverse_samples_per_channel_ = 1.f / samples_per_channel_; +} + +} // namespace webrtc diff --git a/modules/audio_processing/agc2/gain_applier.h b/modules/audio_processing/agc2/gain_applier.h new file mode 100644 index 0000000000..a4f56cee4b --- /dev/null +++ b/modules/audio_processing/agc2/gain_applier.h @@ -0,0 +1,41 @@ +/* + * 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. + */ + +#ifndef MODULES_AUDIO_PROCESSING_AGC2_GAIN_APPLIER_H_ +#define MODULES_AUDIO_PROCESSING_AGC2_GAIN_APPLIER_H_ + +#include "modules/audio_processing/include/audio_frame_view.h" + +namespace webrtc { +class GainApplier { + public: + GainApplier(bool hard_clip_samples, float initial_gain_factor); + + void ApplyGain(AudioFrameView signal); + void SetGainFactor(float gain_factor); + + private: + void Initialize(size_t samples_per_channel); + + // Whether to clip samples after gain is applied. If 'true', result + // will fit in FloatS16 range. + const bool hard_clip_samples_; + float last_gain_factor_; + + // If this value is not equal to 'last_gain_factor', gain will be + // ramped from 'last_gain_factor_' to this value during the next + // 'ApplyGain'. + float current_gain_factor_; + int samples_per_channel_ = -1; + float inverse_samples_per_channel_ = -1.f; +}; +} // namespace webrtc + +#endif // MODULES_AUDIO_PROCESSING_AGC2_GAIN_APPLIER_H_ diff --git a/modules/audio_processing/agc2/gain_applier_unittest.cc b/modules/audio_processing/agc2/gain_applier_unittest.cc new file mode 100644 index 0000000000..bbf3a85d6c --- /dev/null +++ b/modules/audio_processing/agc2/gain_applier_unittest.cc @@ -0,0 +1,94 @@ +/* + * 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/gain_applier.h" + +#include + +#include +#include + +#include "modules/audio_processing/agc2/vector_float_frame.h" +#include "rtc_base/gunit.h" + +namespace webrtc { +TEST(AutomaticGainController2GainApplier, InitialGainIsRespected) { + constexpr float initial_signal_level = 123.f; + constexpr float gain_factor = 10.f; + VectorFloatFrame fake_audio(1, 1, initial_signal_level); + GainApplier gain_applier(true, gain_factor); + + gain_applier.ApplyGain(fake_audio.float_frame_view()); + EXPECT_NEAR(fake_audio.float_frame_view().channel(0)[0], + initial_signal_level * gain_factor, 0.1f); +} + +TEST(AutomaticGainController2GainApplier, ClippingIsDone) { + constexpr float initial_signal_level = 30000.f; + constexpr float gain_factor = 10.f; + VectorFloatFrame fake_audio(1, 1, initial_signal_level); + GainApplier gain_applier(true, gain_factor); + + gain_applier.ApplyGain(fake_audio.float_frame_view()); + EXPECT_NEAR(fake_audio.float_frame_view().channel(0)[0], + std::numeric_limits::max(), 0.1f); +} + +TEST(AutomaticGainController2GainApplier, ClippingIsNotDone) { + constexpr float initial_signal_level = 30000.f; + constexpr float gain_factor = 10.f; + VectorFloatFrame fake_audio(1, 1, initial_signal_level); + GainApplier gain_applier(false, gain_factor); + + gain_applier.ApplyGain(fake_audio.float_frame_view()); + + EXPECT_NEAR(fake_audio.float_frame_view().channel(0)[0], + initial_signal_level * gain_factor, 0.1f); +} + +TEST(AutomaticGainController2GainApplier, RampingIsDone) { + constexpr float initial_signal_level = 30000.f; + constexpr float initial_gain_factor = 1.f; + constexpr float target_gain_factor = 0.5f; + constexpr int num_channels = 3; + constexpr int samples_per_channel = 4; + VectorFloatFrame fake_audio(num_channels, samples_per_channel, + initial_signal_level); + GainApplier gain_applier(false, initial_gain_factor); + + gain_applier.SetGainFactor(target_gain_factor); + gain_applier.ApplyGain(fake_audio.float_frame_view()); + + // The maximal gain change should be close to that in linear interpolation. + for (size_t channel = 0; channel < num_channels; ++channel) { + float max_signal_change = 0.f; + float last_signal_level = initial_signal_level; + for (const auto sample : fake_audio.float_frame_view().channel(channel)) { + const float current_change = fabs(last_signal_level - sample); + max_signal_change = + std::max(max_signal_change, current_change); + last_signal_level = sample; + } + const float total_gain_change = + fabs((initial_gain_factor - target_gain_factor) * initial_signal_level); + EXPECT_NEAR(max_signal_change, total_gain_change / samples_per_channel, + 0.1f); + } + + // Next frame should have the desired level. + VectorFloatFrame next_fake_audio_frame(num_channels, samples_per_channel, + initial_signal_level); + gain_applier.ApplyGain(next_fake_audio_frame.float_frame_view()); + + // The last sample should have the new gain. + EXPECT_NEAR(next_fake_audio_frame.float_frame_view().channel(0)[0], + initial_signal_level * target_gain_factor, 0.1f); +} +} // namespace webrtc