polybar-dwm/src/modules/alsa.cpp
Patrick Ziegler 26be83f893
module: Implement action router (#2336)
* module: Implement proof of concept action router

Action implementation inside module becomes much cleaner because each
module just registers action names together with a callback (pointer to
member function) and the action router does the rest.

* Make input function final

This forces all modules to use the action router

* modules: Catch exceptions in action handlers

* Use action router for all modules

* Use action_ prefix for function names

The mpd module's 'stop' action overwrote the base module's stop function
which caused difficult to debug behavior.

To prevent this in the future we now prefix each function that is
responsible for an action with 'action_'

* Cleanup

* actions: Throw exception when re-registering action

Action names are unique inside modules. Unfortunately there is no way to
ensure this statically, the next best thing is to crash the module and
let the user know that this is a bug.

* Formatting

* actions: Ignore data for actions without data

This is the same behavior as before.

* action_router: Write tests
2021-01-04 10:25:52 +01:00

284 lines
9.7 KiB
C++

#include "modules/alsa.hpp"
#include "adapters/alsa/control.hpp"
#include "adapters/alsa/generic.hpp"
#include "adapters/alsa/mixer.hpp"
#include "drawtypes/label.hpp"
#include "drawtypes/progressbar.hpp"
#include "drawtypes/ramp.hpp"
#include "modules/meta/base.inl"
#include "settings.hpp"
#include "utils/math.hpp"
POLYBAR_NS
using namespace alsa;
namespace modules {
template class module<alsa_module>;
alsa_module::alsa_module(const bar_settings& bar, string name_) : event_module<alsa_module>(bar, move(name_)) {
if (m_handle_events) {
m_router->register_action(EVENT_DEC, &alsa_module::action_dec);
m_router->register_action(EVENT_INC, &alsa_module::action_inc);
m_router->register_action(EVENT_TOGGLE, &alsa_module::action_toggle);
}
// Load configuration values
m_mapped = m_conf.get(name(), "mapped", m_mapped);
m_interval = m_conf.get(name(), "interval", m_interval);
auto master_mixer_name = m_conf.get(name(), "master-mixer", "Master"s);
auto speaker_mixer_name = m_conf.get(name(), "speaker-mixer", ""s);
auto headphone_mixer_name = m_conf.get(name(), "headphone-mixer", ""s);
// m_soundcard_name: Master Soundcard Name
// s_soundcard_name: Speaker Soundcard Name
// h_soundcard_name: Headphone Soundcard Name
auto m_soundcard_name = m_conf.get(name(), "master-soundcard", "default"s);
auto s_soundcard_name = m_conf.get(name(), "speaker-soundcard", "default"s);
auto h_soundcard_name = m_conf.get(name(), "headphone-soundcard", "default"s);
if (!headphone_mixer_name.empty()) {
m_headphoneid = m_conf.get<decltype(m_headphoneid)>(name(), "headphone-id");
}
if (string_util::compare(speaker_mixer_name, "master")) {
throw module_error("Master mixer is already defined");
}
if (string_util::compare(headphone_mixer_name, "master")) {
throw module_error("Master mixer is already defined");
}
// Setup mixers
try {
if (!master_mixer_name.empty()) {
m_mixer[mixer::MASTER].reset(new mixer_t::element_type{move(master_mixer_name), move(m_soundcard_name)});
}
if (!speaker_mixer_name.empty()) {
m_mixer[mixer::SPEAKER].reset(new mixer_t::element_type{move(speaker_mixer_name), move(s_soundcard_name)});
}
if (!headphone_mixer_name.empty()) {
m_mixer[mixer::HEADPHONE].reset(new mixer_t::element_type{move(headphone_mixer_name), move(h_soundcard_name)});
}
if (m_mixer[mixer::HEADPHONE]) {
m_ctrl[control::HEADPHONE].reset(new control_t::element_type{m_headphoneid});
}
if (m_mixer.empty()) {
throw module_error("No configured mixers");
}
} catch (const mixer_error& err) {
throw module_error(err.what());
} catch (const control_error& err) {
throw module_error(err.what());
}
// Add formats and elements
m_formatter->add(FORMAT_VOLUME, TAG_LABEL_VOLUME, {TAG_RAMP_VOLUME, TAG_LABEL_VOLUME, TAG_BAR_VOLUME});
m_formatter->add(FORMAT_MUTED, TAG_LABEL_MUTED, {TAG_RAMP_VOLUME, TAG_LABEL_MUTED, TAG_BAR_VOLUME});
if (m_formatter->has(TAG_BAR_VOLUME)) {
m_bar_volume = load_progressbar(m_bar, m_conf, name(), TAG_BAR_VOLUME);
}
if (m_formatter->has(TAG_LABEL_VOLUME, FORMAT_VOLUME)) {
m_label_volume = load_optional_label(m_conf, name(), TAG_LABEL_VOLUME, "%percentage%%");
}
if (m_formatter->has(TAG_LABEL_MUTED, FORMAT_MUTED)) {
m_label_muted = load_optional_label(m_conf, name(), TAG_LABEL_MUTED, "%percentage%%");
}
if (m_formatter->has(TAG_RAMP_VOLUME)) {
m_ramp_volume = load_ramp(m_conf, name(), TAG_RAMP_VOLUME);
m_ramp_headphones = load_ramp(m_conf, name(), TAG_RAMP_HEADPHONES, false);
}
}
void alsa_module::teardown() {
m_mixer.clear();
m_ctrl.clear();
snd_config_update_free_global();
}
bool alsa_module::has_event() {
// Poll for mixer and control events
try {
if (m_mixer[mixer::MASTER] && m_mixer[mixer::MASTER]->wait(25)) {
return true;
}
if (m_mixer[mixer::SPEAKER] && m_mixer[mixer::SPEAKER]->wait(25)) {
return true;
}
if (m_mixer[mixer::HEADPHONE] && m_mixer[mixer::HEADPHONE]->wait(25)) {
return true;
}
if (m_ctrl[control::HEADPHONE] && m_ctrl[control::HEADPHONE]->wait(25)) {
return true;
}
} catch (const alsa_exception& e) {
m_log.err("%s: %s", name(), e.what());
}
return false;
}
bool alsa_module::update() {
// Consume pending events
if (m_mixer[mixer::MASTER]) {
m_mixer[mixer::MASTER]->process_events();
}
if (m_mixer[mixer::SPEAKER]) {
m_mixer[mixer::SPEAKER]->process_events();
}
if (m_mixer[mixer::HEADPHONE]) {
m_mixer[mixer::HEADPHONE]->process_events();
}
if (m_ctrl[control::HEADPHONE]) {
m_ctrl[control::HEADPHONE]->process_events();
}
// Get volume, mute and headphone state
m_volume = 100;
m_muted = false;
m_headphones = false;
try {
if (m_mixer[mixer::MASTER]) {
m_volume = m_volume * (m_mapped ? m_mixer[mixer::MASTER]->get_normalized_volume() / 100.0f
: m_mixer[mixer::MASTER]->get_volume() / 100.0f);
m_muted = m_muted || m_mixer[mixer::MASTER]->is_muted();
}
} catch (const alsa_exception& err) {
m_log.err("%s: Failed to query master mixer (%s)", name(), err.what());
}
try {
if (m_ctrl[control::HEADPHONE] && m_ctrl[control::HEADPHONE]->test_device_plugged()) {
m_headphones = true;
m_volume = m_volume * (m_mapped ? m_mixer[mixer::HEADPHONE]->get_normalized_volume() / 100.0f
: m_mixer[mixer::HEADPHONE]->get_volume() / 100.0f);
m_muted = m_muted || m_mixer[mixer::HEADPHONE]->is_muted();
}
} catch (const alsa_exception& err) {
m_log.err("%s: Failed to query headphone mixer (%s)", name(), err.what());
}
try {
if (!m_headphones && m_mixer[mixer::SPEAKER]) {
m_volume = m_volume * (m_mapped ? m_mixer[mixer::SPEAKER]->get_normalized_volume() / 100.0f
: m_mixer[mixer::SPEAKER]->get_volume() / 100.0f);
m_muted = m_muted || m_mixer[mixer::SPEAKER]->is_muted();
}
} catch (const alsa_exception& err) {
m_log.err("%s: Failed to query speaker mixer (%s)", name(), err.what());
}
// Replace label tokens
if (m_label_volume) {
m_label_volume->reset_tokens();
m_label_volume->replace_token("%percentage%", to_string(m_volume));
}
if (m_label_muted) {
m_label_muted->reset_tokens();
m_label_muted->replace_token("%percentage%", to_string(m_volume));
}
return true;
}
string alsa_module::get_format() const {
return m_muted ? FORMAT_MUTED : FORMAT_VOLUME;
}
string alsa_module::get_output() {
// Get the module output early so that
// the format prefix/suffix also gets wrapper
// with the cmd handlers
string output{module::get_output()};
if (m_handle_events) {
m_builder->action(mousebtn::LEFT, *this, EVENT_TOGGLE, "");
m_builder->action(mousebtn::SCROLL_UP, *this, EVENT_INC, "");
m_builder->action(mousebtn::SCROLL_DOWN, *this, EVENT_DEC, "");
}
m_builder->append(output);
return m_builder->flush();
}
bool alsa_module::build(builder* builder, const string& tag) const {
if (tag == TAG_BAR_VOLUME) {
builder->node(m_bar_volume->output(m_volume));
} else if (tag == TAG_RAMP_VOLUME && (!m_headphones || !*m_ramp_headphones)) {
builder->node(m_ramp_volume->get_by_percentage(m_volume));
} else if (tag == TAG_RAMP_VOLUME && m_headphones && *m_ramp_headphones) {
builder->node(m_ramp_headphones->get_by_percentage(m_volume));
} else if (tag == TAG_LABEL_VOLUME) {
builder->node(m_label_volume);
} else if (tag == TAG_LABEL_MUTED) {
builder->node(m_label_muted);
} else {
return false;
}
return true;
}
void alsa_module::action_inc() {
change_volume(m_interval);
}
void alsa_module::action_dec() {
change_volume(-m_interval);
}
void alsa_module::action_toggle() {
if (!m_mixer[mixer::MASTER]) {
return;
}
auto mixers = get_mixers();
for (auto&& mixer : mixers) {
mixer->set_mute(m_muted || mixers[0]->is_muted());
}
}
void alsa_module::change_volume(int interval) {
if (!m_mixer[mixer::MASTER]) {
return;
}
auto mixers = get_mixers();
for (auto&& mixer : mixers) {
m_mapped ? mixer->set_normalized_volume(math_util::cap<float>(mixer->get_normalized_volume() + interval, 0, 100))
: mixer->set_volume(math_util::cap<float>(mixer->get_volume() + interval, 0, 100));
}
}
void action_epilogue(const vector<mixer_t>& mixers) {
for (auto&& mixer : mixers) {
if (mixer->wait(0)) {
mixer->process_events();
}
}
}
vector<mixer_t> alsa_module::get_mixers() {
vector<mixer_t> mixers;
bool headphones{m_headphones};
if (m_mixer[mixer::MASTER] && !m_mixer[mixer::MASTER]->get_name().empty()) {
mixers.emplace_back(new mixer_t::element_type(
string{m_mixer[mixer::MASTER]->get_name()}, string{m_mixer[mixer::MASTER]->get_sound_card()}));
}
if (m_mixer[mixer::HEADPHONE] && !m_mixer[mixer::HEADPHONE]->get_name().empty() && headphones) {
mixers.emplace_back(new mixer_t::element_type(
string{m_mixer[mixer::HEADPHONE]->get_name()}, string{m_mixer[mixer::HEADPHONE]->get_sound_card()}));
}
if (m_mixer[mixer::SPEAKER] && !m_mixer[mixer::SPEAKER]->get_name().empty() && !headphones) {
mixers.emplace_back(new mixer_t::element_type(
string{m_mixer[mixer::SPEAKER]->get_name()}, string{m_mixer[mixer::SPEAKER]->get_sound_card()}));
}
return mixers;
}
} // namespace modules
POLYBAR_NS_END