blob: e18f87400785c44e68c323d813d3b49d621d5d5c [file] [log] [blame]
// Copyright 2020 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/audio/cras/audio_manager_chromeos.h"
#include <stddef.h>
#include <algorithm>
#include <map>
#include <utility>
#include "ash/components/audio/audio_device.h"
#include "ash/components/audio/cras_audio_handler.h"
#include "base/bind.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/cxx17_backports.h"
#include "base/environment.h"
#include "base/metrics/field_trial_params.h"
#include "base/nix/xdg_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/system/sys_info.h"
#include "base/threading/thread_task_runner_handle.h"
#include "media/audio/audio_device_description.h"
#include "media/audio/audio_features.h"
#include "media/audio/cras/cras_input.h"
#include "media/audio/cras/cras_unified.h"
#include "media/base/channel_layout.h"
#include "media/base/limits.h"
#include "media/base/localized_strings.h"
namespace media {
namespace {
using ::ash::AudioDevice;
using ::ash::AudioDeviceList;
using ::ash::CrasAudioHandler;
// Default sample rate for input and output streams.
const int kDefaultSampleRate = 48000;
// Default input buffer size.
const int kDefaultInputBufferSize = 1024;
const char kInternalInputVirtualDevice[] = "Built-in mic";
const char kInternalOutputVirtualDevice[] = "Built-in speaker";
const char kHeadphoneLineOutVirtualDevice[] = "Headphone/Line Out";
// Used for the Media.CrosBeamformingDeviceState histogram, currently not used
// since beamforming is disabled.
enum CrosBeamformingDeviceState {
BEAMFORMING_DEFAULT_ENABLED = 0,
BEAMFORMING_USER_ENABLED,
BEAMFORMING_DEFAULT_DISABLED,
BEAMFORMING_USER_DISABLED,
BEAMFORMING_STATE_MAX = BEAMFORMING_USER_DISABLED
};
bool HasKeyboardMic(const AudioDeviceList& devices) {
for (const auto& device : devices) {
if (device.is_input &&
device.type == chromeos::AudioDeviceType::kKeyboardMic) {
return true;
}
}
return false;
}
const AudioDevice* GetDeviceFromId(const AudioDeviceList& devices,
uint64_t id) {
for (const auto& device : devices) {
if (device.id == id) {
return &device;
}
}
return nullptr;
}
// Process |device_list| that two shares the same dev_index by creating a
// virtual device name for them.
void ProcessVirtualDeviceName(AudioDeviceNames* device_names,
const AudioDeviceList& device_list) {
DCHECK_EQ(2U, device_list.size());
if (device_list[0].type == chromeos::AudioDeviceType::kLineout ||
device_list[1].type == chromeos::AudioDeviceType::kLineout) {
device_names->emplace_back(kHeadphoneLineOutVirtualDevice,
base::NumberToString(device_list[0].id));
} else if (device_list[0].type ==
chromeos::AudioDeviceType::kInternalSpeaker ||
device_list[1].type ==
chromeos::AudioDeviceType::kInternalSpeaker) {
device_names->emplace_back(kInternalOutputVirtualDevice,
base::NumberToString(device_list[0].id));
} else {
DCHECK(device_list[0].IsInternalMic() || device_list[1].IsInternalMic());
device_names->emplace_back(kInternalInputVirtualDevice,
base::NumberToString(device_list[0].id));
}
}
// Collects flags values for whether, and in what way, the AEC, NS or AGC
// effects should be enforced in spite of them not being flagged as supported by
// the board.
void RetrieveSystemEffectFeatures(bool& enforce_system_aec,
bool& enforce_system_ns,
bool& enforce_system_agc,
bool& tuned_system_aec_allowed) {
const bool enforce_system_aec_ns_agc_feature =
base::FeatureList::IsEnabled(features::kCrOSEnforceSystemAecNsAgc);
const bool enforce_system_aec_ns_feature =
base::FeatureList::IsEnabled(features::kCrOSEnforceSystemAecNs);
const bool enforce_system_aec_agc_feature =
base::FeatureList::IsEnabled(features::kCrOSEnforceSystemAecAgc);
const bool enforce_system_aec_feature =
base::FeatureList::IsEnabled(features::kCrOSEnforceSystemAec);
enforce_system_aec =
enforce_system_aec_feature || enforce_system_aec_ns_agc_feature ||
enforce_system_aec_ns_feature || enforce_system_aec_agc_feature;
enforce_system_ns =
enforce_system_aec_ns_agc_feature || enforce_system_aec_ns_feature;
enforce_system_agc =
enforce_system_aec_ns_agc_feature || enforce_system_aec_agc_feature;
tuned_system_aec_allowed =
base::FeatureList::IsEnabled(features::kCrOSSystemAEC);
}
// Checks if a system AEC with a specific group ID is flagged to be deactivated
// by the field trial.
bool IsSystemAecDeactivated(int aec_group_id) {
return base::GetFieldTrialParamByFeatureAsBool(
features::kCrOSSystemAECDeactivatedGroups, std::to_string(aec_group_id),
false);
}
} // namespace
bool AudioManagerChromeOS::HasAudioOutputDevices() {
return true;
}
bool AudioManagerChromeOS::HasAudioInputDevices() {
AudioDeviceList devices;
GetAudioDevices(&devices);
for (size_t i = 0; i < devices.size(); ++i) {
if (devices[i].is_input && devices[i].is_for_simple_usage())
return true;
}
return false;
}
AudioManagerChromeOS::AudioManagerChromeOS(
std::unique_ptr<AudioThread> audio_thread,
AudioLogFactory* audio_log_factory)
: AudioManagerCrasBase(std::move(audio_thread), audio_log_factory),
on_shutdown_(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED),
main_task_runner_(base::ThreadTaskRunnerHandle::Get()),
weak_ptr_factory_(this) {
weak_this_ = weak_ptr_factory_.GetWeakPtr();
}
AudioManagerChromeOS::~AudioManagerChromeOS() = default;
void AudioManagerChromeOS::GetAudioDeviceNamesImpl(
bool is_input,
AudioDeviceNames* device_names) {
DCHECK(device_names->empty());
device_names->push_back(AudioDeviceName::CreateDefault());
AudioDeviceList devices;
GetAudioDevices(&devices);
// |dev_idx_map| is a map of dev_index and their audio devices.
std::map<int, AudioDeviceList> dev_idx_map;
for (const auto& device : devices) {
if (device.is_input != is_input || !device.is_for_simple_usage())
continue;
dev_idx_map[dev_index_of(device.id)].push_back(device);
}
for (const auto& item : dev_idx_map) {
if (1 == item.second.size()) {
const AudioDevice& device = item.second.front();
device_names->emplace_back(device.display_name,
base::NumberToString(device.id));
} else {
// Create virtual device name for audio nodes that share the same device
// index.
ProcessVirtualDeviceName(device_names, item.second);
}
}
}
void AudioManagerChromeOS::GetAudioInputDeviceNames(
AudioDeviceNames* device_names) {
GetAudioDeviceNamesImpl(true, device_names);
}
void AudioManagerChromeOS::GetAudioOutputDeviceNames(
AudioDeviceNames* device_names) {
GetAudioDeviceNamesImpl(false, device_names);
}
AudioParameters AudioManagerChromeOS::GetInputStreamParameters(
const std::string& device_id) {
DCHECK(GetTaskRunner()->BelongsToCurrentThread());
// Check if the device has keyboard.
AudioDeviceList devices;
GetAudioDevices(&devices);
const bool has_keyboard = HasKeyboardMic(devices);
// Retrieve buffer size.
int user_buffer_size = GetUserBufferSize();
user_buffer_size =
user_buffer_size != 0 ? user_buffer_size : kDefaultInputBufferSize;
// Retrieve the board support in terms of APM effects and properties.
const SystemAudioProcessingInfo system_apm_info =
GetSystemApmEffectsSupportedPerBoard();
// TODO(hshi): Fine-tune audio parameters based on |device_id|. The optimal
// parameters for the loopback stream may differ from the default.
return GetStreamParametersForSystem(user_buffer_size, has_keyboard,
system_apm_info);
}
std::string AudioManagerChromeOS::GetAssociatedOutputDeviceID(
const std::string& input_device_id) {
AudioDeviceList devices;
GetAudioDevices(&devices);
if (input_device_id == AudioDeviceDescription::kDefaultDeviceId) {
// Note: the default input should not be associated to any output, as this
// may lead to accidental uses of a pinned stream.
return "";
}
const std::string device_name =
GetHardwareDeviceFromDeviceId(devices, true, input_device_id);
if (device_name.empty())
return "";
// Now search for an output device with the same device name.
auto output_device_it = std::find_if(
devices.begin(), devices.end(), [device_name](const AudioDevice& device) {
return !device.is_input && device.device_name == device_name;
});
return output_device_it == devices.end()
? ""
: base::NumberToString(output_device_it->id);
}
std::string AudioManagerChromeOS::GetDefaultInputDeviceID() {
DCHECK(GetTaskRunner()->BelongsToCurrentThread());
return base::NumberToString(GetPrimaryActiveInputNode());
}
std::string AudioManagerChromeOS::GetDefaultOutputDeviceID() {
DCHECK(GetTaskRunner()->BelongsToCurrentThread());
return base::NumberToString(GetPrimaryActiveOutputNode());
}
std::string AudioManagerChromeOS::GetGroupIDOutput(
const std::string& output_device_id) {
AudioDeviceList devices;
GetAudioDevices(&devices);
return GetHardwareDeviceFromDeviceId(devices, false, output_device_id);
}
std::string AudioManagerChromeOS::GetGroupIDInput(
const std::string& input_device_id) {
AudioDeviceList devices;
GetAudioDevices(&devices);
return GetHardwareDeviceFromDeviceId(devices, true, input_device_id);
}
bool AudioManagerChromeOS::Shutdown() {
DCHECK(main_task_runner_->BelongsToCurrentThread());
weak_ptr_factory_.InvalidateWeakPtrs();
on_shutdown_.Signal();
return AudioManager::Shutdown();
}
int AudioManagerChromeOS::GetDefaultOutputBufferSizePerBoard() {
DCHECK(GetTaskRunner()->BelongsToCurrentThread());
int32_t buffer_size = 512;
base::WaitableEvent event(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED);
if (main_task_runner_->BelongsToCurrentThread()) {
// Unittest may use the same thread for audio thread.
GetDefaultOutputBufferSizeOnMainThread(&buffer_size, &event);
} else {
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
&AudioManagerChromeOS::GetDefaultOutputBufferSizeOnMainThread,
weak_this_, base::Unretained(&buffer_size),
base::Unretained(&event)));
}
WaitEventOrShutdown(&event);
return static_cast<int>(buffer_size);
}
AudioManagerChromeOS::SystemAudioProcessingInfo
AudioManagerChromeOS::GetSystemApmEffectsSupportedPerBoard() {
DCHECK(GetTaskRunner()->BelongsToCurrentThread());
base::WaitableEvent event(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED);
SystemAudioProcessingInfo system_apm_info;
if (main_task_runner_->BelongsToCurrentThread()) {
// Unittest may use the same thread for audio thread.
GetSystemApmEffectsSupportedOnMainThread(&system_apm_info, &event);
} else {
// Using base::Unretained is safe here because we wait for callback be
// executed in main thread before local variables are destructed.
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
&AudioManagerChromeOS::GetSystemApmEffectsSupportedOnMainThread,
weak_this_, base::Unretained(&system_apm_info),
base::Unretained(&event)));
}
WaitEventOrShutdown(&event);
return system_apm_info;
}
AudioParameters AudioManagerChromeOS::GetPreferredOutputStreamParameters(
const std::string& output_device_id,
const AudioParameters& input_params) {
DCHECK(GetTaskRunner()->BelongsToCurrentThread());
ChannelLayout channel_layout = CHANNEL_LAYOUT_STEREO;
int sample_rate = kDefaultSampleRate;
int buffer_size = GetUserBufferSize();
if (input_params.IsValid()) {
channel_layout = input_params.channel_layout();
sample_rate = input_params.sample_rate();
if (!buffer_size) // Not user-provided.
buffer_size =
std::min(static_cast<int>(limits::kMaxAudioBufferSize),
std::max(static_cast<int>(limits::kMinAudioBufferSize),
input_params.frames_per_buffer()));
return AudioParameters(
AudioParameters::AUDIO_PCM_LOW_LATENCY, channel_layout, sample_rate,
buffer_size,
AudioParameters::HardwareCapabilities(limits::kMinAudioBufferSize,
limits::kMaxAudioBufferSize));
}
// Get max supported channels from |output_device_id| or the primary active
// one if |output_device_id| is the default device.
uint64_t preferred_device_id;
if (AudioDeviceDescription::IsDefaultDevice(output_device_id)) {
preferred_device_id = GetPrimaryActiveOutputNode();
} else {
if (!base::StringToUint64(output_device_id, &preferred_device_id))
preferred_device_id = 0; // 0 represents invalid |output_device_id|.
}
if (preferred_device_id) {
AudioDeviceList devices;
GetAudioDevices(&devices);
const AudioDevice* device = GetDeviceFromId(devices, preferred_device_id);
if (device && device->is_input == false) {
channel_layout =
GuessChannelLayout(static_cast<int>(device->max_supported_channels));
// Fall-back to old fashion: always fixed to STEREO layout.
if (channel_layout == CHANNEL_LAYOUT_UNSUPPORTED) {
channel_layout = CHANNEL_LAYOUT_STEREO;
}
}
}
if (!buffer_size) // Not user-provided.
buffer_size = GetDefaultOutputBufferSizePerBoard();
return AudioParameters(
AudioParameters::AUDIO_PCM_LOW_LATENCY, channel_layout, sample_rate,
buffer_size,
AudioParameters::HardwareCapabilities(limits::kMinAudioBufferSize,
limits::kMaxAudioBufferSize));
}
bool AudioManagerChromeOS::IsDefault(const std::string& device_id,
bool is_input) {
AudioDeviceNames device_names;
GetAudioDeviceNamesImpl(is_input, &device_names);
DCHECK(!device_names.empty());
const AudioDeviceName& device_name = device_names.front();
return device_name.unique_id == device_id;
}
std::string AudioManagerChromeOS::GetHardwareDeviceFromDeviceId(
const AudioDeviceList& devices,
bool is_input,
const std::string& device_id) {
uint64_t u64_device_id = 0;
if (AudioDeviceDescription::IsDefaultDevice(device_id)) {
u64_device_id =
is_input ? GetPrimaryActiveInputNode() : GetPrimaryActiveOutputNode();
} else {
if (!base::StringToUint64(device_id, &u64_device_id))
return "";
}
const AudioDevice* device = GetDeviceFromId(devices, u64_device_id);
return device ? device->device_name : "";
}
void AudioManagerChromeOS::GetAudioDevices(AudioDeviceList* devices) {
DCHECK(GetTaskRunner()->BelongsToCurrentThread());
base::WaitableEvent event(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED);
if (main_task_runner_->BelongsToCurrentThread()) {
GetAudioDevicesOnMainThread(devices, &event);
} else {
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&AudioManagerChromeOS::GetAudioDevicesOnMainThread,
weak_this_, base::Unretained(devices),
base::Unretained(&event)));
}
WaitEventOrShutdown(&event);
}
void AudioManagerChromeOS::GetAudioDevicesOnMainThread(
AudioDeviceList* devices,
base::WaitableEvent* event) {
DCHECK(main_task_runner_->BelongsToCurrentThread());
// CrasAudioHandler is shut down before AudioManagerChromeOS.
if (CrasAudioHandler::Get())
CrasAudioHandler::Get()->GetAudioDevices(devices);
event->Signal();
}
uint64_t AudioManagerChromeOS::GetPrimaryActiveInputNode() {
DCHECK(GetTaskRunner()->BelongsToCurrentThread());
uint64_t device_id = 0;
base::WaitableEvent event(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED);
if (main_task_runner_->BelongsToCurrentThread()) {
GetPrimaryActiveInputNodeOnMainThread(&device_id, &event);
} else {
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
&AudioManagerChromeOS::GetPrimaryActiveInputNodeOnMainThread,
weak_this_, &device_id, &event));
}
WaitEventOrShutdown(&event);
return device_id;
}
uint64_t AudioManagerChromeOS::GetPrimaryActiveOutputNode() {
DCHECK(GetTaskRunner()->BelongsToCurrentThread());
base::WaitableEvent event(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED);
uint64_t device_id = 0;
if (main_task_runner_->BelongsToCurrentThread()) {
// Unittest may use the same thread for audio thread.
GetPrimaryActiveOutputNodeOnMainThread(&device_id, &event);
} else {
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
&AudioManagerChromeOS::GetPrimaryActiveOutputNodeOnMainThread,
weak_this_, base::Unretained(&device_id),
base::Unretained(&event)));
}
WaitEventOrShutdown(&event);
return device_id;
}
void AudioManagerChromeOS::GetPrimaryActiveInputNodeOnMainThread(
uint64_t* active_input_node_id,
base::WaitableEvent* event) {
DCHECK(main_task_runner_->BelongsToCurrentThread());
if (CrasAudioHandler::Get()) {
*active_input_node_id =
CrasAudioHandler::Get()->GetPrimaryActiveInputNode();
}
event->Signal();
}
void AudioManagerChromeOS::GetPrimaryActiveOutputNodeOnMainThread(
uint64_t* active_output_node_id,
base::WaitableEvent* event) {
DCHECK(main_task_runner_->BelongsToCurrentThread());
if (CrasAudioHandler::Get()) {
*active_output_node_id =
CrasAudioHandler::Get()->GetPrimaryActiveOutputNode();
}
event->Signal();
}
void AudioManagerChromeOS::GetDefaultOutputBufferSizeOnMainThread(
int32_t* buffer_size,
base::WaitableEvent* event) {
DCHECK(main_task_runner_->BelongsToCurrentThread());
if (CrasAudioHandler::Get())
CrasAudioHandler::Get()->GetDefaultOutputBufferSize(buffer_size);
event->Signal();
}
void AudioManagerChromeOS::GetSystemApmEffectsSupportedOnMainThread(
SystemAudioProcessingInfo* system_apm_info,
base::WaitableEvent* event) {
DCHECK(main_task_runner_->BelongsToCurrentThread());
if (CrasAudioHandler::Get()) {
system_apm_info->aec_supported =
CrasAudioHandler::Get()->system_aec_supported();
system_apm_info->aec_group_id =
CrasAudioHandler::Get()->system_aec_group_id();
system_apm_info->ns_supported =
CrasAudioHandler::Get()->system_ns_supported();
system_apm_info->agc_supported =
CrasAudioHandler::Get()->system_agc_supported();
}
event->Signal();
}
void AudioManagerChromeOS::WaitEventOrShutdown(base::WaitableEvent* event) {
base::WaitableEvent* waitables[] = {event, &on_shutdown_};
base::WaitableEvent::WaitMany(waitables, base::size(waitables));
}
enum CRAS_CLIENT_TYPE AudioManagerChromeOS::GetClientType() {
return CRAS_CLIENT_TYPE_CHROME;
}
AudioParameters AudioManagerChromeOS::GetStreamParametersForSystem(
int user_buffer_size,
bool has_keyboard,
const AudioManagerChromeOS::SystemAudioProcessingInfo& system_apm_info) {
AudioParameters params(
AudioParameters::AUDIO_PCM_LOW_LATENCY, CHANNEL_LAYOUT_STEREO,
kDefaultSampleRate, user_buffer_size,
AudioParameters::HardwareCapabilities(limits::kMinAudioBufferSize,
limits::kMaxAudioBufferSize));
if (has_keyboard)
params.set_effects(AudioParameters::KEYBOARD_MIC);
bool enforce_system_aec;
bool enforce_system_ns;
bool enforce_system_agc;
bool tuned_system_aec_allowed;
RetrieveSystemEffectFeatures(enforce_system_aec, enforce_system_ns,
enforce_system_agc, tuned_system_aec_allowed);
// Activation of the system AEC. Allow experimentation with system AEC with
// all devices, but enable it by default on devices that actually support it.
params.set_effects(params.effects() |
AudioParameters::EXPERIMENTAL_ECHO_CANCELLER);
// Rephrase the field aec_supported to properly reflect its meaning in this
// context (since it currently signals whether an CrAS APM with tuned settings
// is available).
const bool tuned_system_apm_available = system_apm_info.aec_supported;
// Don't use the system AEC if it is deactivated for this group ID. Also never
// activate NS nor AGC for this board if the AEC is not activated, since this
// will cause issues for the Browser AEC.
bool use_system_aec =
(tuned_system_apm_available && tuned_system_aec_allowed) ||
enforce_system_aec;
if (!use_system_aec || IsSystemAecDeactivated(system_apm_info.aec_group_id)) {
return params;
}
// Activation of the system AEC.
params.set_effects(params.effects() | AudioParameters::ECHO_CANCELLER);
// Don't use system NS or AGC if the AEC has board-specific tunings.
if (tuned_system_apm_available) {
return params;
}
// Activation of the system NS.
if (system_apm_info.ns_supported || enforce_system_ns) {
params.set_effects(params.effects() | AudioParameters::NOISE_SUPPRESSION);
}
// Activation of the system AGC.
if (system_apm_info.agc_supported || enforce_system_agc) {
params.set_effects(params.effects() |
AudioParameters::AUTOMATIC_GAIN_CONTROL);
}
return params;
}
} // namespace media