AEC3: Unbounded echo spectrum for dominant nearend detection.

The dominant nearend detector uses the residual echo spectrum for
determining whether in nearend state. The residual echo spectrum in
computed using the ERLE. To reduce the risk of echo leaks in the
suppressor, the ERLE is capped. While minimizing echo leaks, the
capping of the ERLE can affect the dominant nearend classification
negatively as the residual echo spectrum is often over estimated.

This change enables the dominant nearend detector to use a residual
echo spectrum computed with a virtually non-capped ERLE. This ERLE
is only used for dominant nearend detection and leads to increased
transparency.

The feature is currently disabled by default and can be enabled
with the field trial "WebRTC-Aec3UseUnboundedEchoSpectrum".

Bug: webrtc:12870
Change-Id: Icb675c6f5d42ab9286e623b5fb38424d5c9cbee4
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/221920
Reviewed-by: Jesus de Vicente Pena <devicentepena@webrtc.org>
Commit-Queue: Gustaf Ullberg <gustaf@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#34270}
diff --git a/modules/audio_processing/aec3/aec_state.h b/modules/audio_processing/aec3/aec_state.h
index 125ae83..e2f70a4 100644
--- a/modules/audio_processing/aec3/aec_state.h
+++ b/modules/audio_processing/aec3/aec_state.h
@@ -75,6 +75,12 @@
     return erle_estimator_.Erle(onset_compensated);
   }
 
+  // Returns the non-capped ERLE.
+  rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>> ErleUnbounded()
+      const {
+    return erle_estimator_.ErleUnbounded();
+  }
+
   // Returns the fullband ERLE estimate in log2 units.
   float FullBandErleLog2() const { return erle_estimator_.FullbandErleLog2(); }
 
diff --git a/modules/audio_processing/aec3/echo_remover.cc b/modules/audio_processing/aec3/echo_remover.cc
index 6c177c9..2bfaa95 100644
--- a/modules/audio_processing/aec3/echo_remover.cc
+++ b/modules/audio_processing/aec3/echo_remover.cc
@@ -172,6 +172,7 @@
   std::vector<std::array<float, kFftLengthBy2Plus1>> Y2_heap_;
   std::vector<std::array<float, kFftLengthBy2Plus1>> E2_heap_;
   std::vector<std::array<float, kFftLengthBy2Plus1>> R2_heap_;
+  std::vector<std::array<float, kFftLengthBy2Plus1>> R2_unbounded_heap_;
   std::vector<std::array<float, kFftLengthBy2Plus1>> S2_linear_heap_;
   std::vector<FftData> Y_heap_;
   std::vector<FftData> E_heap_;
@@ -218,6 +219,7 @@
       Y2_heap_(NumChannelsOnHeap(num_capture_channels_)),
       E2_heap_(NumChannelsOnHeap(num_capture_channels_)),
       R2_heap_(NumChannelsOnHeap(num_capture_channels_)),
+      R2_unbounded_heap_(NumChannelsOnHeap(num_capture_channels_)),
       S2_linear_heap_(NumChannelsOnHeap(num_capture_channels_)),
       Y_heap_(NumChannelsOnHeap(num_capture_channels_)),
       E_heap_(NumChannelsOnHeap(num_capture_channels_)),
@@ -265,6 +267,8 @@
   std::array<std::array<float, kFftLengthBy2Plus1>, kMaxNumChannelsOnStack>
       R2_stack;
   std::array<std::array<float, kFftLengthBy2Plus1>, kMaxNumChannelsOnStack>
+      R2_unbounded_stack;
+  std::array<std::array<float, kFftLengthBy2Plus1>, kMaxNumChannelsOnStack>
       S2_linear_stack;
   std::array<FftData, kMaxNumChannelsOnStack> Y_stack;
   std::array<FftData, kMaxNumChannelsOnStack> E_stack;
@@ -280,6 +284,8 @@
       E2_stack.data(), num_capture_channels_);
   rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2(
       R2_stack.data(), num_capture_channels_);
