blob: 322d3bf186e52cfbf7597c1af82903bdb20cca8d [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromecast/media/cma/backend/alsa/alsa_volume_control.h"
#include <utility>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "chromecast/base/chromecast_switches.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";
} // namespace
class AlsaVolumeControl::ScopedAlsaMixer {
public:
ScopedAlsaMixer(::media::AlsaWrapper* alsa,
const std::string& mixer_device_name,
const std::string& mixer_element_name)
: alsa_(alsa) {
DCHECK(alsa_);
VLOG(1) << "Opening mixer element \"" << mixer_element_name
<< "\" on device \"" << mixer_device_name << "\"";
int alsa_err = alsa_->MixerOpen(&mixer, 0);
if (alsa_err < 0) {
LOG(ERROR) << "MixerOpen error: " << alsa_->StrError(alsa_err);
mixer = nullptr;
return;
}
alsa_err = alsa_->MixerAttach(mixer, mixer_device_name.c_str());
if (alsa_err < 0) {
LOG(ERROR) << "MixerAttach error: " << alsa_->StrError(alsa_err);
mixer = nullptr;
return;
}
ALSA_ASSERT(MixerElementRegister, mixer, NULL, NULL);
ALSA_ASSERT(MixerLoad, mixer);
snd_mixer_selem_id_t* sid = NULL;
ALSA_ASSERT(MixerSelemIdMalloc, &sid);
alsa_->MixerSelemIdSetIndex(sid, 0);
alsa_->MixerSelemIdSetName(sid, mixer_element_name.c_str());
element = alsa_->MixerFindSelem(mixer, sid);
if (!element) {
LOG(ERROR) << "Simple mixer control element \"" << mixer_element_name
<< "\" not found.";
}
alsa_->MixerSelemIdFree(sid);
}
~ScopedAlsaMixer() {
if (mixer) {
alsa_->MixerClose(mixer);
}
}
snd_mixer_elem_t* element = nullptr;
snd_mixer_t* mixer = nullptr;
private:
::media::AlsaWrapper* const alsa_;
};
// 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::string AlsaVolumeControl::GetAmpElementName() {
std::string mixer_element_name =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
switches::kAlsaAmpElementName);
if (!mixer_element_name.empty()) {
return mixer_element_name;
}
return std::string();
}
// 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)
: delegate_(delegate),
alsa_(std::make_unique<::media::AlsaWrapper>()),
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_name_(GetAmpElementName()),
volume_range_min_(0),
volume_range_max_(0),
mute_mixer_ptr_(nullptr) {
DCHECK(delegate_);
VLOG(1) << "Volume device = " << volume_mixer_device_name_
<< ", element = " << volume_mixer_element_name_;
VLOG(1) << "Mute device = " << mute_mixer_device_name_
<< ", element = " << mute_mixer_element_name_;
VLOG(1) << "Idle device = " << amp_mixer_device_name_
<< ", element = " << amp_mixer_element_name_;
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_);
alsa_->MixerElemSetCallback(volume_mixer_->element,
&AlsaVolumeControl::VolumeOrMuteChangeCallback);
alsa_->MixerElemSetCallbackPrivate(volume_mixer_->element,
reinterpret_cast<void*>(this));
RefreshMixerFds(volume_mixer_.get());
}
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();
alsa_->MixerElemSetCallback(
mute_mixer_->element, &AlsaVolumeControl::VolumeOrMuteChangeCallback);
alsa_->MixerElemSetCallbackPrivate(mute_mixer_->element,
reinterpret_cast<void*>(this));
RefreshMixerFds(mute_mixer_.get());
}
} else {
mute_mixer_ptr_ = volume_mixer_.get();
}
if (!amp_mixer_element_name_.empty()) {
amp_mixer_ = std::make_unique<ScopedAlsaMixer>(
alsa_.get(), amp_mixer_device_name_, amp_mixer_element_name_);
if (amp_mixer_->element) {
RefreshMixerFds(amp_mixer_.get());
}
}
}
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((volume * (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::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() {
if (!mute_mixer_ptr_->element ||
!alsa_->MixerSelemHasPlaybackSwitch(mute_mixer_ptr_->element)) {
LOG(ERROR) << "Mute failed: no mute switch on mixer element.";
return false;
}
bool muted = false;
for (int32_t channel = 0; channel <= SND_MIXER_SCHN_LAST; ++channel) {
int channel_enabled = 0;
int err = alsa_->MixerSelemGetPlaybackSwitch(
mute_mixer_ptr_->element,
static_cast<snd_mixer_selem_channel_id_t>(channel), &channel_enabled);
if (err == 0) {
muted = muted || (channel_enabled == 0);
}
}
return muted;
}
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) {
if (!SetElementMuted(amp_mixer_.get(), power_save_on)) {
LOG(INFO) << "Amp toggle failed: no amp switch on mixer element.";
}
}
bool AlsaVolumeControl::SetElementMuted(ScopedAlsaMixer* mixer, bool muted) {
if (!mixer || !mixer->element ||
!alsa_->MixerSelemHasPlaybackSwitch(mixer->element)) {
return false;
}
for (int32_t channel = 0; channel <= SND_MIXER_SCHN_LAST; ++channel) {
alsa_->MixerSelemSetPlaybackSwitch(
mixer->element, static_cast<snd_mixer_selem_channel_id_t>(channel),
!muted);
}
return true;
}
void AlsaVolumeControl::RefreshMixerFds(ScopedAlsaMixer* mixer) {
int num_fds = alsa_->MixerPollDescriptorsCount(mixer->mixer);
DCHECK_GT(num_fds, 0);
struct pollfd pfds[num_fds];
num_fds = alsa_->MixerPollDescriptors(mixer->mixer, pfds, num_fds);
DCHECK_GT(num_fds, 0);
for (int i = 0; i < num_fds; ++i) {
auto watcher =
std::make_unique<base::MessageLoopForIO::FileDescriptorWatcher>(
FROM_HERE);
base::MessageLoopForIO::current()->WatchFileDescriptor(
pfds[i].fd, true /* persistent */, base::MessageLoopForIO::WATCH_READ,
watcher.get(), this);
file_descriptor_watchers_.push_back(std::move(watcher));
}
}
void AlsaVolumeControl::OnFileCanReadWithoutBlocking(int fd) {
if (volume_mixer_->mixer) {
alsa_->MixerHandleEvents(volume_mixer_->mixer);
}
if (mute_mixer_ && mute_mixer_->mixer) {
alsa_->MixerHandleEvents(mute_mixer_->mixer);
}
if (amp_mixer_ && amp_mixer_->mixer) {
// amixer locks up if we don't call this for unknown reasons.
alsa_->MixerHandleEvents(amp_mixer_->mixer);
}
}
void AlsaVolumeControl::OnFileCanWriteWithoutBlocking(int fd) {
// Nothing to do.
}
void AlsaVolumeControl::OnVolumeOrMuteChanged() {
delegate_->OnSystemVolumeOrMuteChange(GetVolume(), IsMuted());
}
// 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);
}
} // namespace media
} // namespace chromecast