APM: add AudioProcessingImpl::capture_::applied_input_volume(_changed)

The `recommended_stream_analog_level()` getter is used to retrieve
both the applied and the recommended input volume. This behavior is
error-prone since the caller must know what is returned based on
the point in the code (namely, before/after the AGC has changed
the last applied input volume into a recommended level).

This CL is a first step to make clarity on which input volume is
handled in different parts of APM. Next in the pipeline: make
`recommended_stream_analog_level()` a trivial getter that always
returns the recommended level.

Main changes:
- When `recommended_stream_analog_level()` is called but
  `set_stream_analog_level()` is not called, APM logs an error
  and returns a fall-back volume (which should not be applied
  since, when `set_stream_analog_level()` is not called, no
  external input volume is expected to be present
- When APM is used without calling the `*_stream_analog_level()`
  methods (e.g., when the caller does not provide any input volume),
  the recorded AEC dumps won't store `Stream::applied_input_level`

Other changes:
- Removed `AudioProcessingImpl::capture_::prev_analog_mic_level`
- Removed redundant code in `GainController2` around detecting
  input volume changes (already done by APM)
- Adapted the `audioproc_f` and `unpack_aecdump` tools
- Data dumps clean-up: the applied and the recommended input
  volumes are now recorded in an AGC implementation agnostic way

Bug: webrtc:7494, b/241923537
Change-Id: I3cb4a731fd9f3dc19bf6ac679b7ed8c969ea283b
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/271544
Reviewed-by: Per Ã…hgren <peah@webrtc.org>
Reviewed-by: Hanna Silen <silen@webrtc.org>
Commit-Queue: Alessio Bazzica <alessiob@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#38054}
diff --git a/modules/audio_processing/BUILD.gn b/modules/audio_processing/BUILD.gn
index 24c66b1..ba9d01c 100644
--- a/modules/audio_processing/BUILD.gn
+++ b/modules/audio_processing/BUILD.gn
@@ -114,7 +114,10 @@
     ":api",
     ":audio_frame_view",
   ]
-  absl_deps = [ "//third_party/abseil-cpp/absl/base:core_headers" ]
+  absl_deps = [
+    "//third_party/abseil-cpp/absl/base:core_headers",
+    "//third_party/abseil-cpp/absl/types:optional",
+  ]
 }
 
 rtc_library("gain_controller2") {
diff --git a/modules/audio_processing/aec_dump/capture_stream_info.cc b/modules/audio_processing/aec_dump/capture_stream_info.cc
index 207fad9..7d82a39 100644
--- a/modules/audio_processing/aec_dump/capture_stream_info.cc
+++ b/modules/audio_processing/aec_dump/capture_stream_info.cc
@@ -53,7 +53,9 @@
   auto* stream = event_->mutable_stream();
   stream->set_delay(state.delay);
   stream->set_drift(state.drift);
-  stream->set_applied_input_volume(state.applied_input_volume);
+  if (state.applied_input_volume.has_value()) {
+    stream->set_applied_input_volume(*state.applied_input_volume);
+  }
   stream->set_keypress(state.keypress);
 }
 }  // namespace webrtc
diff --git a/modules/audio_processing/audio_processing_impl.cc b/modules/audio_processing/audio_processing_impl.cc
index ebb69f5..b839f91 100644
--- a/modules/audio_processing/audio_processing_impl.cc
+++ b/modules/audio_processing/audio_processing_impl.cc
@@ -143,6 +143,8 @@
                        audio.channels_const()[0] + audio.num_frames());
 }
 
+constexpr int kUnspecifiedDataDumpInputVolume = -100;
+
 }  // namespace
 
 // Throughout webrtc, it's assumed that success is represented by zero.
@@ -1073,7 +1075,6 @@
   if (aec_dump_) {
     RecordProcessedCaptureStream(dest, output_config);
   }
-
   return kNoError;
 }
 
@@ -1088,6 +1089,10 @@
   RTC_DCHECK_LE(
       !!submodules_.echo_controller + !!submodules_.echo_control_mobile, 1);
 
+  data_dumper_->DumpRaw(
+      "applied_input_volume",
+      capture_.applied_input_volume.value_or(kUnspecifiedDataDumpInputVolume));
+
   AudioBuffer* capture_buffer = capture_.capture_audio.get();  // For brevity.
   AudioBuffer* linear_aec_buffer = capture_.linear_aec_output.get();
 