+  rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2_unbounded(
+      R2_unbounded_stack.data(), num_capture_channels_);
   rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> S2_linear(
       S2_linear_stack.data(), num_capture_channels_);
   rtc::ArrayView<FftData> Y(Y_stack.data(), num_capture_channels_);
@@ -301,6 +307,8 @@
         E2_heap_.data(), num_capture_channels_);
     R2 = rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>>(
         R2_heap_.data(), num_capture_channels_);
+    R2_unbounded = rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>>(
+        R2_unbounded_heap_.data(), num_capture_channels_);
     S2_linear = rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>>(
         S2_linear_heap_.data(), num_capture_channels_);
     Y = rtc::ArrayView<FftData>(Y_heap_.data(), num_capture_channels_);
@@ -406,8 +414,8 @@
   if (capture_output_used_) {
     // Estimate the residual echo power.
     residual_echo_estimator_.Estimate(aec_state_, *render_buffer, S2_linear, Y2,
-                                      suppression_gain_.IsDominantNearend(),
-                                      R2);
+                                      suppression_gain_.IsDominantNearend(), R2,
+                                      R2_unbounded);
 
     // Suppressor nearend estimate.
     if (aec_state_.UsableLinearEstimate()) {
@@ -430,7 +438,7 @@
 
     // Compute preferred gains.
     float high_bands_gain;
-    suppression_gain_.GetGain(nearend_spectrum, echo_spectrum, R2,
+    suppression_gain_.GetGain(nearend_spectrum, echo_spectrum, R2, R2_unbounded,
                               cng_.NoiseSpectrum(), render_signal_analyzer_,
                               aec_state_, x, clock_drift, &high_bands_gain, &G);
 
diff --git a/modules/audio_processing/aec3/erle_estimator.h b/modules/audio_processing/aec3/erle_estimator.h
index cae896e..5579759 100644
--- a/modules/audio_processing/aec3/erle_estimator.h
+++ b/modules/audio_processing/aec3/erle_estimator.h
@@ -62,6 +62,18 @@
                : subband_erle_estimator_.Erle(onset_compensated);
   }
 
+  // Returns the non-capped subband ERLE.
+  rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>> ErleUnbounded()
+      const {
+    // Unbounded ERLE is only used with the subband erle estimator where the
+    // ERLE is often capped at low values. When the signal dependent ERLE
+    // estimator is used the capped ERLE is returned.
+    return !signal_dependent_erle_estimator_
+               ? subband_erle_estimator_.ErleUnbounded()
+               : signal_dependent_erle_estimator_->Erle(
+                     /*onset_compensated=*/false);
+  }
+
   // Returns the subband ERLE that are estimated during onsets (only used for
   // testing).
   rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>> ErleDuringOnsets()
diff --git a/modules/audio_processing/aec3/erle_estimator_unittest.cc b/modules/audio_processing/aec3/erle_estimator_unittest.cc
index 6df7142..e38f238 100644
--- a/modules/audio_processing/aec3/erle_estimator_unittest.cc
+++ b/modules/audio_processing/aec3/erle_estimator_unittest.cc
@@ -50,6 +50,16 @@
   EXPECT_NEAR(kTrueErle, erle_time_domain, 0.5);
 }
 
