diff --git a/include/factory.hpp b/include/factory.hpp index 47ef530..a75f3dd 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -67,6 +67,9 @@ #include "modules/bluetooth.hpp" #include "modules/inhibitor.hpp" #endif +#ifdef HAVE_LIBJACK +#include "modules/jack.hpp" +#endif #include "bar.hpp" #include "modules/custom.hpp" #include "modules/temperature.hpp" diff --git a/include/modules/jack.hpp b/include/modules/jack.hpp new file mode 100644 index 0000000..a3555be --- /dev/null +++ b/include/modules/jack.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class JACK : public ALabel { + public: + JACK(const std::string&, const Json::Value&); + ~JACK() = default; + auto update() -> void; + + int bufSize(jack_nframes_t size); + int sampleRate(jack_nframes_t rate); + int xrun(); + void shutdown(); + + private: + std::string JACKState(); + + jack_client_t* client_; + jack_nframes_t bufsize_; + jack_nframes_t samplerate_; + unsigned int xruns_; + float load_; + bool running_; + std::mutex mutex_; + std::string state_; + util::SleeperThread thread_; +}; + +} // namespace waybar::modules + +int bufSizeCallback(jack_nframes_t size, void *obj); +int sampleRateCallback(jack_nframes_t rate, void *obj); +int xrunCallback(void *obj); +void shutdownCallback(void *obj); diff --git a/man/waybar-jack.5.scd b/man/waybar-jack.5.scd new file mode 100644 index 0000000..b8a4ceb --- /dev/null +++ b/man/waybar-jack.5.scd @@ -0,0 +1,112 @@ +waybar-jack(5) + +# NAME + +waybar - JACK module + +# DESCRIPTION + +The *jack* module displays the current state of the JACK server. + +# CONFIGURATION + +Addressed by *jack* + +*format*: ++ + typeof: string ++ + default: *{load}%* ++ + The format, how information should be displayed. This format is used when other formats aren't specified. + +*format-connected*: ++ + typeof: string ++ + This format is used when the module is connected to the JACK server. + +*format-disconnected*: ++ + typeof: string ++ + This format is used when the module is not connected to the JACK server. + +*format-xrun*: ++ + typeof: string ++ + This format is used for one polling interval, when the JACK server reports an xrun. + +*realtime*: ++ + typeof: bool ++ + default: *true* ++ + Option to drop real-time privileges for the JACK client opened by Waybar. + +*tooltip*: ++ + typeof: bool ++ + default: *true* ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + default: *{bufsize}/{samplerate} {latency}ms* ++ + The format of information displayed in the tooltip. + +*interval*: ++ + typeof: integer ++ + default: 1 ++ + The interval in which the information gets polled. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label. + +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right clicked on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +# FORMAT REPLACEMENTS + +*{load}*: The current CPU load estimated by JACK. + +*{bufsize}*: The size of the JACK buffer. + +*{samplerate}*: The samplerate at which the JACK server is running. + +*{latency}*: The duration, in ms, of the current buffer size. + +*{xruns}*: The number of xruns reported by the JACK server since starting Waybar. + +# EXAMPLES + +``` +"jack": { + "format": "DSP {}%", + "format-xrun": "{xruns} xruns", + "format-disconnected": "DSP off", + "realtime": true +} +``` + +# STYLE + +- *#jack* +- *#jack.connected* +- *#jack.disconnected* +- *#jack.xrun* diff --git a/meson.build b/meson.build index 3c32006..0a48e97 100644 --- a/meson.build +++ b/meson.build @@ -98,6 +98,7 @@ libudev = dependency('libudev', required: get_option('libudev')) libevdev = dependency('libevdev', required: get_option('libevdev')) libmpdclient = dependency('libmpdclient', required: get_option('mpd')) xkbregistry = dependency('xkbregistry') +libjack = dependency('jack', required: get_option('jack')) libsndio = compiler.find_library('sndio', required: get_option('sndio')) if libsndio.found() @@ -228,6 +229,11 @@ if libpulse.found() src_files += 'src/modules/pulseaudio.cpp' endif +if libjack.found() + add_project_arguments('-DHAVE_LIBJACK', language: 'cpp') + src_files += 'src/modules/jack.cpp' +endif + if dbusmenu_gtk.found() add_project_arguments('-DHAVE_DBUSMENU', language: 'cpp') src_files += files( @@ -308,6 +314,7 @@ executable( libnlgen, upower_glib, libpulse, + libjack, libudev, libepoll, libmpdclient, diff --git a/meson_options.txt b/meson_options.txt index d2e9847..b8eb921 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -14,3 +14,4 @@ option('sndio', type: 'feature', value: 'auto', description: 'Enable support for option('logind', type: 'feature', value: 'auto', description: 'Enable support for logind') option('tests', type: 'feature', value: 'auto', description: 'Enable tests') option('experimental', type : 'boolean', value : false, description: 'Enable experimental features') +option('jack', type: 'feature', value: 'auto', description: 'Enable support for JACK') diff --git a/src/factory.cpp b/src/factory.cpp index 6df69d5..c53e26a 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -123,6 +123,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "inhibitor") { return new waybar::modules::Inhibitor(id, bar_, config_[name]); } +#endif +#ifdef HAVE_LIBJACK + if (ref == "jack") { + return new waybar::modules::JACK(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); diff --git a/src/modules/jack.cpp b/src/modules/jack.cpp new file mode 100644 index 0000000..3a92110 --- /dev/null +++ b/src/modules/jack.cpp @@ -0,0 +1,126 @@ +#include "modules/jack.hpp" + +namespace waybar::modules { + +JACK::JACK(const std::string &id, const Json::Value &config) + : ALabel(config, "jack", id, "{load}%", 1) { + running_ = false; + client_ = NULL; + + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; +} + +std::string JACK::JACKState() { + std::lock_guard lock(mutex_); + if (running_) { + load_ = jack_cpu_load(client_); + return state_; + } + + xruns_ = 0; + load_ = 0; + bufsize_ = 0; + samplerate_ = 0; + + if (client_) { + jack_client_close(client_); + client_ = NULL; + } + + client_ = jack_client_open("waybar", JackNoStartServer, NULL); + if (!client_) return "disconnected"; + + if (config_["realtime"].isBool() && !config_["realtime"].asBool()) { + pthread_t jack_thread = jack_client_thread_id(client_); + jack_drop_real_time_scheduling(jack_thread); + } + + bufsize_ = jack_get_buffer_size(client_); + samplerate_ = jack_get_sample_rate(client_); + jack_set_sample_rate_callback(client_, sampleRateCallback, this); + jack_set_buffer_size_callback(client_, bufSizeCallback, this); + jack_set_xrun_callback(client_, xrunCallback, this); + jack_on_shutdown(client_, shutdownCallback, this); + if (jack_activate(client_)) return "disconnected"; + + running_ = true; + return "connected"; +} + +auto JACK::update() -> void { + std::string format; + std::string state = JACKState(); + float latency = 1000 * (float)bufsize_ / (float)samplerate_; + + if (label_.get_style_context()->has_class("xrun")) { + label_.get_style_context()->remove_class("xrun"); + state = "connected"; + } + + if (label_.get_style_context()->has_class(state_)) + label_.get_style_context()->remove_class(state_); + label_.get_style_context()->add_class(state); + state_ = state; + + if (config_["format-" + state].isString()) { + format = config_["format-" + state].asString(); + } else if (config_["format"].isString()) { + format = config_["format"].asString(); + } else + format = "{load}%"; + + label_.set_markup(fmt::format(format, fmt::arg("load", std::round(load_)), + fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_), + fmt::arg("latency", fmt::format("{:.2f}", latency)), + fmt::arg("xruns", xruns_))); + + if (tooltipEnabled()) { + std::string tooltip_format = "{bufsize}/{samplerate} {latency}ms"; + if (config_["tooltip-format"].isString()) tooltip_format = config_["tooltip-format"].asString(); + label_.set_tooltip_text(fmt::format( + tooltip_format, fmt::arg("load", std::round(load_)), fmt::arg("bufsize", bufsize_), + fmt::arg("samplerate", samplerate_), fmt::arg("latency", fmt::format("{:.2f}", latency)), + fmt::arg("xruns", xruns_))); + } + + // Call parent update + ALabel::update(); +} + +int JACK::bufSize(jack_nframes_t size) { + bufsize_ = size; + return 0; +} + +int JACK::sampleRate(jack_nframes_t rate) { + samplerate_ = rate; + return 0; +} + +int JACK::xrun() { + xruns_ += 1; + state_ = "xrun"; + return 0; +} + +void JACK::shutdown() { + std::lock_guard lock(mutex_); + running_ = false; +} + +} // namespace waybar::modules + +int bufSizeCallback(jack_nframes_t size, void *obj) { + return static_cast(obj)->bufSize(size); +} + +int sampleRateCallback(jack_nframes_t rate, void *obj) { + return static_cast(obj)->sampleRate(rate); +} + +int xrunCallback(void *obj) { return static_cast(obj)->xrun(); } + +void shutdownCallback(void *obj) { return static_cast(obj)->shutdown(); }