@@ -1123,16 +1128,16 @@
                                 levels.peak, 1, RmsLevel::kMinLevelDb, 64);
   }
 
-  // Detect an analog gain change.
-  int analog_mic_level = recommended_stream_analog_level_locked();
-  const bool analog_mic_level_changed =
-      capture_.prev_analog_mic_level != analog_mic_level &&
-      capture_.prev_analog_mic_level != -1;
-  capture_.prev_analog_mic_level = analog_mic_level;
-  analog_gain_stats_reporter_.UpdateStatistics(analog_mic_level);
+  if (capture_.applied_input_volume.has_value()) {
+    // Log the applied input volume only when available.
+    input_volume_stats_reporter_.UpdateStatistics(
+        *capture_.applied_input_volume);
+  }
 
   if (submodules_.echo_controller) {
-    capture_.echo_path_gain_change = analog_mic_level_changed;
+    // Determine if the echo path gain has changed by checking all the gains
+    // applied before AEC.
+    capture_.echo_path_gain_change = capture_.applied_input_volume_changed;
 
     // Detect and flag any change in the capture level adjustment pre-gain.
     if (submodules_.capture_levels_adjuster) {
@@ -1141,7 +1146,7 @@
       capture_.echo_path_gain_change =
           capture_.echo_path_gain_change ||
           (capture_.prev_pre_adjustment_gain != pre_adjustment_gain &&
-           capture_.prev_pre_adjustment_gain >= 0.f);
+           capture_.prev_pre_adjustment_gain >= 0.0f);
       capture_.prev_pre_adjustment_gain = pre_adjustment_gain;
     }
 
@@ -1312,9 +1317,9 @@
     }
 
     if (submodules_.gain_controller2) {
-      submodules_.gain_controller2->NotifyAnalogLevel(
-          recommended_stream_analog_level_locked());
-      submodules_.gain_controller2->Process(voice_probability, capture_buffer);
+      submodules_.gain_controller2->Process(
+          voice_probability, capture_.applied_input_volume_changed,
+          capture_buffer);
     }
 
     if (submodules_.capture_post_processor) {
@@ -1333,12 +1338,6 @@
                                   levels.peak, 1, RmsLevel::kMinLevelDb, 64);
     }
 
-    if (submodules_.agc_manager) {
-      int level = recommended_stream_analog_level_locked();
-      data_dumper_->DumpRaw("experimental_gain_control_stream_analog_level", 1,
-                            &level);
-    }
-
     // Compute echo-detector stats.
     if (submodules_.echo_detector) {
       auto ed_metrics = submodules_.echo_detector->GetMetrics();
@@ -1388,6 +1387,9 @@
   capture_.capture_output_used_last_frame = capture_.capture_output_used;
 
   capture_.was_stream_delay_set = false;
+
+  // TODO(bugs.webrtc.org/7494): Dump recommended input volume.
+
   return kNoError;
 }
 
@@ -1605,14 +1607,13 @@
 }
 
 void AudioProcessingImpl::set_stream_analog_level_locked(int level) {
-  // Cache the level for later reporting back as the recommended input volume to
-  // use.
-  capture_.cached_stream_analog_level_ = level;
+  capture_.applied_input_volume_changed =
+      capture_.applied_input_volume.has_value() &&
+      *capture_.applied_input_volume != level;
+  capture_.applied_input_volume = level;
 
   if (submodules_.agc_manager) {
     submodules_.agc_manager->set_stream_analog_level(level);
-    data_dumper_->DumpRaw("experimental_gain_control_set_stream_analog_level",
-                          1, &level);
     return;
   }
 
@@ -1629,6 +1630,10 @@
 }
 
 int AudioProcessingImpl::recommended_stream_analog_level_locked() const {
+  if (!capture_.applied_input_volume.has_value()) {
+    RTC_LOG(LS_ERROR) << "set_stream_analog_level has not been called";
+  }
+
   if (submodules_.agc_manager) {
     return submodules_.agc_manager->recommended_analog_level();
   }
@@ -1637,7 +1642,9 @@
     return submodules_.gain_control->stream_analog_level();
   }
 
