AudioProcessing: Add integration tests for NeuralResidualEchoEstimator

Verifies the ML-REE integration path within APM by injecting a noop
TFLite model via BuiltinAudioProcessingBuilder and processing 250 frames
with a simulated 100 ms delayed and attenuated echo path.

Bug: webrtc:442444736
Change-Id: I56e0bc0546573fc4e6dc1b2fdeccd6a23560a62c
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/470982
Reviewed-by: Sam Zackrisson <saza@webrtc.org>
Commit-Queue: Jesus de Vicente Pena <devicentepena@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47791}
diff --git a/api/audio/neural_residual_echo_estimator_creator.h b/api/audio/neural_residual_echo_estimator_creator.h
index 1d756b7..781313d 100644
--- a/api/audio/neural_residual_echo_estimator_creator.h
+++ b/api/audio/neural_residual_echo_estimator_creator.h
@@ -28,7 +28,8 @@
 // Returns nullptr if unable to read the file or initialize a model from the
 // file contents.
 //
-// Beware: This is an experimental API and may change without notice.
+// `model` needs to outlive the created estimator. `op_resolver` is only used
+// during this call.
 RTC_EXPORT
 absl_nullable std::unique_ptr<NeuralResidualEchoEstimator>
 CreateNeuralResidualEchoEstimator(const tflite::FlatBufferModel* model,
diff --git a/modules/audio_processing/BUILD.gn b/modules/audio_processing/BUILD.gn
index 1536a4b..daf0275 100644
--- a/modules/audio_processing/BUILD.gn
+++ b/modules/audio_processing/BUILD.gn
@@ -413,7 +413,9 @@
           "../../api/audio:audio_frame_api",
           "../../api/audio:echo_control",
           "../../api/audio:neural_residual_echo_estimator_api",
+          "../../api/audio:neural_residual_echo_estimator_creator",
           "../../rtc_base:rtc_base_tests_utils",
+          "aec3/neural_residual_echo_estimator:neural_residual_echo_estimator_test_helper",
           "aec_dump",
           "aec_dump:aec_dump_unittests",
           "//third_party/abseil-cpp/absl/flags:flag",
diff --git a/modules/audio_processing/aec3/neural_residual_echo_estimator/BUILD.gn b/modules/audio_processing/aec3/neural_residual_echo_estimator/BUILD.gn
index 029a011..1a0c14b 100644
--- a/modules/audio_processing/aec3/neural_residual_echo_estimator/BUILD.gn
+++ b/modules/audio_processing/aec3/neural_residual_echo_estimator/BUILD.gn
@@ -54,6 +54,33 @@
 
 if (rtc_include_tests) {
   if (rtc_enable_protobuf) {
+    rtc_library("neural_residual_echo_estimator_test_helper") {
+      testonly = true
+      visibility = [ "*" ]
+      configs += [ "//third_party/tflite:tflite_config_no_undef" ]
+      sources = [
+        "neural_residual_echo_estimator_test_helper.cc",
+        "neural_residual_echo_estimator_test_helper.h",
+      ]
+      deps = [
+        "../../../../api/audio:neural_residual_echo_estimator_api",
+        "../../../../api/audio:neural_residual_echo_estimator_creator",
+        "../../../../rtc_base:checks",
+        "../../../../test:fileutils",
+      ]
+      if (build_with_chromium) {
+        deps += [
+          "//third_party/tflite",
+          "//third_party/tflite:tflite_builtin_op_resolver",
+        ]
+      } else {
+        deps += [
+          "//third_party/tflite:tflite_builtin_op_resolver_standalone",
+          "//third_party/tflite:tflite_standalone",
+        ]
+      }
+    }
+
     rtc_library("neural_residual_echo_estimator_unittest") {
       testonly = true
       configs += [
diff --git a/modules/audio_processing/aec3/neural_residual_echo_estimator/neural_residual_echo_estimator_test_helper.cc b/modules/audio_processing/aec3/neural_residual_echo_estimator/neural_residual_echo_estimator_test_helper.cc
new file mode 100644
index 0000000..fb7ad82
--- /dev/null
+++ b/modules/audio_processing/aec3/neural_residual_echo_estimator/neural_residual_echo_estimator_test_helper.cc
@@ -0,0 +1,52 @@
+/*
+ *  Copyright (c) 2026 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/aec3/neural_residual_echo_estimator/neural_residual_echo_estimator_test_helper.h"
+
+#include <memory>
+#include <string>
+
+#include "api/audio/neural_residual_echo_estimator.h"
+#include "api/audio/neural_residual_echo_estimator_creator.h"
+#include "rtc_base/checks.h"
+#include "test/testsupport/file_utils.h"
+#include "third_party/tflite/src/tensorflow/lite/kernels/register.h"
+#include "third_party/tflite/src/tensorflow/lite/model_builder.h"
+
+namespace webrtc {
+
+class NeuralResidualEchoEstimatorTestHelperImpl
+    : public NeuralResidualEchoEstimatorTestHelper {
+ public:
+  NeuralResidualEchoEstimatorTestHelperImpl() {
+    std::string model_path = test::ResourcePath(
+        "audio_processing/aec3/noop_ml_aec_model_for_testing", "tflite");
+    model_ = tflite::FlatBufferModel::BuildFromFile(model_path.c_str());
+    RTC_CHECK(model_);
+  }
+
+  ~NeuralResidualEchoEstimatorTestHelperImpl() = default;
+
+  std::unique_ptr<NeuralResidualEchoEstimator> GetNeuralResidualEchoEstimator()
+      override {
+    tflite::ops::builtin::BuiltinOpResolver ops_resolver;
+    return CreateNeuralResidualEchoEstimator(model_.get(), &ops_resolver);
+  }
+
+ private:
+  std::unique_ptr<tflite::FlatBufferModel> model_;
+};
+
+std::unique_ptr<NeuralResidualEchoEstimatorTestHelper>
+CreateNeuralResidualEchoEstimatorTestHelper() {
+  return std::make_unique<NeuralResidualEchoEstimatorTestHelperImpl>();
+}
+
+}  // namespace webrtc
diff --git a/modules/audio_processing/aec3/neural_residual_echo_estimator/neural_residual_echo_estimator_test_helper.h b/modules/audio_processing/aec3/neural_residual_echo_estimator/neural_residual_echo_estimator_test_helper.h
new file mode 100644
index 0000000..82b13ff
--- /dev/null
+++ b/modules/audio_processing/aec3/neural_residual_echo_estimator/neural_residual_echo_estimator_test_helper.h
@@ -0,0 +1,44 @@
+/*
+ *  Copyright (c) 2026 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_AEC3_NEURAL_RESIDUAL_ECHO_ESTIMATOR_NEURAL_RESIDUAL_ECHO_ESTIMATOR_TEST_HELPER_H_
+#define MODULES_AUDIO_PROCESSING_AEC3_NEURAL_RESIDUAL_ECHO_ESTIMATOR_NEURAL_RESIDUAL_ECHO_ESTIMATOR_TEST_HELPER_H_
+
+#include <memory>
+
+#include "api/audio/neural_residual_echo_estimator.h"
+
+namespace webrtc {
+// Abstract interface for testing the NeuralResidualEchoEstimator.
+// Encapsulating the underlying implementation here allows test suites to
+// inject the estimator without requiring direct linkage or visibility into
+// TFLite headers and dependencies.
+//
+// The NeuralResidualEchoEstimatorTestHelper instance must outlive the usage of
+// the returned NeuralResidualEchoEstimator, as it owns the underlying model.
+class NeuralResidualEchoEstimatorTestHelper {
+ public:
+  virtual ~NeuralResidualEchoEstimatorTestHelper() = default;
+
+  // Returns the encapsulated estimator instance, transferring ownership to the
+  // caller. For example, this can be used for injection into an APM pipeline
+  // via BuiltinAudioProcessingBuilder::SetNeuralResidualEchoEstimator().
+  virtual std::unique_ptr<NeuralResidualEchoEstimator>
+  GetNeuralResidualEchoEstimator() = 0;
+};
+
+// Creates a test helper instance initializing a no-op TFLite model and
+// estimator for injection.
+std::unique_ptr<NeuralResidualEchoEstimatorTestHelper>
+CreateNeuralResidualEchoEstimatorTestHelper();
+
+}  // namespace webrtc
+
+#endif  // MODULES_AUDIO_PROCESSING_AEC3_NEURAL_RESIDUAL_ECHO_ESTIMATOR_NEURAL_RESIDUAL_ECHO_ESTIMATOR_TEST_HELPER_H_
diff --git a/modules/audio_processing/audio_processing_impl_unittest.cc b/modules/audio_processing/audio_processing_impl_unittest.cc
index 3b28dd2..491f9d0 100644
--- a/modules/audio_processing/audio_processing_impl_unittest.cc
+++ b/modules/audio_processing/audio_processing_impl_unittest.cc
@@ -24,10 +24,12 @@
 #include "api/audio/audio_processing.h"
 #include "api/audio/builtin_audio_processing_builder.h"
 #include "api/audio/echo_control.h"
+#include "api/audio/neural_residual_echo_estimator.h"
 #include "api/environment/environment.h"
 #include "api/make_ref_counted.h"
 #include "api/ref_count.h"
 #include "api/scoped_refptr.h"
+#include "modules/audio_processing/aec3/neural_residual_echo_estimator/neural_residual_echo_estimator_test_helper.h"
 #include "modules/audio_processing/test/echo_canceller_test_tools.h"
 #include "modules/audio_processing/test/echo_control_mock.h"
 #include "modules/audio_processing/test/test_utils.h"
@@ -640,6 +642,68 @@
             test_echo_detector->last_render_audio_first_sample());
 }
 
+TEST(AudioProcessingImplTest, NeuralResidualEchoEstimatorInjection) {
+  std::unique_ptr<NeuralResidualEchoEstimatorTestHelper> ree_helper =
+      CreateNeuralResidualEchoEstimatorTestHelper();
+  std::unique_ptr<NeuralResidualEchoEstimator> ree_estimator =
+      ree_helper->GetNeuralResidualEchoEstimator();
+  ASSERT_NE(ree_estimator, nullptr);
+  scoped_refptr<AudioProcessing> apm =
+      BuiltinAudioProcessingBuilder()
+          .SetNeuralResidualEchoEstimator(std::move(ree_estimator))
+          .Build(CreateTestEnvironment());
+  ASSERT_NE(apm, nullptr);
+  AudioProcessing::Config apm_config;
+  apm_config.echo_canceller.enabled = true;
+  apm->ApplyConfig(apm_config);
+  constexpr int kSampleRateHz = 16000;
+  constexpr size_t kNumChannels = 2;
+  const ProcessingConfig processing_config = {{
+      {kSampleRateHz, kNumChannels},
+      {kSampleRateHz, kNumChannels},
+      {kSampleRateHz, kNumChannels},
+      {kSampleRateHz, kNumChannels},
+  }};
+  apm->Initialize(processing_config);
+
+  constexpr size_t kFrameSize = kSampleRateHz / 100;
+  std::array<std::array<float, kFrameSize>, kNumChannels> render_buffer;
+  std::array<std::array<float, kFrameSize>, kNumChannels> capture_buffer;
+  float* render_channel_pointers[] = {render_buffer[0].data(),
+                                      render_buffer[1].data()};
+  float* capture_channel_pointers[] = {capture_buffer[0].data(),
+                                       capture_buffer[1].data()};
+  StreamConfig stream_config(kSampleRateHz, kNumChannels);
+  Random random_generator(2341U);
+  constexpr size_t kRenderDelaySamples = kSampleRateHz * 10 / 1000;  // 10 ms
+  DelayBuffer<float> render_delay_buffer(kRenderDelaySamples);
+  constexpr size_t kCaptureDelaySamples = kSampleRateHz * 100 / 1000;  // 100 ms
+  DelayBuffer<float> capture_delay_buffer(kCaptureDelaySamples);
+  constexpr int kFramesToProcess = 250;
+  for (int i = 0; i < kFramesToProcess; ++i) {
+    RandomizeSampleVector(&random_generator, render_buffer[0]);
+    render_delay_buffer.Delay(render_buffer[0], render_buffer[1]);
+    std::array<float, kFrameSize> sum_render;
+    for (size_t sample_idx = 0; sample_idx < kFrameSize; ++sample_idx) {
+      sum_render[sample_idx] =
+          render_buffer[0][sample_idx] + render_buffer[1][sample_idx];
+    }
+    std::array<float, kFrameSize> delayed_sum_render;
+    capture_delay_buffer.Delay(sum_render, delayed_sum_render);
+    for (size_t sample_idx = 0; sample_idx < kFrameSize; ++sample_idx) {
+      float val = delayed_sum_render[sample_idx] * 0.1f;
+      capture_buffer[0][sample_idx] = val;
+      capture_buffer[1][sample_idx] = val;
+    }
+    ASSERT_EQ(apm->ProcessReverseStream(render_channel_pointers, stream_config,
+                                        stream_config, render_channel_pointers),
+              AudioProcessing::Error::kNoError);
+    ASSERT_EQ(apm->ProcessStream(capture_channel_pointers, stream_config,
+                                 stream_config, capture_channel_pointers),
+              AudioProcessing::Error::kNoError);
+  }
+}
+
 class StartupInputVolumeParameterizedTest
     : public ::testing::TestWithParam<int> {};