fix(volume): Improve mixer event handling

Fixes jaagr/lemonbuddy#14
This commit is contained in:
Michael Carlberg 2016-06-10 01:09:54 +02:00
parent 737da87e22
commit 0e9900db74
8 changed files with 191 additions and 105 deletions

View File

@ -749,18 +749,20 @@ See [the bspwm module](#user-content-dependencies) for details on `label:dimmed`
This module is still WIP.
Mute and volume changes should affect the appropriate mixers depending on
if the headphones are plugged in or not. Still need to add separate output formats
weather the headphones are plugged in or not. Still need to add separate output formats
to indicate it.
~~~ ini
[module/volume]
type = internal/volume
;master_mixer = Master
; Use the following command to list available mixer controls:
; $ amixer scontrols | sed -nr "s/.*'([[:alnum:]]+)'.*/\1/p"
speaker_mixer = Speaker
headphone_mixer = Headphone
; NOTE: This is required if headphone_mixer is defined
; Use the following command to list available device controls
; $ amixer controls | sed -r "/CARD/\!d; s/.*=([0-9]+).*name='([^']+)'.*/printf '%3.0f: %s\n' '\1' '\2'/e" | sort
headphone_control_numid = 9

View File

@ -24,7 +24,7 @@ font:0 = sans:size=8;0
font:1 = font awesome:size=10:weight=heavy;0
modules:left = label
modules:right = cpu ram clock
modules:right = volume cpu ram clock
[module/label]
type = custom/text
@ -58,4 +58,22 @@ format:underline = #7a6
format:overline = #7a6
format:padding = 2
[module/volume]
type = internal/volume
;speaker_mixer = Speaker
;headphone_mixer = Headphone
;headphone_control_numid = 9
format:volume:background = #933484
format:volume:underline = #9d6294
format:volume:overline = #9d6294
format:volume:padding = 2
format:muted:background = #933484
format:muted:underline = #9d6294
format:muted:overline = #9d6294
format:muted:padding = 2
label:volume = Volume: %percentage%
label:muted = Sound is muted
; vim:ft=dosini

View File

@ -7,11 +7,15 @@
#include <mutex>
#include "exception.hpp"
#include "utils/concurrency.hpp"
#include "utils/macros.hpp"
#define STRSNDERR(s) std::string(snd_strerror(s))
#define StrSndErr(s) ToStr(snd_strerror(s))
namespace alsa
{
// Errors {{{
class Exception : public ::Exception
{
public:
@ -25,9 +29,16 @@ namespace alsa
: Exception(msg +" ["+ std::to_string(code) +"]") {}
};
class MixerError : public Exception {
using Exception::Exception;
};
// }}}
// ControlInterface {{{
class ControlInterface
{
std::mutex mtx;
concurrency::SpinLock lock;
snd_hctl_t *hctl;
snd_hctl_elem_t *elem;
@ -40,20 +51,21 @@ namespace alsa
public:
explicit ControlInterface(int numid);
~ControlInterface();
ControlInterface(const ControlInterface &) = delete;
ControlInterface &operator=(const ControlInterface &) = delete;
bool wait(int timeout = -1);
void process_events();
bool test_device_plugged();
};
class MixerError : public Exception {
using Exception::Exception;
};
// }}}
// Mixer {{{
class Mixer
{
std::mutex mtx;
concurrency::SpinLock lock;
snd_mixer_t *hardware_mixer = nullptr;
snd_mixer_elem_t *mixer_element = nullptr;
@ -61,8 +73,11 @@ namespace alsa
public:
explicit Mixer(const std::string& mixer_control_name);
~Mixer();
Mixer(const Mixer &) = delete;
Mixer &operator=(const Mixer &) = delete;
bool wait(int timeout = -1);
int process_events();
int get_volume();
void set_volume(float percentage);
@ -73,4 +88,6 @@ namespace alsa
protected:
void error_handler(const std::string& message);
};
// }}}
}

View File

@ -19,18 +19,11 @@ namespace modules
static constexpr auto TAG_LABEL_VOLUME = "<label:volume>";
static constexpr auto TAG_LABEL_MUTED = "<label:muted>";
static constexpr auto EVENT_PREFIX = "vol";
static constexpr auto EVENT_VOLUME_UP = "volup";
static constexpr auto EVENT_VOLUME_DOWN = "voldown";
static constexpr auto EVENT_TOGGLE_MUTE = "volmute";
std::unique_ptr<alsa::Mixer> master_mixer;
std::unique_ptr<alsa::Mixer> speaker_mixer;
std::unique_ptr<alsa::Mixer> headphone_mixer;
std::unique_ptr<alsa::ControlInterface> headphone_ctrl;
int headphone_ctrl_numid;
std::unique_ptr<Builder> builder;
std::unique_ptr<drawtypes::Bar> bar_volume;
std::unique_ptr<drawtypes::Ramp> ramp_volume;
std::unique_ptr<drawtypes::Label> label_volume;
@ -38,8 +31,16 @@ namespace modules
std::unique_ptr<drawtypes::Label> label_muted;
std::unique_ptr<drawtypes::Label> label_muted_tokenized;
int volume = 0;
bool muted = false;
std::unique_ptr<alsa::Mixer> master_mixer;
std::unique_ptr<alsa::Mixer> speaker_mixer;
std::unique_ptr<alsa::Mixer> headphone_mixer;
std::unique_ptr<alsa::ControlInterface> headphone_ctrl;
int headphone_ctrl_numid;
concurrency::Atomic<int> volume;
concurrency::Atomic<bool> muted;
concurrency::Atomic<bool> has_changed;
public:
explicit VolumeModule(const std::string& name);

View File

@ -56,7 +56,6 @@ namespace concurrency
template<typename T>
class Atomic
{
concurrency::SpinLock lock;
std::atomic<T> value;
public:
@ -67,25 +66,21 @@ namespace concurrency
void operator=(T value)
{
std::lock_guard<concurrency::SpinLock> lck(this->lock);
this->value = value;
}
T operator()()
{
std::lock_guard<concurrency::SpinLock> lck(this->lock);
return this->value;
}
operator bool()
{
std::lock_guard<concurrency::SpinLock> lck(this->lock);
return this->value;
}
bool operator==(T const& b)
{
std::lock_guard<concurrency::SpinLock> lck(this->lock);
return this->value == b;
}
};

View File

@ -164,7 +164,7 @@ void EventLoop::read_stdin()
std::string input;
while ((input = io::readline(this->fd_stdin)).empty() == false) {
this->logger->debug("Input value: \'"+ input +"\"");
this->logger->debug("Input value: \""+ input +"\"");
bool input_processed = false;

View File

@ -8,6 +8,8 @@
namespace alsa
{
// ControlInterface {{{
ControlInterface::ControlInterface(int numid)
{
int err;
@ -20,49 +22,48 @@ namespace alsa
snd_ctl_elem_info_set_id(this->info, this->id);
if ((err = snd_ctl_open(&this->ctl, ALSA_SOUNDCARD, SND_CTL_NONBLOCK | SND_CTL_READONLY)) < 0)
throw ControlInterfaceError(err, "Could not open control \""+ ToStr(ALSA_SOUNDCARD) +"\": "+ STRSNDERR(err));
throw ControlInterfaceError(err, "Could not open control \""+ ToStr(ALSA_SOUNDCARD) +"\": "+ StrSndErr(err));
if ((err = snd_ctl_elem_info(this->ctl, this->info)) < 0)
throw ControlInterfaceError(err, "Could not get control data: "+ STRSNDERR(err));
throw ControlInterfaceError(err, "Could not get control data: "+ StrSndErr(err));
snd_ctl_elem_info_get_id(this->info, this->id);
if ((err = snd_hctl_open(&this->hctl, ALSA_SOUNDCARD, 0)) < 0)
throw ControlInterfaceError(err, STRSNDERR(err));
throw ControlInterfaceError(err, StrSndErr(err));
if ((err = snd_hctl_load(this->hctl)) < 0)
throw ControlInterfaceError(err, STRSNDERR(err));
throw ControlInterfaceError(err, StrSndErr(err));
if ((elem = snd_hctl_find_elem(this->hctl, this->id)) == nullptr)
throw ControlInterfaceError(err, "Could not find control with id "+ IntToStr(snd_ctl_elem_id_get_numid(this->id)));
if ((err = snd_ctl_subscribe_events(this->ctl, 1)) < 0)
throw ControlInterfaceError(err, "Could not subscribe to events: "+ IntToStr(snd_ctl_elem_id_get_numid(this->id)));
log_trace("Successfully initialized control interface");
log_trace("Successfully initialized control interface with ID: "+ IntToStr(numid));
}
ControlInterface::~ControlInterface() {
std::lock_guard<std::mutex> lck(this->mtx);
ControlInterface::~ControlInterface()
{
std::lock_guard<concurrency::SpinLock> lck(this->lock);
snd_ctl_close(this->ctl);
snd_hctl_close(this->hctl);
}
bool ControlInterface::wait(int timeout)
{
std::lock_guard<std::mutex> lck(this->mtx);
std::lock_guard<concurrency::SpinLock> lck(this->lock);
int err;
if ((err = snd_ctl_wait(this->ctl, timeout)) < 0)
throw ControlInterfaceError(err, "Failed to wait for events: "+ STRSNDERR(err));
throw ControlInterfaceError(err, "Failed to wait for events: "+ StrSndErr(err));
snd_ctl_event_t *event;
snd_ctl_event_alloca(&event);
if ((err = snd_ctl_read(this->ctl, event)) < 0) {
log_trace(err);
if ((err = snd_ctl_read(this->ctl, event)) < 0)
return false;
}
if (snd_ctl_event_get_type(event) != SND_CTL_EVENT_ELEM)
return false;
@ -73,14 +74,18 @@ namespace alsa
bool ControlInterface::test_device_plugged()
{
std::lock_guard<concurrency::SpinLock> lck(this->lock);
int err;
if ((err = snd_hctl_elem_read(this->elem, this->value)) < 0)
throw ControlInterfaceError(err, "Could not read control value: "+ STRSNDERR(err));
throw ControlInterfaceError(err, "Could not read control value: "+ StrSndErr(err));
return snd_ctl_elem_value_get_boolean(this->value, 0);
}
// }}}
// Mixer {{{
Mixer::Mixer(const std::string& mixer_control_name)
{
@ -103,33 +108,46 @@ namespace alsa
if ((this->mixer_element = snd_mixer_find_selem(this->hardware_mixer, mixer_id)) == nullptr)
throw MixerError("Cannot find simple element");
log_trace("Successfully initialized mixer");
log_trace("Successfully initialized mixer: "+ mixer_control_name);
}
Mixer::~Mixer() {
std::lock_guard<std::mutex> lck(this->mtx);
Mixer::~Mixer()
{
std::lock_guard<concurrency::SpinLock> lck(this->lock);
snd_mixer_elem_remove(this->mixer_element);
snd_mixer_detach(this->hardware_mixer, ALSA_SOUNDCARD);
snd_mixer_close(this->hardware_mixer);
}
int Mixer::process_events()
{
int num_events = snd_mixer_handle_events(this->hardware_mixer);
if (num_events < 0)
throw MixerError("Failed to process pending events: "+ StrSndErr(num_events));
return num_events;
}
bool Mixer::wait(int timeout)
{
std::lock_guard<std::mutex> lck(this->mtx);
assert(this->hardware_mixer);
int err, pend_n = 0;
std::lock_guard<concurrency::SpinLock> lck(this->lock);
if (this->hardware_mixer != nullptr && (err = snd_mixer_wait(this->hardware_mixer, timeout)) < 0)
throw MixerError("Failed to wait for events: "+ STRSNDERR(err));
int err = snd_mixer_wait(this->hardware_mixer, timeout);
if (this->hardware_mixer != nullptr && (pend_n = snd_mixer_handle_events(this->hardware_mixer)) < 0)
throw MixerError("Failed to process pending events: "+ STRSNDERR(err));
if (err < 0)
throw MixerError("Failed to wait for events: "+ StrSndErr(err));
return pend_n > 0;
return this->process_events() > 0;
}
int Mixer::get_volume()
{
std::lock_guard<concurrency::SpinLock> lck(this->lock);
long chan_n = 0, vol_total = 0, vol, vol_min, vol_max;
snd_mixer_selem_get_playback_volume_range(this->mixer_element, &vol_min, &vol_max);
@ -143,7 +161,7 @@ namespace alsa
}
}
return (int) 100 * (vol_total / chan_n) / vol_max + 0.5f;
return (int) 100.0f * (vol_total / chan_n) / vol_max + 0.5f;
}
void Mixer::set_volume(float percentage)
@ -151,18 +169,25 @@ namespace alsa
if (this->is_muted())
return;
std::lock_guard<concurrency::SpinLock> lck(this->lock);
long vol_min, vol_max;
snd_mixer_selem_get_playback_volume_range(this->mixer_element, &vol_min, &vol_max);
snd_mixer_selem_set_playback_volume_all(this->mixer_element, vol_max * percentage / 100);
}
void Mixer::set_mute(bool mode) {
void Mixer::set_mute(bool mode)
{
std::lock_guard<concurrency::SpinLock> lck(this->lock);
snd_mixer_selem_set_playback_switch_all(this->mixer_element, mode);
}
void Mixer::toggle_mute()
{
std::lock_guard<concurrency::SpinLock> lck(this->lock);
int state;
snd_mixer_selem_get_playback_switch(this->mixer_element, SND_MIXER_SCHN_FRONT_LEFT, &state);
snd_mixer_selem_set_playback_switch_all(this->mixer_element, !state);
@ -170,6 +195,8 @@ namespace alsa
bool Mixer::is_muted()
{
std::lock_guard<concurrency::SpinLock> lck(this->lock);
int state = 0;
repeat(SND_MIXER_SCHN_LAST)
{
@ -179,6 +206,9 @@ namespace alsa
return true;
}
}
return false;
}
// }}}
}

View File

@ -8,6 +8,8 @@ using namespace modules;
VolumeModule::VolumeModule(const std::string& name_) : EventModule(name_)
{
// Load configuration values {{{
auto master_mixer = config::get<std::string>(name(), "master_mixer", "Master");
auto speaker_mixer = config::get<std::string>(name(), "speaker_mixer", "");
auto headphone_mixer = config::get<std::string>(name(), "headphone_mixer", "");
@ -15,14 +17,16 @@ VolumeModule::VolumeModule(const std::string& name_) : EventModule(name_)
if (!headphone_mixer.empty() && this->headphone_ctrl_numid == -1)
throw ModuleError("[VolumeModule] Missing required property value for \"headphone_control_numid\"...");
else if (headphone_mixer.empty())
else if (headphone_mixer.empty() && this->headphone_ctrl_numid != -1)
throw ModuleError("[VolumeModule] Missing required property value for \"headphone_mixer\"...");
if (string::lower(speaker_mixer) == "master")
throw ModuleError("[VolumeModule] The \"Master\" mixer is already processed internally. Specify another mixer or comment out the \"speaker_mixer\" parameter...");
if (string::lower(headphone_mixer) == "master")
throw ModuleError("[VolumeModule] The \"Master\" mixer is already processed internally. Specify another mixer or comment out the \"headphone_mixer\" parameter...");
// }}}
// Setup mixers {{{
auto create_mixer = [](std::string mixer_name)
{
std::unique_ptr<alsa::Mixer> mixer;
@ -37,7 +41,7 @@ VolumeModule::VolumeModule(const std::string& name_) : EventModule(name_)
return mixer;
};
this->master_mixer = create_mixer("Master");
this->master_mixer = create_mixer(master_mixer);
if (!speaker_mixer.empty())
this->speaker_mixer = create_mixer(speaker_mixer);
@ -57,9 +61,9 @@ VolumeModule::VolumeModule(const std::string& name_) : EventModule(name_)
this->headphone_ctrl.reset();
}
}
// }}}
this->builder = std::make_unique<Builder>();
// Add formats and elements {{{
this->formatter->add(FORMAT_VOLUME, TAG_LABEL_VOLUME,
{ TAG_RAMP_VOLUME, TAG_LABEL_VOLUME, TAG_BAR_VOLUME });
this->formatter->add(FORMAT_MUTED, TAG_LABEL_MUTED,
@ -77,14 +81,16 @@ VolumeModule::VolumeModule(const std::string& name_) : EventModule(name_)
this->label_muted = drawtypes::get_optional_config_label(name(), get_tag_name(TAG_LABEL_MUTED), "%percentage%");
this->label_muted_tokenized = this->label_muted->clone();
}
// }}}
// Sign up for stdin events {{{
register_command_handler(name());
// }}}
}
VolumeModule::~VolumeModule()
{
std::lock_guard<concurrency::SpinLock> lck(this->update_lock);
this->master_mixer.reset();
this->speaker_mixer.reset();
this->headphone_mixer.reset();
@ -95,14 +101,17 @@ bool VolumeModule::has_event()
{
bool has_event = false;
if (this->has_changed())
has_event = true;
try {
if (this->master_mixer)
if (!has_event && this->master_mixer)
has_event |= this->master_mixer->wait(25);
if (this->speaker_mixer)
if (!has_event && this->speaker_mixer)
has_event |= this->speaker_mixer->wait(25);
if (this->headphone_mixer)
if (!has_event && this->headphone_mixer)
has_event |= this->headphone_mixer->wait(25);
if (this->headphone_ctrl)
if (!has_event && this->headphone_ctrl)
has_event |= this->headphone_ctrl->wait(25);
} catch (alsa::Exception &e) {
log_error(e.what());
@ -113,21 +122,26 @@ bool VolumeModule::has_event()
bool VolumeModule::update()
{
int volume = 0;
// Consume any other pending events
this->has_changed = false;
if (this->master_mixer)
this->master_mixer->process_events();
if (this->speaker_mixer)
this->speaker_mixer->process_events();
if (this->headphone_mixer)
this->headphone_mixer->process_events();
if (this->headphone_ctrl)
this->headphone_ctrl->wait(0);
int volume = 100;
bool muted = false;
auto headphones_connected = false;
if (this->master_mixer) {
volume = this->master_mixer->get_volume();
volume *= this->master_mixer->get_volume() / 100.0f;
muted |= this->master_mixer->is_muted();
} else {
volume = 100;
}
if (this->headphone_ctrl && this->headphone_mixer)
headphones_connected = this->headphone_ctrl->test_device_plugged();
if (headphones_connected) {
if (this->headphone_mixer && this->headphone_ctrl && this->headphone_ctrl->test_device_plugged()) {
volume *= this->headphone_mixer->get_volume() / 100.0f;
muted |= this->headphone_mixer->is_muted();
} else if (this->speaker_mixer) {
@ -139,26 +153,29 @@ bool VolumeModule::update()
this->muted = muted;
this->label_volume_tokenized->text = this->label_volume->text;
this->label_volume_tokenized->replace_token("%percentage%", std::to_string(this->volume) +"%");
this->label_volume_tokenized->replace_token("%percentage%", std::to_string(this->volume()) +"%");
this->label_muted_tokenized->text = this->label_muted->text;
this->label_muted_tokenized->replace_token("%percentage%", std::to_string(this->volume) +"%");
this->label_muted_tokenized->replace_token("%percentage%", std::to_string(this->volume()) +"%");
return true;
}
std::string VolumeModule::get_format() {
return this->muted ? FORMAT_MUTED : FORMAT_VOLUME;
std::string VolumeModule::get_format()
{
return this->muted() == true ? FORMAT_MUTED : FORMAT_VOLUME;
}
std::string VolumeModule::get_output()
{
this->builder->cmd(Cmd::LEFT_CLICK, EVENT_TOGGLE_MUTE);
if (volume < 100)
this->builder->cmd(Cmd::SCROLL_UP, EVENT_VOLUME_UP, volume < 100);
if (volume > 0)
this->builder->cmd(Cmd::SCROLL_DOWN, EVENT_VOLUME_DOWN);
if (!this->muted()) {
if (this->volume() < 100)
this->builder->cmd(Cmd::SCROLL_UP, EVENT_VOLUME_UP);
if (this->volume() > 0)
this->builder->cmd(Cmd::SCROLL_DOWN, EVENT_VOLUME_DOWN);
}
this->builder->node(this->Module::get_output());
@ -167,8 +184,6 @@ std::string VolumeModule::get_output()
bool VolumeModule::build(Builder *builder, const std::string& tag)
{
bool built = true;
if (tag == TAG_BAR_VOLUME)
builder->node(this->bar_volume, volume);
else if (tag == TAG_RAMP_VOLUME)
@ -178,52 +193,60 @@ bool VolumeModule::build(Builder *builder, const std::string& tag)
else if (tag == TAG_LABEL_MUTED)
builder->node(this->label_muted_tokenized);
else
built = false;
return false;
return built;
return true;
}
bool VolumeModule::handle_command(const std::string& cmd)
{
if (cmd.length() < 3 || cmd.substr(0, 3) != "vol")
if (cmd.length() < std::strlen(EVENT_PREFIX))
return false;
if (std::strncmp(cmd.c_str(), EVENT_PREFIX, 3) != 0)
return false;
std::lock_guard<concurrency::SpinLock> lck(this->update_lock);
alsa::Mixer *master_mixer = nullptr;
alsa::Mixer *other_mixer = nullptr;
bool headphones_connected = false;
if (this->master_mixer)
master_mixer = this->master_mixer.get();
if (this->headphone_ctrl && this->headphone_mixer)
headphones_connected = this->headphone_ctrl->test_device_plugged();
if (master_mixer == nullptr)
return false;
if (headphones_connected)
if (this->headphone_mixer && this->headphone_ctrl && this->headphone_ctrl->test_device_plugged())
other_mixer = this->headphone_mixer.get();
else if (this->speaker_mixer)
other_mixer = this->speaker_mixer.get();
if (this->master_mixer)
master_mixer = this->master_mixer.get();
// Toggle mute state
if (std::strncmp(cmd.c_str(), EVENT_TOGGLE_MUTE, std::strlen(EVENT_TOGGLE_MUTE)) == 0) {
master_mixer->set_mute(this->muted());
if (other_mixer != nullptr)
other_mixer->set_mute(this->muted());
// Increase volume
} else if (std::strncmp(cmd.c_str(), EVENT_VOLUME_UP, std::strlen(EVENT_VOLUME_UP)) == 0) {
master_mixer->set_volume(math::cap<float>(master_mixer->get_volume() + 5, 0, 100));
if (other_mixer != nullptr)
other_mixer->set_volume(math::cap<float>(other_mixer->get_volume() + 5, 0, 100));
// Decrease volume
} else if (std::strncmp(cmd.c_str(), EVENT_VOLUME_DOWN, std::strlen(EVENT_VOLUME_DOWN)) == 0) {
master_mixer->set_volume(math::cap<float>(master_mixer->get_volume() - 5, 0, 100));
if (other_mixer != nullptr)
other_mixer->set_volume(math::cap<float>(other_mixer->get_volume() - 5, 0, 100));
if (cmd == EVENT_VOLUME_UP) {
auto vol = math::cap<float>(this->master_mixer->get_volume() + 5, 0, 100);
if (master_mixer != nullptr)
master_mixer->set_volume(vol);
} else if (cmd == EVENT_VOLUME_DOWN) {
auto vol = math::cap<float>(this->master_mixer->get_volume() - 5, 0, 100);
if (master_mixer != nullptr)
master_mixer->set_volume(vol);
} else if (cmd == EVENT_TOGGLE_MUTE) {
if (master_mixer != nullptr)
master_mixer->toggle_mute();
if (other_mixer != nullptr) {
if (master_mixer != nullptr)
other_mixer->set_mute(!master_mixer->is_muted());
else
other_mixer->toggle_mute();
}
} else {
return false;
}
this->has_changed = true;
return true;
}