-  return capture_.cached_stream_analog_level_;
+  // Input volume to recommend when `set_stream_analog_level()` is not called.
+  constexpr int kFallBackInputVolume = 255;
+  return capture_.applied_input_volume.value_or(kFallBackInputVolume);
 }
 
 bool AudioProcessingImpl::CreateAndAttachAecDump(absl::string_view file_name,
@@ -2137,10 +2144,7 @@
   AecDump::AudioProcessingState audio_proc_state;
   audio_proc_state.delay = capture_nonlocked_.stream_delay_ms;
   audio_proc_state.drift = 0;
-  // TODO(bugs.webrtc.org/7494): Refactor to clarify that `stream_analog_level`
-  // is in fact assigned to the applied volume and not to the recommended one.
-  audio_proc_state.applied_input_volume =
-      recommended_stream_analog_level_locked();
+  audio_proc_state.applied_input_volume = capture_.applied_input_volume;
   audio_proc_state.keypress = capture_.key_pressed;
   aec_dump_->AddAudioProcessingState(audio_proc_state);
 }
@@ -2153,10 +2157,10 @@
       capture_processing_format(kSampleRate16kHz),
       split_rate(kSampleRate16kHz),
       echo_path_gain_change(false),
-      prev_analog_mic_level(-1),
-      prev_pre_adjustment_gain(-1.f),
+      prev_pre_adjustment_gain(-1.0f),
       playout_volume(-1),
-      prev_playout_volume(-1) {}
+      prev_playout_volume(-1),
+      applied_input_volume_changed(false) {}
 
 AudioProcessingImpl::ApmCaptureState::~ApmCaptureState() = default;
 
diff --git a/modules/audio_processing/audio_processing_impl.h b/modules/audio_processing/audio_processing_impl.h
index a535310..e28d1f6 100644
--- a/modules/audio_processing/audio_processing_impl.h
+++ b/modules/audio_processing/audio_processing_impl.h
@@ -20,6 +20,7 @@
 #include <vector>
 
 #include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
 #include "api/array_view.h"
 #include "api/function_view.h"
 #include "modules/audio_processing/aec3/echo_canceller3.h"
@@ -466,12 +467,14 @@
     StreamConfig capture_processing_format;
     int split_rate;
     bool echo_path_gain_change;
-    int prev_analog_mic_level;
     float prev_pre_adjustment_gain;
     int playout_volume;
     int prev_playout_volume;
     AudioProcessingStats stats;
-    int cached_stream_analog_level_ = 0;
+    // Input volume applied on the audio input device when the audio is
+    // acquired. Unspecified when unknown.
+    absl::optional<int> applied_input_volume;
+    bool applied_input_volume_changed;
   } capture_ RTC_GUARDED_BY(mutex_capture_);
 
   struct ApmCaptureNonLockedState {
@@ -532,7 +535,7 @@
   RmsLevel capture_output_rms_ RTC_GUARDED_BY(mutex_capture_);
   int capture_rms_interval_counter_ RTC_GUARDED_BY(mutex_capture_) = 0;
 
-  AnalogGainStatsReporter analog_gain_stats_reporter_
+  AnalogGainStatsReporter input_volume_stats_reporter_
       RTC_GUARDED_BY(mutex_capture_);
 
   // Lock protection not needed.
diff --git a/modules/audio_processing/audio_processing_unittest.cc b/modules/audio_processing/audio_processing_unittest.cc
index 523afe9..326ae48 100644
--- a/modules/audio_processing/audio_processing_unittest.cc
+++ b/modules/audio_processing/audio_processing_unittest.cc
@@ -955,8 +955,8 @@
   EXPECT_EQ(config.pre_amplifier.fixed_gain_factor, 1.5f);
 }
 