+void VerifyErleGreaterOrEqual(
+    rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>> erle1,
+    rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>> erle2) {
+  for (size_t ch = 0; ch < erle1.size(); ++ch) {
+    for (size_t i = 0; i < kFftLengthBy2Plus1; ++i) {
+      EXPECT_GE(erle1[ch][i], erle2[ch][i]);
+    }
+  }
+}
+
 void FormFarendTimeFrame(std::vector<std::vector<std::vector<float>>>* x) {
   const std::array<float, kBlockSize> frame = {
       7459.88, 17209.6, 17383,   20768.9, 16816.7, 18386.3, 4492.83, 9675.85,
@@ -156,9 +166,10 @@
       kNumBands, std::vector<std::vector<float>>(
                      num_render_channels, std::vector<float>(kBlockSize, 0.f)));
   std::vector<std::vector<std::array<float, kFftLengthBy2Plus1>>>
-  filter_frequency_response(
-      config.filter.refined.length_blocks,
-      std::vector<std::array<float, kFftLengthBy2Plus1>>(num_capture_channels));
+      filter_frequency_response(
+          config.filter.refined.length_blocks,
+          std::vector<std::array<float, kFftLengthBy2Plus1>>(
+              num_capture_channels));
   std::unique_ptr<RenderDelayBuffer> render_delay_buffer(
       RenderDelayBuffer::Create(config, kSampleRateHz, num_render_channels));
 
@@ -181,6 +192,10 @@
   VerifyErle(estimator.Erle(/*onset_compensated=*/true),
              std::pow(2.f, estimator.FullbandErleLog2()), config.erle.max_l,
              config.erle.max_h);
+  VerifyErleGreaterOrEqual(estimator.Erle(/*onset_compensated=*/false),
+                           estimator.Erle(/*onset_compensated=*/true));
+  VerifyErleGreaterOrEqual(estimator.ErleUnbounded(),
+                           estimator.Erle(/*onset_compensated=*/false));
 
   FormNearendFrame(&x, &X2, E2, Y2);
   // Verifies that the ERLE is not immediately decreased during nearend
@@ -194,6 +209,10 @@
   VerifyErle(estimator.Erle(/*onset_compensated=*/true),
              std::pow(2.f, estimator.FullbandErleLog2()), config.erle.max_l,
              config.erle.max_h);
+  VerifyErleGreaterOrEqual(estimator.Erle(/*onset_compensated=*/false),
+                           estimator.Erle(/*onset_compensated=*/true));
+  VerifyErleGreaterOrEqual(estimator.ErleUnbounded(),
+                           estimator.Erle(/*onset_compensated=*/false));
 }
 
 TEST_P(ErleEstimatorMultiChannel, VerifyErleTrackingOnOnsets) {
@@ -212,9 +231,10 @@
       kNumBands, std::vector<std::vector<float>>(
                      num_render_channels, std::vector<float>(kBlockSize, 0.f)));
   std::vector<std::vector<std::array<float, kFftLengthBy2Plus1>>>
-  filter_frequency_response(
-      config.filter.refined.length_blocks,
-      std::vector<std::array<float, kFftLengthBy2Plus1>>(num_capture_channels));
+      filter_frequency_response(
+          config.filter.refined.length_blocks,
+          std::vector<std::array<float, kFftLengthBy2Plus1>>(
+              num_capture_channels));
   std::unique_ptr<RenderDelayBuffer> render_delay_buffer(
       RenderDelayBuffer::Create(config, kSampleRateHz, num_render_channels));
 
diff --git a/modules/audio_processing/aec3/residual_echo_estimator.cc b/modules/audio_processing/aec3/residual_echo_estimator.cc
index 0688429..15bebec 100644
--- a/modules/audio_processing/aec3/residual_echo_estimator.cc
+++ b/modules/audio_processing/aec3/residual_echo_estimator.cc
@@ -177,7 +177,8 @@
     rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>> S2_linear,
     rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>> Y2,
     bool dominant_nearend,
