AGC1: min mic level override always applied

When the minimum mic level is overridden via the field trial named
WebRTC-Audio-AgcMinMicLevelExperiment, AGC1 can still lower the gain
beyond the minimum value (namely, when clipping is observed).

This CL changes the behavior of the field trial. When specified, the
override always applies and therefore the mic level is guaranteed to
never become lower than what the field trial specifies.

Tested: RTC call in Chromium with and without --force-fieldtrials="
WebRTC-Audio-AgcMinMicLevelExperiment/Enabled-255"

Bug: chromium:1275566
Change-Id: I42ff45add54c11084f5ca6a2b95887c627c3c3aa
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/250141
Reviewed-by: Hanna Silen <silen@webrtc.org>
Commit-Queue: Alessio Bazzica <alessiob@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#35914}
diff --git a/modules/audio_processing/agc/BUILD.gn b/modules/audio_processing/agc/BUILD.gn
index e6c0b19..eef1b77 100644
--- a/modules/audio_processing/agc/BUILD.gn
+++ b/modules/audio_processing/agc/BUILD.gn
@@ -180,6 +180,7 @@
       "../../../rtc_base:checks",
       "../../../rtc_base:rtc_base_approved",
       "../../../rtc_base:safe_conversions",
+      "../../../rtc_base:stringutils",
       "../../../system_wrappers:metrics",
       "../../../test:field_trial",
       "../../../test:fileutils",
diff --git a/modules/audio_processing/agc/agc_manager_direct.cc b/modules/audio_processing/agc/agc_manager_direct.cc
index 0bcbb01..8bce769 100644
--- a/modules/audio_processing/agc/agc_manager_direct.cc
+++ b/modules/audio_processing/agc/agc_manager_direct.cc
@@ -63,28 +63,27 @@
   return field_trial::IsEnabled("WebRTC-UseMaxAnalogAgcChannelLevel");
 }
 
-// Returns kMinMicLevel if no field trial exists or if it has been disabled.
-// Returns a value between 0 and 255 depending on the field-trial string.
-// Example: 'WebRTC-Audio-AgcMinMicLevelExperiment/Enabled-80' => returns 80.
-int GetMinMicLevel() {
-  RTC_LOG(LS_INFO) << "[agc] GetMinMicLevel";
+// If the "WebRTC-Audio-AgcMinMicLevelExperiment" field trial is specified,
+// parses it and returns a value between 0 and 255 depending on the field-trial
+// string. Returns an unspecified value if the field trial is not specified, if
+// disabled or if it cannot be parsed. Example:
+// 'WebRTC-Audio-AgcMinMicLevelExperiment/Enabled-80' => returns 80.
+absl::optional<int> GetMinMicLevelOverride() {
   constexpr char kMinMicLevelFieldTrial[] =
       "WebRTC-Audio-AgcMinMicLevelExperiment";
   if (!webrtc::field_trial::IsEnabled(kMinMicLevelFieldTrial)) {
-    RTC_LOG(LS_INFO) << "[agc] Using default min mic level: " << kMinMicLevel;
-    return kMinMicLevel;
+    return absl::nullopt;
   }
   const auto field_trial_string =
       webrtc::field_trial::FindFullName(kMinMicLevelFieldTrial);
   int min_mic_level = -1;
   sscanf(field_trial_string.c_str(), "Enabled-%d", &min_mic_level);
   if (min_mic_level >= 0 && min_mic_level <= 255) {
-    RTC_LOG(LS_INFO) << "[agc] Experimental min mic level: " << min_mic_level;
     return min_mic_level;
   } else {
     RTC_LOG(LS_WARNING) << "[agc] Invalid parameter for "
                         << kMinMicLevelFieldTrial << ", ignored.";
-    return kMinMicLevel;
+    return absl::nullopt;
   }
 }
 
@@ -125,7 +124,7 @@
     int num_clipped_in_ch = 0;
     for (size_t i = 0; i < samples_per_channel; ++i) {
       RTC_DCHECK(audio[ch]);
-      if (audio[ch][i] >= 32767.f || audio[ch][i] <= -32768.f) {
+      if (audio[ch][i] >= 32767.0f || audio[ch][i] <= -32768.0f) {
         ++num_clipped_in_ch;
       }
     }
@@ -472,7 +471,8 @@
     float clipped_ratio_threshold,
     int clipped_wait_frames,
     const ClippingPredictorConfig& clipping_config)
-    : data_dumper_(
+    : min_mic_level_override_(GetMinMicLevelOverride()),
+      data_dumper_(
           new ApmDataDumper(rtc::AtomicOps::Increment(&instance_counter_))),
       use_min_channel_level_(!UseMaxAnalogChannelLevel()),
       num_capture_channels_(num_capture_channels),
@@ -492,7 +492,11 @@
       clipping_predictor_log_counter_(0),
       clipping_rate_log_(0.0f),
       clipping_rate_log_counter_(0) {
-  const int min_mic_level = GetMinMicLevel();
+  const int min_mic_level = min_mic_level_override_.value_or(kMinMicLevel);
+  RTC_LOG(LS_INFO) << "[agc] Min mic level: " << min_mic_level
+                   << " (overridden: "
+                   << (min_mic_level_override_.has_value() ? "yes" : "no")
+                   << ")";
   for (size_t ch = 0; ch < channel_agcs_.size(); ++ch) {
     ApmDataDumper* data_dumper_ch = ch == 0 ? data_dumper_.get() : nullptr;
 
@@ -715,6 +719,10 @@
       }
     }
   }
+  if (min_mic_level_override_.has_value()) {
+    stream_analog_level_ =
+        std::max(stream_analog_level_, *min_mic_level_override_);
+  }
 }
 
 }  // namespace webrtc