-// This test a simple test that ensures that the emulated analog mic gain
-// functionality runs without crashing.
+// Ensures that the emulated analog mic gain functionality runs without
+// crashing.
 TEST_F(ApmTest, AnalogMicGainEmulation) {
   // Fill the audio frame with a sawtooth pattern.
   rtc::ArrayView<int16_t> frame_data = GetMutableFrameData(&frame_);
diff --git a/modules/audio_processing/gain_controller2.cc b/modules/audio_processing/gain_controller2.cc
index f1907bb..aebac52 100644
--- a/modules/audio_processing/gain_controller2.cc
+++ b/modules/audio_processing/gain_controller2.cc
@@ -28,7 +28,6 @@
 
 using Agc2Config = AudioProcessing::Config::GainController2;
 
-constexpr int kUnspecifiedAnalogLevel = -1;
 constexpr int kLogLimiterStatsPeriodMs = 30'000;
 constexpr int kFrameLengthMs = 10;
 constexpr int kLogLimiterStatsPeriodNumFrames =
@@ -81,8 +80,7 @@
                                           num_channels,
                                           &data_dumper_)),
       limiter_(sample_rate_hz, &data_dumper_, /*histogram_name_prefix=*/"Agc2"),
-      calls_since_last_limiter_log_(0),
-      analog_level_(kUnspecifiedAnalogLevel) {
+      calls_since_last_limiter_log_(0) {
   RTC_DCHECK(Validate(config));
   data_dumper_.InitiateNewSetOfRecordings();
   const bool use_vad = config.adaptive_digital.enabled;
@@ -112,7 +110,6 @@
   }
   data_dumper_.InitiateNewSetOfRecordings();
   calls_since_last_limiter_log_ = 0;
-  analog_level_ = kUnspecifiedAnalogLevel;
 }
 
 void GainController2::SetFixedGainDb(float gain_db) {
@@ -126,8 +123,14 @@
 }
 
 void GainController2::Process(absl::optional<float> speech_probability,
+                              bool input_volume_changed,
                               AudioBuffer* audio) {
-  data_dumper_.DumpRaw("agc2_notified_analog_level", analog_level_);
+  data_dumper_.DumpRaw("agc2_applied_input_volume_changed",
+                       input_volume_changed);
+  if (input_volume_changed && !!adaptive_digital_controller_) {
+    adaptive_digital_controller_->HandleInputGainChange();
+  }
+
   AudioFrameView<float> float_frame(audio->channels(), audio->num_channels(),
                                     audio->num_frames());
   if (vad_) {
@@ -159,13 +162,6 @@
   }
 }
 
-void GainController2::NotifyAnalogLevel(int level) {
-  if (analog_level_ != level && adaptive_digital_controller_) {
-    adaptive_digital_controller_->HandleInputGainChange();
-  }
-  analog_level_ = level;
-}
-
 bool GainController2::Validate(
     const AudioProcessing::Config::GainController2& config) {
   const auto& fixed = config.fixed_digital;
diff --git a/modules/audio_processing/gain_controller2.h b/modules/audio_processing/gain_controller2.h
index 304fa40..ec3816f 100644
--- a/modules/audio_processing/gain_controller2.h
+++ b/modules/audio_processing/gain_controller2.h
@@ -50,11 +50,12 @@
   // Applies fixed and adaptive digital gains to `audio` and runs a limiter.
   // If the internal VAD is used, `speech_probability` is ignored. Otherwise
   // `speech_probability` is used for digital adaptive gain if it's available
-  // (limited to values [0.0, 1.0]).
-  void Process(absl::optional<float> speech_probability, AudioBuffer* audio);
-
-  // Handles analog level changes.
-  void NotifyAnalogLevel(int level);
+  // (limited to values [0.0, 1.0]). Handles input volume changes; if the caller
+  // cannot determine whether an input volume change occurred, set
+  // `input_volume_changed` to false.
+  void Process(absl::optional<float> speech_probability,
+               bool input_volume_changed,
+               AudioBuffer* audio);
 
   static bool Validate(const AudioProcessing::Config::GainController2& config);
 
@@ -69,7 +70,6 @@
   std::unique_ptr<AdaptiveDigitalGainController> adaptive_digital_controller_;
   Limiter limiter_;
   int calls_since_last_limiter_log_;
-  int analog_level_;
 };
 
 }  // namespace webrtc
diff --git a/modules/audio_processing/gain_controller2_unittest.cc b/modules/audio_processing/gain_controller2_unittest.cc
index 88a93b0..83ea5f1 100644
--- a/modules/audio_processing/gain_controller2_unittest.cc
+++ b/modules/audio_processing/gain_controller2_unittest.cc
@@ -47,7 +47,8 @@
   // Give time to the level estimator to converge.
   for (int i = 0; i < num_frames + 1; ++i) {
     SetAudioBufferSamples(input_level, ab);
-    agc2.Process(/*speech_probability=*/absl::nullopt, &ab);
+    agc2.Process(/*speech_probability=*/absl::nullopt,
+                 /*input_volume_changed=*/false, &ab);
   }
 
   // Return the last sample from the last processed frame.