-    rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2) {
+    rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2,
+    rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2_unbounded) {
   RTC_DCHECK_EQ(R2.size(), Y2.size());
   RTC_DCHECK_EQ(R2.size(), S2_linear.size());
 
@@ -193,14 +194,18 @@
     if (aec_state.SaturatedEcho()) {
       for (size_t ch = 0; ch < num_capture_channels; ++ch) {
         std::copy(Y2[ch].begin(), Y2[ch].end(), R2[ch].begin());
+        std::copy(Y2[ch].begin(), Y2[ch].end(), R2_unbounded[ch].begin());
       }
     } else {
       const bool onset_compensated =
           erle_onset_compensation_in_dominant_nearend_ || !dominant_nearend;
       LinearEstimate(S2_linear, aec_state.Erle(onset_compensated), R2);
+      LinearEstimate(S2_linear, aec_state.ErleUnbounded(), R2_unbounded);
     }
 
-    AddReverb(ReverbType::kLinear, aec_state, render_buffer, R2);
+    UpdateReverb(ReverbType::kLinear, aec_state, render_buffer);
+    AddReverb(R2);
+    AddReverb(R2_unbounded);
   } else {
     const float echo_path_gain =
         GetEchoPathGain(aec_state, /*gain_for_early_reflections=*/true);
@@ -210,6 +215,7 @@
     if (aec_state.SaturatedEcho()) {
       for (size_t ch = 0; ch < num_capture_channels; ++ch) {
         std::copy(Y2[ch].begin(), Y2[ch].end(), R2[ch].begin());
+        std::copy(Y2[ch].begin(), Y2[ch].end(), R2_unbounded[ch].begin());
       }
     } else {
       // Estimate the echo generating signal power.
@@ -229,11 +235,14 @@
       }
 
       NonLinearEstimate(echo_path_gain, X2, R2);
+      NonLinearEstimate(echo_path_gain, X2, R2_unbounded);
     }
 
     if (config_.echo_model.model_reverb_in_nonlinear_mode &&
         !aec_state.TransparentModeActive()) {
-      AddReverb(ReverbType::kNonLinear, aec_state, render_buffer, R2);
+      UpdateReverb(ReverbType::kNonLinear, aec_state, render_buffer);
+      AddReverb(R2);
+      AddReverb(R2_unbounded);
     }
   }
 
@@ -244,6 +253,7 @@
     for (size_t ch = 0; ch < num_capture_channels; ++ch) {
       for (size_t k = 0; k < kFftLengthBy2Plus1; ++k) {
         R2[ch][k] *= residual_scaling[k];
+        R2_unbounded[ch][k] *= residual_scaling[k];
       }
     }
   }
@@ -292,14 +302,10 @@
   }
 }
 
-// Adds the estimated power of the reverb to the residual echo power.
-void ResidualEchoEstimator::AddReverb(
-    ReverbType reverb_type,
-    const AecState& aec_state,
-    const RenderBuffer& render_buffer,
-    rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2) {
-  const size_t num_capture_channels = R2.size();
-
+// Updates the reverb estimation.
+void ResidualEchoEstimator::UpdateReverb(ReverbType reverb_type,
+                                         const AecState& aec_state,
+                                         const RenderBuffer& render_buffer) {
   // Choose reverb partition based on what type of echo power model is used.
   const size_t first_reverb_partition =
       reverb_type == ReverbType::kLinear
@@ -334,6 +340,11 @@
     echo_reverb_.UpdateReverbNoFreqShaping(render_power, echo_path_gain,
                                            aec_state.ReverbDecay());
   }
+}
+// Adds the estimated power of the reverb to the residual echo power.
+void ResidualEchoEstimator::AddReverb(
+    rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2) const {
+  const size_t num_capture_channels = R2.size();
 
   // Add the reverb power.
   rtc::ArrayView<const float, kFftLengthBy2Plus1> reverb_power =
diff --git a/modules/audio_processing/aec3/residual_echo_estimator.h b/modules/audio_processing/aec3/residual_echo_estimator.h
index 9e97776..c071854 100644
--- a/modules/audio_processing/aec3/residual_echo_estimator.h
+++ b/modules/audio_processing/aec3/residual_echo_estimator.h
@@ -40,7 +40,8 @@
       rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>> S2_linear,
       rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>> Y2,
       bool dominant_nearend,
-      rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2);
+      rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2,
+      rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2_unbounded);
 
  private:
   enum class ReverbType { kLinear, kNonLinear };
@@ -52,12 +53,15 @@
   // render signal.
   void UpdateRenderNoisePower(const RenderBuffer& render_buffer);
 
+  // Updates the reverb estimation.
+  void UpdateReverb(ReverbType reverb_type,
+                    const AecState& aec_state,
+                    const RenderBuffer& render_buffer);
+
   // Adds the estimated unmodelled echo power to the residual echo power
   // estimate.