diff --git a/modules/audio_processing/agc/agc_manager_direct.h b/modules/audio_processing/agc/agc_manager_direct.h
index 327f731..ce67a97 100644
--- a/modules/audio_processing/agc/agc_manager_direct.h
+++ b/modules/audio_processing/agc/agc_manager_direct.h
@@ -126,6 +126,7 @@
 
   void AggregateChannelLevels();
 
+  const absl::optional<int> min_mic_level_override_;
   std::unique_ptr<ApmDataDumper> data_dumper_;
   static int instance_counter_;
   const bool use_min_channel_level_;
diff --git a/modules/audio_processing/agc/agc_manager_direct_unittest.cc b/modules/audio_processing/agc/agc_manager_direct_unittest.cc
index e02508e..d727449 100644
--- a/modules/audio_processing/agc/agc_manager_direct_unittest.cc
+++ b/modules/audio_processing/agc/agc_manager_direct_unittest.cc
@@ -15,6 +15,7 @@
 #include "modules/audio_processing/agc/gain_control.h"
 #include "modules/audio_processing/agc/mock_agc.h"
 #include "modules/audio_processing/include/mock_audio_processing.h"
+#include "rtc_base/strings/string_builder.h"
 #include "test/field_trial.h"
 #include "test/gmock.h"
 #include "test/gtest.h"
@@ -39,6 +40,9 @@
 constexpr float kClippedRatioThreshold = 0.1f;
 constexpr int kClippedWaitFrames = 300;
 
+constexpr AudioProcessing::Config::GainController1::AnalogGainController
+    kDefaultAnalogConfig{};
+
 using ClippingPredictorConfig = AudioProcessing::Config::GainController1::
     AnalogGainController::ClippingPredictor;
 
@@ -72,7 +76,8 @@
   return std::make_unique<AgcManagerDirect>(
       /*num_capture_channels=*/1, startup_min_level, kClippedMin,
       /*disable_digital_adaptive=*/true, clipped_level_step,
-      clipped_ratio_threshold, clipped_wait_frames, ClippingPredictorConfig());
+      clipped_ratio_threshold, clipped_wait_frames,
+      kDefaultAnalogConfig.clipping_predictor);
 }
 
 std::unique_ptr<AgcManagerDirect> CreateAgcManagerDirect(
@@ -87,26 +92,35 @@
       clipped_ratio_threshold, clipped_wait_frames, clipping_cfg);
 }
 
