diff --git a/.travis.yml b/.travis.yml index d5a311307..08783c04f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ addons: - gcc-5 - i3-wm - libasound2-dev + - libpulse-dev - libcairo2-dev - libiw-dev - libmpdclient-dev diff --git a/build.sh b/build.sh index b9b3ac45e..3c8bae0ac 100755 --- a/build.sh +++ b/build.sh @@ -30,6 +30,7 @@ function main local build_ipc_msg="ON" local enable_alsa="ON" + local enable_pulseaudio="ON" local enable_i3="ON" local enable_network="ON" local enable_mpd="ON" @@ -41,6 +42,8 @@ function main [[ "${p^^}" != "Y" ]] && enable_i3="OFF" read -r -p "$(msg "Include support for \"internal/volume\" (requires alsalib) ---------- [Y/n]: ")" -n 1 p && echo [[ "${p^^}" != "Y" ]] && enable_alsa="OFF" + read -r -p "$(msg "Include support for \"internal/pulseaudio\" (requires libpulse) ----- [Y/n]: ")" -n 1 p && echo + [[ "${p^^}" != "Y" ]] && enable_pulseaudio="OFF" read -r -p "$(msg "Include support for \"internal/network\" (requires wireless_tools) -- [Y/n]: ")" -n 1 p && echo [[ "${p^^}" != "Y" ]] && enable_network="OFF" read -r -p "$(msg "Include support for \"internal/mpd\" (requires libmpdclient) -------- [Y/n]: ")" -n 1 p && echo @@ -68,6 +71,7 @@ function main -DCMAKE_C_COMPILER="${cc}" \ -DCMAKE_CXX_COMPILER="${cxx}" \ -DENABLE_ALSA:BOOL="${enable_alsa}" \ + -DENABLE_PULSEAUDIO:BOOL="${enable_pulseaudio}"\ -DENABLE_I3:BOOL="${enable_i3}" \ -DENABLE_MPD:BOOL="${enable_mpd}" \ -DENABLE_NETWORK:BOOL="${enable_network}" \ diff --git a/cmake/02-opts.cmake b/cmake/02-opts.cmake index adefa582b..557e9a148 100644 --- a/cmake/02-opts.cmake +++ b/cmake/02-opts.cmake @@ -7,6 +7,8 @@ checklib(ENABLE_CURL "pkg-config" libcurl) checklib(ENABLE_I3 "binary" i3) checklib(ENABLE_MPD "pkg-config" libmpdclient) checklib(ENABLE_NETWORK "cmake" Libiw) +checklib(ENABLE_PULSEAUDIO "pkg-config" libpulse) +checklib(ENABLE_PULSEAUDIO "binary" pulseaudio) checklib(WITH_XRM "pkg-config" xcb-xrm) checklib(WITH_XRANDR_MONITORS "pkg-config" "xcb-randr>=1.12") checklib(WITH_XCURSOR "pkg-config" "xcb-cursor") @@ -28,6 +30,7 @@ option(ENABLE_I3 "Enable i3 support" ON) option(ENABLE_MPD "Enable mpd support" ON) option(ENABLE_NETWORK "Enable network support" ON) option(ENABLE_XKEYBOARD "Enable xkeyboard support" ON) +option(ENABLE_PULSEAUDIO "Enable PulseAudio support" ON) option(WITH_XRANDR "xcb-randr support" ON) option(WITH_XRANDR_MONITORS "xcb-randr monitor support" ON) diff --git a/cmake/03-libs.cmake b/cmake/03-libs.cmake index 44ddf0985..be5d4b54b 100644 --- a/cmake/03-libs.cmake +++ b/cmake/03-libs.cmake @@ -11,6 +11,7 @@ querylib(ENABLE_ALSA "pkg-config" alsa libs dirs) querylib(ENABLE_CURL "pkg-config" libcurl libs dirs) querylib(ENABLE_MPD "pkg-config" libmpdclient libs dirs) querylib(ENABLE_NETWORK "cmake" Libiw libs dirs) +querylib(ENABLE_PULSEAUDIO "pkg-config" libpulse libs dirs) querylib(WITH_XCOMPOSITE "pkg-config" xcb-composite libs dirs) querylib(WITH_XDAMAGE "pkg-config" xcb-damage libs dirs) diff --git a/cmake/05-summary.cmake b/cmake/05-summary.cmake index 0e3924c9c..f9e426af8 100644 --- a/cmake/05-summary.cmake +++ b/cmake/05-summary.cmake @@ -18,6 +18,7 @@ colored_option(" curl" ENABLE_CURL) colored_option(" i3" ENABLE_I3) colored_option(" mpd" ENABLE_MPD) colored_option(" network" ENABLE_NETWORK) +colored_option(" pulseaudio" ENABLE_PULSEAUDIO) message(STATUS " X extensions:") colored_option(" xcb-randr" WITH_XRANDR) diff --git a/include/adapters/pulseaudio.hpp b/include/adapters/pulseaudio.hpp new file mode 100644 index 000000000..1e76df92c --- /dev/null +++ b/include/adapters/pulseaudio.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +#include "common.hpp" +#include "settings.hpp" +#include "errors.hpp" + +#include "utils/math.hpp" +// fwd +struct pa_context; +struct pa_threaded_mainloop; +struct pa_cvolume; +typedef struct pa_context pa_context; +typedef struct pa_threaded_mainloop pa_threaded_mainloop; + +POLYBAR_NS +class logger; + +DEFINE_ERROR(pulseaudio_error); + +class pulseaudio { + // events to add to our queue + enum class evtype { NEW = 0, CHANGE, REMOVE }; + using queue = std::queue; + + public: + explicit pulseaudio(const logger& logger, string&& sink_name); + ~pulseaudio(); + + pulseaudio(const pulseaudio& o) = delete; + pulseaudio& operator=(const pulseaudio& o) = delete; + + const string& get_name(); + + bool wait(); + int process_events(); + + int get_volume(); + void set_volume(float percentage); + void inc_volume(int delta_perc); + void set_mute(bool mode); + void toggle_mute(); + bool is_muted(); + + private: + void update_volume(pa_operation *o); + static void check_mute_callback(pa_context *context, const pa_sink_info *info, int eol, void *userdata); + static void get_sink_volume_callback(pa_context *context, const pa_sink_info *info, int is_last, void *userdata); + static void subscribe_callback(pa_context* context, pa_subscription_event_type_t t, uint32_t idx, void* userdata); + static void simple_callback(pa_context *context, int success, void *userdata); + static void get_default_sink_callback(pa_context *context, const pa_server_info *info, void *userdata); + static void sink_info_callback(pa_context *context, const pa_sink_info *info, int eol, void *userdata); + static void context_state_callback(pa_context *context, void *userdata); + + inline void wait_loop(pa_operation *op, pa_threaded_mainloop *loop); + + const logger& m_log; + + // used for temporary callback results + int success{0}; + pa_cvolume cv; + bool muted{false}; + // default sink name + string def_s_name; + + pa_context* m_context{nullptr}; + pa_threaded_mainloop* m_mainloop{nullptr}; + + queue m_events; + + // specified sink name + string spec_s_name; + string s_name; + uint32_t m_index{0}; +}; + +POLYBAR_NS_END diff --git a/include/modules/meta/factory.hpp b/include/modules/meta/factory.hpp index 9c6ca9d82..bd94482e5 100644 --- a/include/modules/meta/factory.hpp +++ b/include/modules/meta/factory.hpp @@ -34,13 +34,16 @@ #if ENABLE_ALSA #include "modules/volume.hpp" #endif +#if ENABLE_PULSEAUDIO +#include "modules/pulseaudio.hpp" +#endif #if ENABLE_CURL #include "modules/github.hpp" #endif #if ENABLE_XKEYBOARD #include "modules/xkeyboard.hpp" #endif -#if not(ENABLE_I3 && ENABLE_MPD && ENABLE_NETWORK && ENABLE_ALSA && ENABLE_CURL && ENABLE_XKEYBOARD) +#if not(ENABLE_I3 && ENABLE_MPD && ENABLE_NETWORK && ENABLE_ALSA && ENABLE_PULSEAUDIO && ENABLE_CURL && ENABLE_XKEYBOARD) #include "modules/unsupported.hpp" #endif @@ -74,6 +77,8 @@ namespace { return new mpd_module(bar, move(module_name)); } else if (name == "internal/volume") { return new volume_module(bar, move(module_name)); + } else if (name == "internal/pulseaudio") { + return new pulseaudio_module(bar, move(module_name)); } else if (name == "internal/network") { return new network_module(bar, move(module_name)); #if DEBUG diff --git a/include/modules/pulseaudio.hpp b/include/modules/pulseaudio.hpp new file mode 100644 index 000000000..02331c637 --- /dev/null +++ b/include/modules/pulseaudio.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "settings.hpp" +#include "modules/meta/event_module.hpp" +#include "modules/meta/input_handler.hpp" + +POLYBAR_NS + +// fwd +class pulseaudio; + +namespace modules { + using pulseaudio_t = shared_ptr; + + class pulseaudio_module : public event_module, public input_handler { + public: + explicit pulseaudio_module(const bar_settings&, string); + + void teardown(); + bool has_event(); + bool update(); + string get_format() const; + string get_output(); + bool build(builder* builder, const string& tag) const; + + protected: + bool input(string&& cmd); + + private: + static constexpr auto FORMAT_VOLUME = "format-volume"; + static constexpr auto FORMAT_MUTED = "format-muted"; + + static constexpr auto TAG_RAMP_VOLUME = ""; + static constexpr auto TAG_BAR_VOLUME = ""; + 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"; + + progressbar_t m_bar_volume; + ramp_t m_ramp_volume; + label_t m_label_volume; + label_t m_label_muted; + + pulseaudio_t m_pulseaudio; + + atomic m_muted{false}; + atomic m_volume{0}; + }; +} + +POLYBAR_NS_END diff --git a/include/modules/unsupported.hpp b/include/modules/unsupported.hpp index c391a1c2d..398229004 100644 --- a/include/modules/unsupported.hpp +++ b/include/modules/unsupported.hpp @@ -1,5 +1,5 @@ #pragma once -#if ENABLE_I3 && ENABLE_MPD && ENABLE_NETWORK && ENABLE_ALSA && ENABLE_CURL && ENABLE_XKEYBOARD +#if ENABLE_I3 && ENABLE_MPD && ENABLE_NETWORK && ENABLE_ALSA && ENABLE_PULSEAUDIO && ENABLE_CURL && ENABLE_XKEYBOARD #error "Support has been enabled for all optional modules" #endif @@ -43,6 +43,9 @@ namespace modules { #if not ENABLE_ALSA DEFINE_UNSUPPORTED_MODULE(volume_module, "internal/volume"); #endif +#if not ENABLE_PULSEAUDIO + DEFINE_UNSUPPORTED_MODULE(pulseaudio_module, "internal/pulseaudio"); +#endif #if not ENABLE_CURL DEFINE_UNSUPPORTED_MODULE(github_module, "internal/github"); #endif diff --git a/include/settings.hpp.cmake b/include/settings.hpp.cmake index 00d318977..22fce3d61 100644 --- a/include/settings.hpp.cmake +++ b/include/settings.hpp.cmake @@ -22,6 +22,7 @@ #cmakedefine01 ENABLE_NETWORK #cmakedefine01 ENABLE_I3 #cmakedefine01 ENABLE_CURL +#cmakedefine01 ENABLE_PULSEAUDIO #cmakedefine01 WITH_XRANDR #cmakedefine01 WITH_XRENDER @@ -99,12 +100,13 @@ const auto version_details = [](const std::vector& args) { // clang-format off const auto print_build_info = [](bool extended = false) { printf("%s %s\n\n", APP_NAME, APP_VERSION); - printf("Features: %calsa %ccurl %ci3 %cmpd %cnetwork\n", + printf("Features: %calsa %ccurl %ci3 %cmpd %cnetwork %cpulseaudio\n", (ENABLE_ALSA ? '+' : '-'), (ENABLE_CURL ? '+' : '-'), (ENABLE_I3 ? '+' : '-'), (ENABLE_MPD ? '+' : '-'), - (ENABLE_NETWORK ? '+' : '-')); + (ENABLE_NETWORK ? '+' : '-'), + (ENABLE_PULSEAUDIO ? '+' : '-')); if (extended) { printf("\n"); printf("X extensions: %crandr (%cmonitors) %crender %cdamage %csync %ccomposite %cxkb %cxrm %cxcursor\n", diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d2ef77fd5..f49aa88d9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -28,6 +28,10 @@ if(NOT ENABLE_I3) list(REMOVE_ITEM files modules/i3.cpp) list(REMOVE_ITEM files utils/i3.cpp) endif() +if(NOT ENABLE_PULSEAUDIO) + list(REMOVE_ITEM files modules/pulseaudio.cpp) + list(REMOVE_ITEM files adapters/pulseaudio.cpp) +endif() if(NOT WITH_XRANDR) list(REMOVE_ITEM files x11/extensions/randr.cpp) endif() diff --git a/src/adapters/pulseaudio.cpp b/src/adapters/pulseaudio.cpp new file mode 100644 index 000000000..5ec3948ba --- /dev/null +++ b/src/adapters/pulseaudio.cpp @@ -0,0 +1,326 @@ +#include "adapters/pulseaudio.hpp" +#include "components/logger.hpp" + +POLYBAR_NS + +/** + * Construct pulseaudio object + */ +pulseaudio::pulseaudio(const logger& logger, string&& sink_name) : m_log(logger), spec_s_name(sink_name) { + m_mainloop = pa_threaded_mainloop_new(); + if (!m_mainloop) { + throw pulseaudio_error("Could not create pulseaudio threaded mainloop."); + } + pa_threaded_mainloop_lock(m_mainloop); + + m_context = pa_context_new(pa_threaded_mainloop_get_api(m_mainloop), "polybar"); + if (!m_context) { + pa_threaded_mainloop_unlock(m_mainloop); + pa_threaded_mainloop_free(m_mainloop); + throw pulseaudio_error("Could not create pulseaudio context."); + } + + pa_context_set_state_callback(m_context, context_state_callback, this); + + if (pa_context_connect(m_context, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0) { + pa_context_disconnect(m_context); + pa_context_unref(m_context); + pa_threaded_mainloop_unlock(m_mainloop); + pa_threaded_mainloop_free(m_mainloop); + throw pulseaudio_error("Could not connect pulseaudio context."); + } + + if (pa_threaded_mainloop_start(m_mainloop) < 0) { + pa_context_disconnect(m_context); + pa_context_unref(m_context); + pa_threaded_mainloop_unlock(m_mainloop); + pa_threaded_mainloop_free(m_mainloop); + throw pulseaudio_error("Could not start pulseaudio mainloop."); + } + + m_log.trace("pulseaudio: started mainloop"); + + pa_threaded_mainloop_wait(m_mainloop); + if (pa_context_get_state(m_context) != PA_CONTEXT_READY) { + pa_threaded_mainloop_unlock(m_mainloop); + pa_threaded_mainloop_stop(m_mainloop); + pa_context_disconnect(m_context); + pa_context_unref(m_context); + pa_threaded_mainloop_free(m_mainloop); + throw pulseaudio_error("Could not connect to pulseaudio server."); + } + + pa_operation *op{nullptr}; + if (!sink_name.empty()) { + op = pa_context_get_sink_info_by_name(m_context, sink_name.c_str(), sink_info_callback, this); + wait_loop(op, m_mainloop); + } + if (s_name.empty()) { + op = pa_context_get_server_info(m_context, get_default_sink_callback, this); + if (!op) { + throw pulseaudio_error("Failed to get pulseaudio server info."); + } + wait_loop(op, m_mainloop); + if (def_s_name.empty()) + throw pulseaudio_error("Failed to get default sink."); + // get the sink index + op = pa_context_get_sink_info_by_name(m_context, def_s_name.c_str(), sink_info_callback, this); + wait_loop(op, m_mainloop); + m_log.warn("pulseaudio: using default sink %s", s_name); + } else { + m_log.trace("pulseaudio: using sink %s", s_name); + } + + op = pa_context_subscribe(m_context, PA_SUBSCRIPTION_MASK_SINK, simple_callback, this); + wait_loop(op, m_mainloop); + if (!success) + throw pulseaudio_error("Failed to subscribe to server."); + pa_context_set_subscribe_callback(m_context, subscribe_callback, this); + + update_volume(op); + + pa_threaded_mainloop_unlock(m_mainloop); + +} + +/** + * Deconstruct pulseaudio + */ +pulseaudio::~pulseaudio() { + pa_threaded_mainloop_stop(m_mainloop); + pa_context_disconnect(m_context); + pa_context_unref(m_context); + pa_threaded_mainloop_free(m_mainloop); + +} + +/** + * Get sink name + */ +const string& pulseaudio::get_name() { + return s_name; +} + +/** + * Wait for events + */ +bool pulseaudio::wait() { + return m_events.size() > 0; +} + +/** + * Process queued pulseaudio events + */ +int pulseaudio::process_events() { + int ret = m_events.size(); + pa_threaded_mainloop_lock(m_mainloop); + pa_operation *o{nullptr}; + // clear the queue + while (!m_events.empty()) { + switch (m_events.front()) { + // try to get specified sink + case evtype::NEW: + // redundant if already using specified sink + if (!spec_s_name.empty()) { + o = pa_context_get_sink_info_by_name(m_context, spec_s_name.c_str(), sink_info_callback, this); + wait_loop(o, m_mainloop); + } + break; + // get default sink + case evtype::REMOVE: + o = pa_context_get_server_info(m_context, get_default_sink_callback, this); + wait_loop(o, m_mainloop); + if (def_s_name.empty()) + throw pulseaudio_error("Failed to get default sink."); + o = pa_context_get_sink_info_by_name(m_context, def_s_name.c_str(), sink_info_callback, this); + wait_loop(o, m_mainloop); + if (spec_s_name != s_name) + m_log.warn("pulseaudio: using default sink %s", s_name); + break; + default: + break; + } + update_volume(o); + m_events.pop(); + } + pa_threaded_mainloop_unlock(m_mainloop); + return ret; +} + +/** + * Get volume in percentage + */ +int pulseaudio::get_volume() { + // alternatively, user pa_cvolume_avg_mask() to average selected channels + return static_cast(pa_cvolume_max(&cv) * 100.0f / PA_VOLUME_NORM + 0.5f); +} + +/** + * Set volume to given percentage + */ +void pulseaudio::set_volume(float percentage) { + pa_threaded_mainloop_lock(m_mainloop); + pa_volume_t vol = math_util::percentage_to_value(percentage, PA_VOLUME_MUTED, PA_VOLUME_NORM); + pa_cvolume_scale(&cv, vol); + pa_operation *op = pa_context_set_sink_volume_by_index(m_context, m_index, &cv, simple_callback, this); + wait_loop(op, m_mainloop); + if (!success) + throw pulseaudio_error("Failed to set sink volume."); + pa_threaded_mainloop_unlock(m_mainloop); +} + +/** + * Increment or decrement volume by given percentage (prevents accumulation of rounding errors from get_volume) + */ +void pulseaudio::inc_volume(int delta_perc) { + pa_threaded_mainloop_lock(m_mainloop); + pa_volume_t vol = math_util::percentage_to_value(abs(delta_perc), PA_VOLUME_NORM); + if (delta_perc > 0) { + if (pa_cvolume_max(&cv) + vol <= PA_VOLUME_UI_MAX) { + pa_cvolume_inc(&cv, vol); + } else { + m_log.warn("pulseaudio: maximum volume reached"); + } + } else + pa_cvolume_dec(&cv, vol); + pa_operation *op = pa_context_set_sink_volume_by_index(m_context, m_index, &cv, simple_callback, this); + wait_loop(op, m_mainloop); + if (!success) + throw pulseaudio_error("Failed to set sink volume."); + pa_threaded_mainloop_unlock(m_mainloop); +} + +/** + * Set mute state + */ +void pulseaudio::set_mute(bool mode) { + pa_threaded_mainloop_lock(m_mainloop); + pa_operation *op = pa_context_set_sink_mute_by_index(m_context, m_index, mode, simple_callback, this); + wait_loop(op, m_mainloop); + if (!success) + throw pulseaudio_error("Failed to mute sink."); + pa_threaded_mainloop_unlock(m_mainloop); +} + +/** + * Toggle mute state + */ +void pulseaudio::toggle_mute() { + set_mute(!is_muted()); +} + +/** + * Get current mute state + */ +bool pulseaudio::is_muted() { + return muted; +} + +/** + * Update local volume cache + */ +void pulseaudio::update_volume(pa_operation *o) { + o = pa_context_get_sink_info_by_index(m_context, m_index, get_sink_volume_callback, this); + wait_loop(o, m_mainloop); +} + +/** + * Callback when getting volume + */ +void pulseaudio::get_sink_volume_callback(pa_context *, const pa_sink_info *info, int, void *userdata) { + pulseaudio* This = static_cast(userdata); + if (info) { + This->cv = info->volume; + This->muted = info->mute; + } + pa_threaded_mainloop_signal(This->m_mainloop, 0); +} + +/** + * Callback when subscribing to changes + */ +void pulseaudio::subscribe_callback(pa_context *, pa_subscription_event_type_t t, uint32_t idx, void* userdata) { + pulseaudio *This = static_cast(userdata); + if (idx == PA_INVALID_INDEX) + return; + switch(t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { + case PA_SUBSCRIPTION_EVENT_SINK: + switch(t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) { + case PA_SUBSCRIPTION_EVENT_NEW: + This->m_events.emplace(evtype::NEW); + break; + case PA_SUBSCRIPTION_EVENT_CHANGE: + if (idx == This->m_index) + This->m_events.emplace(evtype::CHANGE); + break; + case PA_SUBSCRIPTION_EVENT_REMOVE: + if (idx == This->m_index) + This->m_events.emplace(evtype::REMOVE); + break; + } + break; + } + pa_threaded_mainloop_signal(This->m_mainloop, 0); +} + +/** + * Simple callback to check for success + */ +void pulseaudio::simple_callback(pa_context *, int success, void *userdata) { + pulseaudio *This = static_cast(userdata); + This->success = success; + pa_threaded_mainloop_signal(This->m_mainloop, 0); +} + +/** + * Callback when getting default sink name + */ +void pulseaudio::get_default_sink_callback(pa_context *, const pa_server_info *info, void *userdata) { + pulseaudio *This = static_cast(userdata); + if (info->default_sink_name) { + This->def_s_name = info->default_sink_name; + This->s_name = info->default_sink_name; + } else + This->def_s_name = ""s; + pa_threaded_mainloop_signal(This->m_mainloop, 0); +} + +/** + * Callback when getting sink info & existence + */ +void pulseaudio::sink_info_callback(pa_context *, const pa_sink_info *info, int eol, void *userdata) { + pulseaudio *This = static_cast(userdata); + if (!eol && info) { + This->m_index = info->index; + This->s_name = info->name; + } + pa_threaded_mainloop_signal(This->m_mainloop, 0); +} + +/** + * Callback when context state changes + */ +void pulseaudio::context_state_callback(pa_context *context, void *userdata) { + pulseaudio* This = static_cast(userdata); + switch (pa_context_get_state(context)) { + case PA_CONTEXT_READY: + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + pa_threaded_mainloop_signal(This->m_mainloop, 0); + break; + + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + } +} + +inline void pulseaudio::wait_loop(pa_operation *op, pa_threaded_mainloop *loop) { + while (pa_operation_get_state(op) != PA_OPERATION_DONE) + pa_threaded_mainloop_wait(loop); + pa_operation_unref(op); +} + +POLYBAR_NS_END diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp new file mode 100644 index 000000000..f65523cfc --- /dev/null +++ b/src/modules/pulseaudio.cpp @@ -0,0 +1,154 @@ +#include "modules/pulseaudio.hpp" +#include "adapters/pulseaudio.hpp" +#include "drawtypes/label.hpp" +#include "drawtypes/progressbar.hpp" +#include "drawtypes/ramp.hpp" +#include "utils/math.hpp" + +#include "modules/meta/base.inl" + +#include "settings.hpp" + +POLYBAR_NS + +namespace modules { + template class module; + + pulseaudio_module::pulseaudio_module(const bar_settings& bar, string name_) : event_module(bar, move(name_)) { + // Load configuration values + auto sink_name = m_conf.get(name(), "sink", ""s); + try { + m_pulseaudio = factory_util::unique(m_log, move(sink_name)); + } catch (const pulseaudio_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); + } + } + + void pulseaudio_module::teardown() { + m_pulseaudio.reset(); + } + + bool pulseaudio_module::has_event() { + // Poll for mixer and control events + try { + if (m_pulseaudio->wait()) + return true; + } catch (const pulseaudio_error& e) { + m_log.err("%s: %s", name(), e.what()); + } + return false; + } + + bool pulseaudio_module::update() { + // Consume pending events + m_pulseaudio->process_events(); + + // Get volume and mute state + m_volume = 100; + m_muted = false; + + try { + if (m_pulseaudio) { + m_volume = m_volume * m_pulseaudio->get_volume() / 100.0f; + m_muted = m_muted || m_pulseaudio->is_muted(); + } + } catch (const pulseaudio_error& err) { + m_log.err("%s: Failed to query pulseaudio sink (%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 pulseaudio_module::get_format() const { + return m_muted ? FORMAT_MUTED : FORMAT_VOLUME; + } + + string pulseaudio_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->cmd(mousebtn::LEFT, EVENT_TOGGLE_MUTE); + m_builder->cmd(mousebtn::SCROLL_UP, EVENT_VOLUME_UP); + m_builder->cmd(mousebtn::SCROLL_DOWN, EVENT_VOLUME_DOWN); + } + + m_builder->append(output); + + return m_builder->flush(); + } + + bool pulseaudio_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) { + builder->node(m_ramp_volume->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; + } + + bool pulseaudio_module::input(string&& cmd) { + if (!m_handle_events) { + return false; + } else if (cmd.compare(0, 3, EVENT_PREFIX) != 0) { + return false; + } + + try { + if (m_pulseaudio && !m_pulseaudio->get_name().empty()) { + if (cmd.compare(0, strlen(EVENT_TOGGLE_MUTE), EVENT_TOGGLE_MUTE) == 0) { + m_pulseaudio->toggle_mute(); + } else if (cmd.compare(0, strlen(EVENT_VOLUME_UP), EVENT_VOLUME_UP) == 0) { + // cap above 100 (~150)? + m_pulseaudio->inc_volume(5); + } else if (cmd.compare(0, strlen(EVENT_VOLUME_DOWN), EVENT_VOLUME_DOWN) == 0) { + m_pulseaudio->inc_volume(-5); + } else { + return false; + } + } + } catch (const exception& err) { + m_log.err("%s: Failed to handle command (%s)", name(), err.what()); + } + + return true; + } +} + +POLYBAR_NS_END