-  void AddReverb(ReverbType reverb_type,
-                 const AecState& aec_state,
-                 const RenderBuffer& render_buffer,
-                 rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2);
+  void AddReverb(
+      rtc::ArrayView<std::array<float, kFftLengthBy2Plus1>> R2) const;
 
   // Gets the echo path gain to apply.
   float GetEchoPathGain(const AecState& aec_state,
diff --git a/modules/audio_processing/aec3/residual_echo_estimator_unittest.cc b/modules/audio_processing/aec3/residual_echo_estimator_unittest.cc
index e80838b..3d760b7 100644
--- a/modules/audio_processing/aec3/residual_echo_estimator_unittest.cc
+++ b/modules/audio_processing/aec3/residual_echo_estimator_unittest.cc
@@ -48,6 +48,8 @@
       num_capture_channels);
   std::vector<std::array<float, kFftLengthBy2Plus1>> Y2(num_capture_channels);
   std::vector<std::array<float, kFftLengthBy2Plus1>> R2(num_capture_channels);
+  std::vector<std::array<float, kFftLengthBy2Plus1>> R2_unbounded(
+      num_capture_channels);
   std::vector<std::vector<std::vector<float>>> x(
       kNumBands, std::vector<std::vector<float>>(
                      num_render_channels, std::vector<float>(kBlockSize, 0.f)));
@@ -100,7 +102,8 @@
                      output);
 
     estimator.Estimate(aec_state, *render_delay_buffer->GetRenderBuffer(),
-                       S2_linear, Y2, /*dominant_nearend=*/false, R2);
+                       S2_linear, Y2, /*dominant_nearend=*/false, R2,
+                       R2_unbounded);
   }
 }
 
diff --git a/modules/audio_processing/aec3/subband_erle_estimator.cc b/modules/audio_processing/aec3/subband_erle_estimator.cc
index 1e957f2..dc7f92f 100644
--- a/modules/audio_processing/aec3/subband_erle_estimator.cc
+++ b/modules/audio_processing/aec3/subband_erle_estimator.cc
@@ -49,6 +49,7 @@
       accum_spectra_(num_capture_channels),
       erle_(num_capture_channels),
       erle_onset_compensated_(num_capture_channels),
+      erle_unbounded_(num_capture_channels),
       erle_during_onsets_(num_capture_channels),
       coming_onset_(num_capture_channels),
       hold_counters_(num_capture_channels) {
@@ -62,6 +63,7 @@
   for (size_t ch = 0; ch < num_capture_channels; ++ch) {
     erle_[ch].fill(min_erle_);
     erle_onset_compensated_[ch].fill(min_erle_);
+    erle_unbounded_[ch].fill(min_erle_);
     erle_during_onsets_[ch].fill(min_erle_);
     coming_onset_[ch].fill(true);
     hold_counters_[ch].fill(0);
@@ -90,6 +92,10 @@
     auto& erle_oc = erle_onset_compensated_[ch];
     erle_oc[0] = erle_oc[1];
     erle_oc[kFftLengthBy2] = erle_oc[kFftLengthBy2 - 1];
+
+    auto& erle_u = erle_unbounded_[ch];
+    erle_u[0] = erle_u[1];
+    erle_u[kFftLengthBy2] = erle_u[kFftLengthBy2 - 1];
   }
 }
 
@@ -163,6 +169,11 @@
           update_erle_band(erle_onset_compensated_[ch][k], new_erle[k],
                            low_render_energy, min_erle_, max_erle_[k]);
         }
+
+        // Virtually unbounded ERLE.
+        constexpr float kUnboundedErleMax = 100000.0f;
+        update_erle_band(erle_unbounded_[ch][k], new_erle[k], low_render_energy,
+                         min_erle_, kUnboundedErleMax);
       }
     }
   }
diff --git a/modules/audio_processing/aec3/subband_erle_estimator.h b/modules/audio_processing/aec3/subband_erle_estimator.h
index ffed6a5..8bf9c4d 100644
--- a/modules/audio_processing/aec3/subband_erle_estimator.h
+++ b/modules/audio_processing/aec3/subband_erle_estimator.h
@@ -47,6 +47,12 @@
                                                      : erle_;
   }
 
