chromium/chromeos/ash/components/audio/cros_audio_config_impl.cc

// Copyright 2022 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/cros_audio_config_impl.h"

#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chromeos/ash/components/audio/audio_device.h"
#include "chromeos/ash/components/audio/cras_audio_handler.h"

namespace ash::audio_config {

namespace {

constexpr int kDefaultInternalMicId = 0;
constexpr base::TimeDelta kMetricsDelayTimerInterval = base::Seconds(2);

// Histogram names.
constexpr char kOutputMuteChangeHistogramName[] =
    "ChromeOS.CrosAudioConfig.OutputMuteStateChange";
constexpr char kInputMuteChangeHistogramName[] =
    "ChromeOS.CrosAudioConfig.InputMuteStateChange";
constexpr char kNoiseCancellationEnabledHistogramName[] =
    "ChromeOS.CrosAudioConfig.NoiseCancellationEnabled";
constexpr char kOutputVolumeChangeHistogramName[] =
    "ChromeOS.CrosAudioConfig.OutputVolumeSetTo";
constexpr char kInputGainChangeHistogramName[] =
    "ChromeOS.CrosAudioConfig.InputGainSetTo";
constexpr char kAudioDeviceChangeHistogramName[] =
    "ChromeOS.CrosAudioConfig.DeviceChange";
constexpr char kOutputDeviceTypeHistogramName[] =
    "ChromeOS.CrosAudioConfig.OutputDeviceTypeChangedTo";
constexpr char kInputDeviceTypeHistogramName[] =
    "ChromeOS.CrosAudioConfig.InputDeviceTypeChangedTo";

// Creates an inactive input device with default property configuration.
AudioDevice CreateStubInternalMic() {
  AudioDevice internal_mic;
  internal_mic.id = kDefaultInternalMicId;
  internal_mic.is_input = true;
  internal_mic.stable_device_id_version = 2;
  internal_mic.type = AudioDeviceType::kInternalMic;
  internal_mic.active = false;
  return internal_mic;
}

// Updates active and id properties on stub `internal_mic` based on provided
// front or rear device.
void UpdateInternalMicBasedOnAudioDevice(AudioDevice& internal_mic,
                                         const AudioDevice& device) {
  DCHECK(device.is_input && (device.type == AudioDeviceType::kFrontMic ||
                             device.type == AudioDeviceType::kRearMic));

  // Update internal_mic id if it has not been set or if the incoming device is
  // active.
  if (internal_mic.id == kDefaultInternalMicId || device.active) {
    internal_mic.id = device.id;
  }

  // Update active
  if (device.active) {
    internal_mic.active = true;
  }
}

// Determines the correct `mojom::AudioEffectState` for an audio device
// depending on if:
//   - the overall device(chromebook) supports noise cancellation
//   - the provided audio device supports noise cancellation
//   - if noise cancellation is enabled in CrasAudioHandler
mojom::AudioEffectState GetNoiseCancellationState(const AudioDevice& device) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();

  if (!audio_handler->IsNoiseCancellationSupportedForDevice(device.id)) {
    return mojom::AudioEffectState::kNotSupported;
  }

  // Device supports noise cancellation, get current device wide preference
  // state from `CrasAudioHandler`.
  return audio_handler->GetNoiseCancellationState()
             ? mojom::AudioEffectState::kEnabled
             : mojom::AudioEffectState::kNotEnabled;
}

// Determines the correct `mojom::AudioEffectState` for an audio device
mojom::AudioEffectState GetStyleTransferState(const AudioDevice& device) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();

  if (!audio_handler->IsStyleTransferSupportedForDevice(device.id)) {
    return mojom::AudioEffectState::kNotSupported;
  }

  // Device supports style transfer, get current device wide preference
  // state from `CrasAudioHandler`.
  return audio_handler->GetStyleTransferState()
             ? mojom::AudioEffectState::kEnabled
             : mojom::AudioEffectState::kNotEnabled;
}