@@ -283,12 +284,14 @@
       x *= gain;
     }
     test::CopyVectorToAudioBuffer(stream_config, frame, &audio_buffer);
-    agc2.Process(/*speech_probability=*/absl::nullopt, &audio_buffer);
+    agc2.Process(/*speech_probability=*/absl::nullopt,
+                 /*input_volume_changed=*/false, &audio_buffer);
   }
 
   // Estimate the applied gain by processing a probing frame.
   SetAudioBufferSamples(/*value=*/1.0f, audio_buffer);
-  agc2.Process(/*speech_probability=*/absl::nullopt, &audio_buffer);
+  agc2.Process(/*speech_probability=*/absl::nullopt,
+               /*input_volume_changed=*/false, &audio_buffer);
   const float applied_gain_db =
       20.0f * std::log10(audio_buffer.channels_const()[0][0]);
 
@@ -343,10 +346,13 @@
       x *= gain;
     }
     test::CopyVectorToAudioBuffer(stream_config, frame, &audio_buffer);
-    agc2.Process(kSpeechProbabilities[j], &audio_buffer);
+    agc2.Process(kSpeechProbabilities[j], /*input_volume_changed=*/false,
+                 &audio_buffer);
     test::CopyVectorToAudioBuffer(stream_config, frame,
                                   &audio_buffer_reference);
-    agc2_reference.Process(absl::nullopt, &audio_buffer_reference);
+    agc2_reference.Process(/*speech_probability=*/absl::nullopt,
+                           /*input_volume_changed=*/false,
+                           &audio_buffer_reference);
 
     // Check the output buffers.
     for (int i = 0; i < kStereo; ++i) {
@@ -407,10 +413,13 @@
       x *= gain;
     }
     test::CopyVectorToAudioBuffer(stream_config, frame, &audio_buffer);
-    agc2.Process(kSpeechProbabilities[j], &audio_buffer);
+    agc2.Process(kSpeechProbabilities[j], /*input_volume_changed=*/false,
+                 &audio_buffer);
     test::CopyVectorToAudioBuffer(stream_config, frame,
                                   &audio_buffer_reference);
