blob: 3eaaea90f30e5e015ca6399a29604940d309a21a [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/webrtc/audio_processor.h"
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "media/webrtc/webrtc_switches.h"
#include "third_party/webrtc/api/audio/echo_canceller3_factory.h"
#include "third_party/webrtc/modules/audio_processing/aec_dump/aec_dump_factory.h"
namespace media {
namespace {
constexpr int kBuffersPerSecond = 100; // 10 ms per buffer.
webrtc::AudioProcessing::ChannelLayout MediaLayoutToWebRtcLayout(
ChannelLayout media_layout) {
switch (media_layout) {
case CHANNEL_LAYOUT_MONO:
return webrtc::AudioProcessing::kMono;
case CHANNEL_LAYOUT_STEREO:
case CHANNEL_LAYOUT_DISCRETE:
// TODO(https://crbug.com/868026): currently mapping all discrete channel
// layouts to two channels assuming that any required channel remix takes
// place in the native audio layer.
return webrtc::AudioProcessing::kStereo;
case CHANNEL_LAYOUT_STEREO_AND_KEYBOARD_MIC:
return webrtc::AudioProcessing::kStereoAndKeyboard;
default:
NOTREACHED() << "Layout not supported: " << media_layout;
return webrtc::AudioProcessing::kMono;
}
}
} // namespace
AudioProcessor::ProcessingResult::ProcessingResult(
const AudioBus& audio,
base::Optional<double> new_volume)
: audio(audio), new_volume(new_volume) {}
AudioProcessor::ProcessingResult::ProcessingResult(const ProcessingResult& b) =
default;
AudioProcessor::ProcessingResult::~ProcessingResult() = default;
AudioProcessor::AudioProcessor(const AudioParameters& audio_parameters,
const AudioProcessingSettings& settings)
: audio_parameters_(audio_parameters),
settings_(settings),
output_bus_(AudioBus::Create(audio_parameters_)),
audio_delay_stats_reporter_(kBuffersPerSecond) {
DCHECK(audio_parameters.IsValid());
DCHECK_EQ(audio_parameters_.GetBufferDuration(),
base::TimeDelta::FromMilliseconds(10));
InitializeAPM();
output_ptrs_.reserve(audio_parameters_.channels());
for (int i = 0; i < audio_parameters_.channels(); ++i) {
output_ptrs_.push_back(output_bus_->channel(i));
}
}
AudioProcessor::~AudioProcessor() {
StopEchoCancellationDump();
if (audio_processing_)
audio_processing_->UpdateHistogramsOnCallEnd();
// EchoInformation does this by itself on destruction, but since the stats are
// reset, they won't get doubly reported.
echo_information_.ReportAndResetAecDivergentFilterStats();
}
// Process the audio from source and return a pointer to the processed data.
AudioProcessor::ProcessingResult AudioProcessor::ProcessCapture(
const AudioBus& source,
base::TimeTicks capture_time,
double volume,
bool key_pressed) {
base::Optional<double> new_volume;
if (audio_processing_) {
UpdateDelayEstimate(capture_time);
UpdateAnalogLevel(volume);
audio_processing_->set_stream_key_pressed(key_pressed);
// Writes to |output_bus_|.
FeedDataToAPM(source);
UpdateTypingDetected(key_pressed);
new_volume = GetNewVolumeFromAGC(volume);
} else {
source.CopyTo(output_bus_.get());
}
if (settings_.stereo_mirroring &&
audio_parameters_.channel_layout() == CHANNEL_LAYOUT_STEREO) {
output_bus_->SwapChannels(0, 1);
}
return {*output_bus_, new_volume};
}
void AudioProcessor::AnalyzePlayout(const AudioBus& audio,
const AudioParameters& parameters,
base::TimeTicks playout_time) {
if (!audio_processing_)
return;
render_delay_ = playout_time - base::TimeTicks::Now();
constexpr int kMaxChannels = 2;
DCHECK_GE(parameters.channels(), 1);
DCHECK_LE(parameters.channels(), kMaxChannels);
const float* channel_ptrs[kMaxChannels];
channel_ptrs[0] = audio.channel(0);
webrtc::AudioProcessing::ChannelLayout webrtc_layout =
webrtc::AudioProcessing::ChannelLayout::kMono;
if (parameters.channels() == 2) {
channel_ptrs[1] = audio.channel(1);
webrtc_layout = webrtc::AudioProcessing::ChannelLayout::kStereo;
}
const int apm_error = audio_processing_->AnalyzeReverseStream(
channel_ptrs, parameters.frames_per_buffer(), parameters.sample_rate(),
webrtc_layout);
DCHECK_EQ(apm_error, webrtc::AudioProcessing::kNoError);
}
void AudioProcessor::UpdateInternalStats() {
if (audio_processing_)
echo_information_.UpdateAecStats(
audio_processing_->GetStatistics(has_reverse_stream_));
}
void AudioProcessor::GetStats(GetStatsCB callback) {
webrtc::AudioProcessorInterface::AudioProcessorStatistics out = {};
if (audio_processing_) {
out.typing_noise_detected = typing_detected_;
out.apm_statistics = audio_processing_->GetStatistics(has_reverse_stream_);
}
std::move(callback).Run(out);
}
void AudioProcessor::StartEchoCancellationDump(base::File file) {
if (!audio_processing_) {
// The destructor of File is blocking. Post it to a task runner to avoid
// blocking the main thread.
base::PostTaskWithTraits(
FROM_HERE, {base::TaskPriority::LOWEST, base::MayBlock()},
base::BindOnce([](base::File) {}, std::move(file)));
return;
}
// Here tasks will be posted on the |worker_queue_|. It must be kept alive
// until StopEchoCancellationDump is called or the webrtc::AudioProcessing
// instance is destroyed.
DCHECK(file.IsValid());
base::PlatformFile stream = file.TakePlatformFile();
if (!worker_queue_) {
worker_queue_ = std::make_unique<rtc::TaskQueue>(
"aecdump-worker-queue", rtc::TaskQueue::Priority::LOW);
}
auto aec_dump = webrtc::AecDumpFactory::Create(
stream, -1 /* max_log_size_bytes */, worker_queue_.get());
if (!aec_dump) {
// AecDumpFactory::Create takes ownership of stream even if it fails, so we
// don't need to close it.
LOG(ERROR) << "Failed to start AEC debug recording";
return;
}
audio_processing_->AttachAecDump(std::move(aec_dump));
}
void AudioProcessor::StopEchoCancellationDump() {
if (audio_processing_)
audio_processing_->DetachAecDump();
// Note that deleting an rtc::TaskQueue has to be done from the
// thread that created it.
worker_queue_.reset();
}
void AudioProcessor::InitializeAPM() {
// Most features must have some configuration applied before constructing the
// APM, and some after; the initialization is divided into "part 1" and "part
// 2" in those cases.
// If we use nothing but, possibly, audio mirroring, don't initialize the APM.
if (settings_.echo_cancellation != EchoCancellationType::kAec2 &&
settings_.echo_cancellation != EchoCancellationType::kAec3 &&
settings_.noise_suppression == NoiseSuppressionType::kDisabled &&
settings_.automatic_gain_control == AutomaticGainControlType::kDisabled &&
!settings_.high_pass_filter && !settings_.typing_detection) {
return;
}
webrtc::Config ap_config; // Note: ap_config.Set(x) transfers ownership of x.
webrtc::AudioProcessingBuilder ap_builder;
// AEC setup part 1.
// AEC2 options. Doesn't do anything if AEC2 isn't used.
ap_config.Set<webrtc::RefinedAdaptiveFilter>(
new webrtc::RefinedAdaptiveFilter(
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kAecRefinedAdaptiveFilter)));
ap_config.Set<webrtc::ExtendedFilter>(new webrtc::ExtendedFilter(true));
ap_config.Set<webrtc::DelayAgnostic>(new webrtc::DelayAgnostic(true));
// Echo cancellation is configured both before and after AudioProcessing
// construction, but before Initialize.
if (settings_.echo_cancellation == EchoCancellationType::kAec3) {
ap_builder.SetEchoControlFactory(
std::make_unique<webrtc::EchoCanceller3Factory>());
}
// AGC setup part 1.
if (settings_.automatic_gain_control ==
AutomaticGainControlType::kExperimental ||
settings_.automatic_gain_control ==
AutomaticGainControlType::kHybridExperimental) {
std::string min_volume_str(
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kAgcStartupMinVolume));
int startup_min_volume;
// |startup_min_volume| set to 0 in case of failure/empty string, which is
// the default.
base::StringToInt(min_volume_str, &startup_min_volume);
auto* experimental_agc =
new webrtc::ExperimentalAgc(true, startup_min_volume);
experimental_agc->digital_adaptive_disabled =
settings_.automatic_gain_control ==
AutomaticGainControlType::kHybridExperimental;
ap_config.Set<webrtc::ExperimentalAgc>(experimental_agc);
} else {
ap_config.Set<webrtc::ExperimentalAgc>(new webrtc::ExperimentalAgc(false));
}
// Noise suppression setup part 1.
ap_config.Set<webrtc::ExperimentalNs>(new webrtc::ExperimentalNs(
settings_.noise_suppression == NoiseSuppressionType::kExperimental));
// Audio processing module construction.
audio_processing_ = base::WrapUnique(ap_builder.Create(ap_config));
// Noise suppression setup part 2.
if (settings_.noise_suppression != NoiseSuppressionType::kDisabled) {
int err = audio_processing_->noise_suppression()->set_level(
webrtc::NoiseSuppression::kHigh);
err |= audio_processing_->noise_suppression()->Enable(true);
DCHECK_EQ(err, 0);
}
// AGC setup part 2.
if (settings_.automatic_gain_control != AutomaticGainControlType::kDisabled) {
int err = audio_processing_->gain_control()->set_mode(
webrtc::GainControl::kAdaptiveAnalog);
err |= audio_processing_->gain_control()->Enable(true);
DCHECK_EQ(err, 0);
}
webrtc::AudioProcessing::Config apm_config = audio_processing_->GetConfig();
// Typing detection setup.
if (settings_.typing_detection) {
typing_detector_ = std::make_unique<webrtc::TypingDetection>();
// Configure the update period to 1s (100 * 10ms) in the typing detector.
typing_detector_->SetParameters(0, 0, 0, 0, 0, 100);
apm_config.voice_detection.enabled = true;
}
// AEC setup part 2.
apm_config.echo_canceller.enabled =
settings_.echo_cancellation == EchoCancellationType::kAec2 ||
settings_.echo_cancellation == EchoCancellationType::kAec3;
apm_config.echo_canceller.mobile_mode = false;
// High-pass filter setup.
apm_config.high_pass_filter.enabled = settings_.high_pass_filter;
// AGC setup part 3.
if (settings_.automatic_gain_control ==
AutomaticGainControlType::kExperimental ||
settings_.automatic_gain_control ==
AutomaticGainControlType::kHybridExperimental) {
apm_config.gain_controller2.enabled =
settings_.automatic_gain_control ==
AutomaticGainControlType::kHybridExperimental;
apm_config.gain_controller2.fixed_digital.gain_db = 0.f;
apm_config.gain_controller2.adaptive_digital.enabled = true;
const bool use_peaks_not_rms = base::GetFieldTrialParamByFeatureAsBool(
features::kWebRtcHybridAgc, "use_peaks_not_rms", false);
using Shortcut =
webrtc::AudioProcessing::Config::GainController2::LevelEstimator;
apm_config.gain_controller2.adaptive_digital.level_estimator =
use_peaks_not_rms ? Shortcut::kPeak : Shortcut::kRms;
const int saturation_margin = base::GetFieldTrialParamByFeatureAsInt(
features::kWebRtcHybridAgc, "saturation_margin", -1);
if (saturation_margin != -1) {
apm_config.gain_controller2.adaptive_digital.extra_saturation_margin_db =
saturation_margin;
}
}
audio_processing_->ApplyConfig(apm_config);
}
void AudioProcessor::UpdateDelayEstimate(base::TimeTicks capture_time) {
const base::TimeDelta capture_delay = base::TimeTicks::Now() - capture_time;
// Note: this delay calculation doesn't make sense, but it's how it's done
// right now. Instead, the APM should probably attach the playout time to each
// reference buffer it gets and store that internally?
const base::TimeDelta render_delay = render_delay_.load();
audio_delay_stats_reporter_.ReportDelay(capture_delay, render_delay);
const base::TimeDelta total_delay = capture_delay + render_delay;
audio_processing_->set_stream_delay_ms(total_delay.InMilliseconds());
if (total_delay > base::TimeDelta::FromMilliseconds(300)) {
DLOG(WARNING) << "Large audio delay, capture delay: "
<< capture_delay.InMilliseconds() << "ms; render delay: "
<< render_delay_.load().InMilliseconds() << "ms";
}
}
void AudioProcessor::UpdateAnalogLevel(double volume) {
DCHECK_LE(volume, 1.0);
constexpr double kWebRtcMaxVolume = 255;
const int webrtc_volume = volume * kWebRtcMaxVolume;
int err =
audio_processing_->gain_control()->set_stream_analog_level(webrtc_volume);
DCHECK_EQ(err, 0) << "set_stream_analog_level() error: " << err;
}
void AudioProcessor::FeedDataToAPM(const AudioBus& source) {
std::vector<const float*> input_ptrs(source.channels());
for (int i = 0; i < source.channels(); ++i) {
input_ptrs[i] = source.channel(i);
}
const auto layout =
MediaLayoutToWebRtcLayout(audio_parameters_.channel_layout());
const int sample_rate = audio_parameters_.sample_rate();
int err = audio_processing_->ProcessStream(
input_ptrs.data(), audio_parameters_.frames_per_buffer(), sample_rate,
layout, sample_rate, layout, output_ptrs_.data());
DCHECK_EQ(err, 0) << "ProcessStream() error: " << err;
}
void AudioProcessor::UpdateTypingDetected(bool key_pressed) {
if (typing_detector_) {
// Ignore remote tracks to avoid unnecessary stats computation.
auto voice_detected =
audio_processing_->GetStatistics(false /* has_remote_tracks */)
.voice_detected;
DCHECK(voice_detected.has_value());
typing_detected_ = typing_detector_->Process(key_pressed, *voice_detected);
}
}
base::Optional<double> AudioProcessor::GetNewVolumeFromAGC(double volume) {
constexpr double kWebRtcMaxVolume = 255;
const int webrtc_volume = volume * kWebRtcMaxVolume;
const int new_webrtc_volume =
audio_processing_->gain_control()->stream_analog_level();
return new_webrtc_volume == webrtc_volume
? base::nullopt
: base::Optional<double>(static_cast<double>(new_webrtc_volume) /
kWebRtcMaxVolume);
}
} // namespace media