diff --git a/README.md b/README.md index 19171494..ba534625 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/config b/examples/config index bd5adcab..9c6950a9 100644 --- a/examples/config +++ b/examples/config @@ -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 diff --git a/include/interfaces/alsa.hpp b/include/interfaces/alsa.hpp index 5a678d0e..057138a0 100644 --- a/include/interfaces/alsa.hpp +++ b/include/interfaces/alsa.hpp @@ -7,11 +7,15 @@ #include #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); }; + + // }}} } diff --git a/include/modules/volume.hpp b/include/modules/volume.hpp index 6c85a5cc..b089e405 100644 --- a/include/modules/volume.hpp +++ b/include/modules/volume.hpp @@ -19,18 +19,11 @@ namespace modules static constexpr auto TAG_LABEL_VOLUME = ""; static constexpr auto TAG_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 master_mixer; - std::unique_ptr speaker_mixer; - std::unique_ptr headphone_mixer; - std::unique_ptr headphone_ctrl; - int headphone_ctrl_numid; - - std::unique_ptr builder; - std::unique_ptr bar_volume; std::unique_ptr ramp_volume; std::unique_ptr label_volume; @@ -38,8 +31,16 @@ namespace modules std::unique_ptr label_muted; std::unique_ptr label_muted_tokenized; - int volume = 0; - bool muted = false; + std::unique_ptr master_mixer; + std::unique_ptr speaker_mixer; + std::unique_ptr headphone_mixer; + std::unique_ptr headphone_ctrl; + + int headphone_ctrl_numid; + + concurrency::Atomic volume; + concurrency::Atomic muted; + concurrency::Atomic has_changed; public: explicit VolumeModule(const std::string& name); diff --git a/include/utils/concurrency.hpp b/include/utils/concurrency.hpp index 8f0970cc..9cef07b1 100644 --- a/include/utils/concurrency.hpp +++ b/include/utils/concurrency.hpp @@ -56,7 +56,6 @@ namespace concurrency template class Atomic { - concurrency::SpinLock lock; std::atomic value; public: @@ -67,25 +66,21 @@ namespace concurrency void operator=(T value) { - std::lock_guard lck(this->lock); this->value = value; } T operator()() { - std::lock_guard lck(this->lock); return this->value; } operator bool() { - std::lock_guard lck(this->lock); return this->value; } bool operator==(T const& b) { - std::lock_guard lck(this->lock); return this->value == b; } }; diff --git a/src/eventloop.cpp b/src/eventloop.cpp index 7acf4a0a..acb8dcfa 100644 --- a/src/eventloop.cpp +++ b/src/eventloop.cpp @@ -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; diff --git a/src/interfaces/alsa.cpp b/src/interfaces/alsa.cpp index 0e5823d2..8266e8ef 100644 --- a/src/interfaces/alsa.cpp +++ b/src/interfaces/alsa.cpp @@ -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 lck(this->mtx); + ControlInterface::~ControlInterface() + { + std::lock_guard lck(this->lock); + snd_ctl_close(this->ctl); snd_hctl_close(this->hctl); } bool ControlInterface::wait(int timeout) { - std::lock_guard lck(this->mtx); + std::lock_guard 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 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 lck(this->mtx); + Mixer::~Mixer() + { + std::lock_guard 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 lck(this->mtx); + assert(this->hardware_mixer); - int err, pend_n = 0; + std::lock_guard 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 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 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 lck(this->lock); + snd_mixer_selem_set_playback_switch_all(this->mixer_element, mode); } void Mixer::toggle_mute() { + std::lock_guard 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 lck(this->lock); + int state = 0; repeat(SND_MIXER_SCHN_LAST) { @@ -179,6 +206,9 @@ namespace alsa return true; } } + return false; } + + // }}} } diff --git a/src/modules/volume.cpp b/src/modules/volume.cpp index 8ca9d05f..229922f1 100644 --- a/src/modules/volume.cpp +++ b/src/modules/volume.cpp @@ -8,6 +8,8 @@ using namespace modules; VolumeModule::VolumeModule(const std::string& name_) : EventModule(name_) { + // Load configuration values {{{ + auto master_mixer = config::get(name(), "master_mixer", "Master"); auto speaker_mixer = config::get(name(), "speaker_mixer", ""); auto headphone_mixer = config::get(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 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(); - + // 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 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 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(master_mixer->get_volume() + 5, 0, 100)); + + if (other_mixer != nullptr) + other_mixer->set_volume(math::cap(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(master_mixer->get_volume() - 5, 0, 100)); + + if (other_mixer != nullptr) + other_mixer->set_volume(math::cap(other_mixer->get_volume() - 5, 0, 100)); - if (cmd == EVENT_VOLUME_UP) { - auto vol = math::cap(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(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; }