chromium/chromecast/media/cma/backend/alsa/alsa_volume_control.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromecast/media/cma/backend/alsa/alsa_volume_control.h"

#include <algorithm>
#include <utility>

#include "base/check.h"
#include "base/command_line.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/strings/string_split.h"
#include "base/task/current_thread.h"
#include "chromecast/base/chromecast_switches.h"
#include "chromecast/base/metrics/cast_metrics_helper.h"
#include "chromecast/media/cma/backend/alsa/scoped_alsa_mixer.h"
#include "media/base/media_switches.h"

#define ALSA_ASSERT(func, ...)                                        \
  do {                                                                \
    int err = alsa_->func(__VA_ARGS__);                               \
    LOG_ASSERT(err >= 0) << #func " error: " << alsa_->StrError(err); \
  } while (0)

namespace chromecast {
namespace media {

namespace {

const char kAlsaDefaultDeviceName[] = "default";
const char kAlsaDefaultVolumeElementName[] = "Master";
const char kAlsaMuteMixerElementName[] = "Mute";

constexpr base::TimeDelta kPowerSaveCheckTime = base::Minutes(5);

}  // namespace

// static
std::string AlsaVolumeControl::GetVolumeElementName() {
  std::string mixer_element_name =
      base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kAlsaVolumeElementName);
  if (mixer_element_name.empty()) {
    mixer_element_name = kAlsaDefaultVolumeElementName;
  }
  return mixer_element_name;
}

// static
std::string AlsaVolumeControl::GetVolumeDeviceName() {
  auto* command_line = base::CommandLine::ForCurrentProcess();
  std::string mixer_device_name =
      command_line->GetSwitchValueASCII(switches::kAlsaVolumeDeviceName);
  if (!mixer_device_name.empty()) {
    return mixer_device_name;
  }

  // If the output device was overridden, then the mixer should default to
  // that device.
  mixer_device_name =
      command_line->GetSwitchValueASCII(switches::kAlsaOutputDevice);
  if (!mixer_device_name.empty()) {
    return mixer_device_name;
  }
  return kAlsaDefaultDeviceName;
}

// Mixers that are implemented with ALSA's softvol plugin don't have mute
// switches available. This function allows ALSA-based AvSettings to fall back
// on another mixer which solely implements mute for the system.
// static
std::string AlsaVolumeControl::GetMuteElementName(
    ::media::AlsaWrapper* alsa,
    const std::string& mixer_device_name,
    const std::string& mixer_element_name,
    const std::string& mute_device_name) {
  DCHECK(alsa);
  std::string mute_element_name =
      base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kAlsaMuteElementName);
  if (!mute_element_name.empty()) {
    return mute_element_name;
  }

  ScopedAlsaMixer mixer(alsa, mixer_device_name, mixer_element_name);
  if (!mixer.element) {
    LOG(WARNING) << "The default ALSA mixer element does not exist.";
    return mixer_element_name;
  }
  if (alsa->MixerSelemHasPlaybackSwitch(mixer.element)) {
    return mixer_element_name;
  }

  ScopedAlsaMixer mute(alsa, mute_device_name, kAlsaMuteMixerElementName);
  if (!mute.element) {
    LOG(WARNING) << "The default ALSA mixer does not have a playback switch "
                    "and a fallback mute element was not found, "
                    "mute will not work.";
    return mixer_element_name;
  }
  if (alsa->MixerSelemHasPlaybackSwitch(mute.element)) {
    return kAlsaMuteMixerElementName;
  }

  LOG(WARNING) << "The default ALSA mixer does not have a playback switch "
                  "and the fallback mute element does not have a playback "
                  "switch, mute will not work.";
  return mixer_element_name;
}

// static
std::string AlsaVolumeControl::GetMuteDeviceName() {
  std::string mute_device_name =
      base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kAlsaMuteDeviceName);
  if (!mute_device_name.empty()) {
    return mute_device_name;
  }

  // If the mute mixer device was not specified directly, use the same device as
  // the volume mixer.
  return GetVolumeDeviceName();
}

// static
std::vector<std::string> AlsaVolumeControl::GetAmpElementNames() {
  std::vector<std::string> mixer_element_names = base::SplitString(
      base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kAlsaAmpElementName),
      ",", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);

  return mixer_element_names;
}

// static
std::string AlsaVolumeControl::GetAmpDeviceName() {
  auto* command_line = base::CommandLine::ForCurrentProcess();
  std::string mixer_device_name =
      command_line->GetSwitchValueASCII(switches::kAlsaAmpDeviceName);
  if (!mixer_device_name.empty()) {
    return mixer_device_name;
  }

  // If the amp mixer device was not specified directly, use the same device as
  // the volume mixer.
  return GetVolumeDeviceName();
}