+  // Returns the non-capped ERLE estimate.
+  rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>> ErleUnbounded()
+      const {
+    return erle_unbounded_;
+  }
+
   // Returns the ERLE estimate at onsets (only used for testing).
   rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>> ErleDuringOnsets()
       const {
@@ -88,6 +94,7 @@
   std::vector<std::array<float, kFftLengthBy2Plus1>> erle_;
   // ERLE lowered during render onsets.
   std::vector<std::array<float, kFftLengthBy2Plus1>> erle_onset_compensated_;
+  std::vector<std::array<float, kFftLengthBy2Plus1>> erle_unbounded_;
   // Estimation of ERLE during render onsets.
   std::vector<std::array<float, kFftLengthBy2Plus1>> erle_during_onsets_;
   std::vector<std::array<bool, kFftLengthBy2Plus1>> coming_onset_;
diff --git a/modules/audio_processing/aec3/suppression_gain.cc b/modules/audio_processing/aec3/suppression_gain.cc
index 601c9c2..6405d71 100644
--- a/modules/audio_processing/aec3/suppression_gain.cc
+++ b/modules/audio_processing/aec3/suppression_gain.cc
@@ -23,10 +23,15 @@
 #include "modules/audio_processing/logging/apm_data_dumper.h"
 #include "rtc_base/atomic_ops.h"
 #include "rtc_base/checks.h"
+#include "system_wrappers/include/field_trial.h"
 
 namespace webrtc {
 namespace {
 
+bool UseUnboundedEchoSpectrum() {
+  return field_trial::IsEnabled("WebRTC-Aec3UseUnboundedEchoSpectrum");
+}
+
 void LimitLowFrequencyGains(std::array<float, kFftLengthBy2Plus1>* gain) {
   // Limit the low frequency gains to avoid the impact of the high-pass filter
   // on the lower-frequency gain influencing the overall achieved gain.
@@ -342,7 +347,8 @@
                       config_.suppressor.nearend_tuning),
       normal_params_(config_.suppressor.last_lf_band,
                      config_.suppressor.first_hf_band,
-                     config_.suppressor.normal_tuning) {
+                     config_.suppressor.normal_tuning),
+      use_unbounded_echo_spectrum_(UseUnboundedEchoSpectrum()) {
   RTC_DCHECK_LT(0, state_change_duration_blocks_);
   last_gain_.fill(1.f);
   if (config_.suppressor.use_subband_nearend_detection) {
@@ -364,6 +370,8 @@
     rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>>
         residual_echo_spectrum,
     rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>>
+        residual_echo_spectrum_unbounded,
+    rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>>
         comfort_noise_spectrum,
     const RenderSignalAnalyzer& render_signal_analyzer,
     const AecState& aec_state,
@@ -374,8 +382,13 @@
   RTC_DCHECK(high_bands_gain);
   RTC_DCHECK(low_band_gain);
 
+  // Choose residual echo spectrum for the dominant nearend detector.
+  const auto echo = use_unbounded_echo_spectrum_
+                        ? residual_echo_spectrum_unbounded
+                        : residual_echo_spectrum;
+
   // Update the nearend state selection.
-  dominant_nearend_detector_->Update(nearend_spectrum, residual_echo_spectrum,
+  dominant_nearend_detector_->Update(nearend_spectrum, echo,
                                      comfort_noise_spectrum, initial_state_);
 
   // Compute gain for the lower band.
@@ -391,6 +404,9 @@
   *high_bands_gain =
       UpperBandsGain(echo_spectrum, comfort_noise_spectrum, narrow_peak_band,
                      aec_state.SaturatedEcho(), render, *low_band_gain);
+
+  data_dumper_->DumpRaw("aec3_dominant_nearend",
+                        dominant_nearend_detector_->IsNearendState());
 }
 
 void SuppressionGain::SetInitialState(bool state) {
diff --git a/modules/audio_processing/aec3/suppression_gain.h b/modules/audio_processing/aec3/suppression_gain.h
index 57264c2..7c4a1c9 100644
--- a/modules/audio_processing/aec3/suppression_gain.h
+++ b/modules/audio_processing/aec3/suppression_gain.h
@@ -43,6 +43,8 @@
       rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>>
           residual_echo_spectrum,
       rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>>
+          residual_echo_spectrum_unbounded,
+      rtc::ArrayView<const std::array<float, kFftLengthBy2Plus1>>
           comfort_noise_spectrum,
       const RenderSignalAnalyzer& render_signal_analyzer,
       const AecState& aec_state,
@@ -128,6 +130,9 @@
   std::vector<aec3::MovingAverage> nearend_smoothers_;
   const GainParameters nearend_params_;
   const GainParameters normal_params_;
+  // Determines if the dominant nearend detector uses the unbounded residual
+  // echo spectrum.
+  const bool use_unbounded_echo_spectrum_;
   std::unique_ptr<NearendDetector> dominant_nearend_detector_;
 
   RTC_DISALLOW_COPY_AND_ASSIGN(SuppressionGain);
diff --git a/modules/audio_processing/aec3/suppression_gain_unittest.cc b/modules/audio_processing/aec3/suppression_gain_unittest.cc
index 26bfc24..999b0f2 100644
--- a/modules/audio_processing/aec3/suppression_gain_unittest.cc
+++ b/modules/audio_processing/aec3/suppression_gain_unittest.cc
@@ -26,29 +26,30 @@
 
 // Verifies that the check for non-null output gains works.
 TEST(SuppressionGainDeathTest, NullOutputGains) {
-  std::vector<std::array<float, kFftLengthBy2Plus1>> E2(1, {0.f});
-  std::vector<std::array<float, kFftLengthBy2Plus1>> R2(1, {0.f});
+  std::vector<std::array<float, kFftLengthBy2Plus1>> E2(1, {0.0f});
+  std::vector<std::array<float, kFftLengthBy2Plus1>> R2(1, {0.0f});
+  std::vector<std::array<float, kFftLengthBy2Plus1>> R2_unbounded(1, {0.0f});
   std::vector<std::array<float, kFftLengthBy2Plus1>> S2(1);
-  std::vector<std::array<float, kFftLengthBy2Plus1>> N2(1, {0.f});
+  std::vector<std::array<float, kFftLengthBy2Plus1>> N2(1, {0.0f});
   for (auto& S2_k : S2) {
-    S2_k.fill(.1f);
+    S2_k.fill(0.1f);
   }
   FftData E;
   FftData Y;
-  E.re.fill(0.f);
-  E.im.fill(0.f);
-  Y.re.fill(0.f);
-  Y.im.fill(0.f);
+  E.re.fill(0.0f);
+  E.im.fill(0.0f);
+  Y.re.fill(0.0f);
+  Y.im.fill(0.0f);
 
   float high_bands_gain;
   AecState aec_state(EchoCanceller3Config{}, 1);
   EXPECT_DEATH(
       SuppressionGain(EchoCanceller3Config{}, DetectOptimization(), 16000, 1)
-          .GetGain(E2, S2, R2, N2,
+          .GetGain(E2, S2, R2, R2_unbounded, N2,
                    RenderSignalAnalyzer((EchoCanceller3Config{})), aec_state,
                    std::vector<std::vector<std::vector<float>>>(
                        3, std::vector<std::vector<float>>(
-                              1, std::vector<float>(kBlockSize, 0.f))),
+                              1, std::vector<float>(kBlockSize, 0.0f))),
                    false, &high_bands_gain, nullptr),
       "");
 }
@@ -67,15 +68,17 @@
   float high_bands_gain;
   std::vector<std::array<float, kFftLengthBy2Plus1>> E2(kNumCaptureChannels);
   std::vector<std::array<float, kFftLengthBy2Plus1>> S2(kNumCaptureChannels,
-                                                        {0.f});
+                                                        {0.0f});
   std::vector<std::array<float, kFftLengthBy2Plus1>> Y2(kNumCaptureChannels);
   std::vector<std::array<float, kFftLengthBy2Plus1>> R2(kNumCaptureChannels);
+  std::vector<std::array<float, kFftLengthBy2Plus1>> R2_unbounded(
+      kNumCaptureChannels);
   std::vector<std::array<float, kFftLengthBy2Plus1>> N2(kNumCaptureChannels);
   std::array<float, kFftLengthBy2Plus1> g;
   std::vector<SubtractorOutput> output(kNumCaptureChannels);
   std::vector<std::vector<std::vector<float>>> x(
       kNumBands, std::vector<std::vector<float>>(
-                     kNumRenderChannels, std::vector<float>(kBlockSize, 0.f)));
+                     kNumRenderChannels, std::vector<float>(kBlockSize, 0.0f)));
   EchoCanceller3Config config;
   AecState aec_state(config, kNumCaptureChannels);
   ApmDataDumper data_dumper(42);
@@ -89,8 +92,9 @@
   for (size_t ch = 0; ch < kNumCaptureChannels; ++ch) {
     E2[ch].fill(10.f);
     Y2[ch].fill(10.f);
-    R2[ch].fill(.1f);
-    N2[ch].fill(100.f);
+    R2[ch].fill(0.1f);
+    R2_unbounded[ch].fill(0.1f);
+    N2[ch].fill(100.0f);
   }
   for (auto& subtractor_output : output) {
     subtractor_output.Reset();
@@ -107,17 +111,18 @@
     aec_state.Update(delay_estimate, subtractor.FilterFrequencyResponses(),
                      subtractor.FilterImpulseResponses(),
                      *render_delay_buffer->GetRenderBuffer(), E2, Y2, output);
-    suppression_gain.GetGain(E2, S2, R2, N2, analyzer, aec_state, x, false,
-                             &high_bands_gain, &g);
+    suppression_gain.GetGain(E2, S2, R2, R2_unbounded, N2, analyzer, aec_state,
+                             x, false, &high_bands_gain, &g);
   }
   std::for_each(g.begin(), g.end(),
-                [](float a) { EXPECT_NEAR(1.f, a, 0.001); });
+                [](float a) { EXPECT_NEAR(1.0f, a, 0.001f); });
 
   // Ensure that a strong nearend is detected to mask any echoes.
   for (size_t ch = 0; ch < kNumCaptureChannels; ++ch) {
     E2[ch].fill(100.f);
     Y2[ch].fill(100.f);
     R2[ch].fill(0.1f);
+    R2_unbounded[ch].fill(0.1f);
     S2[ch].fill(0.1f);
     N2[ch].fill(0.f);
   }
@@ -126,22 +131,23 @@
     aec_state.Update(delay_estimate, subtractor.FilterFrequencyResponses(),
                      subtractor.FilterImpulseResponses(),
                      *render_delay_buffer->GetRenderBuffer(), E2, Y2, output);
-    suppression_gain.GetGain(E2, S2, R2, N2, analyzer, aec_state, x, false,
-                             &high_bands_gain, &g);
+    suppression_gain.GetGain(E2, S2, R2, R2_unbounded, N2, analyzer, aec_state,
+                             x, false, &high_bands_gain, &g);
   }
   std::for_each(g.begin(), g.end(),
-                [](float a) { EXPECT_NEAR(1.f, a, 0.001); });
+                [](float a) { EXPECT_NEAR(1.0f, a, 0.001f); });
 
   // Add a strong echo to one of the channels and ensure that it is suppressed.
-  E2[1].fill(1000000000.f);
-  R2[1].fill(10000000000000.f);
+  E2[1].fill(1000000000.0f);
+  R2[1].fill(10000000000000.0f);
+  R2_unbounded[1].fill(10000000000000.0f);
 
   for (int k = 0; k < 10; ++k) {
-    suppression_gain.GetGain(E2, S2, R2, N2, analyzer, aec_state, x, false,
-                             &high_bands_gain, &g);
+    suppression_gain.GetGain(E2, S2, R2, R2_unbounded, N2, analyzer, aec_state,
+                             x, false, &high_bands_gain, &g);
   }
   std::for_each(g.begin(), g.end(),
-                [](float a) { EXPECT_NEAR(0.f, a, 0.001); });
+                [](float a) { EXPECT_NEAR(0.0f, a, 0.001f); });
 }
 
 }  // namespace aec3