| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chromeos/ash/components/audio/cras_audio_handler.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <cstdint> |
| #include <optional> |
| #include <set> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "ash/constants/ash_features.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/system/sys_info.h" |
| #include "base/system/system_monitor.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "chromeos/ash/components/audio/audio_device.h" |
| #include "chromeos/ash/components/audio/audio_device_encoding.h" |
| #include "chromeos/ash/components/audio/audio_device_id.h" |
| #include "chromeos/ash/components/audio/audio_devices_pref_handler_stub.h" |
| #include "chromeos/ash/components/dbus/audio/cras_audio_client.h" |
| #include "chromeos/ash/components/dbus/audio/fake_cras_audio_client.h" |
| #include "chromeos/ash/components/dbus/audio/floss_media_client.h" |
| #include "chromeos/ash/components/dbus/audio/voice_isolation_ui_appearance.h" |
| #include "device/bluetooth/floss/floss_features.h" |
| #include "third_party/cros_system_api/dbus/audio/dbus-constants.h" |
| #include "third_party/cros_system_api/dbus/service_constants.h" |
| |
| namespace ash { |
| namespace { |
| |
| using ::std::max; |
| using ::std::min; |
| |
| // Default value for unmuting, as a percent in the range [0, 100]. |
| // Used when sound is unmuted, but volume was less than kMuteThresholdPercent. |
| const int kDefaultUnmuteVolumePercent = 4; |
| |
| // Volume value which should be considered as muted in range [0, 100]. |
| const int kMuteThresholdPercent = 1; |
| |
| // Mixer matrix, [0.5, 0.5; 0.5, 0.5] |
| const double kStereoToMono[] = {0.5, 0.5, 0.5, 0.5}; |
| // Mixer matrix, [1, 0; 0, 1] |
| const double kStereoToStereo[] = {1, 0, 0, 1}; |
| |
| // Number of entries we're willing to store in preferences. |
| const int kMaxDeviceStoredInPref = 100; |
| |
| CrasAudioHandler* g_cras_audio_handler = nullptr; |
| |
| bool IsSameAudioDevice(const AudioDevice& a, const AudioDevice& b) { |
| return a.stable_device_id == b.stable_device_id && a.is_input == b.is_input && |
| a.type == b.type && a.device_name == b.device_name; |
| } |
| |
| bool IsDeviceInList(const AudioDevice& device, const AudioNodeList& node_list) { |
| for (const AudioNode& node : node_list) { |
| if (device.stable_device_id == node.StableDeviceId()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Gets the current state of the microphone mute switch. If the switch is on, |
| // cras will be kept in the muted state. The switch disables the internal audio |
| // input. |
| bool IsMicrophoneMuteSwitchOn() { |
| return ui::MicrophoneMuteSwitchMonitor::Get()->microphone_mute_switch_on(); |
| } |
| |
| } // namespace |
| |
| // TODO(b/277300962): Clean up the default value and handle the unset case. |
| CrasAudioHandler::AudioSurvey::AudioSurvey() : type_(SurveyType::kGeneral) {} |
| |
| CrasAudioHandler::AudioSurvey::~AudioSurvey() = default; |
| |
| CrasAudioHandler::AudioObserver::AudioObserver() = default; |
| |
| CrasAudioHandler::AudioObserver::~AudioObserver() = default; |
| |
| void CrasAudioHandler::AudioObserver::OnOutputNodeVolumeChanged( |
| uint64_t /* node_id */, |
| int /* volume */) {} |
| |
| void CrasAudioHandler::AudioObserver::OnInputNodeGainChanged( |
| uint64_t /* node_id */, |
| int /* gain */) {} |
| |
| void CrasAudioHandler::AudioObserver::OnOutputMuteChanged(bool /* mute_on */) {} |
| |
| void CrasAudioHandler::AudioObserver::OnInputMuteChanged( |
| bool /* mute_on */, |
| InputMuteChangeMethod /* method */) {} |
| |
| void CrasAudioHandler::AudioObserver::OnInputMutedByMicrophoneMuteSwitchChanged( |
| bool /* muted */) {} |
| |
| void CrasAudioHandler::AudioObserver::OnInputMutedBySecurityCurtainChanged( |
| bool /* muted */) {} |
| |
| void CrasAudioHandler::AudioObserver::OnAudioNodesChanged() {} |
| |
| void CrasAudioHandler::AudioObserver::OnActiveOutputNodeChanged() {} |
| |
| void CrasAudioHandler::AudioObserver::OnActiveInputNodeChanged() {} |
| |
| void CrasAudioHandler::AudioObserver::OnOutputChannelRemixingChanged( |
| bool /* mono_on */) {} |
| |
| void CrasAudioHandler::AudioObserver::OnVoiceIsolationUIAppearanceChanged( |
| VoiceIsolationUIAppearance appearance) {} |
| |
| void CrasAudioHandler::AudioObserver::OnNoiseCancellationStateChanged() {} |
| |
| void CrasAudioHandler::AudioObserver::OnStyleTransferStateChanged() {} |
| |
| void CrasAudioHandler::AudioObserver::OnForceRespectUiGainsStateChanged() {} |
| |
| void CrasAudioHandler::AudioObserver::OnHfpMicSrStateChanged() {} |
| |
| void CrasAudioHandler::AudioObserver::OnSpatialAudioStateChanged() {} |
| |
| void CrasAudioHandler::AudioObserver::OnHotwordTriggered( |
| uint64_t /* tv_sec */, |
| uint64_t /* tv_nsec */) {} |
| |
| void CrasAudioHandler::AudioObserver::OnBluetoothBatteryChanged( |
| const std::string& /* address */, |
| uint32_t /* level */) {} |
| |
| void CrasAudioHandler::AudioObserver:: |
| OnNumberOfInputStreamsWithPermissionChanged() {} |
| |
| void CrasAudioHandler::AudioObserver::OnOutputStarted() {} |
| |
| void CrasAudioHandler::AudioObserver::OnOutputStopped() {} |
| |
| void CrasAudioHandler::AudioObserver::OnNonChromeOutputStarted() {} |
| |
| void CrasAudioHandler::AudioObserver::OnNonChromeOutputStopped() {} |
| |
| void CrasAudioHandler::AudioObserver::OnSurveyTriggered( |
| const AudioSurvey& /*survey*/) {} |
| |
| void CrasAudioHandler::AudioObserver::OnSpeakOnMuteDetected() {} |
| |
| void CrasAudioHandler::AudioObserver::OnNumStreamIgnoreUiGainsChanged( |
| int32_t num) {} |
| |
| void CrasAudioHandler::AudioObserver::OnNumberOfArcStreamsChanged(int32_t num) { |
| } |
| |
| void CrasAudioHandler::NumberOfNonChromeOutputStreamsChanged() { |
| GetNumberOfNonChromeOutputStreams(); |
| } |
| |
| void CrasAudioHandler::NumberOfArcStreamsChanged() { |
| GetNumberOfArcStreams(); |
| } |
| |
| // static |
| void CrasAudioHandler::Initialize( |
| mojo::PendingRemote<media_session::mojom::MediaControllerManager> |
| media_controller_manager, |
| scoped_refptr<AudioDevicesPrefHandler> audio_pref_handler) { |
| g_cras_audio_handler = new CrasAudioHandler( |
| std::move(media_controller_manager), audio_pref_handler); |
| } |
| |
| // static |
| void CrasAudioHandler::InitializeDelegate( |
| mojo::PendingRemote<media_session::mojom::MediaControllerManager> |
| media_controller_manager, |
| scoped_refptr<AudioDevicesPrefHandler> audio_pref_handler, |
| std::unique_ptr<Delegate> delegate) { |
| g_cras_audio_handler = |
| new CrasAudioHandler(std::move(media_controller_manager), |
| audio_pref_handler, std::move(delegate)); |
| } |
| |
| // static |
| void CrasAudioHandler::InitializeForTesting() { |
| CHECK(CrasAudioClient::Get()) << "CrasAudioClient must be initialized."; |
| |
| // Make sure FlossMediaClient has been initialized. |
| // TODO(b/228608730): Remove this after Floss bypasses CRAS to receive media |
| // information directly from the provider. |
| if (floss::features::IsFlossEnabled() && !FlossMediaClient::Get()) { |
| FlossMediaClient::InitializeFake(); |
| } |
| CrasAudioHandler::Initialize(mojo::NullRemote(), |
| new AudioDevicesPrefHandlerStub()); |
| } |
| |
| // static |
| void CrasAudioHandler::Shutdown() { |
| delete g_cras_audio_handler; |
| g_cras_audio_handler = nullptr; |
| } |
| |
| // static |
| CrasAudioHandler* CrasAudioHandler::Get() { |
| return g_cras_audio_handler; |
| } |
| |
| void CrasAudioHandler::OnVideoCaptureStarted(media::VideoFacingMode facing) { |
| DCHECK(main_task_runner_); |
| if (!main_task_runner_->BelongsToCurrentThread()) { |
| main_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&CrasAudioHandler::OnVideoCaptureStartedOnMainThread, |
| weak_ptr_factory_.GetWeakPtr(), facing)); |
| return; |
| } |
| // Unittest may call this from the main thread. |
| OnVideoCaptureStartedOnMainThread(facing); |
| } |
| |
| void CrasAudioHandler::OnVideoCaptureStopped(media::VideoFacingMode facing) { |
| DCHECK(main_task_runner_); |
| if (!main_task_runner_->BelongsToCurrentThread()) { |
| main_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(&CrasAudioHandler::OnVideoCaptureStoppedOnMainThread, |
| weak_ptr_factory_.GetWeakPtr(), facing)); |
| return; |
| } |
| // Unittest may call this from the main thread. |
| OnVideoCaptureStoppedOnMainThread(facing); |
| } |
| |
| void CrasAudioHandler::OnVideoCaptureStartedOnMainThread( |
| media::VideoFacingMode facing) { |
| DCHECK(main_task_runner_->BelongsToCurrentThread()); |
| // Do nothing if the device doesn't have both front and rear microphones. |
| if (!HasDualInternalMic()) { |
| return; |
| } |
| |
| bool camera_is_already_on = IsCameraOn(); |
| switch (facing) { |
| case media::MEDIA_VIDEO_FACING_USER: |
| front_camera_on_ = true; |
| break; |
| case media::MEDIA_VIDEO_FACING_ENVIRONMENT: |
| rear_camera_on_ = true; |
| break; |
| default: |
| LOG_IF(WARNING, facing == media::NUM_MEDIA_VIDEO_FACING_MODES) |
| << "On the device with dual microphone, got video capture " |
| << "notification with invalid camera facing mode value"; |
| return; |
| } |
| |
| // If the camera is already on before this notification, don't change active |
| // input. In the case that both cameras are turned on at the same time, we |
| // won't change the active input after the first camera is turned on. We only |
| // support the use case of one camera on at a time. The third party |
| // developer can turn on/off both microphones with extension api if they like |
| // to. |
| if (camera_is_already_on) { |
| return; |
| } |
| |
| // If the current active input is an external device, keep it. |
| const AudioDevice* active_input = GetDeviceFromId(active_input_node_id_); |
| if (active_input && active_input->IsExternalDevice()) { |
| return; |
| } |
| |
| // Activate the correct mic for the current active camera. |
| ActivateMicForCamera(facing); |
| } |
| |
| void CrasAudioHandler::OnVideoCaptureStoppedOnMainThread( |
| media::VideoFacingMode facing) { |
| DCHECK(main_task_runner_->BelongsToCurrentThread()); |
| // Do nothing if the device doesn't have both front and rear microphones. |
| if (!HasDualInternalMic()) { |
| return; |
| } |
| |
| switch (facing) { |
| case media::MEDIA_VIDEO_FACING_USER: |
| front_camera_on_ = false; |
| break; |
| case media::MEDIA_VIDEO_FACING_ENVIRONMENT: |
| rear_camera_on_ = false; |
| break; |
| default: |
| LOG_IF(WARNING, facing == media::NUM_MEDIA_VIDEO_FACING_MODES) |
| << "On the device with dual microphone, got video capture " |
| << "notification with invalid camera facing mode value"; |
| return; |
| } |
| |
| // If not all cameras are turned off, don't change active input. In the case |
| // that both cameras are turned on at the same time before one of them is |
| // stopped, we won't change active input until all of them are stopped. |
| // We only support the use case of one camera on at a time. The third party |
| // developer can turn on/off both microphones with extension api if they like |
| // to. |
| if (IsCameraOn()) { |
| return; |
| } |
| |
| // If the current active input is an external device, keep it. |
| const AudioDevice* active_input = GetDeviceFromId(active_input_node_id_); |
| if (active_input && active_input->IsExternalDevice()) { |
| return; |
| } |
| |
| // Switch to front mic properly. |
| DeviceActivateType activated_by = |
| HasExternalDevice(true) ? DeviceActivateType::kActivateByUser |
| : DeviceActivateType::kActivateByPriority; |
| SwitchToDevice(*GetDeviceByType(AudioDeviceType::kFrontMic), true, |
| activated_by); |
| } |
| |
| void CrasAudioHandler::HandleMediaSessionMetadataReset() { |
| const std::map<std::string, std::string> empty_metadata_map = { |
| {"title", ""}, {"artist", ""}, {"album", ""}}; |
| |
| // TODO(b/228608730): Remove this after Floss bypasses CRAS to receive media |
| // information directly from the provider. |
| if (floss::features::IsFlossEnabled()) { |
| FlossMediaClient::Get()->SetPlayerMetadata(empty_metadata_map); |
| FlossMediaClient::Get()->SetPlayerIdentity(""); |
| FlossMediaClient::Get()->SetPlayerPlaybackStatus("stopped"); |
| } else { |
| CrasAudioClient::Get()->SetPlayerMetadata(empty_metadata_map); |
| CrasAudioClient::Get()->SetPlayerIdentity(""); |
| CrasAudioClient::Get()->SetPlayerPlaybackStatus("stopped"); |
| } |
| } |
| |
| void CrasAudioHandler::MediaSessionInfoChanged( |
| media_session::mojom::MediaSessionInfoPtr session_info) { |
| if (!session_info) { |
| return; |
| } |
| |
| std::string state = session_info->playback_state == |
| media_session::mojom::MediaPlaybackState::kPlaying |
| ? "playing" |
| : "paused"; |
| |
| // TODO(b/228608730): Remove this after Floss bypasses CRAS to receive media |
| // information directly from the provider. |
| if (floss::features::IsFlossEnabled()) { |
| FlossMediaClient::Get()->SetPlayerPlaybackStatus(state); |
| } else { |
| CrasAudioClient::Get()->SetPlayerPlaybackStatus(state); |
| } |
| } |
| |
| void CrasAudioHandler::MediaSessionMetadataChanged( |
| const std::optional<media_session::MediaMetadata>& metadata) { |
| if (!metadata || metadata->IsEmpty()) { |
| HandleMediaSessionMetadataReset(); |
| return; |
| } |
| |
| const std::map<std::string, std::string> metadata_map = { |
| {"title", base::UTF16ToUTF8(metadata->title)}, |
| {"artist", base::UTF16ToUTF8(metadata->artist)}, |
| {"album", base::UTF16ToUTF8(metadata->album)}}; |
| const std::string source_title = base::UTF16ToUTF8(metadata->source_title); |
| |
| // Assume media duration/length should always change with new metadata. |
| fetch_media_session_duration_ = true; |
| |
| // TODO(b/228608730): Remove this after Floss bypasses CRAS to receive media |
| // information directly from the provider. |
| if (floss::features::IsFlossEnabled()) { |
| FlossMediaClient::Get()->SetPlayerMetadata(metadata_map); |
| FlossMediaClient::Get()->SetPlayerIdentity(source_title); |
| } else { |
| CrasAudioClient::Get()->SetPlayerMetadata(metadata_map); |
| CrasAudioClient::Get()->SetPlayerIdentity(source_title); |
| } |
| } |
| |
| void CrasAudioHandler::MediaSessionPositionChanged( |
| const std::optional<media_session::MediaPosition>& position) { |
| if (!position) { |
| return; |
| } |
| |
| int64_t duration = 0; |
| if (fetch_media_session_duration_) { |
| duration = position->duration().InMicroseconds(); |
| if (duration > 0) { |
| // TODO(b/228608730): Remove this after Floss bypasses the media |
| // information from CRAS. |
| if (floss::features::IsFlossEnabled()) { |
| FlossMediaClient::Get()->SetPlayerDuration(duration); |
| } else { |
| CrasAudioClient::Get()->SetPlayerDuration(duration); |
| } |
| fetch_media_session_duration_ = false; |
| } |
| } |
| |
| int64_t current_position = position->GetPosition().InMicroseconds(); |
| if (current_position < 0 || (duration > 0 && current_position > duration)) { |
| return; |
| } |
| |
| // TODO(b/228608730): Remove this after Floss bypasses CRAS to receive media |
| // information directly from the provider. |
| if (floss::features::IsFlossEnabled()) { |
| FlossMediaClient::Get()->SetPlayerPosition(current_position); |
| } else { |
| CrasAudioClient::Get()->SetPlayerPosition(current_position); |
| } |
| } |
| |
| void CrasAudioHandler::OnMicrophoneMuteSwitchValueChanged(bool muted) { |
| input_muted_by_microphone_mute_switch_ = muted; |
| SetInputMute(muted, InputMuteChangeMethod::kPhysicalShutter); |
| |
| for (auto& observer : observers_) { |
| observer.OnInputMutedByMicrophoneMuteSwitchChanged(muted); |
| } |
| } |
| |
| void CrasAudioHandler::AddAudioObserver(AudioObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void CrasAudioHandler::RemoveAudioObserver(AudioObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| bool CrasAudioHandler::HasKeyboardMic() { |
| return GetKeyboardMic() != nullptr; |
| } |
| |
| bool CrasAudioHandler::HasHotwordDevice() { |
| return GetHotwordDevice() != nullptr; |
| } |
| |
| bool CrasAudioHandler::IsOutputMuted() { |
| return output_mute_on_; |
| } |
| |
| bool CrasAudioHandler::IsOutputMutedForDevice(uint64_t device_id) { |
| const AudioDevice* device = GetDeviceFromId(device_id); |
| if (!device) { |
| return false; |
| } |
| DCHECK(!device->is_input); |
| return audio_pref_handler_->GetMuteValue(*device); |
| } |
| |
| bool CrasAudioHandler::IsOutputMutedByPolicy() { |
| return output_mute_forced_by_policy_; |
| } |
| |
| bool CrasAudioHandler::IsOutputMutedBySecurityCurtain() { |
| return output_mute_forced_by_security_curtain_; |
| } |
| |
| bool CrasAudioHandler::IsOutputVolumeBelowDefaultMuteLevel() { |
| return output_volume_ <= kMuteThresholdPercent; |
| } |
| |
| bool CrasAudioHandler::IsInputMuted() { |
| return input_mute_on_; |
| } |
| |
| bool CrasAudioHandler::IsInputMutedBySecurityCurtain() { |
| return input_mute_forced_by_security_curtain_; |
| } |
| |
| bool CrasAudioHandler::IsInputMutedForDevice(uint64_t device_id) { |
| const AudioDevice* device = GetDeviceFromId(device_id); |
| if (!device) { |
| return false; |
| } |
| DCHECK(device->is_input); |
| // We don't record input mute state for each device in the prefs, |
| // for any non-active input device, we assume mute is off. |
| if (device->id == active_input_node_id_) { |
| return input_mute_on_; |
| } |
| return false; |
| } |
| |
| int CrasAudioHandler::GetOutputDefaultVolumeMuteThreshold() const { |
| return kMuteThresholdPercent; |
| } |
| |
| int CrasAudioHandler::GetOutputVolumePercent() { |
| return output_volume_; |
| } |
| |
| int CrasAudioHandler::GetOutputVolumePercentForDevice(uint64_t device_id) { |
| if (device_id == active_output_node_id_) { |
| return output_volume_; |
| } |
| const AudioDevice* device = GetDeviceFromId(device_id); |
| return static_cast<int>(audio_pref_handler_->GetOutputVolumeValue(device)); |
| } |
| |
| int CrasAudioHandler::GetInputGainPercent() { |
| return input_gain_; |
| } |
| |
| int CrasAudioHandler::GetInputGainPercentForDevice(uint64_t device_id) { |
| if (device_id == active_input_node_id_) { |
| return input_gain_; |
| } |
| const AudioDevice* device = GetDeviceFromId(device_id); |
| return static_cast<int>(audio_pref_handler_->GetInputGainValue(device)); |
| } |
| |
| uint64_t CrasAudioHandler::GetPrimaryActiveOutputNode() const { |
| return active_output_node_id_; |
| } |
| |
| uint64_t CrasAudioHandler::GetPrimaryActiveInputNode() const { |
| return active_input_node_id_; |
| } |
| |
| void CrasAudioHandler::GetAudioDevices(AudioDeviceList* device_list) const { |
| device_list->clear(); |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| device_list->push_back(device); |
| } |
| } |
| |
| // static. |
| AudioDeviceList CrasAudioHandler::GetSimpleUsageAudioDevices( |
| const AudioDeviceMap& audio_devices, |
| bool is_input) { |
| AudioDeviceList device_list; |
| for (const auto& item : audio_devices) { |
| const AudioDevice& device = item.second; |
| // Do not count non simple usage devices. |
| if (!device.is_for_simple_usage()) { |
| continue; |
| } |
| if (device.is_input == is_input) { |
| device_list.push_back(device); |
| } |
| } |
| return device_list; |
| } |
| |
| const AudioDeviceMap& CrasAudioHandler::GetAudioDevicesMapForTesting( |
| bool is_current_device) const { |
| return is_current_device ? audio_devices_ : previous_audio_devices_; |
| } |
| |
| bool CrasAudioHandler::GetPrimaryActiveOutputDevice(AudioDevice* device) const { |
| const AudioDevice* active_device = GetDeviceFromId(active_output_node_id_); |
| if (!active_device || !device) { |
| return false; |
| } |
| *device = *active_device; |
| return true; |
| } |
| |
| bool CrasAudioHandler::GetPrimaryActiveInputDevice(AudioDevice* device) const { |
| const AudioDevice* active_device = GetDeviceFromId(active_input_node_id_); |
| if (!active_device || !device) { |
| return false; |
| } |
| *device = *active_device; |
| return true; |
| } |
| |
| const AudioDevice* CrasAudioHandler::GetDeviceByType(AudioDeviceType type) { |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (device.type == type) { |
| return &device; |
| } |
| } |
| return nullptr; |
| } |
| |
| base::flat_map<CrasAudioHandler::ClientType, uint32_t> |
| CrasAudioHandler::GetNumberOfInputStreamsWithPermission() const { |
| return number_of_input_streams_with_permission_; |
| } |
| |
| void CrasAudioHandler::GetDefaultOutputBufferSize(int32_t* buffer_size) const { |
| *buffer_size = default_output_buffer_size_; |
| } |
| |
| void CrasAudioHandler::RequestGetAudioEffectDlcs() { |
| CrasAudioClient::Get()->GetAudioEffectDlcs( |
| base::BindOnce(&CrasAudioHandler::HandleGetAudioEffectDlcs, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::HandleGetAudioEffectDlcs( |
| std::optional<std::string> audio_effect_dlcs) { |
| if (!audio_effect_dlcs.has_value()) { |
| LOG(ERROR) << "cras_audio_handler: Failed to retrieve audio effect dlcs"; |
| } else { |
| audio_effect_dlcs_ = |
| base::SplitString(audio_effect_dlcs.value(), ",", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY); |
| } |
| } |
| |
| std::optional<std::vector<std::string>> CrasAudioHandler::GetAudioEffectDlcs() |
| const { |
| return audio_effect_dlcs_; |
| } |
| |
| void CrasAudioHandler::RequestVoiceIsolationUIAppearance() { |
| CrasAudioClient::Get()->GetVoiceIsolationUIAppearance( |
| base::BindOnce(&CrasAudioHandler::HandleGetVoiceIsolationUIAppearance, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::ResetVoiceIsolationPreferredEffectIfNeeded() { |
| uint32_t effect_mode_options = |
| voice_isolation_ui_appearance_.effect_mode_options; |
| bool pref_is_zero = !audio_pref_handler_->GetVoiceIsolationPreferredEffect(); |
| if (effect_mode_options != 0 && pref_is_zero) { |
| // Default preferred effect is Style Transfer. |
| // Currently Style Transfer is always in effect_mode_options if it's not 0. |
| audio_pref_handler_->SetVoiceIsolationPreferredEffect( |
| cras::AudioEffectType::EFFECT_TYPE_STYLE_TRANSFER); |
| RefreshVoiceIsolationPreferredEffect(); |
| } |
| |
| // Reset to NONE if the preferred effect is not in the effect_mode_options. |
| if (!(effect_mode_options & |
| audio_pref_handler_->GetVoiceIsolationPreferredEffect())) { |
| audio_pref_handler_->SetVoiceIsolationPreferredEffect( |
| cras::AudioEffectType::EFFECT_TYPE_NONE); |
| RefreshVoiceIsolationPreferredEffect(); |
| } |
| } |
| |
| void CrasAudioHandler::HandleGetVoiceIsolationUIAppearance( |
| std::optional<VoiceIsolationUIAppearance> appearance) { |
| if (!appearance.has_value()) { |
| LOG(ERROR) << "cras_audio_handler: Failed to retrieve voice isolation UI " |
| "appearance."; |
| } else { |
| voice_isolation_ui_appearance_ = appearance.value(); |
| ResetVoiceIsolationPreferredEffectIfNeeded(); |
| } |
| |
| for (auto& observer : observers_) { |
| observer.OnVoiceIsolationUIAppearanceChanged( |
| voice_isolation_ui_appearance_); |
| } |
| } |
| |
| VoiceIsolationUIAppearance CrasAudioHandler::GetVoiceIsolationUIAppearance() { |
| return voice_isolation_ui_appearance_; |
| } |
| |
| bool CrasAudioHandler::GetVoiceIsolationState() const { |
| return audio_pref_handler_->GetVoiceIsolationState(); |
| } |
| |
| void CrasAudioHandler::RefreshVoiceIsolationState() { |
| // Refresh should only update the state in CRAS and leave the preference |
| // as-is. |
| CrasAudioClient::Get()->SetVoiceIsolationUIEnabled(GetVoiceIsolationState()); |
| } |
| |
| void CrasAudioHandler::RecordVoiceIsolationEnabledChangeSource( |
| AudioSettingsChangeSource source) { |
| base::UmaHistogramEnumeration(kVoiceIsolationEnabledChangeSourceHistogramName, |
| source); |
| } |
| |
| uint32_t CrasAudioHandler::GetVoiceIsolationPreferredEffect() const { |
| return audio_pref_handler_->GetVoiceIsolationPreferredEffect(); |
| } |
| |
| void CrasAudioHandler::RefreshVoiceIsolationPreferredEffect() { |
| CrasAudioClient::Get()->SetVoiceIsolationUIPreferredEffect( |
| GetVoiceIsolationPreferredEffect()); |
| } |
| |
| void CrasAudioHandler::RecordVoiceIsolationPreferredEffectChange( |
| audio_config::mojom::AudioEffectType preferred_effect) { |
| base::UmaHistogramEnumeration( |
| kVoiceIsolationPreferredEffectChangeHistogramName, preferred_effect); |
| } |
| |
| bool CrasAudioHandler::IsNoiseCancellationSupportedForDevice( |
| uint64_t device_id) { |
| if (!noise_cancellation_supported()) { |
| return false; |
| } |
| |
| const AudioDevice* device = GetDeviceFromId(device_id); |
| if (!device) { |
| return false; |
| } |
| |
| if (!device->is_input) { |
| return false; |
| } |
| |
| return device->audio_effect & cras::EFFECT_TYPE_NOISE_CANCELLATION; |
| } |
| |
| bool CrasAudioHandler::GetNoiseCancellationState() const { |
| return audio_pref_handler_->GetNoiseCancellationState(); |
| } |
| |
| void CrasAudioHandler::SetNoiseCancellationState( |
| bool noise_cancellation_on, |
| AudioSettingsChangeSource source) { |
| CrasAudioClient::Get()->SetVoiceIsolationUIEnabled(noise_cancellation_on); |
| audio_pref_handler_->SetVoiceIsolationState(noise_cancellation_on); |
| |
| for (auto& observer : observers_) { |
| observer.OnNoiseCancellationStateChanged(); |
| } |
| base::UmaHistogramEnumeration(kNoiseCancellationEnabledSourceHistogramName, |
| source); |
| } |
| |
| void CrasAudioHandler::RequestNoiseCancellationSupported( |
| OnNoiseCancellationSupportedCallback callback) { |
| CrasAudioClient::Get()->GetNoiseCancellationSupported( |
| base::BindOnce(&CrasAudioHandler::HandleGetNoiseCancellationSupported, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void CrasAudioHandler::HandleGetNoiseCancellationSupported( |
| OnNoiseCancellationSupportedCallback callback, |
| std::optional<bool> noise_cancellation_supported) { |
| if (!noise_cancellation_supported.has_value()) { |
| LOG(ERROR) |
| << "cras_audio_handler: Failed to retrieve noise cancellation support"; |
| } else { |
| noise_cancellation_supported_ = noise_cancellation_supported.value(); |
| } |
| |
| std::move(callback).Run(); |
| } |
| |
| void CrasAudioHandler::SetVoiceIsolationUIAppearanceForTesting( |
| cras::AudioEffectType toggle_type, |
| uint32_t effect_mode_options, |
| bool show_effect_fallback_message) { |
| voice_isolation_ui_appearance_ = VoiceIsolationUIAppearance( |
| toggle_type, effect_mode_options, show_effect_fallback_message); |
| } |
| |
| void CrasAudioHandler::SetNoiseCancellationSupportedForTesting(bool supported) { |
| noise_cancellation_supported_ = supported; |
| } |
| |
| bool CrasAudioHandler::IsStyleTransferSupportedForDevice(uint64_t device_id) { |
| if (!style_transfer_supported()) { |
| return false; |
| } |
| |
| const AudioDevice* device = GetDeviceFromId(device_id); |
| if (!device) { |
| return false; |
| } |
| |
| return device->audio_effect & cras::EFFECT_TYPE_STYLE_TRANSFER; |
| } |
| |
| bool CrasAudioHandler::GetStyleTransferState() const { |
| return audio_pref_handler_->GetVoiceIsolationState(); |
| } |
| |
| void CrasAudioHandler::SetStyleTransferState(bool style_transfer_on) { |
| CrasAudioClient::Get()->SetVoiceIsolationUIEnabled(style_transfer_on); |
| audio_pref_handler_->SetVoiceIsolationState(style_transfer_on); |
| |
| for (auto& observer : observers_) { |
| observer.OnStyleTransferStateChanged(); |
| } |
| } |
| |
| void CrasAudioHandler::RequestStyleTransferSupported( |
| OnStyleTransferSupportedCallback callback) { |
| CrasAudioClient::Get()->GetStyleTransferSupported( |
| base::BindOnce(&CrasAudioHandler::HandleGetStyleTransferSupported, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void CrasAudioHandler::HandleGetStyleTransferSupported( |
| OnStyleTransferSupportedCallback callback, |
| std::optional<bool> system_style_transfer_supported) { |
| if (!system_style_transfer_supported.has_value()) { |
| LOG(ERROR) |
| << "cras_audio_handler: Failed to retrieve style transfer support"; |
| } else { |
| style_transfer_supported_ = system_style_transfer_supported.value(); |
| } |
| |
| std::move(callback).Run(); |
| } |
| |
| void CrasAudioHandler::SetStyleTransferSupportedForTesting(bool supported) { |
| style_transfer_supported_ = supported; |
| } |
| |
| bool CrasAudioHandler::GetForceRespectUiGainsState() const { |
| return audio_pref_handler_->GetForceRespectUiGainsState(); |
| } |
| |
| void CrasAudioHandler::RefreshForceRespectUiGainsState() { |
| // Refresh should only update the state in CRAS and leave the preference |
| // as-is. |
| CrasAudioClient::Get()->SetForceRespectUiGains(GetForceRespectUiGainsState()); |
| } |
| |
| void CrasAudioHandler::SetForceRespectUiGainsState(bool state) { |
| base::UmaHistogramBoolean(CrasAudioHandler::kForceRespectUiGainsHistogramName, |
| state); |
| CrasAudioClient::Get()->SetForceRespectUiGains(state); |
| audio_pref_handler_->SetForceRespectUiGainsState(state); |
| |
| for (auto& observer : observers_) { |
| observer.OnForceRespectUiGainsStateChanged(); |
| } |
| } |
| |
| bool CrasAudioHandler::IsHfpMicSrSupportedForDevice(uint64_t device_id) { |
| if (!hfp_mic_sr_supported()) { |
| return false; |
| } |
| |
| const AudioDevice* device = GetDeviceFromId(device_id); |
| if (!device || device->type != AudioDeviceType::kBluetoothNbMic) { |
| return false; |
| } |
| |
| return device->audio_effect & cras::EFFECT_TYPE_HFP_MIC_SR; |
| } |
| |
| void CrasAudioHandler::RequestHfpMicSrSupported( |
| OnHfpMicSrSupportedCallback callback) { |
| CrasAudioClient::Get()->GetHfpMicSrSupported( |
| base::BindOnce(&CrasAudioHandler::HandleGetHfpMicSrSupported, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void CrasAudioHandler::SetHfpMicSrSupportedForTesting(bool supported) { |
| hfp_mic_sr_supported_ = supported; |
| } |
| |
| void CrasAudioHandler::HandleGetHfpMicSrSupported( |
| OnHfpMicSrSupportedCallback callback, |
| std::optional<bool> hfp_mic_sr_supported) { |
| if (!hfp_mic_sr_supported.has_value()) { |
| LOG(ERROR) << "cras_audio_handler: Failed to retrieve hfp_mic_sr support"; |
| } else { |
| hfp_mic_sr_supported_ = hfp_mic_sr_supported.value(); |
| } |
| |
| std::move(callback).Run(); |
| } |
| |
| bool CrasAudioHandler::GetHfpMicSrState() const { |
| return audio_pref_handler_->GetHfpMicSrState(); |
| } |
| |
| void CrasAudioHandler::RefreshHfpMicSrState() { |
| if (!hfp_mic_sr_supported()) { |
| return; |
| } |
| |
| const AudioDevice* device = GetDeviceByType(AudioDeviceType::kBluetoothNbMic); |
| if (!device) { |
| return; |
| } |
| |
| // Refresh should only update the state in CRAS and leave the preference |
| // as-is. |
| CrasAudioClient::Get()->SetHfpMicSrEnabled( |
| GetHfpMicSrState() && |
| (device->audio_effect & cras::EFFECT_TYPE_HFP_MIC_SR)); |
| } |
| |
| void CrasAudioHandler::SetHfpMicSrState(bool hfp_mic_sr_on, |
| AudioSettingsChangeSource source) { |
| CrasAudioClient::Get()->SetHfpMicSrEnabled(hfp_mic_sr_on); |
| audio_pref_handler_->SetHfpMicSrState(hfp_mic_sr_on); |
| |
| for (auto& observer : observers_) { |
| observer.OnHfpMicSrStateChanged(); |
| } |
| } |
| |
| bool CrasAudioHandler::IsSpatialAudioSupportedForDevice(uint64_t device_id) { |
| if (!spatial_audio_supported()) { |
| return false; |
| } |
| |
| const AudioDevice* device = GetDeviceFromId(device_id); |
| if (!device || device->type != AudioDeviceType::kInternalSpeaker) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void CrasAudioHandler::RequestSpatialAudioSupported( |
| OnSpatialAudioSupportedCallback callback) { |
| CrasAudioClient::Get()->GetSpatialAudioSupported( |
| base::BindOnce(&CrasAudioHandler::HandleGetSpatialAudioSupported, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void CrasAudioHandler::HandleGetSpatialAudioSupported( |
| OnSpatialAudioSupportedCallback callback, |
| std::optional<bool> spatial_audio_supported) { |
| if (!spatial_audio_supported.has_value()) { |
| LOG(ERROR) |
| << "cras_audio_handler: Failed to retrieve spatial audio support"; |
| } else { |
| spatial_audio_supported_ = spatial_audio_supported.value(); |
| } |
| |
| std::move(callback).Run(); |
| } |
| |
| bool CrasAudioHandler::GetSpatialAudioState() const { |
| return audio_pref_handler_->GetSpatialAudioState(); |
| } |
| |
| void CrasAudioHandler::RefreshSpatialAudioState() { |
| // Refresh should only update the state in CRAS and leave the preference |
| // as-is. |
| CrasAudioClient::Get()->SetSpatialAudio(GetSpatialAudioState()); |
| } |
| |
| void CrasAudioHandler::SetSpatialAudioState(bool state) { |
| base::UmaHistogramBoolean(CrasAudioHandler::kSpatialAudioHistogramName, |
| state); |
| CrasAudioClient::Get()->SetSpatialAudio(state); |
| audio_pref_handler_->SetSpatialAudioState(state); |
| |
| for (auto& observer : observers_) { |
| observer.OnSpatialAudioStateChanged(); |
| } |
| } |
| |
| void CrasAudioHandler::SetSpatialAudioSupportedForTesting(bool supported) { |
| spatial_audio_supported_ = supported; |
| } |
| |
| void CrasAudioHandler::SetKeyboardMicActive(bool active) { |
| const AudioDevice* keyboard_mic = GetKeyboardMic(); |
| if (!keyboard_mic) { |
| return; |
| } |
| // Keyboard mic is invisible to chromeos users. It is always added or removed |
| // as additional active node. |
| DCHECK(active_input_node_id_ && active_input_node_id_ != keyboard_mic->id); |
| if (active) { |
| AddActiveNode(keyboard_mic->id, true); |
| } else { |
| RemoveActiveNodeInternal(keyboard_mic->id, true); |
| } |
| } |
| |
| void CrasAudioHandler::SetSpeakOnMuteDetection(bool som_on) { |
| CrasAudioClient::Get()->SetSpeakOnMuteDetection(som_on); |
| speak_on_mute_detection_on_ = som_on; |
| } |
| |
| void CrasAudioHandler::SetEwmaPowerReportEnabled(bool enabled) { |
| CrasAudioClient::Get()->SetEwmaPowerReportEnabled(enabled); |
| ewma_power_report_enabled_ = enabled; |
| } |
| |
| double CrasAudioHandler::GetEwmaPower() { |
| return ewma_power_; |
| } |
| |
| void CrasAudioHandler::SetSidetoneEnabled(bool enabled) { |
| CrasAudioClient::Get()->SetSidetoneEnabled(enabled); |
| sidetone_enabled_ = enabled; |
| } |
| |
| bool CrasAudioHandler::GetSidetoneEnabled() const { |
| return sidetone_enabled_; |
| } |
| |
| bool CrasAudioHandler::IsSidetoneSupported() const { |
| return sidetone_supported_; |
| } |
| |
| void CrasAudioHandler::UpdateSidetoneSupportedState() { |
| CrasAudioClient::Get()->GetSidetoneSupported( |
| base::BindOnce(&CrasAudioHandler::HandleGetSidetoneSupported, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::HandleGetSidetoneSupported( |
| std::optional<bool> supported) { |
| if (!supported.has_value()) { |
| LOG(ERROR) << "Failed to check whether sidetone is supported."; |
| return; |
| } |
| |
| sidetone_supported_ = *supported; |
| } |
| |
| void CrasAudioHandler::AddActiveNode(uint64_t node_id, bool notify) { |
| const AudioDevice* device = GetDeviceFromId(node_id); |
| if (!device) { |
| VLOG(1) << "AddActiveInputNode: Cannot find device id=" << "0x" << std::hex |
| << node_id; |
| return; |
| } |
| |
| // If there is no primary active device, set |node_id| to primary active node. |
| if ((device->is_input && !active_input_node_id_) || |
| (!device->is_input && !active_output_node_id_)) { |
| SwitchToDevice(*device, notify, DeviceActivateType::kActivateByUser); |
| return; |
| } |
| |
| AddAdditionalActiveNode(node_id, notify); |
| } |
| |
| void CrasAudioHandler::ChangeActiveNodes(const NodeIdList& new_active_ids) { |
| AudioDeviceList input_devices; |
| AudioDeviceList output_devices; |
| |
| for (uint64_t id : new_active_ids) { |
| const AudioDevice* device = GetDeviceFromId(id); |
| if (!device) { |
| continue; |
| } |
| if (device->is_input) { |
| input_devices.push_back(*device); |
| } else { |
| output_devices.push_back(*device); |
| } |
| } |
| if (!input_devices.empty()) { |
| SetActiveDevices(input_devices, true /* is_input */); |
| } |
| if (!output_devices.empty()) { |
| SetActiveDevices(output_devices, false /* is_input */); |
| } |
| } |
| |
| bool CrasAudioHandler::SetActiveInputNodes(const NodeIdList& node_ids) { |
| return SetActiveNodes(node_ids, true /* is_input */); |
| } |
| |
| bool CrasAudioHandler::SetActiveOutputNodes(const NodeIdList& node_ids) { |
| return SetActiveNodes(node_ids, false /* is_input */); |
| } |
| |
| bool CrasAudioHandler::SetActiveNodes(const NodeIdList& node_ids, |
| bool is_input) { |
| AudioDeviceList devices; |
| for (uint64_t id : node_ids) { |
| const AudioDevice* device = GetDeviceFromId(id); |
| if (!device || device->is_input != is_input) { |
| return false; |
| } |
| |
| devices.push_back(*device); |
| } |
| |
| SetActiveDevices(devices, is_input); |
| return true; |
| } |
| |
| void CrasAudioHandler::SetActiveDevices(const AudioDeviceList& devices, |
| bool is_input) { |
| std::set<uint64_t> new_active_ids; |
| for (const auto& active_device : devices) { |
| CHECK_EQ(is_input, active_device.is_input); |
| new_active_ids.insert(active_device.id); |
| } |
| |
| bool active_devices_changed = false; |
| |
| // If primary active node has to be switched, do that before adding or |
| // removing any other active devices. Switching to a device can change |
| // activity state of other devices - final device activity may end up being |
| // unintended if it is set before active device switches. |
| uint64_t primary_active = |
| is_input ? active_input_node_id_ : active_output_node_id_; |
| if (!new_active_ids.count(primary_active) && !devices.empty()) { |
| active_devices_changed = true; |
| SwitchToDevice(devices[0], false /* notify */, |
| DeviceActivateType::kActivateByUser); |
| } |
| |
| AudioDeviceList existing_devices; |
| GetAudioDevices(&existing_devices); |
| for (const auto& existing_device : existing_devices) { |
| if (existing_device.is_input != is_input) { |
| continue; |
| } |
| |
| bool should_be_active = new_active_ids.count(existing_device.id); |
| if (existing_device.active == should_be_active) { |
| continue; |
| } |
| active_devices_changed = true; |
| |
| if (should_be_active) { |
| AddActiveNode(existing_device.id, false /* notify */); |
| } else { |
| RemoveActiveNodeInternal(existing_device.id, false /* notify */); |
| } |
| } |
| |
| if (active_devices_changed) { |
| NotifyActiveNodeChanged(is_input); |
| } |
| } |
| |
| void CrasAudioHandler::SetHotwordModel(uint64_t node_id, |
| const std::string& hotword_model, |
| VoidCrasAudioHandlerCallback callback) { |
| CrasAudioClient::Get()->SetHotwordModel(node_id, hotword_model, |
| std::move(callback)); |
| } |
| |
| void CrasAudioHandler::SwapInternalSpeakerLeftRightChannel(bool swap) { |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (!device.is_input && device.type == AudioDeviceType::kInternalSpeaker) { |
| CrasAudioClient::Get()->SwapLeftRight(device.id, swap); |
| break; |
| } |
| } |
| } |
| |
| void CrasAudioHandler::SetDisplayRotation(cras::DisplayRotation rotation) { |
| display_rotation_ = rotation; |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (device.type == AudioDeviceType::kInternalSpeaker) { |
| CrasAudioClient::Get()->SetDisplayRotation(device.id, display_rotation_); |
| break; |
| } |
| } |
| } |
| |
| void CrasAudioHandler::SetOutputMonoEnabled(bool enabled) { |
| if (output_mono_enabled_ == enabled) { |
| return; |
| } |
| output_mono_enabled_ = enabled; |
| if (enabled) { |
| CrasAudioClient::Get()->SetGlobalOutputChannelRemix( |
| output_channels_, |
| std::vector<double>(kStereoToMono, std::end(kStereoToMono))); |
| } else { |
| CrasAudioClient::Get()->SetGlobalOutputChannelRemix( |
| output_channels_, |
| std::vector<double>(kStereoToStereo, std::end(kStereoToStereo))); |
| } |
| |
| for (auto& observer : observers_) { |
| observer.OnOutputChannelRemixingChanged(enabled); |
| } |
| } |
| |
| bool CrasAudioHandler::has_alternative_input() const { |
| return has_alternative_input_; |
| } |
| |
| bool CrasAudioHandler::has_alternative_output() const { |
| return has_alternative_output_; |
| } |
| |
| void CrasAudioHandler::SetOutputVolumePercent(int volume_percent) { |
| // Set all active devices to the same volume. |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (!device.is_input && device.active) { |
| SetOutputNodeVolumePercent(device.id, volume_percent); |
| } |
| } |
| } |
| |
| // TODO: Rename the 'Percent' to something more meaningful. |
| void CrasAudioHandler::SetInputGainPercent(int gain_percent) { |
| // TODO(jennyz): Should we set all input devices' gain to the same level? |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (device.is_input && device.active) { |
| SetInputNodeGainPercent(active_input_node_id_, gain_percent); |
| } |
| } |
| } |
| |
| void CrasAudioHandler::AdjustOutputVolumeByPercent(int adjust_by_percent) { |
| SetOutputVolumePercent(output_volume_ + adjust_by_percent); |
| } |
| |
| void CrasAudioHandler::IncreaseOutputVolumeByOneStep(int one_step_percent) { |
| // Set all active devices to the same volume. |
| int new_output_volume = 0; |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (!device.is_input && device.active) { |
| // only USB device should depend on number_of_volume_steps |
| if (device.type == AudioDeviceType::kUsb) { |
| int32_t number_of_volume_steps = device.number_of_volume_steps; |
| if (number_of_volume_steps == 0) { |
| LOG(ERROR) |
| << device.ToString() |
| << ": No valid number_of_volume_steps. Falling back to default."; |
| number_of_volume_steps = NUMBER_OF_VOLUME_STEPS_DEFAULT; |
| } |
| int32_t volume_level = std::round( |
| (double)output_volume_ * (double)number_of_volume_steps * 0.01); |
| if (volume_level == 0 && output_volume_ > 0) { |
| volume_level = 1; |
| } |
| // increase one level and convert to volume |
| new_output_volume = std::min( |
| 100, static_cast<int>(std::floor(((double)(volume_level + 1)) / |
| number_of_volume_steps * 100))); |
| } else { |
| new_output_volume = std::min(100, output_volume_ + one_step_percent); |
| } |
| SetOutputNodeVolumePercent(device.id, new_output_volume); |
| } |
| } |
| } |
| |
| void CrasAudioHandler::DecreaseOutputVolumeByOneStep(int one_step_percent) { |
| // Set all active devices to the same volume. |
| int new_output_volume = 0; |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (!device.is_input && device.active) { |
| if (device.type == AudioDeviceType::kUsb) { |
| int32_t number_of_volume_steps = device.number_of_volume_steps; |
| if (number_of_volume_steps == 0) { |
| LOG(ERROR) |
| << device.ToString() |
| << ": No valid number_of_volume_steps. Falling back to default."; |
| number_of_volume_steps = NUMBER_OF_VOLUME_STEPS_DEFAULT; |
| } |
| int32_t volume_level = std::round( |
| (double)output_volume_ * (double)number_of_volume_steps * 0.01); |
| |
| // decrease one level and convert to volume |
| new_output_volume = std::max( |
| 0, static_cast<int>(std::floor(((double)(volume_level - 1)) / |
| number_of_volume_steps * 100))); |
| } else { |
| new_output_volume = std::max(0, output_volume_ - one_step_percent); |
| } |
| SetOutputNodeVolumePercent(device.id, new_output_volume); |
| } |
| } |
| } |
| |
| void CrasAudioHandler::SetOutputMute(bool mute_on) { |
| if (!SetOutputMuteInternal(mute_on)) { |
| return; |
| } |
| |
| // Save the mute state for all active output audio devices. |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (!device.is_input && device.active) { |
| audio_pref_handler_->SetMuteValue(device, output_mute_on_); |
| } |
| } |
| |
| for (auto& observer : observers_) { |
| observer.OnOutputMuteChanged(output_mute_on_); |
| } |
| } |
| |
| void CrasAudioHandler::SetOutputMute( |
| bool mute_on, |
| CrasAudioHandler::AudioSettingsChangeSource source) { |
| SetOutputMute(mute_on); |
| base::UmaHistogramEnumeration( |
| CrasAudioHandler::kOutputVolumeMuteSourceHistogramName, source); |
| } |
| |
| void CrasAudioHandler::SetOutputMuteLockedBySecurityCurtain(bool mute_on) { |
| if (output_mute_forced_by_security_curtain_ == mute_on) { |
| return; |
| } |
| |
| output_mute_forced_by_security_curtain_ = mute_on; |
| UpdateAudioOutputMute(); |
| } |
| |
| void CrasAudioHandler::AdjustOutputVolumeToAudibleLevel() { |
| if (output_volume_ <= kMuteThresholdPercent) { |
| for (const auto& item : audio_devices_) { |
| int unmute_volume = kDefaultUnmuteVolumePercent; |
| const AudioDevice& device = item.second; |
| if (!device.is_input && device.active) { |
| if (device.type == AudioDeviceType::kUsb) { |
| int32_t number_of_volume_steps = device.number_of_volume_steps; |
| DCHECK(number_of_volume_steps > 0); |
| unmute_volume = 100 / number_of_volume_steps; |
| } |
| SetOutputNodeVolumePercent(device.id, unmute_volume); |
| } |
| } |
| } |
| } |
| |
| void CrasAudioHandler::SetInputMute(bool mute_on, |
| InputMuteChangeMethod method) { |
| const bool old_mute_on = input_mute_on_; |
| SetInputMuteInternal(mute_on); |
| |
| if (old_mute_on != input_mute_on_) { |
| for (auto& observer : observers_) { |
| observer.OnInputMuteChanged(input_mute_on_, method); |
| } |
| } |
| } |
| |
| void CrasAudioHandler::SetInputMute( |
| bool mute_on, |
| InputMuteChangeMethod method, |
| CrasAudioHandler::AudioSettingsChangeSource source) { |
| SetInputMute(mute_on, method); |
| base::UmaHistogramEnumeration( |
| CrasAudioHandler::kInputGainMuteSourceHistogramName, source); |
| } |
| |
| void CrasAudioHandler::SetInputMuteLockedBySecurityCurtain(bool mute_on) { |
| if (input_mute_forced_by_security_curtain_ == mute_on) { |
| return; |
| } |
| |
| input_mute_forced_by_security_curtain_ = mute_on; |
| SetInputMute(mute_on, InputMuteChangeMethod::kOther); |
| |
| for (auto& observer : observers_) { |
| observer.OnInputMutedBySecurityCurtainChanged(mute_on); |
| } |
| } |
| |
| void CrasAudioHandler::SetActiveDevice(const AudioDevice& active_device, |
| bool notify, |
| DeviceActivateType activate_by) { |
| if (activate_by == DeviceActivateType::kActivateByUser) { |
| audio_device_metrics_handler_.RecordUserSwitchAudioDevice( |
| active_device.is_input); |
| } else { |
| audio_device_metrics_handler_.MaybeRecordSystemSwitchDecisionAndContext( |
| active_device.is_input, |
| active_device.is_input ? has_alternative_input_ |
| : has_alternative_output_, |
| /*is_switched=*/true, audio_devices_, previous_audio_devices_); |
| } |
| |
| // Update *_selected_by_user_. |
| // Including to unset it when selected by priority or by camera. |
| if (active_device.is_input) { |
| audio_device_metrics_handler_.set_input_device_selected_by_user( |
| activate_by == DeviceActivateType::kActivateByUser); |
| } else { |
| audio_device_metrics_handler_.set_output_device_selected_by_user( |
| activate_by == DeviceActivateType::kActivateByUser); |
| } |
| |
| if (active_device.is_input) { |
| CrasAudioClient::Get()->SetActiveInputNode(active_device.id); |
| } else { |
| CrasAudioClient::Get()->SetActiveOutputNode(active_device.id); |
| } |
| |
| if (notify) { |
| NotifyActiveNodeChanged(active_device.is_input); |
| } |
| |
| // Active device has changed, update user preference and add the device to |
| // most recently activated device list. |
| if (features::IsAudioSelectionImprovementEnabled()) { |
| SyncDevicePrefSetMap(active_device.is_input); |
| audio_pref_handler_->UpdateMostRecentActivatedDeviceIdList(active_device); |
| |
| if (activate_by == DeviceActivateType::kActivateByUser) { |
| // Remove notification if the hotplugged device that triggered the |
| // notification has already been activated by user via settings or quick |
| // settings. |
| audio_selection_notification_handler_ |
| .RemoveNotificationIfHotpluggedDeviceActivated({active_device}); |
| } |
| } |
| |
| // Save active state for the nodes. |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (device.is_input != active_device.is_input) { |
| continue; |
| } |
| SaveDeviceState(device, device.active, activate_by); |
| } |
| } |
| |
| void CrasAudioHandler::SaveDeviceState(const AudioDevice& device, |
| bool active, |
| DeviceActivateType activate_by) { |
| // Don't save the active state for non-simple usage device, which is invisible |
| // to end users. |
| if (!device.is_for_simple_usage()) { |
| return; |
| } |
| |
| if (!active) { |
| audio_pref_handler_->SetDeviceActive(device, false, false); |
| } else { |
| switch (activate_by) { |
| case DeviceActivateType::kActivateByUser: |
| audio_pref_handler_->SetDeviceActive(device, true, true); |
| break; |
| case DeviceActivateType::kActivateByPriority: |
| audio_pref_handler_->SetDeviceActive(device, true, false); |
| break; |
| default: |
| // The device is made active due to its previous active state in prefs, |
| // don't change its active state settings in prefs. |
| break; |
| } |
| } |
| } |
| |
| void CrasAudioHandler::SetVolumeGainPercentForDevice(uint64_t device_id, |
| int value) { |
| const AudioDevice* device = GetDeviceFromId(device_id); |
| if (!device) { |
| return; |
| } |
| |
| if (device->is_input) { |
| SetInputNodeGainPercent(device_id, value); |
| } else { |
| SetOutputNodeVolumePercent(device_id, value); |
| } |
| } |
| |
| void CrasAudioHandler::SetMuteForDevice(uint64_t device_id, bool mute_on) { |
| if (device_id == active_output_node_id_) { |
| SetOutputMute(mute_on); |
| return; |
| } |
| if (device_id == active_input_node_id_) { |
| VLOG(1) << "SetMuteForDevice sets active input device id=" << "0x" |
| << std::hex << device_id << " mute=" << mute_on; |
| SetInputMute(mute_on, InputMuteChangeMethod::kOther); |
| return; |
| } |
| |
| const AudioDevice* device = GetDeviceFromId(device_id); |
| // Input device's mute state is not recorded in the pref. crbug.com/365050. |
| if (device && !device->is_input) { |
| audio_pref_handler_->SetMuteValue(*device, mute_on); |
| } |
| } |
| |
| void CrasAudioHandler::SetMuteForDevice( |
| uint64_t device_id, |
| bool mute_on, |
| CrasAudioHandler::AudioSettingsChangeSource source) { |
| SetMuteForDevice(device_id, mute_on); |
| if (device_id == active_output_node_id_) { |
| base::UmaHistogramEnumeration( |
| CrasAudioHandler::kOutputVolumeMuteSourceHistogramName, source); |
| } else if (device_id == active_input_node_id_) { |
| base::UmaHistogramEnumeration( |
| CrasAudioHandler::kInputGainMuteSourceHistogramName, source); |
| } |
| } |
| |
| // If the HDMI device is the active output device, when the device enters/exits |
| // docking mode, or HDMI display changes resolution, or chromeos device |
| // suspends/resumes, cras will lose the HDMI output node for a short period of |
| // time, then rediscover it. This hotplug behavior will cause the audio output |
| // be leaked to the alternatvie active audio output during HDMI re-discovering |
| // period. See crbug.com/503667. |
| void CrasAudioHandler::SetActiveHDMIOutoutRediscoveringIfNecessary( |
| bool force_rediscovering) { |
| if (!GetDeviceFromId(active_output_node_id_)) { |
| return; |
| } |
| |
| // Marks the start of the HDMI re-discovering grace period, during which we |
| // will mute the audio output to prevent it to be be leaked to the |
| // alternative output device. |
| if ((hdmi_rediscovering_ && force_rediscovering) || |
| (!hdmi_rediscovering_ && IsHDMIPrimaryOutputDevice())) { |
| StartHDMIRediscoverGracePeriod(); |
| } |
| } |
| |
| CrasAudioHandler::CrasAudioHandler( |
| mojo::PendingRemote<media_session::mojom::MediaControllerManager> |
| media_controller_manager, |
| scoped_refptr<AudioDevicesPrefHandler> audio_pref_handler, |
| std::unique_ptr<Delegate> delegate) |
| : media_controller_manager_(std::move(media_controller_manager)), |
| audio_pref_handler_(audio_pref_handler), |
| delegate_(std::move(delegate)) { |
| SetupCrasAudioHandler(audio_pref_handler); |
| } |
| |
| CrasAudioHandler::CrasAudioHandler( |
| mojo::PendingRemote<media_session::mojom::MediaControllerManager> |
| media_controller_manager, |
| scoped_refptr<AudioDevicesPrefHandler> audio_pref_handler) |
| : media_controller_manager_(std::move(media_controller_manager)), |
| audio_pref_handler_(audio_pref_handler) { |
| SetupCrasAudioHandler(audio_pref_handler); |
| } |
| |
| void CrasAudioHandler::SetupCrasAudioHandler( |
| scoped_refptr<AudioDevicesPrefHandler> audio_pref_handler) { |
| DCHECK(audio_pref_handler); |
| DCHECK(CrasAudioClient::Get()); |
| CrasAudioClient::Get()->AddObserver(this); |
| audio_pref_handler_->AddAudioPrefObserver(this); |
| ui::MicrophoneMuteSwitchMonitor::Get()->AddObserver(this); |
| |
| BindMediaControllerObserver(); |
| InitializeAudioState(); |
| // Unittest may not have the task runner for the current thread. |
| if (base::SingleThreadTaskRunner::HasCurrentDefault()) { |
| main_task_runner_ = base::SingleThreadTaskRunner::GetCurrentDefault(); |
| } |
| |
| DCHECK(!g_cras_audio_handler); |
| g_cras_audio_handler = this; |
| } |
| |
| CrasAudioHandler::~CrasAudioHandler() { |
| hdmi_rediscover_timer_.Stop(); |
| DCHECK(CrasAudioClient::Get()); |
| CrasAudioClient::Get()->RemoveObserver(this); |
| audio_pref_handler_->RemoveAudioPrefObserver(this); |
| ui::MicrophoneMuteSwitchMonitor::Get()->RemoveObserver(this); |
| |
| DCHECK(g_cras_audio_handler); |
| g_cras_audio_handler = nullptr; |
| } |
| |
| void CrasAudioHandler::BindMediaControllerObserver() { |
| if (!media_controller_manager_) { |
| return; |
| } |
| media_controller_manager_->CreateActiveMediaController( |
| media_session_controller_remote_.BindNewPipeAndPassReceiver()); |
| media_session_controller_remote_->AddObserver( |
| media_controller_observer_receiver_.BindNewPipeAndPassRemote()); |
| } |
| |
| void CrasAudioHandler::AudioClientRestarted() { |
| InitializeAudioState(); |
| } |
| |
| void CrasAudioHandler::NodesChanged() { |
| if (cras_service_available_) { |
| GetNodes(); |
| RequestVoiceIsolationUIAppearance(); |
| } |
| } |
| |
| void CrasAudioHandler::OutputNodeVolumeChanged(uint64_t node_id, int volume) { |
| const AudioDevice* device = this->GetDeviceFromId(node_id); |
| if (!device || device->is_input) { |
| LOG(ERROR) << "Unexpected OutputNodeVolumeChanged received on node: 0x" |
| << std::hex << node_id; |
| return; |
| } |
| if (volume < 0 || volume > 100) { |
| LOG(ERROR) << "Unexpected OutputNodeVolumeChanged received on volume: " |
| << volume; |
| return; |
| } |
| |
| // Sync internal volume state and notify UI for the change. We trust cras |
| // signal to report the volume state of the device, no matter which source |
| // set the volume, i.e., volume could be set from non-chrome source, like |
| // Bluetooth headset, etc. Assume all active output devices share a single |
| // volume. |
| if (device->active) { |
| output_volume_ = volume; |
| } |
| audio_pref_handler_->SetVolumeGainValue(*device, volume); |
| |
| if (initializing_audio_state_) { |
| // Do not notify the observers for volume changed event if CrasAudioHandler |
| // is initializing its state, i.e., the volume change event is in responding |
| // to SetOutputNodeVolume request from intializaing audio state, not |
| // from user action, no need to notify UI to pop uo the volume slider bar. |
| if (init_node_id_ == node_id && init_volume_ == volume) { |
| --init_volume_count_; |
| if (!init_volume_count_) { |
| initializing_audio_state_ = false; |
| } |
| return; |
| } else { |
| // Reset the initializing_audio_state_ in case SetOutputNodeVolume request |
| // is lost by cras due to cras is not ready when CrasAudioHandler is being |
| // initialized. |
| initializing_audio_state_ = false; |
| init_volume_count_ = 0; |
| } |
| } |
| |
| for (auto& observer : observers_) { |
| observer.OnOutputNodeVolumeChanged(node_id, volume); |
| } |
| } |
| |
| void CrasAudioHandler::InputNodeGainChanged(uint64_t node_id, int gain) { |
| const AudioDevice* device = this->GetDeviceFromId(node_id); |
| |
| if (!device || !device->is_input) { |
| LOG(ERROR) << "Unexpexted InputNodeGainChanged received on node: 0x" |
| << std::hex << node_id; |
| return; |
| } |
| |
| if (device->active) { |
| input_gain_ = gain; |
| } |
| |
| audio_pref_handler_->SetVolumeGainValue(*device, gain); |
| |
| for (auto& observer : observers_) { |
| observer.OnInputNodeGainChanged(node_id, gain); |
| } |
| } |
| |
| void CrasAudioHandler::ActiveOutputNodeChanged(uint64_t node_id) { |
| if (active_output_node_id_ == node_id) { |
| return; |
| } |
| |
| // Active audio output device should always be changed by chrome. |
| // During system boot, cras may change active input to unknown device 0x1, |
| // we don't need to log it, since it is not an valid device. |
| if (GetDeviceFromId(node_id)) { |
| LOG(WARNING) << "Active output node changed unexpectedly by system node_id=" |
| << "0x" << std::hex << node_id; |
| } |
| } |
| |
| void CrasAudioHandler::ActiveInputNodeChanged(uint64_t node_id) { |
| if (active_input_node_id_ == node_id) { |
| return; |
| } |
| |
| // Active audio input device should always be changed by chrome. |
| // During system boot, cras may change active input to unknown device 0x2, |
| // we don't need to log it, since it is not an valid device. |
| if (GetDeviceFromId(node_id)) { |
| LOG(WARNING) << "Active input node changed unexpectedly by system node_id=" |
| << "0x" << std::hex << node_id; |
| } |
| } |
| |
| void CrasAudioHandler::HotwordTriggered(uint64_t tv_sec, uint64_t tv_nsec) { |
| for (auto& observer : observers_) { |
| observer.OnHotwordTriggered(tv_sec, tv_nsec); |
| } |
| } |
| |
| void CrasAudioHandler::NumberOfActiveStreamsChanged() { |
| GetNumberOfOutputStreams(); |
| } |
| |
| void CrasAudioHandler::BluetoothBatteryChanged(const std::string& address, |
| uint32_t level) { |
| for (auto& observer : observers_) { |
| observer.OnBluetoothBatteryChanged(address, level); |
| } |
| } |
| |
| void CrasAudioHandler::NumberOfInputStreamsWithPermissionChanged( |
| const base::flat_map<std::string, uint32_t>& num_input_streams) { |
| HandleGetNumberOfInputStreamsWithPermission(num_input_streams); |
| for (auto& observer : observers_) { |
| observer.OnNumberOfInputStreamsWithPermissionChanged(); |
| } |
| } |
| |
| // static |
| std::unique_ptr<CrasAudioHandler::AudioSurvey> |
| CrasAudioHandler::AbstractAudioSurvey( |
| const base::flat_map<std::string, std::string>& survey_specific_data) { |
| auto survey = std::make_unique<CrasAudioHandler::AudioSurvey>(); |
| for (const auto& it : survey_specific_data) { |
| if (it.first == CrasAudioHandler::kSurveyNameKey) { |
| if (it.second == CrasAudioHandler::kSurveyNameGeneral) { |
| survey->set_type(SurveyType::kGeneral); |
| } else if (it.second == CrasAudioHandler::kSurveyNameBluetooth) { |
| survey->set_type(SurveyType::kBluetooth); |
| } else if (it.second == CrasAudioHandler::kSurveyNameOutputProc) { |
| survey->set_type(SurveyType::kOutputProc); |
| } |
| } else { |
| survey->AddData(it.first, it.second); |
| } |
| } |
| return survey; |
| } |
| |
| void CrasAudioHandler::SurveyTriggered( |
| const base::flat_map<std::string, std::string>& survey_specific_data) { |
| auto survey = CrasAudioHandler::AbstractAudioSurvey(survey_specific_data); |
| |
| for (auto& observer : observers_) { |
| observer.OnSurveyTriggered(*survey); |
| } |
| } |
| |
| void CrasAudioHandler::SpeakOnMuteDetected() { |
| for (auto& observer : observers_) { |
| observer.OnSpeakOnMuteDetected(); |
| } |
| } |
| |
| void CrasAudioHandler::EwmaPowerReported(double power) { |
| ewma_power_ = power; |
| } |
| |
| void CrasAudioHandler::NumStreamIgnoreUiGains(int32_t num) { |
| num_stream_ignore_ui_gains_ = num; |
| for (auto& observer : observers_) { |
| observer.OnNumStreamIgnoreUiGainsChanged(num); |
| } |
| } |
| |
| void CrasAudioHandler::SidetoneSupportedChanged(bool supported) { |
| sidetone_supported_ = supported; |
| } |
| |
| void CrasAudioHandler::AudioEffectUIAppearanceChanged( |
| VoiceIsolationUIAppearance appearance) { |
| HandleGetVoiceIsolationUIAppearance(appearance); |
| } |
| |
| void CrasAudioHandler::ResendBluetoothBattery() { |
| CrasAudioClient::Get()->ResendBluetoothBattery(); |
| } |
| |
| void CrasAudioHandler::SetPrefHandlerForTesting( |
| scoped_refptr<AudioDevicesPrefHandler> audio_pref_handler) { |
| audio_pref_handler_->RemoveAudioPrefObserver(this); |
| audio_pref_handler_ = audio_pref_handler; |
| audio_pref_handler_->AddAudioPrefObserver(this); |
| } |
| |
| void CrasAudioHandler::OnAudioPolicyPrefChanged() { |
| ApplyAudioPolicy(); |
| } |
| |
| void CrasAudioHandler::OnVoiceIsolationPrefChanged() { |
| RefreshVoiceIsolationState(); |
| RefreshVoiceIsolationPreferredEffect(); |
| } |
| |
| const AudioDevice* CrasAudioHandler::GetDeviceFromId(uint64_t device_id) const { |
| AudioDeviceMap::const_iterator it = audio_devices_.find(device_id); |
| if (it == audio_devices_.end()) { |
| return nullptr; |
| } |
| return &it->second; |
| } |
| |
| AudioDevice CrasAudioHandler::ConvertAudioNodeWithModifiedPriority( |
| const AudioNode& node) { |
| AudioDevice device(node); |
| device.user_priority = audio_pref_handler_->GetUserPriority(device); |
| return device; |
| } |
| |
| const AudioDevice* CrasAudioHandler::GetDeviceFromStableDeviceId( |
| bool is_input, |
| uint64_t stable_device_id) const { |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (device.is_input == is_input && |
| device.stable_device_id == stable_device_id) { |
| return &device; |
| } |
| } |
| return nullptr; |
| } |
| |
| const AudioDevice* CrasAudioHandler::GetKeyboardMic() const { |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (device.is_input && device.type == AudioDeviceType::kKeyboardMic) { |
| return &device; |
| } |
| } |
| return nullptr; |
| } |
| |
| const AudioDevice* CrasAudioHandler::GetHotwordDevice() const { |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (device.is_input && device.type == AudioDeviceType::kHotword) { |
| return &device; |
| } |
| } |
| return nullptr; |
| } |
| |
| void CrasAudioHandler::SetupAudioInputState() { |
| // Set the initial audio state to the ones read from audio prefs. |
| const AudioDevice* device = GetDeviceFromId(active_input_node_id_); |
| if (!device) { |
| LOG(ERROR) << "Can't set up audio state for unknown input device id =" |
| << "0x" << std::hex << active_input_node_id_; |
| return; |
| } |
| input_gain_ = audio_pref_handler_->GetInputGainValue(device); |
| VLOG(1) << "SetupAudioInputState for active device id=" << "0x" << std::hex |
| << device->id << " mute=" << input_mute_on_; |
| SetInputMuteInternal(input_mute_on_); |
| |
| SetInputNodeGain(active_input_node_id_, input_gain_); |
| } |
| |
| void CrasAudioHandler::SetupAudioOutputState() { |
| const AudioDevice* device = GetDeviceFromId(active_output_node_id_); |
| if (!device) { |
| LOG(ERROR) << "Can't set up audio state for unknown output device id =" |
| << "0x" << std::hex << active_output_node_id_; |
| return; |
| } |
| DCHECK(!device->is_input); |
| // Mute the output during HDMI re-discovering grace period. |
| if (hdmi_rediscovering_ && !IsHDMIPrimaryOutputDevice()) { |
| VLOG(1) << "Mute the output during HDMI re-discovering grace period"; |
| SetOutputMuteInternal(true); |
| } else { |
| SetOutputMuteInternal(audio_pref_handler_->GetMuteValue(*device)); |
| } |
| output_volume_ = audio_pref_handler_->GetOutputVolumeValue(device); |
| |
| if (initializing_audio_state_) { |
| // During power up, InitializeAudioState() could be called twice, first |
| // by CrasAudioHandler constructor, then by cras server restarting signal, |
| // both sending SetOutputNodeVolume requests, and could lead to two |
| // OutputNodeVolumeChanged signals. |
| ++init_volume_count_; |
| init_node_id_ = active_output_node_id_; |
| init_volume_ = output_volume_; |
| } |
| SetOutputNodeVolume(active_output_node_id_, output_volume_); |
| } |
| |
| // This sets up the state of an additional active node. |
| void CrasAudioHandler::SetupAdditionalActiveAudioNodeState(uint64_t node_id) { |
| const AudioDevice* device = GetDeviceFromId(node_id); |
| if (!device) { |
| VLOG(1) << "Can't set up audio state for unknown device id =" << "0x" |
| << std::hex << node_id; |
| return; |
| } |
| |
| DCHECK(node_id != active_output_node_id_ && node_id != active_input_node_id_); |
| |
| // Note: The mute state is a system wide state, we don't set mute per device, |
| // but just keep the mute state consistent for the active node in prefs. |
| // The output volume should be set to the same value for all active output |
| // devices. For input devices, we don't restore their gain value so far. |
| // TODO(jennyz): crbug.com/417418, track the status for the decison if |
| // we should persist input gain value in prefs. |
| if (!device->is_input) { |
| audio_pref_handler_->SetMuteValue(*device, IsOutputMuted()); |
| SetOutputNodeVolumePercent(node_id, GetOutputVolumePercent()); |
| } |
| } |
| |
| void CrasAudioHandler::InitializeAudioState() { |
| initializing_audio_state_ = true; |
| ApplyAudioPolicy(); |
| |
| // Defer querying cras for GetNodes until cras service becomes available. |
| cras_service_available_ = false; |
| CrasAudioClient::Get()->WaitForServiceToBeAvailable(base::BindOnce( |
| &CrasAudioHandler::InitializeAudioAfterCrasServiceAvailable, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::InitializeAudioAfterCrasServiceAvailable( |
| bool service_is_available) { |
| if (!service_is_available) { |
| LOG(ERROR) << "Cras service is not available"; |
| cras_service_available_ = false; |
| return; |
| } |
| |
| cras_service_available_ = true; |
| GetDefaultOutputBufferSizeInternal(); |
| GetSystemAecSupported(); |
| GetSystemAecGroupId(); |
| GetSystemNsSupported(); |
| GetSystemAgcSupported(); |
| RequestGetAudioEffectDlcs(); |
| RequestVoiceIsolationUIAppearance(); |
| RequestNoiseCancellationSupported(base::BindOnce( |
| &CrasAudioHandler::GetNodes, weak_ptr_factory_.GetWeakPtr())); |
| RequestStyleTransferSupported(base::BindOnce(&CrasAudioHandler::GetNodes, |
| weak_ptr_factory_.GetWeakPtr())); |
| RequestHfpMicSrSupported(base::BindOnce(&CrasAudioHandler::GetNodes, |
| weak_ptr_factory_.GetWeakPtr())); |
| RequestSpatialAudioSupported(base::BindOnce(&CrasAudioHandler::GetNodes, |
| weak_ptr_factory_.GetWeakPtr())); |
| GetNumberOfOutputStreams(); |
| GetNumberOfNonChromeOutputStreams(); |
| GetNumberOfInputStreamsWithPermissionInternal(); |
| GetNumStreamIgnoreUiGains(); |
| GetNumberOfArcStreams(); |
| CrasAudioClient::Get()->SetFixA2dpPacketSize( |
| base::FeatureList::IsEnabled(features::kBluetoothFixA2dpPacketSize)); |
| |
| // Sets Floss enabled based on feature flag. |
| CrasAudioClient::Get()->SetFlossEnabled(floss::features::IsFlossEnabled()); |
| |
| input_muted_by_microphone_mute_switch_ = IsMicrophoneMuteSwitchOn(); |
| if (input_muted_by_microphone_mute_switch_) { |
| SetInputMute(true, InputMuteChangeMethod::kPhysicalShutter); |
| } |
| |
| // Refreshes voice isolation state in CRAS, in case CRAS restarts. |
| RefreshVoiceIsolationState(); |
| |
| // Sets speak-on-mute detection enabled based on local variable, it re-applies |
| // the previous state if CRAS restarts. |
| CrasAudioClient::Get()->SetSpeakOnMuteDetection(speak_on_mute_detection_on_); |
| |
| // Sets force respect ui gains enabled based on audio pref, it re-applies the |
| // previous state if CRAS restarts. |
| CrasAudioClient::Get()->SetForceRespectUiGains(GetForceRespectUiGainsState()); |
| |
| UpdateSidetoneSupportedState(); |
| CrasAudioClient::Get()->SetSidetoneEnabled(sidetone_enabled_); |
| CrasAudioClient::Get()->SetEwmaPowerReportEnabled(ewma_power_report_enabled_); |
| } |
| |
| void CrasAudioHandler::ApplyAudioPolicy() { |
| bool mute_on = !audio_pref_handler_->GetAudioOutputAllowedValue(); |
| |
| if (output_mute_forced_by_policy_ == mute_on) { |
| return; |
| } |
| |
| output_mute_forced_by_policy_ = mute_on; |
| UpdateAudioOutputMute(); |
| // Policy for audio input is handled by kAudioCaptureAllowed in the Chrome |
| // media system. |
| } |
| |
| void CrasAudioHandler::UpdateAudioOutputMute() { |
| if (output_mute_forced_by_policy_ || |
| output_mute_forced_by_security_curtain_) { |
| // Mute the device, but do not update the preference. |
| SetOutputMuteInternal(true); |
| } else { |
| // Restore the mute state. |
| const AudioDevice* device = GetDeviceFromId(active_output_node_id_); |
| if (device) { |
| SetOutputMuteInternal(audio_pref_handler_->GetMuteValue(*device)); |
| } |
| } |
| } |
| |
| void CrasAudioHandler::SetOutputNodeVolume(uint64_t node_id, int volume) { |
| CrasAudioClient::Get()->SetOutputNodeVolume(node_id, volume); |
| } |
| |
| void CrasAudioHandler::SetOutputNodeVolumePercent(uint64_t node_id, |
| int volume_percent) { |
| const AudioDevice* device = GetDeviceFromId(node_id); |
| if (!device || device->is_input) { |
| return; |
| } |
| |
| volume_percent = min(max(volume_percent, 0), 100); |
| if (volume_percent <= kMuteThresholdPercent) { |
| volume_percent = 0; |
| } |
| |
| // Save the volume setting in pref in case this is called on non-active |
| // node for configuration. |
| audio_pref_handler_->SetVolumeGainValue(*device, volume_percent); |
| |
| if (device->active) { |
| SetOutputNodeVolume(node_id, volume_percent); |
| } |
| } |
| |
| bool CrasAudioHandler::SetOutputMuteInternal(bool mute_on) { |
| bool is_output_mute_forced = (output_mute_forced_by_policy_ || |
| output_mute_forced_by_security_curtain_); |
| |
| if (is_output_mute_forced && !mute_on) { |
| // Do not allow unmuting if the policy forces the device to remain muted. |
| return false; |
| } |
| |
| output_mute_on_ = mute_on; |
| CrasAudioClient::Get()->SetOutputUserMute(mute_on); |
| return true; |
| } |
| |
| void CrasAudioHandler::SetInputNodeGain(uint64_t node_id, int gain) { |
| CrasAudioClient::Get()->SetInputNodeGain(node_id, gain); |
| } |
| |
| void CrasAudioHandler::SetInputNodeGainPercent(uint64_t node_id, |
| int gain_percent) { |
| const AudioDevice* device = GetDeviceFromId(node_id); |
| if (!device || !device->is_input) { |
| return; |
| } |
| |
| // NOTE: We do not sanitize input gain values since the range is completely |
| // dependent on the device. |
| |
| audio_pref_handler_->SetVolumeGainValue(*device, gain_percent); |
| |
| if (device->active) { |
| SetInputNodeGain(node_id, gain_percent); |
| } |
| } |
| |
| void CrasAudioHandler::SetInputMuteInternal(bool mute_on) { |
| // Do not allow unmuting the device if hardware microphone mute switch is on. |
| // The switch disables internal microphone, and cras audio handler is expected |
| // to keep system wide cras mute on while the switch is toggled (which should |
| // ensure non-internal audio input devices are kept muted). |
| // |
| // Also do not allow unmuting the device if the security curtain is showing, |
| // to prevent a remote admin from spying on the user |
| if (!mute_on && (input_muted_by_microphone_mute_switch_ || |
| input_mute_forced_by_security_curtain_)) { |
| return; |
| } |
| |
| input_mute_on_ = mute_on; |
| CrasAudioClient::Get()->SetInputMute(mute_on); |
| } |
| |
| void CrasAudioHandler::GetNodes() { |
| CrasAudioClient::Get()->GetNodes(base::BindOnce( |
| &CrasAudioHandler::HandleGetNodes, weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::GetNumberOfNonChromeOutputStreams() { |
| CrasAudioClient::Get()->GetNumberOfNonChromeOutputStreams( |
| base::BindOnce(&CrasAudioHandler::HandleGetNumberOfNonChromeOutputStreams, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::GetNumberOfOutputStreams() { |
| CrasAudioClient::Get()->GetNumberOfActiveOutputStreams( |
| base::BindOnce(&CrasAudioHandler::HandleGetNumActiveOutputStreams, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| bool CrasAudioHandler::ChangeActiveDevice( |
| const AudioDevice& new_active_device) { |
| uint64_t& current_active_node_id = new_active_device.is_input |
| ? active_input_node_id_ |
| : active_output_node_id_; |
| // If the device we want to switch to is already the current active device, |
| // do nothing. |
| if (new_active_device.active && |
| new_active_device.id == current_active_node_id) { |
| return false; |
| } |
| |
| bool found_new_active_device = false; |
| // Reset all other input or output devices' active status. The active audio |
| // device from the previous user session can be remembered by cras, but not |
| // in chrome. see crbug.com/273271. |
| for (auto& item : audio_devices_) { |
| AudioDevice& device = item.second; |
| if (device.is_input == new_active_device.is_input && |
| device.id != new_active_device.id) { |
| device.active = false; |
| } else if (device.is_input == new_active_device.is_input && |
| device.id == new_active_device.id) { |
| found_new_active_device = true; |
| } |
| } |
| |
| if (!found_new_active_device) { |
| LOG(ERROR) << "Invalid new active device: " << new_active_device.ToString(); |
| return false; |
| } |
| |
| // Update user priority whenever the audio device is activated. |
| const AudioDevice* current_active_device = |
| GetDeviceFromId(current_active_node_id); |
| audio_pref_handler_->SetUserPriorityHigherThan(new_active_device, |
| current_active_device); |
| |
| // Set the current active input/output device to the new_active_device. |
| current_active_node_id = new_active_device.id; |
| audio_devices_[current_active_node_id].active = true; |
| return true; |
| } |
| |
| void CrasAudioHandler::SwitchToDevice(const AudioDevice& device, |
| bool notify, |
| DeviceActivateType activate_by) { |
| if (!ChangeActiveDevice(device)) { |
| // Record the decision of system not switching active device. |
| if (activate_by != DeviceActivateType::kActivateByUser) { |
| audio_device_metrics_handler_.MaybeRecordSystemSwitchDecisionAndContext( |
| device.is_input, |
| device.is_input ? has_alternative_input_ : has_alternative_output_, |
| /*is_switched=*/false, audio_devices_, previous_audio_devices_); |
| } |
| return; |
| } |
| |
| if (device.is_input) { |
| SetupAudioInputState(); |
| } else { |
| SetupAudioOutputState(); |
| } |
| |
| SetActiveDevice(device, notify, activate_by); |
| |
| // content::MediaStreamManager listens to |
| // base::SystemMonitor::DevicesChangedObserver for audio devices, |
| // and updates EnumerateDevices when OnDevicesChanged is called. |
| base::SystemMonitor* monitor = base::SystemMonitor::Get(); |
| // In some unittest, |monitor| might be nullptr. |
| if (!monitor) { |
| return; |
| } |
| monitor->ProcessDevicesChanged( |
| base::SystemMonitor::DeviceType::DEVTYPE_AUDIO); |
| } |
| |
| bool CrasAudioHandler::HasDeviceChange(const AudioNodeList& new_nodes, |
| bool is_input, |
| AudioDeviceList* new_discovered, |
| AudioDeviceList* removed_devices, |
| bool* active_device_removed) { |
| removed_devices->clear(); |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (is_input != device.is_input) { |
| continue; |
| } |
| if (!IsDeviceInList(device, new_nodes)) { |
| removed_devices->push_back(device); |
| if ((is_input && device.id == active_input_node_id_) || |
| (!is_input && device.id == active_output_node_id_)) { |
| *active_device_removed = true; |
| } |
| } |
| } |
| |
| bool new_or_changed_device = false; |
| new_discovered->clear(); |
| |
| for (const AudioNode& node : new_nodes) { |
| if (is_input != node.is_input) { |
| continue; |
| } |
| // Check if the new device is not in the old device list. |
| AudioDevice device = ConvertAudioNodeWithModifiedPriority(node); |
| DeviceStatus status = CheckDeviceStatus(device); |
| if (status == NEW_DEVICE) { |
| // When audio selection improvement flag is off, always put the new device |
| // into the new discovered device list. |
| // When audio selection improvement flag is on, only put simple usage |
| // device into new discovered device list. |
| if (!features::IsAudioSelectionImprovementEnabled() || |
| device.is_for_simple_usage()) { |
| new_discovered->push_back(device); |
| } |
| } |
| if (status == NEW_DEVICE || status == CHANGED_DEVICE) { |
| new_or_changed_device = true; |
| } |
| } |
| return new_or_changed_device || !removed_devices->empty(); |
| } |
| |
| CrasAudioHandler::DeviceStatus CrasAudioHandler::CheckDeviceStatus( |
| const AudioDevice& device) { |
| const AudioDevice* device_found = |
| GetDeviceFromStableDeviceId(device.is_input, device.stable_device_id); |
| if (!device_found) { |
| return NEW_DEVICE; |
| } |
| |
| if (!IsSameAudioDevice(device, *device_found)) { |
| LOG(ERROR) << "Different Audio devices with same stable device id:" |
| << " new device: " << device.ToString() |
| << " old device: " << device_found->ToString(); |
| return CHANGED_DEVICE; |
| } |
| if (device.active != device_found->active) { |
| return CHANGED_DEVICE; |
| } |
| return OLD_DEVICE; |
| } |
| |
| void CrasAudioHandler::NotifyActiveNodeChanged(bool is_input) { |
| if (is_input) { |
| for (auto& observer : observers_) { |
| observer.OnActiveInputNodeChanged(); |
| } |
| } else { |
| for (auto& observer : observers_) { |
| observer.OnActiveOutputNodeChanged(); |
| } |
| } |
| } |
| |
| bool CrasAudioHandler::GetActiveDeviceFromUserPref(bool is_input, |
| AudioDevice* active_device) { |
| bool found_active_device = false; |
| bool last_active_device_activate_by_user = false; |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (device.is_input != is_input || !device.is_for_simple_usage()) { |
| continue; |
| } |
| |
| bool active = false; |
| bool activate_by_user = false; |
| // If the device entry is not found in prefs, it is likley a new audio |
| // device plugged in after the cros is powered down. We should ignore the |
| // previously saved active device, and select the active device by priority. |
| // crbug.com/622045. |
| if (!audio_pref_handler_->GetDeviceActive(device, &active, |
| &activate_by_user)) { |
| return false; |
| } |
| |
| if (!active) { |
| continue; |
| } |
| |
| if (!found_active_device) { |
| found_active_device = true; |
| *active_device = device; |
| last_active_device_activate_by_user = activate_by_user; |
| continue; |
| } |
| |
| // Choose the best one among multiple active devices from prefs. |
| if (activate_by_user) { |
| if (last_active_device_activate_by_user) { |
| // If there are more than one active devices activated by user in the |
| // prefs, most likely, after the device was shut down, and before it |
| // is rebooted, user has plugged in some previously unplugged audio |
| // devices. For such case, it does not make sense to honor the active |
| // states in the prefs. |
| VLOG(1) << "Found more than one user activated devices in the prefs."; |
| return false; |
| } |
| // Device activated by user has higher priority than the one |
| // is not activated by user. |
| *active_device = device; |
| last_active_device_activate_by_user = true; |
| } else if (!last_active_device_activate_by_user) { |
| // If there are more than one active devices activated by priority in the |
| // prefs, most likely, cras is still enumerating the audio devices |
| // progressively. For such case, it does not make sense to honor the |
| // active states in the prefs. |
| VLOG(1) << "Found more than one active devices by priority in the prefs."; |
| return false; |
| } |
| } |
| |
| if (found_active_device && !active_device->is_for_simple_usage()) { |
| // This is an odd case which is rare but possible to happen during cras |
| // initialization depeneding the audio device enumation process. The only |
| // audio node coming from cras is an internal audio device not visible |
| // to user, such as AudioDeviceType::kPostMixLoopback. |
| return false; |
| } |
| |
| return found_active_device; |
| } |
| |
| void CrasAudioHandler::PauseAllStreams() { |
| if (media_controller_manager_) { |
| media_controller_manager_->SuspendAllSessions(); |
| } |
| } |
| |
| void CrasAudioHandler::HandleNonHotplugNodesChange( |
| bool is_input, |
| const AudioDeviceList& devices, |
| const AudioDeviceList& hotplug_devices, |
| bool has_device_change, |
| bool has_device_removed, |
| bool active_device_removed) { |
| bool has_current_active_node = |
| is_input ? active_input_node_id_ : active_output_node_id_; |
| |
| // No device change, extra NodesChanged signal received. |
| if (!has_device_change && has_current_active_node) { |
| return; |
| } |
| |
| if (!hotplug_devices.empty()) { |
| // Looks like a new chrome session starts. |
| if (features::IsAudioSelectionImprovementEnabled()) { |
| HandleSystemBoots(is_input, devices); |
| } else { |
| SwitchToPreviousActiveDeviceIfAvailable(is_input, devices); |
| } |
| return; |
| } |
| |
| if (has_device_removed) { |
| if (!active_device_removed && has_current_active_node) { |
| // Removed a non-active device, keep the current active device. |
| // Record the decision of system not switching active device. |
| audio_device_metrics_handler_.MaybeRecordSystemSwitchDecisionAndContext( |
| is_input, is_input ? has_alternative_input_ : has_alternative_output_, |
| /*is_switched=*/false, audio_devices_, previous_audio_devices_); |
| |
| // When removing a non-active device, if the preferred device in the new |
| // device set is not the currently active device, do not switch but keep |
| // the current active device. Record metrics for Rule #2. |
| if (features::IsAudioSelectionImprovementEnabled()) { |
| const std::optional<AudioDevice> preferred_device = |
| GetPreferredDeviceIfDeviceSetSeenBefore( |
| is_input, GetSimpleUsageAudioDevices(audio_devices_, is_input)); |
| if (preferred_device.has_value()) { |
| const AudioDevice* current_active_device = GetDeviceFromId( |
| is_input ? active_input_node_id_ : active_output_node_id_); |
| if (current_active_device && |
| current_active_device->stable_device_id != |
| preferred_device->stable_device_id) { |
| audio_device_metrics_handler_.RecordExceptionRulesMet( |
| is_input |
| ? AudioDeviceMetricsHandler::AudioSelectionExceptionRules:: |
| kInputRule2UnplugNonActiveDevice |
| : AudioDeviceMetricsHandler::AudioSelectionExceptionRules:: |
| kOutputRule2UnplugNonActiveDevice); |
| } |
| } |
| } |
| |
| return; |
| } |
| |
| // Unplugged the current active device. |
| if (active_device_removed) { |
| // Pauses active streams when the active output device is |
| // removed. |
| if (!is_input) { |
| PauseAllStreams(); |
| } |
| |
| if (!features::IsAudioSelectionImprovementEnabled()) { |
| SwitchToTopPriorityDevice(devices); |
| return; |
| } |
| |
| CHECK(features::IsAudioSelectionImprovementEnabled()); |
| |
| // If there is only one device left, activate it. |
| if (devices.size() == 1) { |
| SwitchToDevice(devices.front(), /*notify=*/true, |
| DeviceActivateType::kActivateByPriority); |
| return; |
| } |
| |
| // Check if the remaining device set was seen before, activate the |
| // preferred one if so, otherwise, activate the most recently active |
| // device. |
| const std::optional<AudioDevice> preferred_device = |
| GetPreferredDeviceIfDeviceSetSeenBefore( |
| is_input, GetSimpleUsageAudioDevices(audio_devices_, is_input)); |
| if (preferred_device.has_value()) { |
| SwitchToDevice(preferred_device.value(), /*notify=*/true, |
| DeviceActivateType::kActivateByPriority); |
| } else { |
| // Record metrics for Exception Rule #4 that unplugging an active device |
| // and the remaining device set was not seen before. |
| audio_device_metrics_handler_.RecordExceptionRulesMet( |
| is_input ? AudioDeviceMetricsHandler::AudioSelectionExceptionRules:: |
| kInputRule4UnplugDeviceCausesUnseenSet |
| : AudioDeviceMetricsHandler::AudioSelectionExceptionRules:: |
| kOutputRule4UnplugDeviceCausesUnseenSet); |
| |
| if (!ActivateMostRecentActiveDevice(is_input)) { |
| // Fall back to previous approach if no device in the most recently |
| // active device list is currently connected. |
| SwitchToTopPriorityDevice(devices); |
| } |
| } |
| |
| return; |
| } |
| } |
| |
| // Some unexpected error happens on cras side. See crbug.com/586026. |
| // Either cras sent stale nodes to chrome again or cras triggered some |
| // error. Restore the previously selected active. |
| VLOG(1) << "Odd case from cras, the active node is lost unexpectedly."; |
| SwitchToPreviousActiveDeviceIfAvailable(is_input, devices); |
| } |
| |
| void CrasAudioHandler::HandleSystemBoots(bool is_input, |
| const AudioDeviceList& devices) { |
| // If there is only one device, activate it. |
| if (devices.size() == 1) { |
| SwitchToDevice(devices.front(), /*notify=*/true, |
| DeviceActivateType::kActivateByPriority); |
| return; |
| } |
| |
| // If the set of devices was seen before, activate the preferred one. |
| const std::optional<AudioDevice> preferred_device = |
| GetPreferredDeviceIfDeviceSetSeenBefore( |
| is_input, GetSimpleUsageAudioDevices(audio_devices_, is_input)); |
| if (preferred_device.has_value()) { |
| // Activate this device. |
| SwitchToDevice(preferred_device.value(), /*notify=*/true, |
| DeviceActivateType::kActivateByPriority); |
| return; |
| } |
| |
| // Otherwise when the device set was not seen before, if a 3.5mm headphone is |
| // connected, activate it. Do not show notification. |
| for (const AudioDevice& device : devices) { |
| if (device.type == AudioDeviceType::kHeadphone || |
| device.type == AudioDeviceType::kMic) { |
| SwitchToDevice(device, /*notify=*/true, |
| DeviceActivateType::kActivateByPriority); |
| return; |
| } |
| } |
| |
| // Otherwise, activate the most recent activated device and show |
| // notification. |
| if (ActivateMostRecentActiveDevice(is_input)) { |
| should_show_notification_ = true; |
| return; |
| } |
| |
| // Otherwise when no most recent activated device, if there is an internal |
| // device, activate it and show notification. |
| for (const AudioDevice& device : devices) { |
| if (device.type == AudioDeviceType::kInternalSpeaker || |
| device.type == AudioDeviceType::kInternalMic) { |
| SwitchToDevice(device, /*notify=*/true, |
| DeviceActivateType::kActivateByPriority); |
| should_show_notification_ = true; |
| return; |
| } |
| } |
| |
| // Otherwise when no most recent activated device and no internal device (a |
| // brand new chromebox), activate the highest priority and show |
| // notification. |
| // TODO(zhangwenyu): Add metrics for this case to understand how frequent it |
| // happens. |
| SwitchToTopPriorityDevice(devices); |
| should_show_notification_ = true; |
| } |
| |
| bool CrasAudioHandler::ActivateMostRecentActiveDevice(bool is_input) { |
| const base::Value::List& ids = |
| audio_pref_handler_->GetMostRecentActivatedDeviceIdList(is_input); |
| for (int i = ids.size() - 1; i >= 0; i--) { |
| std::optional<uint64_t> device_stable_id = |
| ParseDeviceId(ids[i].GetString()); |
| if (!device_stable_id.has_value()) { |
| continue; |
| } |
| const AudioDevice* device = |
| GetDeviceFromStableDeviceId(is_input, *device_stable_id); |
| if (!device) { |
| continue; |
| } |
| SwitchToDevice(*device, /*notify=*/true, |
| DeviceActivateType::kActivateByPriority); |
| return true; |
| } |
| return false; |
| } |
| |
| bool CrasAudioHandler::ShouldSwitchToHotPlugDevice( |
| const AudioDevice& hotplug_device) const { |
| // Whenever 35mm headphone or mic is hot plugged, always pick it as the active |
| // device. |
| if (hotplug_device.type == AudioDeviceType::kHeadphone || |
| hotplug_device.type == AudioDeviceType::kMic) { |
| return true; |
| } |
| |
| const uint64_t active_node_id = |
| hotplug_device.is_input ? active_input_node_id_ : active_output_node_id_; |
| const AudioDevice* current_active_device = GetDeviceFromId(active_node_id); |
| |
| if (!current_active_device) { |
| return true; |
| } |
| |
| // Use built-in priority if hotplug_device or current_active_device has |
| // kUserPriorityNone. Otherwise the newly plugged devices will never be |
| // selected due to having user_priority = 0. |
| // current_active_device can has kUserPriorityNone, if it is a new device |
| // and it is activated when there is no active device (ex: the first active |
| // device after boot). |
| if ((hotplug_device.user_priority == kUserPriorityNone || |
| current_active_device->user_priority == kUserPriorityNone)) { |
| return LessBuiltInPriority(*current_active_device, hotplug_device); |
| } |
| |
| return LessUserPriority(*current_active_device, hotplug_device); |
| } |
| |
| void CrasAudioHandler::HandleHotPlugDeviceByUserPriority( |
| const AudioDevice& hotplug_device) { |
| // This most likely may happen during the transition period of cras |
| // initialization phase, in which a non-simple-usage node may appear like |
| // a hotplug node. |
| if (!hotplug_device.is_for_simple_usage()) { |
| return; |
| } |
| |
| if (features::IsAudioSelectionImprovementEnabled()) { |
| HandleHotPlugDeviceWithNotification(hotplug_device); |
| return; |
| } |
| |
| if (ShouldSwitchToHotPlugDevice(hotplug_device)) { |
| SwitchToDevice(hotplug_device, true, |
| DeviceActivateType::kActivateByPriority); |
| return; |
| } else { |
| // Record the decision of system not switching active device. |
| audio_device_metrics_handler_.MaybeRecordSystemSwitchDecisionAndContext( |
| hotplug_device.is_input, |
| hotplug_device.is_input ? has_alternative_input_ |
| : has_alternative_output_, |
| /*is_switched=*/false, audio_devices_, previous_audio_devices_); |
| } |
| |
| // Do not active the hotplug device. The hotplug device is not the top |
| // priority device. |
| VLOG(1) << "Hotplug device remains inactive as its previous state:" |
| << hotplug_device.ToString(); |
| } |
| |
| void CrasAudioHandler::SwitchToTopPriorityDevice( |
| const AudioDeviceList& devices) { |
| if (devices.empty()) { |
| return; |
| } |
| |
| // When the audio selection improvement flag is on, no user priority will be |
| // maintained. Use built-in priority rather than user priority. |
| AudioDevice top_device = features::IsAudioSelectionImprovementEnabled() |
| ? std::ranges::max(devices, LessBuiltInPriority) |
| : std::ranges::max(devices, LessUserPriority); |
| if (!top_device.is_for_simple_usage()) { |
| return; |
| } |
| |
| // For the dual camera and dual microphone case, choose microphone |
| // that is consistent to the active camera. |
| if (IsFrontOrRearMic(top_device) && HasDualInternalMic() && IsCameraOn()) { |
| ActivateInternalMicForActiveCamera(); |
| return; |
| } |
| |
| SwitchToDevice(top_device, true, DeviceActivateType::kActivateByPriority); |
| } |
| |
| void CrasAudioHandler::SwitchToPreviousActiveDeviceIfAvailable( |
| bool is_input, |
| const AudioDeviceList& devices) { |
| // With new audio selection mechanism, activate the most recently active |
| // device first. |
| if (features::IsAudioSelectionImprovementEnabled() && |
| ActivateMostRecentActiveDevice(is_input)) { |
| return; |
| } |
| |
| AudioDevice previous_active_device; |
| if (GetActiveDeviceFromUserPref(is_input, &previous_active_device)) { |
| DCHECK(previous_active_device.is_for_simple_usage()); |
| // Switch to previous active device stored in user prefs. |
| SwitchToDevice(previous_active_device, true, |
| DeviceActivateType::kActivateByRestorePreviousState); |
| } else { |
| // No previous active device, switch to the top priority device. |
| SwitchToTopPriorityDevice(devices); |
| } |
| } |
| |
| void CrasAudioHandler::HandleRemoveNotification(bool is_input) { |
| audio_selection_notification_handler_ |
| .RemoveNotificationIfHotpluggedDeviceDisconnected( |
| is_input, GetSimpleUsageAudioDevices(audio_devices_, is_input)); |
| } |
| |
| void CrasAudioHandler::UpdateDevicesAndSwitchActive( |
| const AudioNodeList& nodes) { |
| AudioDeviceList output_devices; |
| AudioDeviceList input_devices; |
| AudioDeviceList hotplug_output_devices; |
| AudioDeviceList hotplug_input_devices; |
| AudioDeviceList removed_output_devices; |
| AudioDeviceList removed_input_devices; |
| bool active_output_removed = false; |
| bool active_input_removed = false; |
| bool output_devices_changed = |
| HasDeviceChange(nodes, false, &hotplug_output_devices, |
| &removed_output_devices, &active_output_removed); |
| bool input_devices_changed = |
| HasDeviceChange(nodes, true, &hotplug_input_devices, |
| &removed_input_devices, &active_input_removed); |
| |
| if (!removed_input_devices.empty()) { |
| // Add grace period in case the USB device is not firmly plugged, making |
| // it disconnected and reconnected in a short period of time. In this case, |
| // the notification should not be removed. |
| remove_notification_timer_for_input_.Start( |
| FROM_HERE, kRemoveNotificationDelay, |
| base::BindOnce(&CrasAudioHandler::HandleRemoveNotification, |
| weak_ptr_factory_.GetWeakPtr(), /*is_input=*/true)); |
| } |
| |
| if (!removed_output_devices.empty()) { |
| remove_notification_timer_for_output_.Start( |
| FROM_HERE, kRemoveNotificationDelay, |
| base::BindOnce(&CrasAudioHandler::HandleRemoveNotification, |
| weak_ptr_factory_.GetWeakPtr(), /*is_input=*/false)); |
| } |
| |
| // Record consecutive devices change metrics. |
| if (input_devices_changed) { |
| audio_device_metrics_handler_.RecordConsecutiveAudioDevicsChangeTimeElapsed( |
| /*is_input=*/true, /*is_device_added=*/!hotplug_input_devices.empty()); |
| } |
| if (output_devices_changed) { |
| audio_device_metrics_handler_.RecordConsecutiveAudioDevicsChangeTimeElapsed( |
| /*is_input=*/false, |
| /*is_device_added=*/!hotplug_output_devices.empty()); |
| } |
| |
| std::vector<AudioDevice> devices; |
| devices.reserve(nodes.size()); |
| for (AudioNode node : nodes) { |
| devices.push_back(ConvertAudioNodeWithModifiedPriority(node)); |
| } |
| |
| // Updates the display_rotation to the internal speaker when it's added. |
| for (AudioDevice device : devices) { |
| DeviceStatus status = CheckDeviceStatus(device); |
| if (status == NEW_DEVICE && |
| device.type == AudioDeviceType::kInternalSpeaker) { |
| CrasAudioClient::Get()->SetDisplayRotation(device.id, display_rotation_); |
| } |
| } |
| |
| // Remove the least recently seen devices if there are too many devices. |
| audio_pref_handler_->DropLeastRecentlySeenDevices(devices, |
| kMaxDeviceStoredInPref); |
| |
| // Before audio_devices_ is cleared, saves all audio device types to |
| // previous_audio_devices_. |
| previous_audio_devices_ = audio_devices_; |
| |
| audio_devices_.clear(); |
| has_alternative_input_ = false; |
| has_alternative_output_ = false; |
| |
| // Local variables to help determine if there are more than one audio devices |
| // currently connected. Note that kInternalMic, kFrontMic and kRearMic are |
| // considered one audio device since they are all displayed as internal mic in |
| // UI and user can not switch between them. |
| bool has_internal_input_device = false; |
| bool has_external_input_device = false; |
| bool has_internal_output_device = false; |
| bool has_external_output_device = false; |
| |
| for (AudioDevice device : devices) { |
| // When audio selection improvement flag is on, ignore non simple usage |
| // devices because users can't see/select them. |
| if (features::IsAudioSelectionImprovementEnabled() && |
| !device.is_for_simple_usage()) { |
| continue; |
| } |
| |
| audio_devices_[device.id] = device; |
| |
| if (device.is_input) { |
| input_devices.push_back(device); |
| |
| // If has_alternative_input_ has already been determined as true, no need |
| // to calculate it again. |
| if (has_alternative_input_) { |
| continue; |
| } |
| |
| bool has_two_external_input_devices = |
| has_external_input_device && device.IsExternalDevice(); |
| bool has_one_internal_and_one_external_input_device = |
| has_internal_input_device && device.IsExternalDevice(); |
| bool has_one_external_and_one_internal_input_device = |
| has_external_input_device && device.IsInternalMic(); |
| if (has_two_external_input_devices || |
| has_one_internal_and_one_external_input_device || |
| has_one_external_and_one_internal_input_device) { |
| has_alternative_input_ = true; |
| } |
| |
| if (device.IsInternalMic()) { |
| has_internal_input_device = true; |
| } else if (device.IsExternalDevice()) { |
| has_external_input_device = true; |
| } |
| |
| } else { |
| output_devices.push_back(device); |
| |
| // If has_alternative_output_ has already been determined as true, no need |
| // to calculate it again. |
| if (has_alternative_output_) { |
| continue; |
| } |
| |
| bool has_two_external_output_devices = |
| has_external_output_device && device.IsExternalDevice(); |
| bool has_one_internal_and_one_external_output_device = |
| has_internal_output_device && device.IsExternalDevice(); |
| bool has_one_external_and_one_internal_output_device = |
| has_external_output_device && device.IsInternalSpeaker(); |
| if (has_two_external_output_devices || |
| has_one_internal_and_one_external_output_device || |
| has_one_external_and_one_internal_output_device) { |
| has_alternative_output_ = true; |
| } |
| |
| if (device.IsInternalSpeaker()) { |
| has_internal_output_device = true; |
| } else if (device.IsExternalDevice()) { |
| has_external_output_device = true; |
| } |
| } |
| } |
| |
| // Handle output device changes. |
| HandleAudioDeviceChange( |
| false, output_devices, hotplug_output_devices, output_devices_changed, |
| !removed_output_devices.empty(), active_output_removed); |
| |
| // Handle input device changes. |
| HandleAudioDeviceChange(true, input_devices, hotplug_input_devices, |
| input_devices_changed, !removed_input_devices.empty(), |
| active_input_removed); |
| |
| // At this moment, system has already made the switching or not switching |
| // decision, set this flag to false. |
| audio_device_metrics_handler_.set_is_chrome_restarts(false); |
| |
| if (features::IsAudioSelectionImprovementEnabled() && |
| should_show_notification_) { |
| const AudioDevice* active_input_device = |
| GetDeviceFromId(active_input_node_id_); |
| const AudioDevice* active_output_device = |
| GetDeviceFromId(active_output_node_id_); |
| audio_selection_notification_handler_.ShowAudioSelectionNotification( |
| hotplug_input_devices, hotplug_output_devices, |
| active_input_device |
| ? std::make_optional(active_input_device->display_name) |
| : std::nullopt, |
| active_output_device |
| ? std::make_optional(active_output_device->display_name) |
| : std::nullopt, |
| base::BindRepeating(&CrasAudioHandler::SwitchToDevice, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindRepeating(&CrasAudioHandler::OpenSettingsAudioPage, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // Reset show notification flag. |
| should_show_notification_ = false; |
| } |
| |
| // content::MediaStreamManager listens to |
| // base::SystemMonitor::DevicesChangedObserver for audio devices, |
| // and updates EnumerateDevices when OnDevicesChanged is called. |
| base::SystemMonitor* monitor = base::SystemMonitor::Get(); |
| // In some unittest, |monitor| might be nullptr. |
| if (!monitor) { |
| return; |
| } |
| monitor->ProcessDevicesChanged( |
| base::SystemMonitor::DeviceType::DEVTYPE_AUDIO); |
| } |
| |
| void CrasAudioHandler::HandleAudioDeviceChange( |
| bool is_input, |
| const AudioDeviceList& devices, |
| const AudioDeviceList& hotplug_devices, |
| bool has_device_change, |
| bool has_device_removed, |
| bool active_device_removed) { |
| uint64_t& active_node_id = |
| is_input ? active_input_node_id_ : active_output_node_id_; |
| |
| if (has_device_change) { |
| // Mark device selected by the system, including when the algorithm |
| // does nothing ultimately. We still treat not switching the device |
| // as a decision of the algorithm. |
| if (is_input) { |
| audio_device_metrics_handler_.set_input_device_selected_by_user(false); |
| } else { |
| audio_device_metrics_handler_.set_output_device_selected_by_user(false); |
| } |
| } |
| |
| // No audio devices found. |
| if (devices.empty()) { |
| VLOG(1) << "No " << (is_input ? "input" : "output") << " devices found"; |
| active_node_id = 0; |
| NotifyActiveNodeChanged(is_input); |
| return; |
| } |
| |
| // If the previous active device is removed from the new node list, |
| // or changed to inactive by cras, reset active_node_id. |
| // See crbug.com/478968. |
| const AudioDevice* active_device = GetDeviceFromId(active_node_id); |
| if (!active_device || !active_device->active) { |
| active_node_id = 0; |
| } |
| |
| if (!active_node_id || hotplug_devices.empty() || |
| hotplug_devices.size() > 1) { |
| HandleNonHotplugNodesChange(is_input, devices, hotplug_devices, |
| has_device_change, has_device_removed, |
| active_device_removed); |
| if (features::IsAudioSelectionImprovementEnabled()) { |
| SyncDevicePrefSetMap(is_input); |
| } |
| } else { |
| // Typical user hotplug case. |
| HandleHotPlugDeviceByUserPriority(hotplug_devices.front()); |
| } |
| } |
| |
| void CrasAudioHandler::HandleGetNodes(std::optional<AudioNodeList> node_list) { |
| if (!node_list.has_value()) { |
| LOG(ERROR) << "Failed to retrieve audio nodes data"; |
| return; |
| } |
| |
| if (node_list->empty()) { |
| return; |
| } |
| |
| UpdateDevicesAndSwitchActive(node_list.value()); |
| |
| // Always set the hfp_mic_sr state on NodesChange event. |
| RefreshHfpMicSrState(); |
| |
| for (auto& observer : observers_) { |
| observer.OnAudioNodesChanged(); |
| } |
| } |
| |
| void CrasAudioHandler::HandleGetNumberOfNonChromeOutputStreams( |
| std::optional<int32_t> new_output_streams_count) { |
| if (!new_output_streams_count.has_value()) { |
| LOG(ERROR) << "Failed to retrieve number of active output streams."; |
| return; |
| } |
| DCHECK_GE(*new_output_streams_count, 0); |
| |
| if (*new_output_streams_count > 0 && |
| num_active_nonchrome_output_streams_ == 0) { |
| for (auto& observer : observers_) { |
| observer.OnNonChromeOutputStarted(); |
| } |
| } else if (*new_output_streams_count == 0 && |
| num_active_nonchrome_output_streams_ > 0) { |
| for (auto& observer : observers_) { |
| observer.OnNonChromeOutputStopped(); |
| } |
| } |
| num_active_nonchrome_output_streams_ = *new_output_streams_count; |
| } |
| |
| void CrasAudioHandler::HandleGetNumActiveOutputStreams( |
| std::optional<int> new_output_streams_count) { |
| if (!new_output_streams_count.has_value()) { |
| LOG(ERROR) << "Failed to retrieve number of active output streams"; |
| return; |
| } |
| |
| DCHECK_GE(*new_output_streams_count, 0); |
| if (*new_output_streams_count > 0 && num_active_output_streams_ == 0) { |
| for (auto& observer : observers_) { |
| observer.OnOutputStarted(); |
| } |
| } else if (*new_output_streams_count == 0 && num_active_output_streams_ > 0) { |
| for (auto& observer : observers_) { |
| observer.OnOutputStopped(); |
| } |
| } |
| num_active_output_streams_ = *new_output_streams_count; |
| } |
| |
| void CrasAudioHandler::AddAdditionalActiveNode(uint64_t node_id, bool notify) { |
| const AudioDevice* device = GetDeviceFromId(node_id); |
| if (!device) { |
| VLOG(1) << "AddActiveInputNode: Cannot find device id=" << "0x" << std::hex |
| << node_id; |
| return; |
| } |
| |
| audio_devices_[node_id].active = true; |
| SetupAdditionalActiveAudioNodeState(node_id); |
| |
| if (device->is_input) { |
| DCHECK(node_id != active_input_node_id_); |
| CrasAudioClient::Get()->AddActiveInputNode(node_id); |
| if (notify) { |
| NotifyActiveNodeChanged(true); |
| } |
| } else { |
| DCHECK(node_id != active_output_node_id_); |
| CrasAudioClient::Get()->AddActiveOutputNode(node_id); |
| if (notify) { |
| NotifyActiveNodeChanged(false); |
| } |
| } |
| } |
| |
| void CrasAudioHandler::RemoveActiveNodeInternal(uint64_t node_id, bool notify) { |
| const AudioDevice* device = GetDeviceFromId(node_id); |
| if (!device) { |
| VLOG(1) << "RemoveActiveInputNode: Cannot find device id=" << "0x" |
| << std::hex << node_id; |
| return; |
| } |
| |
| audio_devices_[node_id].active = false; |
| if (device->is_input) { |
| if (node_id == active_input_node_id_) { |
| active_input_node_id_ = 0; |
| } |
| CrasAudioClient::Get()->RemoveActiveInputNode(node_id); |
| if (notify) { |
| NotifyActiveNodeChanged(true); |
| } |
| } else { |
| if (node_id == active_output_node_id_) { |
| active_output_node_id_ = 0; |
| } |
| CrasAudioClient::Get()->RemoveActiveOutputNode(node_id); |
| if (notify) { |
| NotifyActiveNodeChanged(false); |
| } |
| } |
| } |
| |
| void CrasAudioHandler::UpdateAudioAfterHDMIRediscoverGracePeriod() { |
| VLOG(1) << "HDMI output re-discover grace period ends."; |
| hdmi_rediscovering_ = false; |
| if (!IsOutputMutedForDevice(active_output_node_id_)) { |
| // Unmute the audio output after the HDMI transition period. |
| VLOG(1) << "Unmute output after HDMI rediscovering grace period."; |
| SetOutputMuteInternal(false); |
| } |
| } |
| |
| bool CrasAudioHandler::IsHDMIPrimaryOutputDevice() const { |
| const AudioDevice* device = GetDeviceFromId(active_output_node_id_); |
| return device && device->type == AudioDeviceType::kHdmi; |
| } |
| |
| void CrasAudioHandler::StartHDMIRediscoverGracePeriod() { |
| VLOG(1) << "Start HDMI rediscovering grace period."; |
| hdmi_rediscovering_ = true; |
| hdmi_rediscover_timer_.Stop(); |
| hdmi_rediscover_timer_.Start( |
| FROM_HERE, |
| base::Milliseconds(hdmi_rediscover_grace_period_duration_in_ms_), this, |
| &CrasAudioHandler::UpdateAudioAfterHDMIRediscoverGracePeriod); |
| } |
| |
| void CrasAudioHandler::SetHDMIRediscoverGracePeriodForTesting( |
| int duration_in_ms) { |
| hdmi_rediscover_grace_period_duration_in_ms_ = duration_in_ms; |
| } |
| |
| void CrasAudioHandler::ActivateMicForCamera( |
| media::VideoFacingMode camera_facing) { |
| const AudioDevice* mic = GetMicForCamera(camera_facing); |
| if (!mic || mic->active) { |
| return; |
| } |
| |
| SwitchToDevice(*mic, true, DeviceActivateType::kActivateByCamera); |
| } |
| |
| void CrasAudioHandler::ActivateInternalMicForActiveCamera() { |
| DCHECK(IsCameraOn()); |
| if (HasDualInternalMic()) { |
| media::VideoFacingMode facing = front_camera_on_ |
| ? media::MEDIA_VIDEO_FACING_USER |
| : media::MEDIA_VIDEO_FACING_ENVIRONMENT; |
| ActivateMicForCamera(facing); |
| } |
| } |
| |
| // For the dual microphone case, from user point of view, they only see internal |
| // microphone in UI. Chrome will make the best decision on which one to pick. |
| // If the camera is off, the front microphone should be picked as the default |
| // active microphone. Otherwise, it will switch to the microphone that |
| // matches the active camera, i.e. front microphone for front camera and |
| // rear microphone for rear camera. |
| void CrasAudioHandler::SwitchToFrontOrRearMic() { |
| DCHECK(HasDualInternalMic()); |
| if (IsCameraOn()) { |
| ActivateInternalMicForActiveCamera(); |
| } else { |
| SwitchToDevice(*GetDeviceByType(AudioDeviceType::kFrontMic), true, |
| DeviceActivateType::kActivateByUser); |
| } |
| } |
| |
| const AudioDevice* CrasAudioHandler::GetMicForCamera( |
| media::VideoFacingMode camera_facing) { |
| switch (camera_facing) { |
| case media::MEDIA_VIDEO_FACING_USER: |
| return GetDeviceByType(AudioDeviceType::kFrontMic); |
| case media::MEDIA_VIDEO_FACING_ENVIRONMENT: |
| return GetDeviceByType(AudioDeviceType::kRearMic); |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| bool CrasAudioHandler::HasDualInternalMic() const { |
| bool has_front_mic = false; |
| bool has_rear_mic = false; |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (device.type == AudioDeviceType::kFrontMic) { |
| has_front_mic = true; |
| } else if (device.type == AudioDeviceType::kRearMic) { |
| has_rear_mic = true; |
| } |
| if (has_front_mic && has_rear_mic) { |
| break; |
| } |
| } |
| return has_front_mic && has_rear_mic; |
| } |
| |
| bool CrasAudioHandler::HasActiveInputDeviceForSimpleUsage() const { |
| const AudioDevice* active_input_device = |
| GetDeviceFromId(active_input_node_id_); |
| if (active_input_device) { |
| return active_input_device->is_for_simple_usage(); |
| } |
| return false; |
| } |
| |
| bool CrasAudioHandler::IsFrontOrRearMic(const AudioDevice& device) const { |
| return device.is_input && (device.type == AudioDeviceType::kFrontMic || |
| device.type == AudioDeviceType::kRearMic); |
| } |
| |
| bool CrasAudioHandler::IsCameraOn() const { |
| return front_camera_on_ || rear_camera_on_; |
| } |
| |
| bool CrasAudioHandler::HasExternalDevice(bool is_input) const { |
| for (const auto& item : audio_devices_) { |
| const AudioDevice& device = item.second; |
| if (is_input == device.is_input && device.IsExternalDevice()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void CrasAudioHandler::GetNumberOfInputStreamsWithPermissionInternal() { |
| CrasAudioClient::Get()->GetNumberOfInputStreamsWithPermission(base::BindOnce( |
| &CrasAudioHandler::HandleGetNumberOfInputStreamsWithPermission, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| // static |
| CrasAudioHandler::ClientType CrasAudioHandler::ConvertClientTypeStringToEnum( |
| std::string client_type_str) { |
| if (client_type_str == "CRAS_CLIENT_TYPE_PLUGIN") { |
| return ClientType::VM_PLUGIN; |
| } else if (client_type_str == "CRAS_CLIENT_TYPE_CROSVM") { |
| return ClientType::VM_TERMINA; |
| } else if (client_type_str == "CRAS_CLIENT_TYPE_CHROME") { |
| return ClientType::CHROME; |
| } else if (client_type_str == "CRAS_CLIENT_TYPE_ARC" || |
| client_type_str == "CRAS_CLIENT_TYPE_ARCVM") { |
| return ClientType::ARC; |
| } else if (client_type_str == "CRAS_CLIENT_TYPE_BOREALIS") { |
| return ClientType::VM_BOREALIS; |
| } else { |
| return ClientType::UNKNOWN; |
| } |
| } |
| |
| void CrasAudioHandler::HandleGetNumberOfInputStreamsWithPermission( |
| std::optional<base::flat_map<std::string, uint32_t>> num_input_streams) { |
| if (!num_input_streams.has_value()) { |
| LOG(ERROR) << "Failed to retrieve number of input streams with permission"; |
| return; |
| } |
| number_of_input_streams_with_permission_.clear(); |
| for (const auto& it : *num_input_streams) { |
| CrasAudioHandler::ClientType type = ConvertClientTypeStringToEnum(it.first); |
| if (type != ClientType::UNKNOWN) { |
| number_of_input_streams_with_permission_[type] = it.second; |
| } |
| } |
| } |
| |
| void CrasAudioHandler::GetDefaultOutputBufferSizeInternal() { |
| CrasAudioClient::Get()->GetDefaultOutputBufferSize( |
| base::BindOnce(&CrasAudioHandler::HandleGetDefaultOutputBufferSize, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::HandleGetDefaultOutputBufferSize( |
| std::optional<int> buffer_size) { |
| if (!buffer_size.has_value()) { |
| LOG(ERROR) << "Failed to retrieve output buffer size"; |
| return; |
| } |
| |
| default_output_buffer_size_ = buffer_size.value(); |
| } |
| |
| bool CrasAudioHandler::noise_cancellation_supported() const { |
| return noise_cancellation_supported_; |
| } |
| |
| bool CrasAudioHandler::style_transfer_supported() const { |
| return style_transfer_supported_; |
| } |
| |
| bool CrasAudioHandler::hfp_mic_sr_supported() const { |
| return hfp_mic_sr_supported_; |
| } |
| |
| bool CrasAudioHandler::system_aec_supported() const { |
| DCHECK(main_task_runner_->BelongsToCurrentThread()); |
| return system_aec_supported_; |
| } |
| |
| bool CrasAudioHandler::spatial_audio_supported() const { |
| return spatial_audio_supported_; |
| } |
| |
| // GetSystemAecSupported() is only called in the same thread |
| // as the CrasAudioHandler constructor. We are safe here without |
| // thread check, because unittest may not have the task runner |
| // for the current thread. |
| void CrasAudioHandler::GetSystemAecSupported() { |
| CrasAudioClient::Get()->GetSystemAecSupported( |
| base::BindOnce(&CrasAudioHandler::HandleGetSystemAecSupported, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::HandleGetSystemAecSupported( |
| std::optional<bool> system_aec_supported) { |
| if (!system_aec_supported.has_value()) { |
| LOG(ERROR) << "Failed to retrieve system aec supported"; |
| return; |
| } |
| system_aec_supported_ = system_aec_supported.value(); |
| } |
| |
| int32_t CrasAudioHandler::system_aec_group_id() const { |
| DCHECK(main_task_runner_->BelongsToCurrentThread()); |
| return system_aec_group_id_; |
| } |
| |
| // GetSystemAecGroupId() is only called in the same thread |
| // as the CrasAudioHandler constructor. We are safe here without |
| // thread check, because unittest may not have the task runner |
| // for the current thread. |
| void CrasAudioHandler::GetSystemAecGroupId() { |
| CrasAudioClient::Get()->GetSystemAecGroupId( |
| base::BindOnce(&CrasAudioHandler::HandleGetSystemAecGroupId, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::HandleGetSystemAecGroupId( |
| std::optional<int32_t> system_aec_group_id) { |
| if (!system_aec_group_id.has_value()) { |
| // If the group Id is not available, set the ID to reflect that. |
| system_aec_group_id_ = kSystemAecGroupIdNotAvailable; |
| return; |
| } |
| system_aec_group_id_ = system_aec_group_id.value(); |
| } |
| |
| bool CrasAudioHandler::system_ns_supported() const { |
| DCHECK(main_task_runner_->BelongsToCurrentThread()); |
| return system_ns_supported_; |
| } |
| |
| // GetSystemNsSupported() is only called in the same thread |
| // as the CrasAudioHandler constructor. We are safe here without |
| // thread check, because unittest may not have the task runner |
| // for the current thread. |
| void CrasAudioHandler::GetSystemNsSupported() { |
| CrasAudioClient::Get()->GetSystemNsSupported( |
| base::BindOnce(&CrasAudioHandler::HandleGetSystemNsSupported, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::HandleGetSystemNsSupported( |
| std::optional<bool> system_ns_supported) { |
| if (!system_ns_supported.has_value()) { |
| LOG(ERROR) << "Failed to retrieve system ns supported"; |
| return; |
| } |
| system_ns_supported_ = system_ns_supported.value(); |
| } |
| |
| bool CrasAudioHandler::system_agc_supported() const { |
| DCHECK(main_task_runner_->BelongsToCurrentThread()); |
| return system_agc_supported_; |
| } |
| |
| int32_t CrasAudioHandler::num_stream_ignore_ui_gains() const { |
| return num_stream_ignore_ui_gains_; |
| } |
| |
| // GetSystemAgcSupported() is only called in the same thread |
| // as the CrasAudioHandler constructor. We are safe here without |
| // thread check, because unittest may not have the task runner |
| // for the current thread. |
| void CrasAudioHandler::GetSystemAgcSupported() { |
| CrasAudioClient::Get()->GetSystemAgcSupported( |
| base::BindOnce(&CrasAudioHandler::HandleGetSystemAgcSupported, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::HandleGetSystemAgcSupported( |
| std::optional<bool> system_agc_supported) { |
| if (!system_agc_supported.has_value()) { |
| LOG(ERROR) << "Failed to retrieve system agc supported"; |
| return; |
| } |
| system_agc_supported_ = system_agc_supported.value(); |
| } |
| |
| void CrasAudioHandler::GetNumStreamIgnoreUiGains() { |
| CrasAudioClient::Get()->GetNumStreamIgnoreUiGains( |
| base::BindOnce(&CrasAudioHandler::HandleGetNumStreamIgnoreUiGains, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::HandleGetNumStreamIgnoreUiGains( |
| std::optional<int32_t> new_stream_ignore_ui_gains_count) { |
| if (!new_stream_ignore_ui_gains_count.has_value()) { |
| LOG(ERROR) << "Failed to retrieve number of ignore ui gains streams."; |
| return; |
| } |
| DCHECK_GE(*new_stream_ignore_ui_gains_count, 0); |
| |
| if (*new_stream_ignore_ui_gains_count != num_stream_ignore_ui_gains_) { |
| for (auto& observer : observers_) { |
| observer.OnNumStreamIgnoreUiGainsChanged( |
| *new_stream_ignore_ui_gains_count); |
| } |
| } |
| |
| num_stream_ignore_ui_gains_ = *new_stream_ignore_ui_gains_count; |
| } |
| |
| void CrasAudioHandler::GetNumberOfArcStreams() { |
| CrasAudioClient::Get()->GetNumberOfArcStreams( |
| base::BindOnce(&CrasAudioHandler::HandleGetNumberOfArcStreams, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void CrasAudioHandler::HandleGetNumberOfArcStreams( |
| std::optional<int32_t> new_num_arc_streams) { |
| if (!new_num_arc_streams.has_value()) { |
| LOG(ERROR) << "Failed to retrieve number of active ARC streams."; |
| return; |
| } |
| DCHECK_GE(*new_num_arc_streams, 0); |
| |
| if (*new_num_arc_streams != num_arc_streams_) { |
| for (auto& observer : observers_) { |
| observer.OnNumberOfArcStreamsChanged(*new_num_arc_streams); |
| } |
| } |
| |
| num_arc_streams_ = *new_num_arc_streams; |
| } |
| |
| const std::optional<AudioDevice> |
| CrasAudioHandler::GetPreferredDeviceIfDeviceSetSeenBefore( |
| bool is_input, |
| const AudioDeviceList& devices) const { |
| std::optional<uint64_t> id = |
| audio_pref_handler_->GetPreferredDeviceFromPreferenceSet(is_input, |
| devices); |
| |
| if (!id.has_value()) { |
| return std::nullopt; |
| } |
| |
| const AudioDevice* device = GetDeviceFromStableDeviceId(is_input, id.value()); |
| if (!device) { |
| return std::nullopt; |
| } |
| |
| return *device; |
| } |
| |
| void CrasAudioHandler::SyncDevicePrefSetMap(bool is_input) { |
| const uint64_t& active_node_id = |
| is_input ? active_input_node_id_ : active_output_node_id_; |
| const AudioDevice* active_device = GetDeviceFromId(active_node_id); |
| if (!active_device) { |
| VLOG(1) << "SyncDevicePrefSetMap: No active device found."; |
| // Early return since no active_device is available to sync. |
| return; |
| } |
| |
| audio_pref_handler_->UpdateDevicePreferenceSet( |
| GetSimpleUsageAudioDevices(audio_devices_, is_input), *active_device); |
| } |
| |
| void CrasAudioHandler::HandleHotPlugDeviceWithNotification( |
| const AudioDevice& hotplug_device) { |
| // Whenever a device with privilege is hot plugged, always pick it as the |
| // active device. Make sure has_privilege is called only when the |
| // kAudioSelectionImprovement flag is on. |
| CHECK(features::IsAudioSelectionImprovementEnabled()); |
| if (hotplug_device.has_privilege()) { |
| SwitchToDevice(hotplug_device, /*notify=*/true, |
| DeviceActivateType::kActivateByPriority); |
| |
| // Record audio selection exception rule #1. |
| audio_device_metrics_handler_.RecordExceptionRulesMet( |
| hotplug_device.is_input |
| ? AudioDeviceMetricsHandler::AudioSelectionExceptionRules:: |
| kInputRule1HotPlugPrivilegedDevice |
| : AudioDeviceMetricsHandler::AudioSelectionExceptionRules:: |
| kOutputRule1HotPlugPrivilegedDevice); |
| return; |
| } |
| |
| // Check if current set of audio devices was seen before. |
| const std::optional<AudioDevice> preferred_device = |
| GetPreferredDeviceIfDeviceSetSeenBefore( |
| hotplug_device.is_input, |
| GetSimpleUsageAudioDevices(audio_devices_, |
| /*is_input=*/hotplug_device.is_input)); |
| // If the device set was seen and the preferred device is the hotplug device, |
| // activate it. This is the only case that the hot plug device will be |
| // activated. |
| if (preferred_device.has_value() && |
| preferred_device->stable_device_id == hotplug_device.stable_device_id) { |
| SwitchToDevice(hotplug_device, /*notify=*/true, |
| DeviceActivateType::kActivateByPriority); |
| return; |
| } |
| |
| // Otherwise sync the device preference set map. |
| SyncDevicePrefSetMap(hotplug_device.is_input); |
| |
| if (!preferred_device.has_value()) { |
| // The device set was not seen. Show notification to let user make decision. |
| // Early return here since below metrics are not supposed to be recorded. |
| should_show_notification_ = true; |
| return; |
| } |
| |
| // Only record system switch decision metrics when the set of devices is seen |
| // but the system decides to not switch to it. Do not record if this set of |
| // device is not seen and notification is shown. Because the system |
| // technically does not make any decisions but let users make the decision. |
| audio_device_metrics_handler_.MaybeRecordSystemSwitchDecisionAndContext( |
| /*is_input=*/hotplug_device.is_input, |
| /*has_alternative_device=*/hotplug_device.is_input |
| ? has_alternative_input_ |
| : has_alternative_output_, |
| /*is_switched=*/false, audio_devices_, previous_audio_devices_); |
| |
| // If the preferred device is not the hot plug device and also not the |
| // currently activated device, do not switch but keep the currently active |
| // device activated, according to audio selection exception rule #3, detailed |
| // in go/audio-io. |
| const AudioDevice* current_active_device = GetDeviceFromId( |
| hotplug_device.is_input ? active_input_node_id_ : active_output_node_id_); |
| if (current_active_device && current_active_device->stable_device_id != |
| preferred_device->stable_device_id) { |
| audio_device_metrics_handler_.RecordExceptionRulesMet( |
| hotplug_device.is_input |
| ? AudioDeviceMetricsHandler::AudioSelectionExceptionRules:: |
| kInputRule3HotPlugUnpreferredDevice |
| : AudioDeviceMetricsHandler::AudioSelectionExceptionRules:: |
| kOutputRule3HotPlugUnpreferredDevice); |
| } |
| } |
| |
| void CrasAudioHandler::OpenSettingsAudioPage() { |
| if (delegate_) { |
| delegate_->OpenSettingsAudioPage(); |
| } |
| } |
| |
| ScopedCrasAudioHandlerForTesting::ScopedCrasAudioHandlerForTesting() { |
| CHECK(!CrasAudioClient::Get()) |
| << "ScopedCrasAudioHandlerForTesting expects that there is no " |
| "CrasAudioClient running at its constructor."; |
| |
| fake_cras_audio_client_ = std::make_unique<FakeCrasAudioClient>(); |
| |
| CrasAudioHandler::InitializeForTesting(); |
| } |
| |
| ScopedCrasAudioHandlerForTesting::~ScopedCrasAudioHandlerForTesting() { |
| CrasAudioHandler::Shutdown(); |
| } |
| |
| CrasAudioHandler& ScopedCrasAudioHandlerForTesting::Get() { |
| return *CrasAudioHandler::Get(); |
| } |
| |
| int32_t CrasAudioHandler::NumberOfNonChromeOutputStreams() const { |
| return num_active_nonchrome_output_streams_; |
| } |
| |
| int32_t CrasAudioHandler::NumberOfChromeOutputStreams() const { |
| return num_active_output_streams_; |
| } |
| |
| int32_t CrasAudioHandler::NumberOfArcStreams() const { |
| return num_arc_streams_; |
| } |
| |
| void CrasAudioHandler::SetNumberOfArcStreamsForTesting(int32_t num) { |
| num_arc_streams_ = num; |
| } |
| |
| } // namespace ash |