AlsaVolumeControl::AlsaVolumeControl(Delegate* delegate,
                                     std::unique_ptr<::media::AlsaWrapper> alsa)
    : delegate_(delegate),
      alsa_(std::move(alsa)),
      volume_mixer_device_name_(GetVolumeDeviceName()),
      volume_mixer_element_name_(GetVolumeElementName()),
      mute_mixer_device_name_(GetMuteDeviceName()),
      mute_mixer_element_name_(GetMuteElementName(alsa_.get(),
                                                  volume_mixer_device_name_,
                                                  volume_mixer_element_name_,
                                                  mute_mixer_device_name_)),
      amp_mixer_device_name_(GetAmpDeviceName()),
      amp_mixer_element_names_(GetAmpElementNames()),
      volume_range_min_(0),
      volume_range_max_(0),
      mute_mixer_ptr_(nullptr) {
  DCHECK(delegate_);
  LOG(INFO) << "Volume device = " << volume_mixer_device_name_
            << ", element = " << volume_mixer_element_name_;
  LOG(INFO) << "Mute device = " << mute_mixer_device_name_
            << ", element = " << mute_mixer_element_name_;

  std::string amp_element_name_list = "[";
  for (const auto& amp_mixer_element_name : amp_mixer_element_names_) {
    amp_element_name_list += amp_mixer_element_name;
    amp_element_name_list += ",";
  }
  if (!amp_mixer_element_names_.empty()) {
    amp_element_name_list.pop_back();
  }
  amp_element_name_list += "]";

  LOG(INFO) << "Idle device = " << amp_mixer_device_name_
            << ", elements = " << amp_element_name_list;

  volume_mixer_ = std::make_unique<ScopedAlsaMixer>(
      alsa_.get(), volume_mixer_device_name_, volume_mixer_element_name_);
  if (volume_mixer_->element) {
    ALSA_ASSERT(MixerSelemGetPlaybackVolumeRange, volume_mixer_->element,
                &volume_range_min_, &volume_range_max_);
    volume_mixer_->WatchForEvents(
        &AlsaVolumeControl::VolumeOrMuteChangeCallback,
        reinterpret_cast<void*>(this));
  }

  if (mute_mixer_element_name_ != volume_mixer_element_name_) {
    mute_mixer_ = std::make_unique<ScopedAlsaMixer>(
        alsa_.get(), mute_mixer_device_name_, mute_mixer_element_name_);
    if (mute_mixer_->element) {
      mute_mixer_ptr_ = mute_mixer_.get();
      mute_mixer_->WatchForEvents(
          &AlsaVolumeControl::VolumeOrMuteChangeCallback,
          reinterpret_cast<void*>(this));
    }
  } else {
    mute_mixer_ptr_ = volume_mixer_.get();
  }

  for (const auto& amp_mixer_element_name : amp_mixer_element_names_) {
    amp_mixers_.emplace_back(std::make_unique<ScopedAlsaMixer>(
        alsa_.get(), amp_mixer_device_name_, amp_mixer_element_name));
    if (amp_mixers_.back()->element) {
      amp_mixers_.back()->WatchForEvents(nullptr, nullptr);
    }
  }
}

AlsaVolumeControl::~AlsaVolumeControl() = default;

float AlsaVolumeControl::GetRoundtripVolume(float volume) {
  if (volume_range_max_ == volume_range_min_) {
    return 0.0f;
  }

  long level = 0;  // NOLINT(runtime/int)
  level = std::round((std::clamp(volume, 0.0f, 1.0f) *
                      (volume_range_max_ - volume_range_min_)) +
                     volume_range_min_);
  return static_cast<float>(level - volume_range_min_) /
         static_cast<float>(volume_range_max_ - volume_range_min_);
}

float AlsaVolumeControl::VolumeLevelToDb(float volume) {
  long level = 0;  // NOLINT(runtime/int)
  if (volume_range_max_ == volume_range_min_) {
    level = volume_range_max_;
  } else {
    level = std::round((volume * (volume_range_max_ - volume_range_min_)) +
                       volume_range_min_);
  }
  long volume_db = 0;  // NOLINT(runtime/int)
  ALSA_ASSERT(MixerSelemAskPlaybackVolDb, volume_mixer_->element, level,
              &volume_db);
  return static_cast<float>(volume_db * 0.01f);
}

float AlsaVolumeControl::DbToVolumeLevel(float volume_db) {
  if (volume_range_max_ == volume_range_min_) {
    return 0.0f;
  }
  long level = 0.0f;  // NOLINT(runtime/int)
  ALSA_ASSERT(MixerSelemAskPlaybackDbVol, volume_mixer_->element,
              std::round(volume_db * 100.0f), &level);
  return static_cast<float>(level - volume_range_min_) /
         static_cast<float>(volume_range_max_ - volume_range_min_);
}

float AlsaVolumeControl::GetVolume() {
  if (!volume_mixer_->element) {
    return 0.0f;
  }
  long level = 0;  // NOLINT(runtime/int)
  ALSA_ASSERT(MixerSelemGetPlaybackVolume, volume_mixer_->element,
              SND_MIXER_SCHN_MONO, &level);
  return static_cast<float>(level - volume_range_min_) /
         static_cast<float>(volume_range_max_ - volume_range_min_);
}