+// Calls `AnalyzePreProcess()` on `manager` `num_calls` times. `peak_ratio` is a
+// value in [0, 1] which determines the amplitude of the samples (1 maps to full
+// scale). The first half of the calls is made on frames which are half filled
+// with zeros in order to simulate a signal with different crest factors.
 void CallPreProcessAudioBuffer(int num_calls,
                                float peak_ratio,
                                AgcManagerDirect& manager) {
-  RTC_DCHECK_GE(1.f, peak_ratio);
+  RTC_DCHECK_LE(peak_ratio, 1.0f);
   AudioBuffer audio_buffer(kSampleRateHz, 1, kSampleRateHz, 1, kSampleRateHz,
                            1);
   const int num_channels = audio_buffer.num_channels();
   const int num_frames = audio_buffer.num_frames();
+
+  // Make half of the calls with half zeroed frames.
   for (int ch = 0; ch < num_channels; ++ch) {
+    // 50% of the samples in one frame are zero.
     for (int i = 0; i < num_frames; i += 2) {
-      audio_buffer.channels()[ch][i] = peak_ratio * 32767.f;
+      audio_buffer.channels()[ch][i] = peak_ratio * 32767.0f;
       audio_buffer.channels()[ch][i + 1] = 0.0f;
     }
   }
   for (int n = 0; n < num_calls / 2; ++n) {
     manager.AnalyzePreProcess(&audio_buffer);
   }
+
+  // Make the remaining half of the calls with frames whose samples are all set.
   for (int ch = 0; ch < num_channels; ++ch) {
     for (int i = 0; i < num_frames; ++i) {
-      audio_buffer.channels()[ch][i] = peak_ratio * 32767.f;
+      audio_buffer.channels()[ch][i] = peak_ratio * 32767.0f;
     }
   }
   for (int n = 0; n < num_calls - num_calls / 2; ++n) {
@@ -114,16 +128,49 @@
   }
 }
 
-void WriteAudioBufferSamples(float samples_value, AudioBuffer& audio_buffer) {
+std::string GetAgcMinMicLevelExperimentFieldTrial(int enabled_value) {
+  RTC_DCHECK_GE(enabled_value, 0);
+  RTC_DCHECK_LE(enabled_value, 255);
+  char field_trial_buffer[64];
+  rtc::SimpleStringBuilder builder(field_trial_buffer);
+  builder << "WebRTC-Audio-AgcMinMicLevelExperiment/Enabled-" << enabled_value
+          << "/";
+  return builder.str();
+}
+
+// (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, std::numeric_limits<int16_t>::min());
   RTC_DCHECK_LE(samples_value, std::numeric_limits<int16_t>::max());
-  for (size_t ch = 0; ch < audio_buffer.num_channels(); ++ch) {
-    for (size_t i = 0; i < audio_buffer.num_frames(); ++i) {
+  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;
     }
   }
 }
 