-    agc2_reference.Process(absl::nullopt, &audio_buffer_reference);
+    agc2_reference.Process(/*speech_probability=*/absl::nullopt,
+                           /*input_volume_changed=*/false,
+                           &audio_buffer_reference);
     // Check the output buffers.
     for (int i = 0; i < kStereo; ++i) {
       for (int j = 0; j < static_cast<int>(audio_buffer.num_frames()); ++j) {
@@ -472,11 +481,13 @@
     }
     test::CopyVectorToAudioBuffer(stream_config, frame,
                                   &audio_buffer_reference);
-    agc2_reference.Process(absl::nullopt, &audio_buffer_reference);
+    agc2_reference.Process(absl::nullopt, /*input_volume_changed=*/false,
+                           &audio_buffer_reference);
     test::CopyVectorToAudioBuffer(stream_config, frame, &audio_buffer);
-    agc2.Process(vad.Analyze(AudioFrameView<const float>(
-                     audio_buffer.channels(), audio_buffer.num_channels(),
-                     audio_buffer.num_frames())),
+    float speech_probability = vad.Analyze(AudioFrameView<const float>(
+        audio_buffer.channels(), audio_buffer.num_channels(),
+        audio_buffer.num_frames()));
+    agc2.Process(speech_probability, /*input_volume_changed=*/false,
                  &audio_buffer);
     // Check the output buffer.
     for (int i = 0; i < kStereo; ++i) {
diff --git a/modules/audio_processing/include/aec_dump.h b/modules/audio_processing/include/aec_dump.h
index cc31071..6f2eb64 100644
--- a/modules/audio_processing/include/aec_dump.h
+++ b/modules/audio_processing/include/aec_dump.h
@@ -16,6 +16,7 @@
 #include <string>
 
 #include "absl/base/attributes.h"
+#include "absl/types/optional.h"
 #include "modules/audio_processing/include/audio_frame_view.h"
 #include "modules/audio_processing/include/audio_processing.h"
 
@@ -67,7 +68,7 @@
   struct AudioProcessingState {
     int delay;
     int drift;
-    int applied_input_volume;
+    absl::optional<int> applied_input_volume;
     bool keypress;
   };
 
diff --git a/modules/audio_processing/include/audio_processing.h b/modules/audio_processing/include/audio_processing.h
index ff4a44b..36c4a7a3 100644
--- a/modules/audio_processing/include/audio_processing.h
+++ b/modules/audio_processing/include/audio_processing.h
@@ -589,9 +589,10 @@
   // HAL. Must be within the range [0, 255].
   virtual void set_stream_analog_level(int level) = 0;
 
-  // When an analog mode is set, this should be called after ProcessStream()
-  // to obtain the recommended new analog level for the audio HAL. It is the
-  // user's responsibility to apply this level.
+  // When an analog mode is set, this should be called after
+  // `set_stream_analog_level()` and `ProcessStream()` to obtain the recommended
+  // new analog level for the audio HAL. It is the user's responsibility to
+  // apply this level.
   virtual int recommended_stream_analog_level() const = 0;
 
   // This must be called if and only if echo processing is enabled.
diff --git a/modules/audio_processing/test/aec_dump_based_simulator.cc b/modules/audio_processing/test/aec_dump_based_simulator.cc
index 261734d..416e287 100644
--- a/modules/audio_processing/test/aec_dump_based_simulator.cc
+++ b/modules/audio_processing/test/aec_dump_based_simulator.cc
@@ -174,9 +174,11 @@
     }
   }
 
-  // The stream analog level is always logged in the AEC dumps.
-  RTC_CHECK(msg.has_applied_input_volume());
-  aec_dump_mic_level_ = msg.applied_input_volume();
+  // Set the applied input level if available.
+  aec_dump_applied_input_level_ =
+      msg.has_applied_input_volume()
+          ? absl::optional<int>(msg.applied_input_volume())
+          : absl::nullopt;
 }
 
 void AecDumpBasedSimulator::VerifyProcessStreamBitExactness(
diff --git a/modules/audio_processing/test/audio_processing_simulator.cc b/modules/audio_processing/test/audio_processing_simulator.cc
index 4a95b44..7497d49 100644
--- a/modules/audio_processing/test/audio_processing_simulator.cc
+++ b/modules/audio_processing/test/audio_processing_simulator.cc
@@ -118,7 +118,7 @@
     std::unique_ptr<AudioProcessingBuilder> ap_builder)
     : settings_(settings),
       ap_(std::move(audio_processing)),
-      analog_mic_level_(settings.initial_mic_level),
+      applied_input_volume_(settings.initial_mic_level),
       fake_recording_device_(
           settings.initial_mic_level,
           settings_.simulate_mic_gain ? *settings.simulated_mic_kind : 0),