void AlsaVolumeControl::SetVolume(float level) {
  if (!volume_mixer_->element) {
    return;
  }
  float volume = std::round((level * (volume_range_max_ - volume_range_min_)) +
                            volume_range_min_);
  ALSA_ASSERT(MixerSelemSetPlaybackVolumeAll, volume_mixer_->element, volume);
}

bool AlsaVolumeControl::IsMuted() {
  return IsElementAllMuted(mute_mixer_ptr_).value_or(false);
}

void AlsaVolumeControl::SetMuted(bool muted) {
  if (!SetElementMuted(mute_mixer_ptr_, muted)) {
    LOG(ERROR) << "Mute failed: no mute switch on mixer element.";
  }
}

void AlsaVolumeControl::SetPowerSave(bool power_save_on) {
  for (const auto& amp_mixer : amp_mixers_) {
    amp_mixer->RefreshElement();
    if (IsElementAllMuted(amp_mixer.get()).value_or(false) == power_save_on) {
      LOG(INFO) << "Power Save already set to: " << power_save_on;
      continue;
    }
    if (last_power_save_on_ == power_save_on) {
      LOG(WARNING) << "Power Save was set to: " << !last_power_save_on_
                   << " by others";
      metrics::CastMetricsHelper::GetInstance()->RecordSimpleAction(
          (last_power_save_on_
               ? "Cast.Platform.VolumeControl.PowerSaveDisturbedOff"
               : "Cast.Platform.VolumeControl.PowerSaveDisturbedOn"));
    }
    if (!SetElementMuted(amp_mixer.get(), power_save_on)) {
      LOG(ERROR) << "Failed to set Power Save to " << power_save_on
                 << ": no amp switch on mixer element.";
      metrics::CastMetricsHelper::GetInstance()->RecordSimpleAction(
          (power_save_on ? "Cast.Platform.VolumeControl.PowerSaveFailedOn"
                         : "Cast.Platform.VolumeControl.PowerSaveFailedOff"));
    } else {
      LOG(INFO) << "Set Power Save to: " << power_save_on;
    }
  }
  last_power_save_on_ = power_save_on;
  if (last_power_save_on_) {
    // Schedule a checker so underruns will not wake up the amplifier
    // for a long time.
    power_save_timer_.Start(FROM_HERE, kPowerSaveCheckTime, this,
                            &AlsaVolumeControl::CheckPowerSave);
  } else {
    power_save_timer_.Stop();
  }
}

void AlsaVolumeControl::SetLimit(float limit) {}

bool AlsaVolumeControl::SetElementMuted(ScopedAlsaMixer* mixer, bool muted) {
  if (!mixer || !mixer->element ||
      !alsa_->MixerSelemHasPlaybackSwitch(mixer->element)) {
    return false;
  }
  bool success = true;
  for (int32_t channel = 0; channel <= SND_MIXER_SCHN_LAST; ++channel) {
    int err = alsa_->MixerSelemSetPlaybackSwitch(
        mixer->element, static_cast<snd_mixer_selem_channel_id_t>(channel),
        !muted);
    if (err != 0) {
      success = false;
      LOG(ERROR) << "MixerSelemSetPlaybackSwitch: " << alsa_->StrError(err);
    }
  }
  return success;
}

std::optional<bool> AlsaVolumeControl::IsElementAllMuted(
    ScopedAlsaMixer* mixer) {
  if (!mixer || !mixer->element ||
      !alsa_->MixerSelemHasPlaybackSwitch(mixer->element)) {
    return std::nullopt;
  }
  for (int32_t channel = 0; channel <= SND_MIXER_SCHN_LAST; ++channel) {
    int channel_unmuted;
    int err = alsa_->MixerSelemGetPlaybackSwitch(
        mixer->element, static_cast<snd_mixer_selem_channel_id_t>(channel),
        &channel_unmuted);
    if (err != 0) {
      LOG(ERROR) << "MixerSelemGetPlaybackSwitch: " << alsa_->StrError(err);
      return std::nullopt;
    }
    if (channel_unmuted) {
      return false;
    }
  }
  return true;
}

void AlsaVolumeControl::OnVolumeOrMuteChanged() {
  delegate_->OnSystemVolumeOrMuteChange(GetVolume(), IsMuted());
}

void AlsaVolumeControl::CheckPowerSave() {
  SetPowerSave(last_power_save_on_);
}

// static
int AlsaVolumeControl::VolumeOrMuteChangeCallback(snd_mixer_elem_t* elem,
                                                  unsigned int mask) {
  if (!(mask & SND_CTL_EVENT_MASK_VALUE))
    return 0;

  AlsaVolumeControl* instance = static_cast<AlsaVolumeControl*>(
      snd_mixer_elem_get_callback_private(elem));
  instance->OnVolumeOrMuteChanged();
  return 0;
}

// static
std::unique_ptr<SystemVolumeControl> SystemVolumeControl::Create(
    Delegate* delegate) {
  return std::make_unique<AlsaVolumeControl>(
      delegate, std::make_unique<::media::AlsaWrapper>());
}

}  // namespace media
}  // namespace chromecast