// Determines the correct `mojom::AudioEffectState` for an audio device
mojom::AudioEffectState GetForceRespectUiGainsState(const AudioDevice& device) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  CHECK(audio_handler);

  // Get current device wide preference state from `CrasAudioHandler`.
  return audio_handler->GetForceRespectUiGainsState()
             ? mojom::AudioEffectState::kEnabled
             : mojom::AudioEffectState::kNotEnabled;
}

void RecordMuteStateChanged(const char* histogram_name, bool muted) {
  base::UmaHistogramEnumeration(
      histogram_name,
      muted ? AudioMuteButtonAction::kMuted : AudioMuteButtonAction::kUnmuted);
}

// Determines the correct `mojom::AudioEffectState` for an audio device
mojom::AudioEffectState GetHfpMicSrState(const AudioDevice& device) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();

  if (!audio_handler->IsHfpMicSrSupportedForDevice(device.id)) {
    return mojom::AudioEffectState::kNotSupported;
  }

  // Device supports hfp mic sr, get current device wide preference
  // state from `CrasAudioHandler`.
  return audio_handler->GetHfpMicSrState()
             ? mojom::AudioEffectState::kEnabled
             : mojom::AudioEffectState::kNotEnabled;
}

}  // namespace

mojom::AudioDeviceType ComputeDeviceType(const AudioDeviceType& device_type) {
  switch (device_type) {
    case AudioDeviceType::kHeadphone:
      return mojom::AudioDeviceType::kHeadphone;
    case AudioDeviceType::kMic:
      return mojom::AudioDeviceType::kMic;
    case AudioDeviceType::kUsb:
      return mojom::AudioDeviceType::kUsb;
    case AudioDeviceType::kBluetooth:
      return mojom::AudioDeviceType::kBluetooth;
    case AudioDeviceType::kBluetoothNbMic:
      return mojom::AudioDeviceType::kBluetoothNbMic;
    case AudioDeviceType::kHdmi:
      return mojom::AudioDeviceType::kHdmi;
    case AudioDeviceType::kInternalSpeaker:
      return mojom::AudioDeviceType::kInternalSpeaker;
    case AudioDeviceType::kInternalMic:
      return mojom::AudioDeviceType::kInternalMic;
    case AudioDeviceType::kFrontMic:
      return mojom::AudioDeviceType::kFrontMic;
    case AudioDeviceType::kRearMic:
      return mojom::AudioDeviceType::kRearMic;
    case AudioDeviceType::kKeyboardMic:
      return mojom::AudioDeviceType::kKeyboardMic;
    case AudioDeviceType::kHotword:
      return mojom::AudioDeviceType::kHotword;
    case AudioDeviceType::kPostDspLoopback:
      return mojom::AudioDeviceType::kPostDspLoopback;
    case AudioDeviceType::kPostMixLoopback:
      return mojom::AudioDeviceType::kPostMixLoopback;
    case AudioDeviceType::kLineout:
      return mojom::AudioDeviceType::kLineout;
    case AudioDeviceType::kAlsaLoopback:
      return mojom::AudioDeviceType::kAlsaLoopback;
    case AudioDeviceType::kOther:
      return mojom::AudioDeviceType::kOther;
  };
}

mojom::AudioDevicePtr GenerateMojoAudioDevice(const AudioDevice& device) {
  mojom::AudioDevicePtr mojo_device = mojom::AudioDevice::New();
  mojo_device->id = device.id;
  mojo_device->display_name = device.display_name;
  mojo_device->is_active = device.active;
  mojo_device->device_type = ComputeDeviceType(device.type);
  mojo_device->noise_cancellation_state = GetNoiseCancellationState(device);
  mojo_device->style_transfer_state = GetStyleTransferState(device);
  mojo_device->force_respect_ui_gains_state =
      GetForceRespectUiGainsState(device);
  mojo_device->hfp_mic_sr_state = GetHfpMicSrState(device);
  return mojo_device;
}