+void CallPreProcessAndProcess(int num_calls,
+                              const AudioBuffer& audio_buffer,
+                              AgcManagerDirect& manager) {
+  for (int n = 0; n < num_calls; ++n) {
+    manager.AnalyzePreProcess(&audio_buffer);
+    manager.Process(&audio_buffer);
+  }
+}
+
 }  // namespace
 
 class AgcManagerDirectTest : public ::testing::Test {
@@ -151,7 +198,8 @@
     for (size_t ch = 0; ch < kNumChannels; ++ch) {
       audio[ch] = &audio_data[ch * kSamplesPerChannel];
     }
-    WriteAudioBufferSamples(/*samples_value=*/0.0f, audio_buffer);
+    WriteAudioBufferSamples(/*samples_value=*/0.0f, /*clipped_ratio=*/0.0f,
+                            audio_buffer);
   }
 
   void FirstProcess() {
@@ -190,12 +238,13 @@
   }
 
   void CallPreProc(int num_calls, float clipped_ratio) {
-    RTC_DCHECK_GE(1.f, clipped_ratio);
+    RTC_DCHECK_GE(clipped_ratio, 0.0f);
+    RTC_DCHECK_LE(clipped_ratio, 1.0f);
     const int num_clipped = kSamplesPerChannel * clipped_ratio;
     std::fill(audio_data.begin(), audio_data.end(), 0.f);
     for (size_t ch = 0; ch < kNumChannels; ++ch) {
       for (int k = 0; k < num_clipped; ++k) {
-        audio[ch][k] = 32767.f;
+        audio[ch][k] = 32767.0f;
       }
     }
     for (int i = 0; i < num_calls; ++i) {
@@ -871,27 +920,121 @@
 // start volume is larger than the min level and should therefore not be
 // changed.
 TEST(AgcManagerDirectStandaloneTest, AgcMinMicLevelExperimentEnabled50) {
+  constexpr int kMinMicLevelOverride = 50;
   test::ScopedFieldTrials field_trial(
-      "WebRTC-Audio-AgcMinMicLevelExperiment/Enabled-50/");
+      GetAgcMinMicLevelExperimentFieldTrial(kMinMicLevelOverride));
   std::unique_ptr<AgcManagerDirect> manager =
       CreateAgcManagerDirect(kInitialVolume, kClippedLevelStep,
                              kClippedRatioThreshold, kClippedWaitFrames);
-  EXPECT_EQ(manager->channel_agcs_[0]->min_mic_level(), 50);
+  EXPECT_EQ(manager->channel_agcs_[0]->min_mic_level(), kMinMicLevelOverride);
   EXPECT_EQ(manager->channel_agcs_[0]->startup_min_level(), kInitialVolume);
 }
 
-// Uses experiment to reduce the default minimum microphone level, start at a
-// lower level and ensure that the startup level is increased to the min level
-// set by the experiment.
+// Checks that, when the "WebRTC-Audio-AgcMinMicLevelExperiment" field trial is
+// specified with a valid value, the mic level never gets lowered beyond the
+// override value in the presence of clipping.
 TEST(AgcManagerDirectStandaloneTest,
-     AgcMinMicLevelExperimentEnabledAboveStartupLevel) {
-  test::ScopedFieldTrials field_trial(
-      "WebRTC-Audio-AgcMinMicLevelExperiment/Enabled-50/");
-  std::unique_ptr<AgcManagerDirect> manager =
-      CreateAgcManagerDirect(/*startup_min_level=*/30, kClippedLevelStep,
-                             kClippedRatioThreshold, kClippedWaitFrames);
-  EXPECT_EQ(manager->channel_agcs_[0]->min_mic_level(), 50);
-  EXPECT_EQ(manager->channel_agcs_[0]->startup_min_level(), 50);
+     AgcMinMicLevelExperimentCheckMinLevelWithClipping) {
+  constexpr int kMinMicLevelOverride = 250;
+
+  // Create and initialize two AGCs by specifying and leaving unspecified the
+  // relevant field trial.
+  const auto factory = []() {
+    std::unique_ptr<AgcManagerDirect> manager =
+        CreateAgcManagerDirect(kInitialVolume, kClippedLevelStep,
+                               kClippedRatioThreshold, kClippedWaitFrames);
+    manager->Initialize();
+    manager->set_stream_analog_level(kInitialVolume);
+    return manager;
+  };
+  std::unique_ptr<AgcManagerDirect> manager = factory();
+  std::unique_ptr<AgcManagerDirect> manager_with_override;
+  {
+    test::ScopedFieldTrials field_trial(
+        GetAgcMinMicLevelExperimentFieldTrial(kMinMicLevelOverride));
+    manager_with_override = factory();
+  }
+
+  // Create a test input signal which containts 80% of clipped samples.
+  AudioBuffer audio_buffer(kSampleRateHz, 1, kSampleRateHz, 1, kSampleRateHz,
+                           1);
+  WriteAudioBufferSamples(/*samples_value=*/4000.0f, /*clipped_ratio=*/0.8f,
+                          audio_buffer);
+
+  // Simulate 4 seconds of clipping; it is expected to trigger a downward
+  // adjustment of the analog gain.
+  CallPreProcessAndProcess(/*num_calls=*/400, audio_buffer, *manager);
+  CallPreProcessAndProcess(/*num_calls=*/400, audio_buffer,
+                           *manager_with_override);
+
+  // Make sure that an adaptation occurred.
+  ASSERT_GT(manager->stream_analog_level(), 0);
+
+  // Check that the test signal triggers a larger downward adaptation for
+  // `manager`, which is allowed to reach a lower gain.
+  EXPECT_GT(manager_with_override->stream_analog_level(),
+            manager->stream_analog_level());
+  // Check that the gain selected by `manager_with_override` equals the minimum
+  // value overridden via field trial.
+  EXPECT_EQ(manager_with_override->stream_analog_level(), kMinMicLevelOverride);
+}
+
+// Checks that, when the "WebRTC-Audio-AgcMinMicLevelExperiment" field trial is
+// specified with a value lower than the `clipped_level_min`, the behavior of
+// the analog gain controller is the same as that obtained when the field trial
+// is not specified.
+TEST(AgcManagerDirectStandaloneTest,
+     AgcMinMicLevelExperimentCompareMicLevelWithClipping) {
+  // Create and initialize two AGCs by specifying and leaving unspecified the
+  // relevant field trial.
+  const auto factory = []() {
+    // Use a large clipped level step to more quickly decrease the analog gain
+    // with clipping.
+    auto controller = std::make_unique<AgcManagerDirect>(
+        /*num_capture_channels=*/1, kInitialVolume,
+        kDefaultAnalogConfig.clipped_level_min,
+        /*disable_digital_adaptive=*/true, /*clipped_level_step=*/64,
+        kClippedRatioThreshold, kClippedWaitFrames,
+        kDefaultAnalogConfig.clipping_predictor);
+    controller->Initialize();
+    controller->set_stream_analog_level(kInitialVolume);
+    return controller;
+  };
+  std::unique_ptr<AgcManagerDirect> manager = factory();
+  std::unique_ptr<AgcManagerDirect> manager_with_override;
+  {
+    constexpr int kMinMicLevelOverride = 20;
+    static_assert(
+        kDefaultAnalogConfig.clipped_level_min >= kMinMicLevelOverride,
+        "Use a lower override value.");
+    test::ScopedFieldTrials field_trial(
+        GetAgcMinMicLevelExperimentFieldTrial(kMinMicLevelOverride));
+    manager_with_override = factory();
+  }
+
+  // Create a test input signal which containts 80% of clipped samples.
+  AudioBuffer audio_buffer(kSampleRateHz, 1, kSampleRateHz, 1, kSampleRateHz,
+                           1);
+  WriteAudioBufferSamples(/*samples_value=*/4000.0f, /*clipped_ratio=*/0.8f,
+                          audio_buffer);
+
+  // Simulate 4 seconds of clipping; it is expected to trigger a downward
+  // adjustment of the analog gain.
+  CallPreProcessAndProcess(/*num_calls=*/400, audio_buffer, *manager);
+  CallPreProcessAndProcess(/*num_calls=*/400, audio_buffer,
+                           *manager_with_override);
+
+  // Make sure that an adaptation occurred.
+  ASSERT_GT(manager->stream_analog_level(), 0);
+
+  // Check that the selected analog gain is the same for both controllers and
+  // that it equals the minimum level reached when clipping is handled. That is
+  // expected because the minimum microphone level override is less than the
+  // minimum level used when clipping is detected.
+  EXPECT_EQ(manager->stream_analog_level(),
+            manager_with_override->stream_analog_level());
+  EXPECT_EQ(manager_with_override->stream_analog_level(),
+            kDefaultAnalogConfig.clipped_level_min);
 }
 
 // TODO(bugs.webrtc.org/12774): Test the bahavior of `clipped_level_step`.
diff --git a/modules/audio_processing/include/audio_processing.h b/modules/audio_processing/include/audio_processing.h
index 45e0b6a..6282633 100644
--- a/modules/audio_processing/include/audio_processing.h
+++ b/modules/audio_processing/include/audio_processing.h
@@ -788,8 +788,7 @@
  public:
   // sample_rate_hz: The sampling rate of the stream.
   // num_channels: The number of audio channels in the stream.
-  StreamConfig(int sample_rate_hz = 0,
-               size_t num_channels = 0)
+  StreamConfig(int sample_rate_hz = 0, size_t num_channels = 0)
       : sample_rate_hz_(sample_rate_hz),
         num_channels_(num_channels),
         num_frames_(calculate_frames(sample_rate_hz)) {}