@@ -208,29 +208,50 @@
 }
 
 void AudioProcessingSimulator::ProcessStream(bool fixed_interface) {
-  // Optionally use the fake recording device to simulate analog gain.
+  // Optionally simulate the input volume.
   if (settings_.simulate_mic_gain) {
     RTC_DCHECK(!settings_.use_analog_mic_gain_emulation);
-    if (settings_.aec_dump_input_filename) {
-      // When the analog gain is simulated and an AEC dump is used as input, set
-      // the undo level to `aec_dump_mic_level_` to virtually restore the
-      // unmodified microphone signal level.
-      fake_recording_device_.SetUndoMicLevel(aec_dump_mic_level_);
+    // Set the input volume to simulate.
+    fake_recording_device_.SetMicLevel(applied_input_volume_);
+
+    if (settings_.aec_dump_input_filename &&
+        aec_dump_applied_input_level_.has_value()) {
+      // For AEC dumps, use the applied input level, if recorded, to "virtually
+      // restore" the capture signal level before the input volume was applied.
+      fake_recording_device_.SetUndoMicLevel(*aec_dump_applied_input_level_);
     }
 
+    // Apply the input volume.
     if (fixed_interface) {
       fake_recording_device_.SimulateAnalogGain(fwd_frame_.data);
     } else {
       fake_recording_device_.SimulateAnalogGain(in_buf_.get());
     }
+  }
 
-    // Notify the current mic level to AGC.
+  // Let APM know which input volume was applied.
+  // Keep track of whether `set_stream_analog_level()` is called.
+  bool applied_input_volume_set = false;
+  if (settings_.simulate_mic_gain) {
+    // When the input volume is simulated, use the volume applied for
+    // simulation.
     ap_->set_stream_analog_level(fake_recording_device_.MicLevel());
+    applied_input_volume_set = true;
   } else if (!settings_.use_analog_mic_gain_emulation) {
-    // Notify the current mic level to AGC.
-    ap_->set_stream_analog_level(settings_.aec_dump_input_filename
-                                     ? aec_dump_mic_level_
-                                     : analog_mic_level_);
+    // Ignore the recommended input volume stored in `applied_input_volume_` and
+    // instead notify APM with the recorded input volume (if available).
+    if (settings_.aec_dump_input_filename &&
+        aec_dump_applied_input_level_.has_value()) {
+      // The actually applied input volume is available in the AEC dump.
+      ap_->set_stream_analog_level(*aec_dump_applied_input_level_);
+      applied_input_volume_set = true;
+    } else if (!settings_.aec_dump_input_filename) {
+      // Wav files do not include any information about the actually applied
+      // input volume. Hence, use the recommended input volume stored in
+      // `applied_input_volume_`.
+      ap_->set_stream_analog_level(applied_input_volume_);
+      applied_input_volume_set = true;
+    }
   }
 
   // Post any scheduled runtime settings.
@@ -266,13 +287,12 @@
                                     out_config_, out_buf_->channels()));
   }
 
-  // Store the mic level suggested by AGC.
-  // Note that when the analog gain is simulated and an AEC dump is used as
-  // input, `analog_mic_level_` will not be used with set_stream_analog_level().
-  analog_mic_level_ = ap_->recommended_stream_analog_level();
-  if (settings_.simulate_mic_gain) {
-    fake_recording_device_.SetMicLevel(analog_mic_level_);
+  // Retrieve the recommended input volume only if `set_stream_analog_level()`
+  // has been called to stick to the APM API contract.
+  if (applied_input_volume_set) {
+    applied_input_volume_ = ap_->recommended_stream_analog_level();
   }
+
   if (buffer_memory_writer_) {
     RTC_CHECK(!buffer_file_writer_);
     buffer_memory_writer_->Write(*out_buf_);
diff --git a/modules/audio_processing/test/audio_processing_simulator.h b/modules/audio_processing/test/audio_processing_simulator.h
index b63bc12..e40d818 100644
--- a/modules/audio_processing/test/audio_processing_simulator.h
+++ b/modules/audio_processing/test/audio_processing_simulator.h
@@ -219,7 +219,7 @@
   Int16Frame rev_frame_;
   Int16Frame fwd_frame_;
   bool bitexact_output_ = true;
-  int aec_dump_mic_level_ = 0;
+  absl::optional<int> aec_dump_applied_input_level_ = 0;
 
  protected:
   size_t output_reset_counter_ = 0;
@@ -235,7 +235,7 @@
   std::unique_ptr<WavWriter> linear_aec_output_file_writer_;
   ApiCallStatistics api_call_statistics_;
   std::ofstream residual_echo_likelihood_graph_writer_;
-  int analog_mic_level_;
+  int applied_input_volume_;
   FakeRecordingDevice fake_recording_device_;
 
   TaskQueueForTest worker_queue_;
diff --git a/modules/audio_processing/test/debug_dump_replayer.cc b/modules/audio_processing/test/debug_dump_replayer.cc
index 4155173..2f483f5 100644
--- a/modules/audio_processing/test/debug_dump_replayer.cc
+++ b/modules/audio_processing/test/debug_dump_replayer.cc
@@ -121,7 +121,9 @@
   // APM should have been created.
   RTC_CHECK(apm_.get());
 
-  apm_->set_stream_analog_level(msg.applied_input_volume());
+  if (msg.has_applied_input_volume()) {
+    apm_->set_stream_analog_level(msg.applied_input_volume());
+  }
   RTC_CHECK_EQ(AudioProcessing::kNoError,
                apm_->set_stream_delay_ms(msg.delay()));