CrosAudioConfigImpl::CrosAudioConfigImpl()
    : output_volume_metric_delay_timer_(
          FROM_HERE,
          kMetricsDelayTimerInterval,
          this,
          &CrosAudioConfigImpl::RecordOutputVolume),
      input_gain_metric_delay_timer_(FROM_HERE,
                                     kMetricsDelayTimerInterval,
                                     this,
                                     &CrosAudioConfigImpl::RecordInputGain) {
  CrasAudioHandler::Get()->AddAudioObserver(this);
}

CrosAudioConfigImpl::~CrosAudioConfigImpl() {
  if (CrasAudioHandler::Get())
    CrasAudioHandler::Get()->RemoveAudioObserver(this);
}

uint8_t CrosAudioConfigImpl::GetOutputVolumePercent() const {
  return CrasAudioHandler::Get()->GetOutputVolumePercent();
}

uint8_t CrosAudioConfigImpl::GetInputGainPercent() const {
  return CrasAudioHandler::Get()->GetInputGainPercent();
}

mojom::MuteState CrosAudioConfigImpl::GetOutputMuteState() const {
  // TODO(crbug.com/1092970): Add kMutedExternally.
  if (CrasAudioHandler::Get()->IsOutputMutedByPolicy())
    return mojom::MuteState::kMutedByPolicy;

  if (CrasAudioHandler::Get()->IsOutputMuted())
    return mojom::MuteState::kMutedByUser;

  return mojom::MuteState::kNotMuted;
}

void CrosAudioConfigImpl::GetAudioDevices(
    std::vector<mojom::AudioDevicePtr>* output_devices_out,
    std::vector<mojom::AudioDevicePtr>* input_devices_out) const {
  DCHECK(output_devices_out);
  DCHECK(input_devices_out);
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  AudioDeviceList audio_devices_list;
  audio_handler->GetAudioDevices(&audio_devices_list);

  // For device that has dual internal mics, a new AudioDevice will be created
  // to show only one slider for both the internal mics, and the new AudioDevice
  // has a new id that doesn't match either the first or active internal mic.
  bool has_dual_internal_mic = audio_handler->HasDualInternalMic();
  AudioDevice internal_mic = CreateStubInternalMic();

  for (const auto& device : audio_devices_list) {
    if (!device.is_for_simple_usage()) {
      continue;
    }

    // If dual mics is enabled and device is front or rear mic then use device
    // to set common properties on stub internal_mic and skip
    // adding to list of input devices.
    if (has_dual_internal_mic && audio_handler->IsFrontOrRearMic(device)) {
      UpdateInternalMicBasedOnAudioDevice(internal_mic, device);
      continue;
    }

    if (device.is_input) {
      input_devices_out->push_back(GenerateMojoAudioDevice(device));
    } else {
      output_devices_out->push_back(GenerateMojoAudioDevice(device));
    }
  }

  // Add stub internal mic in place of front and rear mic devices.
  if (has_dual_internal_mic) {
    DCHECK(internal_mic.id != kDefaultInternalMicId);
    input_devices_out->push_back(GenerateMojoAudioDevice(internal_mic));
  }
}

mojom::MuteState CrosAudioConfigImpl::GetInputMuteState() const {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  if (audio_handler->input_muted_by_microphone_mute_switch() &&
      audio_handler->IsInputMuted()) {
    return mojom::MuteState::kMutedExternally;
  }

  if (audio_handler->IsInputMuted()) {
    return mojom::MuteState::kMutedByUser;
  }

  return mojom::MuteState::kNotMuted;
}

void CrosAudioConfigImpl::SetOutputMuted(bool muted) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  if (audio_handler->IsOutputMutedByPolicy()) {
    return;
  }

  audio_handler->SetOutputMute(
      muted, CrasAudioHandler::AudioSettingsChangeSource::kOsSettings);
  RecordMuteStateChanged(kOutputMuteChangeHistogramName, muted);
}

