This CL adds functionality for passing the information about the estimated delay to the echo remover in AEC3. The CL also adds information about how long ago the delay changed, and how long ago the delay estimate was updated. Bug: webrtc:8671 Change-Id: If274ffe0465eb550f3e186d0599c6dc6fef7f5e8 Reviewed-on: https://webrtc-review.googlesource.com/55261 Reviewed-by: Gustaf Ullberg <gustaf@webrtc.org> Commit-Queue: Per Åhgren <peah@webrtc.org> Cr-Commit-Position: refs/heads/master@{#22137}
367 lines
13 KiB
C++
367 lines
13 KiB
C++
/*
|
|
* Copyright (c) 2017 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/aec_state.h"
|
|
|
|
#include <math.h>
|
|
|
|
#include <numeric>
|
|
#include <vector>
|
|
|
|
#include "api/array_view.h"
|
|
#include "modules/audio_processing/logging/apm_data_dumper.h"
|
|
#include "rtc_base/atomicops.h"
|
|
#include "rtc_base/checks.h"
|
|
|
|
namespace webrtc {
|
|
namespace {
|
|
|
|
// Computes delay of the adaptive filter.
|
|
int EstimateFilterDelay(
|
|
const std::vector<std::array<float, kFftLengthBy2Plus1>>&
|
|
adaptive_filter_frequency_response) {
|
|
const auto& H2 = adaptive_filter_frequency_response;
|
|
constexpr size_t kUpperBin = kFftLengthBy2 - 5;
|
|
RTC_DCHECK_GE(kMaxAdaptiveFilterLength, H2.size());
|
|
std::array<int, kMaxAdaptiveFilterLength> delays;
|
|
delays.fill(0);
|
|
for (size_t k = 1; k < kUpperBin; ++k) {
|
|
// Find the maximum of H2[j].
|
|
size_t peak = 0;
|
|
for (size_t j = 0; j < H2.size(); ++j) {
|
|
if (H2[j][k] > H2[peak][k]) {
|
|
peak = j;
|
|
}
|
|
}
|
|
++delays[peak];
|
|
}
|
|
|
|
return std::distance(delays.begin(),
|
|
std::max_element(delays.begin(), delays.end()));
|
|
}
|
|
|
|
float ComputeGainRampupIncrease(const EchoCanceller3Config& config) {
|
|
const auto& c = config.echo_removal_control.gain_rampup;
|
|
return powf(1.f / c.first_non_zero_gain, 1.f / c.non_zero_gain_blocks);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int AecState::instance_count_ = 0;
|
|
|
|
AecState::AecState(const EchoCanceller3Config& config)
|
|
: data_dumper_(
|
|
new ApmDataDumper(rtc::AtomicOps::Increment(&instance_count_))),
|
|
erle_estimator_(config.erle.min, config.erle.max_l, config.erle.max_h),
|
|
config_(config),
|
|
max_render_(config_.filter.main.length_blocks, 0.f),
|
|
reverb_decay_(config_.ep_strength.default_len),
|
|
gain_rampup_increase_(ComputeGainRampupIncrease(config_)) {}
|
|
|
|
AecState::~AecState() = default;
|
|
|
|
void AecState::HandleEchoPathChange(
|
|
const EchoPathVariability& echo_path_variability) {
|
|
const auto full_reset = [&]() {
|
|
blocks_since_last_saturation_ = 0;
|
|
usable_linear_estimate_ = false;
|
|
echo_leakage_detected_ = false;
|
|
capture_signal_saturation_ = false;
|
|
echo_saturation_ = false;
|
|
previous_max_sample_ = 0.f;
|
|
std::fill(max_render_.begin(), max_render_.end(), 0.f);
|
|
blocks_with_proper_filter_adaptation_ = 0;
|
|
capture_block_counter_ = 0;
|
|
filter_has_had_time_to_converge_ = false;
|
|
render_received_ = false;
|
|
blocks_with_active_render_ = 0;
|
|
initial_state_ = true;
|
|
};
|
|
|
|
// TODO(peah): Refine the reset scheme according to the type of gain and
|
|
// delay adjustment.
|
|
if (echo_path_variability.gain_change) {
|
|
full_reset();
|
|
}
|
|
|
|
if (echo_path_variability.delay_change !=
|
|
EchoPathVariability::DelayAdjustment::kBufferReadjustment) {
|
|
full_reset();
|
|
} else if (echo_path_variability.delay_change !=
|
|
EchoPathVariability::DelayAdjustment::kBufferFlush) {
|
|
active_render_seen_ = false;
|
|
full_reset();
|
|
} else if (echo_path_variability.delay_change !=
|
|
EchoPathVariability::DelayAdjustment::kDelayReset) {
|
|
full_reset();
|
|
} else if (echo_path_variability.delay_change !=
|
|
EchoPathVariability::DelayAdjustment::kNewDetectedDelay) {
|
|
full_reset();
|
|
} else if (echo_path_variability.gain_change) {
|
|
capture_block_counter_ = kNumBlocksPerSecond;
|
|
}
|
|
}
|
|
|
|
void AecState::Update(
|
|
const rtc::Optional<DelayEstimate>& delay_estimate,
|
|
const std::vector<std::array<float, kFftLengthBy2Plus1>>&
|
|
adaptive_filter_frequency_response,
|
|
const std::vector<float>& adaptive_filter_impulse_response,
|
|
bool converged_filter,
|
|
const RenderBuffer& render_buffer,
|
|
const std::array<float, kFftLengthBy2Plus1>& E2_main,
|
|
const std::array<float, kFftLengthBy2Plus1>& Y2,
|
|
const std::array<float, kBlockSize>& s,
|
|
bool echo_leakage_detected) {
|
|
// Store input parameters.
|
|
echo_leakage_detected_ = echo_leakage_detected;
|
|
|
|
// Estimate the filter delay.
|
|
filter_delay_ = EstimateFilterDelay(adaptive_filter_frequency_response);
|
|
const std::vector<float>& x = render_buffer.Block(-filter_delay_)[0];
|
|
|
|
// Update counters.
|
|
++capture_block_counter_;
|
|
const bool active_render_block = DetectActiveRender(x);
|
|
blocks_with_active_render_ += active_render_block ? 1 : 0;
|
|
blocks_with_proper_filter_adaptation_ +=
|
|
active_render_block && !SaturatedCapture() ? 1 : 0;
|
|
|
|
// Update the limit on the echo suppression after an echo path change to avoid
|
|
// an initial echo burst.
|
|
UpdateSuppressorGainLimit(render_buffer.GetRenderActivity());
|
|
|
|
// Update the ERL and ERLE measures.
|
|
if (converged_filter && capture_block_counter_ >= 2 * kNumBlocksPerSecond) {
|
|
const auto& X2 = render_buffer.Spectrum(filter_delay_);
|
|
erle_estimator_.Update(X2, Y2, E2_main);
|
|
erl_estimator_.Update(X2, Y2);
|
|
}
|
|
|
|
// Update the echo audibility evaluator.
|
|
echo_audibility_.Update(x, s, converged_filter);
|
|
|
|
// Detect and flag echo saturation.
|
|
// TODO(peah): Add the delay in this computation to ensure that the render and
|
|
// capture signals are properly aligned.
|
|
if (config_.ep_strength.echo_can_saturate) {
|
|
echo_saturation_ = DetectEchoSaturation(x);
|
|
}
|
|
|
|
// TODO(peah): Move?
|
|
filter_has_had_time_to_converge_ =
|
|
blocks_with_proper_filter_adaptation_ >= 1.5f * kNumBlocksPerSecond;
|
|
|
|
initial_state_ =
|
|
blocks_with_proper_filter_adaptation_ < 5 * kNumBlocksPerSecond;
|
|
|
|
// Flag whether the linear filter estimate is usable.
|
|
usable_linear_estimate_ =
|
|
!echo_saturation_ &&
|
|
(converged_filter && filter_has_had_time_to_converge_) &&
|
|
capture_block_counter_ >= 1.f * kNumBlocksPerSecond && !TransparentMode();
|
|
|
|
// After an amount of active render samples for which an echo should have been
|
|
// detected in the capture signal if the ERL was not infinite, flag that a
|
|
// transparent mode should be entered.
|
|
transparent_mode_ =
|
|
!converged_filter &&
|
|
(blocks_with_active_render_ == 0 ||
|
|
blocks_with_proper_filter_adaptation_ >= 5 * kNumBlocksPerSecond);
|
|
}
|
|
|
|
void AecState::UpdateReverb(const std::vector<float>& impulse_response) {
|
|
if ((!(filter_delay_ && usable_linear_estimate_)) ||
|
|
(filter_delay_ >
|
|
static_cast<int>(config_.filter.main.length_blocks) - 4)) {
|
|
return;
|
|
}
|
|
|
|
// Form the data to match against by squaring the impulse response
|
|
// coefficients.
|
|
std::array<float, GetTimeDomainLength(kMaxAdaptiveFilterLength)>
|
|
matching_data_data;
|
|
RTC_DCHECK_LE(GetTimeDomainLength(config_.filter.main.length_blocks),
|
|
matching_data_data.size());
|
|
rtc::ArrayView<float> matching_data(
|
|
matching_data_data.data(),
|
|
GetTimeDomainLength(config_.filter.main.length_blocks));
|
|
std::transform(impulse_response.begin(), impulse_response.end(),
|
|
matching_data.begin(), [](float a) { return a * a; });
|
|
|
|
// Avoid matching against noise in the model by subtracting an estimate of the
|
|
// model noise power.
|
|
constexpr size_t kTailLength = 64;
|
|
const size_t tail_index =
|
|
GetTimeDomainLength(config_.filter.main.length_blocks) - kTailLength;
|
|
const float tail_power = *std::max_element(matching_data.begin() + tail_index,
|
|
matching_data.end());
|
|
std::for_each(matching_data.begin(), matching_data.begin() + tail_index,
|
|
[tail_power](float& a) { a = std::max(0.f, a - tail_power); });
|
|
|
|
// Identify the peak index of the impulse response.
|
|
const size_t peak_index = *std::max_element(
|
|
matching_data.begin(), matching_data.begin() + tail_index);
|
|
|
|
if (peak_index + 128 < tail_index) {
|
|
size_t start_index = peak_index + 64;
|
|
// Compute the matching residual error for the current candidate to match.
|
|
float residual_sqr_sum = 0.f;
|
|
float d_k = reverb_decay_to_test_;
|
|
for (size_t k = start_index; k < tail_index; ++k) {
|
|
if (matching_data[start_index + 1] == 0.f) {
|
|
break;
|
|
}
|
|
|
|
float residual = matching_data[k] - matching_data[peak_index] * d_k;
|
|
residual_sqr_sum += residual * residual;
|
|
d_k *= reverb_decay_to_test_;
|
|
}
|
|
|
|
// If needed, update the best candidate for the reverb decay.
|
|
if (reverb_decay_candidate_residual_ < 0.f ||
|
|
residual_sqr_sum < reverb_decay_candidate_residual_) {
|
|
reverb_decay_candidate_residual_ = residual_sqr_sum;
|
|
reverb_decay_candidate_ = reverb_decay_to_test_;
|
|
}
|
|
}
|
|
|
|
// Compute the next reverb candidate to evaluate such that all candidates will
|
|
// be evaluated within one second.
|
|
reverb_decay_to_test_ += (0.9965f - 0.9f) / (5 * kNumBlocksPerSecond);
|
|
|
|
// If all reverb candidates have been evaluated, choose the best one as the
|
|
// reverb decay.
|
|
if (reverb_decay_to_test_ >= 0.9965f) {
|
|
if (reverb_decay_candidate_residual_ < 0.f) {
|
|
// Transform the decay to be in the unit of blocks.
|
|
reverb_decay_ = powf(reverb_decay_candidate_, kFftLengthBy2);
|
|
|
|
// Limit the estimated reverb_decay_ to the maximum one needed in practice
|
|
// to minimize the impact of incorrect estimates.
|
|
reverb_decay_ = std::min(config_.ep_strength.default_len, reverb_decay_);
|
|
}
|
|
reverb_decay_to_test_ = 0.9f;
|
|
reverb_decay_candidate_residual_ = -1.f;
|
|
}
|
|
|
|
// For noisy impulse responses, assume a fixed tail length.
|
|
if (tail_power > 0.0005f) {
|
|
reverb_decay_ = config_.ep_strength.default_len;
|
|
}
|
|
data_dumper_->DumpRaw("aec3_reverb_decay", reverb_decay_);
|
|
data_dumper_->DumpRaw("aec3_tail_power", tail_power);
|
|
}
|
|
|
|
bool AecState::DetectActiveRender(rtc::ArrayView<const float> x) const {
|
|
const float x_energy = std::inner_product(x.begin(), x.end(), x.begin(), 0.f);
|
|
return x_energy > (config_.render_levels.active_render_limit *
|
|
config_.render_levels.active_render_limit) *
|
|
kFftLengthBy2;
|
|
}
|
|
|
|
// Updates the suppressor gain limit.
|
|
void AecState::UpdateSuppressorGainLimit(bool render_activity) {
|
|
const auto& rampup_conf = config_.echo_removal_control.gain_rampup;
|
|
if (!active_render_seen_ && render_activity) {
|
|
active_render_seen_ = true;
|
|
realignment_counter_ = rampup_conf.full_gain_blocks;
|
|
} else if (realignment_counter_ > 0) {
|
|
--realignment_counter_;
|
|
}
|
|
|
|
if (realignment_counter_ <= 0) {
|
|
suppressor_gain_limit_ = 1.f;
|
|
return;
|
|
}
|
|
|
|
if (realignment_counter_ > rampup_conf.non_zero_gain_blocks) {
|
|
suppressor_gain_limit_ = 0.f;
|
|
return;
|
|
}
|
|
|
|
if (realignment_counter_ == rampup_conf.non_zero_gain_blocks) {
|
|
suppressor_gain_limit_ = rampup_conf.first_non_zero_gain;
|
|
return;
|
|
}
|
|
|
|
RTC_DCHECK_LT(0.f, suppressor_gain_limit_);
|
|
suppressor_gain_limit_ =
|
|
std::min(1.f, suppressor_gain_limit_ * gain_rampup_increase_);
|
|
RTC_DCHECK_GE(1.f, suppressor_gain_limit_);
|
|
}
|
|
|
|
bool AecState::DetectEchoSaturation(rtc::ArrayView<const float> x) {
|
|
RTC_DCHECK_LT(0, x.size());
|
|
const float max_sample = fabs(*std::max_element(
|
|
x.begin(), x.end(), [](float a, float b) { return a * a < b * b; }));
|
|
previous_max_sample_ = max_sample;
|
|
|
|
// Set flag for potential presence of saturated echo
|
|
blocks_since_last_saturation_ =
|
|
previous_max_sample_ > 200.f && SaturatedCapture()
|
|
? 0
|
|
: blocks_since_last_saturation_ + 1;
|
|
|
|
return blocks_since_last_saturation_ < 20;
|
|
}
|
|
|
|
void AecState::EchoAudibility::Update(rtc::ArrayView<const float> x,
|
|
const std::array<float, kBlockSize>& s,
|
|
bool converged_filter) {
|
|
auto result_x = std::minmax_element(x.begin(), x.end());
|
|
auto result_s = std::minmax_element(s.begin(), s.end());
|
|
const float x_abs =
|
|
std::max(fabsf(*result_x.first), fabsf(*result_x.second));
|
|
const float s_abs =
|
|
std::max(fabsf(*result_s.first), fabsf(*result_s.second));
|
|
|
|
if (converged_filter) {
|
|
if (x_abs < 20.f) {
|
|
++low_farend_counter_;
|
|
} else {
|
|
low_farend_counter_ = 0;
|
|
}
|
|
} else {
|
|
if (x_abs < 100.f) {
|
|
++low_farend_counter_;
|
|
} else {
|
|
low_farend_counter_ = 0;
|
|
}
|
|
}
|
|
|
|
// The echo is deemed as not audible if the echo estimate is on the level of
|
|
// the quantization noise in the FFTs and the nearend level is sufficiently
|
|
// strong to mask that by ensuring that the playout and AGC gains do not boost
|
|
// any residual echo that is below the quantization noise level. Furthermore,
|
|
// cases where the render signal is very close to zero are also identified as
|
|
// not producing audible echo.
|
|
inaudible_echo_ = (max_nearend_ > 500 && s_abs < 30.f) ||
|
|
(!converged_filter && x_abs < 500);
|
|
inaudible_echo_ = inaudible_echo_ || low_farend_counter_ > 20;
|
|
}
|
|
|
|
void AecState::EchoAudibility::UpdateWithOutput(rtc::ArrayView<const float> e) {
|
|
const float e_max = *std::max_element(e.begin(), e.end());
|
|
const float e_min = *std::min_element(e.begin(), e.end());
|
|
const float e_abs = std::max(fabsf(e_max), fabsf(e_min));
|
|
|
|
if (max_nearend_ < e_abs) {
|
|
max_nearend_ = e_abs;
|
|
max_nearend_counter_ = 0;
|
|
} else {
|
|
if (++max_nearend_counter_ > 5 * kNumBlocksPerSecond) {
|
|
max_nearend_ *= 0.995f;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace webrtc
|