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> {};