void CrosAudioConfigImpl::SetOutputVolumePercent(int8_t volume) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  audio_handler->SetOutputVolumePercent(volume);

  // If the volume is above certain level and it's muted, it should be unmuted.
  if (audio_handler->IsOutputMuted() &&
      volume > audio_handler->GetOutputDefaultVolumeMuteThreshold()) {
    audio_handler->SetOutputMute(false);
  }

  last_set_output_volume_ = volume;
  // Start or reset timer for recording to metrics.
  output_volume_metric_delay_timer_.Reset();
}

void CrosAudioConfigImpl::SetInputGainPercent(uint8_t gain) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  audio_handler->SetInputGainPercent(gain);

  // Unmute if muted.
  if (audio_handler->IsInputMuted()) {
    audio_handler->SetInputMute(
        false, CrasAudioHandler::InputMuteChangeMethod::kOther);
  }

  last_set_input_gain_ = gain;
  // Start or reset timer for recording to metrics.
  input_gain_metric_delay_timer_.Reset();
}

void CrosAudioConfigImpl::SetActiveDevice(uint64_t device_id) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  const AudioDevice* next_active_device =
      audio_handler->GetDeviceFromId(device_id);

  if (!next_active_device) {
    LOG(ERROR) << "SetActiveDevice: Cannot find device id="
               << "0x" << std::hex << device_id;
    return;
  }

  // When device has dual mics the `GetAudioDevices` represents front and rear
  // mic as a single device. To set active internal mic correctly
  // `SwitchToFrontOrRearMic` needs to be called.
  if (audio_handler->HasDualInternalMic() &&
      audio_handler->IsFrontOrRearMic(*next_active_device)) {
    audio_handler->SwitchToFrontOrRearMic();
  } else {
    audio_handler->SwitchToDevice(*next_active_device, /*notify=*/true,
                                  DeviceActivateType::kActivateByUser);
  }

  // Record if it was an output or input device that changed.
  base::UmaHistogramEnumeration(kAudioDeviceChangeHistogramName,
                                next_active_device->is_input
                                    ? AudioDeviceChange::kInputDevice
                                    : AudioDeviceChange::kOutputDevice);
  // Record the type of audio device changed.
  base::UmaHistogramEnumeration(next_active_device->is_input
                                    ? kInputDeviceTypeHistogramName
                                    : kOutputDeviceTypeHistogramName,
                                next_active_device->type);
}

void CrosAudioConfigImpl::SetInputMuted(bool muted) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  if (audio_handler->input_muted_by_microphone_mute_switch()) {
    return;
  }

  audio_handler->SetMuteForDevice(
      audio_handler->GetPrimaryActiveInputNode(), muted,
      CrasAudioHandler::AudioSettingsChangeSource::kOsSettings);
  RecordMuteStateChanged(kInputMuteChangeHistogramName, muted);
}

void CrosAudioConfigImpl::SetNoiseCancellationEnabled(bool enabled) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();

  if (!audio_handler->IsNoiseCancellationSupportedForDevice(
          audio_handler->GetPrimaryActiveInputNode())) {
    LOG(ERROR) << "SetNoiseCancellationEnabled: Noise cancellation is not "
                  "supported by active input node.";
    return;
  }

  audio_handler->SetNoiseCancellationState(
      enabled, CrasAudioHandler::AudioSettingsChangeSource::kOsSettings);
  base::UmaHistogramBoolean(kNoiseCancellationEnabledHistogramName, enabled);
}

void CrosAudioConfigImpl::SetStyleTransferEnabled(bool enabled) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();

  if (!audio_handler->IsStyleTransferSupportedForDevice(
          audio_handler->GetPrimaryActiveInputNode())) {
    LOG(ERROR) << "SetStyleTransferEnabled: Style transfer is not "
                  "supported by active input node.";
    return;
  }

  audio_handler->SetStyleTransferState(enabled);
}

void CrosAudioConfigImpl::SetForceRespectUiGainsEnabled(bool enabled) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  CHECK(audio_handler);

  audio_handler->SetForceRespectUiGainsState(enabled);
}

void CrosAudioConfigImpl::SetHfpMicSrEnabled(bool enabled) {
  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();

  if (!audio_handler->IsHfpMicSrSupportedForDevice(
          audio_handler->GetPrimaryActiveInputNode())) {
    LOG(ERROR) << "SetHfpMicSrEnabled: hfp mic sr is not "
                  "supported by active input node.";
    return;
  }

  audio_handler->SetHfpMicSrState(
      enabled, CrasAudioHandler::AudioSettingsChangeSource::kOsSettings);
}

void CrosAudioConfigImpl::RecordOutputVolume() {
  base::UmaHistogramExactLinear(kOutputVolumeChangeHistogramName,
                                last_set_output_volume_,
                                /*exclusive_max=*/101);
  base::UmaHistogramEnumeration(
      CrasAudioHandler::kOutputVolumeChangedSourceHistogramName,
      CrasAudioHandler::AudioSettingsChangeSource::kOsSettings);
}

void CrosAudioConfigImpl::RecordInputGain() {
  base::UmaHistogramExactLinear(kInputGainChangeHistogramName,
                                last_set_input_gain_,
                                /*exclusive_max=*/101);
  base::UmaHistogramEnumeration(
      CrasAudioHandler::kInputGainChangedSourceHistogramName,
      CrasAudioHandler::AudioSettingsChangeSource::kOsSettings);

  CrasAudioHandler* audio_handler = CrasAudioHandler::Get();
  CHECK(audio_handler);
  if (!audio_handler->GetForceRespectUiGainsState()) {
    base::UmaHistogramEnumeration(
        CrasAudioHandler::kInputGainChangedHistogramName,
        CrasAudioHandler::AudioSettingsChangeSource::kOsSettings);
  }
}

void CrosAudioConfigImpl::OnOutputNodeVolumeChanged(uint64_t node_id,
                                                    int volume) {
  NotifyObserversAudioSystemPropertiesChanged();
}

void CrosAudioConfigImpl::OnInputNodeGainChanged(uint64_t node_id, int gain) {
  NotifyObserversAudioSystemPropertiesChanged();
}

void CrosAudioConfigImpl::OnOutputMuteChanged(bool mute_on) {
  NotifyObserversAudioSystemPropertiesChanged();
}

void CrosAudioConfigImpl::OnAudioNodesChanged() {
  NotifyObserversAudioSystemPropertiesChanged();
}

void CrosAudioConfigImpl::OnActiveOutputNodeChanged() {
  NotifyObserversAudioSystemPropertiesChanged();
}

void CrosAudioConfigImpl::OnActiveInputNodeChanged() {
  NotifyObserversAudioSystemPropertiesChanged();
}

void CrosAudioConfigImpl::OnInputMuteChanged(
    bool mute_on,
    CrasAudioHandler::InputMuteChangeMethod method) {
  NotifyObserversAudioSystemPropertiesChanged();
}

void CrosAudioConfigImpl::OnInputMutedByMicrophoneMuteSwitchChanged(
    bool muted) {
  NotifyObserversAudioSystemPropertiesChanged();
}

void CrosAudioConfigImpl::OnNoiseCancellationStateChanged() {
  NotifyObserversAudioSystemPropertiesChanged();
}

void CrosAudioConfigImpl::OnStyleTransferStateChanged() {
  NotifyObserversAudioSystemPropertiesChanged();
}

void CrosAudioConfigImpl::OnForceRespectUiGainsStateChanged() {
  NotifyObserversAudioSystemPropertiesChanged();
}

void CrosAudioConfigImpl::OnHfpMicSrStateChanged() {
  NotifyObserversAudioSystemPropertiesChanged();
}

}  // namespace ash::audio_config