From 5c979152de965739e525eaafd66ca40839f08223 Mon Sep 17 00:00:00 2001 From: Viktar Lukashonak Date: Mon, 13 Jul 2026 19:50:09 +0300 Subject: [PATCH] refactor(cava): fix thread-safety, resource leaks, and style violations Comprehensive refactor of the cava module backend and frontends. Style & naming - Rename Cava -> CavaRaw; snake_case methods -> lowerCamelCase - Replace NULL with nullptr; replace C-style casts with static_cast - Add explicit standard-library includes (, , ) - Fix missing trailing underscores on member variables Architecture - Remove Gtk::GLArea multiple inheritance in CavaGLSL (composition) - Return std::unique_ptr from factory; add doAction() to GLSL variant - Encapsulate thread timing arithmetic in AdaptiveDelay struct Thread safety & correctness - Replace raw sigc::signal with SafeSignal for cross-thread marshalling - Fix data race between loadConfig() and out_thread_ (recursive_mutex) - Fix audio_raw shallow-copy use-after-free via deep-copy AudioRaw payload - Make loadConfig() exception-safe with CavaConfigGuard RAII - Fix blocking read_thread_ race on shutdown (condition_variable + timeout join) - Fix isSilent() data race (acquire pthread mutex) - Eliminate doUpdate() recursion (iteration instead) - Guard audio_raw_clean() against uninitialized state - Fix format-icons underflow and cava_config buffer overflow - Store config by value to prevent dangling references on reload - Cache frontend config and refresh on runtime changes - Fix signed-char icon lookup bug on x86 - Prevent Json::Value mutation bloat via const-ref lookups - Broaden exception catches in worker threads (std::exception) Resource management & GL robustness - Fix OpenGL resource leaks (persist VBO/IBO/VAO; explicit destructor cleanup) - Fix shader error handling crashes (valid infoLog allocation) - Cache uniform locations instead of querying per frame - Fix gradient color uninitialized stack memory (zero-init + clamped count) - Fix shader time uniform integer division bug (float arithmetic) - Add explicit VAO bind in onRender() - Handle runtime surface config changes independently of shader changes - Clamp negative gradient_count before GL upload Follow-up - Singleton API split (inst() + configure()) intentionally deferred to a dedicated PR because it changes the public constructor contract. --- include/modules/cava/cavaGLSL.hpp | 56 +++- include/modules/cava/cavaRaw.hpp | 26 +- include/modules/cava/cava_backend.hpp | 111 ++++++-- include/modules/cava/cava_frontend.hpp | 10 +- src/factory.cpp | 2 +- src/modules/cava/cavaGLSL.cpp | 363 +++++++++++++++++------- src/modules/cava/cavaRaw.cpp | 87 +++--- src/modules/cava/cava_backend.cpp | 376 +++++++++++++++---------- 8 files changed, 692 insertions(+), 339 deletions(-) diff --git a/include/modules/cava/cavaGLSL.hpp b/include/modules/cava/cavaGLSL.hpp index d2632aff..5372d054 100644 --- a/include/modules/cava/cavaGLSL.hpp +++ b/include/modules/cava/cavaGLSL.hpp @@ -1,36 +1,64 @@ #pragma once #include +#include +#include +#include +#include + +#include #include "AModule.hpp" #include "cava_backend.hpp" namespace waybar::modules::cava { -class CavaGLSL final : public AModule, public Gtk::GLArea { +class CavaGLSL final : public AModule { public: CavaGLSL(const std::string&, const Json::Value&); - ~CavaGLSL() = default; + ~CavaGLSL(); + auto doAction(const std::string& name) -> void override; private: + using Action = void (CavaGLSL::*)(); + + Gtk::GLArea gl_area_; std::shared_ptr backend_; - struct ::cava::config_params prm_; - int frame_counter{0}; + // Cached config params (deep-copied strings to avoid dangling char* on backend reload) + int sdl_width_{0}; + int sdl_height_{0}; + int bar_width_{0}; + int bar_spacing_{0}; + int gradient_count_{0}; + std::string vertex_shader_; + std::string fragment_shader_; + std::string bcolor_; + std::string color_; + std::array gradient_colors_; + int frame_counter_{0}; bool silence_{false}; bool hide_on_silence_{false}; + bool mapped_{false}; // Cava method - auto onUpdate(const ::cava::audio_raw& input) -> void; + void pauseResume(); + auto onUpdate(const CavaBackend::AudioRaw& input) -> void; auto onSilence() -> void; - // Member variable to store the shared pointer - std::shared_ptr<::cava::audio_raw> m_data_; - GLuint shaderProgram_; + auto onBackendConfigChanged() -> void; + void cacheConfigParams(const ::cava::config_params& src); + // Member variable to store audio data + CavaBackend::AudioRaw m_data_; + GLuint shaderProgram_{0}; // OpenGL variables - GLuint fbo_; - GLuint texture_; + GLuint fbo_{0}; + GLuint texture_{0}; + GLuint vbo_{0}; + GLuint ibo_{0}; + GLuint vao_{0}; GLint uniform_bars_; GLint uniform_previous_bars_; GLint uniform_bars_count_; GLint uniform_time_; + GLint uniform_input_texture_; // Methods void onRealize(); bool onRender(const Glib::RefPtr& context); @@ -39,5 +67,13 @@ class CavaGLSL final : public AModule, public Gtk::GLArea { void initSurface(); void initGLSL(); GLuint loadShader(const std::string& fileName, GLenum type); + void cleanupGL(); + + // ModuleActionMap + static const std::map actionMap_; + + sigc::connection audio_raw_update_conn_; + sigc::connection silence_conn_; + sigc::connection config_changed_conn_; }; } // namespace waybar::modules::cava diff --git a/include/modules/cava/cavaRaw.hpp b/include/modules/cava/cavaRaw.hpp index f4ceb603..76f88eff 100644 --- a/include/modules/cava/cavaRaw.hpp +++ b/include/modules/cava/cavaRaw.hpp @@ -1,30 +1,38 @@ #pragma once +#include +#include + +#include + #include "ALabel.hpp" #include "cava_backend.hpp" namespace waybar::modules::cava { -class Cava final : public ALabel, public sigc::trackable { +class CavaRaw final : public ALabel { public: - Cava(const std::string&, const Json::Value&); - ~Cava() = default; + CavaRaw(const std::string&, const Json::Value&); + ~CavaRaw(); auto doAction(const std::string& name) -> void override; private: + using Action = void (CavaRaw::*)(); + std::shared_ptr backend_; // Text to display - Glib::ustring label_text_{""}; + Glib::ustring label_text_; bool silence_{false}; bool hide_on_silence_{false}; - std::string format_silent_{""}; - int ascii_range_{0}; + std::string format_silent_; // Cava method - void pause_resume(); + void pauseResume(); auto onUpdate(const std::string& input) -> void; auto onSilence() -> void; // ModuleActionMap - static inline std::map - actionMap_{{"mode", &waybar::modules::cava::Cava::pause_resume}}; + static const std::map actionMap_; + + sigc::connection update_conn_; + sigc::connection silence_conn_; }; } // namespace waybar::modules::cava diff --git a/include/modules/cava/cava_backend.hpp b/include/modules/cava/cava_backend.hpp index 4022a2ff..bdbac4a1 100644 --- a/include/modules/cava/cava_backend.hpp +++ b/include/modules/cava/cava_backend.hpp @@ -1,8 +1,17 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include + #include #include +#include "util/SafeSignal.hpp" #include "util/sleeper_thread.hpp" namespace cava { @@ -21,7 +30,6 @@ extern "C" { } // namespace cava namespace waybar::modules::cava { -using namespace std::literals::chrono_literals; class CavaBackend final { public: @@ -29,19 +37,38 @@ class CavaBackend final { virtual ~CavaBackend(); // Methods - int getAsciiRange(); + int getAsciiRange() const; void doPauseResume(); - void Update(); - const struct ::cava::config_params* getPrm(); - std::chrono::milliseconds getFrameTimeMilsec(); + void update(); + const ::cava::config_params& getPrm() const; + std::chrono::milliseconds getFrameTimeMilsec() const; + + struct AudioRaw { + std::vector bars_raw; + std::vector previous_bars_raw; + int number_of_bars = 0; + + AudioRaw() = default; + explicit AudioRaw(const ::cava::audio_raw& raw) { + number_of_bars = raw.number_of_bars; + if (raw.bars_raw != nullptr && number_of_bars > 0) { + bars_raw.assign(raw.bars_raw, raw.bars_raw + number_of_bars); + } + if (raw.previous_bars_raw != nullptr && number_of_bars > 0) { + previous_bars_raw.assign(raw.previous_bars_raw, raw.previous_bars_raw + number_of_bars); + } + } + }; // Signal accessor - using type_signal_update = sigc::signal; - type_signal_update signal_update(); - using type_signal_audio_raw_update = sigc::signal; - type_signal_audio_raw_update signal_audio_raw_update(); - using type_signal_silence = sigc::signal; - type_signal_silence signal_silence(); + using SignalUpdate = SafeSignal; + SignalUpdate& signalUpdate(); + using SignalAudioRawUpdate = SafeSignal; + SignalAudioRawUpdate& signalAudioRawUpdate(); + using SignalSilence = SafeSignal<>; + SignalSilence& signalSilence(); + using SignalConfigChanged = SafeSignal<>; + SignalConfigChanged& signalConfigChanged(); private: CavaBackend(const Json::Value& config); @@ -49,36 +76,78 @@ class CavaBackend final { util::SleeperThread out_thread_; // Cava API to read audio source - ::cava::ptr input_source_{NULL}; + ::cava::ptr input_source_{nullptr}; struct ::cava::error_s error_{}; // cava errors struct ::cava::config_params prm_{}; // cava parameters struct ::cava::audio_raw audio_raw_{}; // cava handled raw audio data(is based on audio_data) struct ::cava::audio_data audio_data_{}; // cava audio data - struct ::cava::cava_plan* plan_{NULL}; //{new cava_plan{}}; + struct ::cava::cava_plan* plan_{nullptr}; //{new cava_plan{}}; std::chrono::seconds fetch_input_delay_{4}; - // Delay to handle audio source - std::chrono::milliseconds frame_time_milsec_{1s}; - const Json::Value& config_; + struct AdaptiveDelay { + std::chrono::milliseconds delay; + std::chrono::seconds delta{0}; + + explicit AdaptiveDelay(std::chrono::milliseconds initial = std::chrono::seconds(1)) + : delay(initial) {} + + bool increase() { + if (delta == std::chrono::seconds{0}) { + delta += std::chrono::seconds{1}; + delay += delta; + return true; + } + return false; + } + + bool decrease() { + if (delta > std::chrono::seconds{0}) { + delay -= delta; + delta -= std::chrono::seconds{1}; + return true; + } + return false; + } + + std::chrono::milliseconds current() const { return delay; } + + void reset(std::chrono::milliseconds new_delay) { + delay = new_delay; + delta = std::chrono::seconds{0}; + } + }; + + AdaptiveDelay adaptive_delay_; + + Json::Value config_; int re_paint_{0}; bool silence_{false}; bool silence_prev_{false}; - std::chrono::seconds suspend_silence_delay_{0}; int sleep_counter_{0}; std::string output_{}; // Methods void invoke(); void execute(); - bool isSilence(); + bool isSilent(); void doUpdate(bool force = false); void loadConfig(); void freeBackend(); // Signal - type_signal_update m_signal_update_; - type_signal_audio_raw_update m_signal_audio_raw_; - type_signal_silence m_signal_silence_; + SignalUpdate m_signal_update_; + SignalAudioRawUpdate m_signal_audio_raw_; + SignalSilence m_signal_silence_; + SignalConfigChanged m_signal_config_changed_; + + std::atomic shutdown_{false}; + bool audio_raw_initialized_{false}; + mutable std::recursive_mutex state_mutex_; + + // Synchronization for joining read_thread_ during destruction + bool read_thread_exited_{false}; + mutable std::mutex read_thread_exit_mutex_; + std::condition_variable read_thread_exit_cv_; }; } // namespace waybar::modules::cava diff --git a/include/modules/cava/cava_frontend.hpp b/include/modules/cava/cava_frontend.hpp index 402a9bad..4eefb0fe 100644 --- a/include/modules/cava/cava_frontend.hpp +++ b/include/modules/cava/cava_frontend.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #ifdef HAVE_LIBCAVA #include "cavaRaw.hpp" #include "cava_backend.hpp" @@ -9,16 +11,16 @@ #endif namespace waybar::modules::cava { -AModule* getModule(const std::string& id, const Json::Value& config) { +inline std::unique_ptr getModule(const std::string& id, const Json::Value& config) { #ifdef HAVE_LIBCAVA const std::shared_ptr backend_{waybar::modules::cava::CavaBackend::inst(config)}; - switch (backend_->getPrm()->output) { + switch (backend_->getPrm().output) { #ifdef HAVE_LIBCAVAGLSL case ::cava::output_method::OUTPUT_SDL_GLSL: - return new waybar::modules::cava::CavaGLSL(id, config); + return std::make_unique(id, config); #endif default: - return new waybar::modules::cava::Cava(id, config); + return std::make_unique(id, config); } #else throw std::runtime_error("Unknown module"); diff --git a/src/factory.cpp b/src/factory.cpp index c0d018b7..86abb43d 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -372,7 +372,7 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, } #endif if (ref == "cava") { - return waybar::modules::cava::getModule(id, config_[name]); + return waybar::modules::cava::getModule(id, config_[name]).release(); } #ifdef HAVE_SYSTEMD_MONITOR if (ref == "systemd-failed-units") { diff --git a/src/modules/cava/cavaGLSL.cpp b/src/modules/cava/cavaGLSL.cpp index 4ab03bcd..755c6acd 100644 --- a/src/modules/cava/cavaGLSL.cpp +++ b/src/modules/cava/cavaGLSL.cpp @@ -2,88 +2,214 @@ #include +#include +#include +#include #include +#include +#include + +const std::map + waybar::modules::cava::CavaGLSL::actionMap_{{"mode", &CavaGLSL::pauseResume}}; waybar::modules::cava::CavaGLSL::CavaGLSL(const std::string& id, const Json::Value& config) : AModule(config, "cavaGLSL", id, false, false), backend_{waybar::modules::cava::CavaBackend::inst(config)} { - set_name(name_); + gl_area_.set_name(name_); if (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool(); if (!id.empty()) { - get_style_context()->add_class(id); + gl_area_.get_style_context()->add_class(id); } - get_style_context()->add_class(MODULE_CLASS); + gl_area_.get_style_context()->add_class(MODULE_CLASS); - set_use_es(true); - // set_auto_render(true); - signal_realize().connect(sigc::mem_fun(*this, &CavaGLSL::onRealize)); - signal_render().connect(sigc::mem_fun(*this, &CavaGLSL::onRender), false); + gl_area_.set_use_es(true); + gl_area_.signal_realize().connect(sigc::mem_fun(*this, &CavaGLSL::onRealize)); + gl_area_.signal_render().connect(sigc::mem_fun(*this, &CavaGLSL::onRender), false); + gl_area_.signal_map().connect([this]() { mapped_ = true; }); + gl_area_.signal_unmap().connect([this]() { mapped_ = false; }); - // Get parameters_config struct from the backend - prm_ = *backend_->getPrm(); + cacheConfigParams(backend_->getPrm()); - // Set widget length int length{0}; if (config_["min-length"].isUInt()) length = config_["min-length"].asUInt(); else if (config_["max-length"].isUInt()) length = config_["max-length"].asUInt(); else - length = prm_.sdl_width; + length = sdl_width_; - set_size_request(length, prm_.sdl_height); + gl_area_.set_size_request(length, sdl_height_); - // Subscribe for changes - backend_->signal_audio_raw_update().connect(sigc::mem_fun(*this, &CavaGLSL::onUpdate)); - // Subscribe for silence - backend_->signal_silence().connect(sigc::mem_fun(*this, &CavaGLSL::onSilence)); - event_box_.add(*this); + audio_raw_update_conn_ = + backend_->signalAudioRawUpdate().connect(sigc::mem_fun(*this, &CavaGLSL::onUpdate)); + silence_conn_ = backend_->signalSilence().connect(sigc::mem_fun(*this, &CavaGLSL::onSilence)); + config_changed_conn_ = + backend_->signalConfigChanged().connect(sigc::mem_fun(*this, &CavaGLSL::onBackendConfigChanged)); + event_box_.add(gl_area_); } -auto waybar::modules::cava::CavaGLSL::onUpdate(const ::cava::audio_raw& input) -> void { - Glib::signal_idle().connect_once([this, input]() { - m_data_ = std::make_shared<::cava::audio_raw>(input); - if (silence_) { - get_style_context()->remove_class("silent"); - if (!get_style_context()->has_class("updated")) get_style_context()->add_class("updated"); - show(); - silence_ = false; - } +waybar::modules::cava::CavaGLSL::~CavaGLSL() { + audio_raw_update_conn_.disconnect(); + silence_conn_.disconnect(); + config_changed_conn_.disconnect(); - queue_render(); - }); + if (gl_area_.get_realized()) { + gl_area_.make_current(); + cleanupGL(); + } +} + +void waybar::modules::cava::CavaGLSL::cleanupGL() { + if (shaderProgram_ != 0) { + glDeleteProgram(shaderProgram_); + shaderProgram_ = 0; + } + if (fbo_ != 0) { + glDeleteFramebuffers(1, &fbo_); + fbo_ = 0; + } + if (texture_ != 0) { + glDeleteTextures(1, &texture_); + texture_ = 0; + } + if (vbo_ != 0) { + glDeleteBuffers(1, &vbo_); + vbo_ = 0; + } + if (ibo_ != 0) { + glDeleteBuffers(1, &ibo_); + ibo_ = 0; + } + if (vao_ != 0) { + glDeleteVertexArrays(1, &vao_); + vao_ = 0; + } +} + +auto waybar::modules::cava::CavaGLSL::doAction(const std::string& name) -> void { + auto it = actionMap_.find(name); + if (it != actionMap_.end() && it->second) { + (this->*it->second)(); + } else { + spdlog::error("CavaGLSL. Unsupported action \"{0}\"", name); + } +} + +void waybar::modules::cava::CavaGLSL::pauseResume() { backend_->doPauseResume(); } + +auto waybar::modules::cava::CavaGLSL::onUpdate(const CavaBackend::AudioRaw& input) -> void { + m_data_ = input; + if (silence_) { + gl_area_.get_style_context()->remove_class("silent"); + if (!gl_area_.get_style_context()->has_class("updated")) + gl_area_.get_style_context()->add_class("updated"); + gl_area_.show(); + silence_ = false; + } + + if (mapped_) { + gl_area_.queue_render(); + } } auto waybar::modules::cava::CavaGLSL::onSilence() -> void { - Glib::signal_idle().connect_once([this]() { - if (!silence_) { - if (get_style_context()->has_class("updated")) get_style_context()->remove_class("updated"); + if (!silence_) { + if (gl_area_.get_style_context()->has_class("updated")) + gl_area_.get_style_context()->remove_class("updated"); - if (hide_on_silence_) hide(); - silence_ = true; - get_style_context()->add_class("silent"); - // Set clear color to black - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - queue_render(); + if (hide_on_silence_) gl_area_.hide(); + silence_ = true; + gl_area_.get_style_context()->add_class("silent"); + } +} + +void waybar::modules::cava::CavaGLSL::cacheConfigParams(const ::cava::config_params& src) { + sdl_width_ = src.sdl_width; + sdl_height_ = src.sdl_height; + bar_width_ = src.bar_width; + bar_spacing_ = src.bar_spacing; + gradient_count_ = src.gradient_count; + vertex_shader_ = src.vertex_shader ? src.vertex_shader : ""; + fragment_shader_ = src.fragment_shader ? src.fragment_shader : ""; + bcolor_ = src.bcolor ? src.bcolor : ""; + color_ = src.color ? src.color : ""; + for (size_t i = 0; i < gradient_colors_.size(); ++i) { + gradient_colors_[i] = (src.gradient_colors[i] ? src.gradient_colors[i] : ""); + } +} + +auto waybar::modules::cava::CavaGLSL::onBackendConfigChanged() -> void { + auto new_prm = backend_->getPrm(); + + bool dimensions_changed = + (new_prm.sdl_width != sdl_width_) || (new_prm.sdl_height != sdl_height_); + + bool shaders_changed = false; + std::string new_vertex = new_prm.vertex_shader ? new_prm.vertex_shader : ""; + std::string new_fragment = new_prm.fragment_shader ? new_prm.fragment_shader : ""; + shaders_changed = (vertex_shader_ != new_vertex) || (fragment_shader_ != new_fragment); + + bool surface_changed = false; + if (new_prm.bar_width != bar_width_) surface_changed = true; + if (new_prm.bar_spacing != bar_spacing_) surface_changed = true; + if (new_prm.gradient_count != gradient_count_) surface_changed = true; + std::string new_bcolor = new_prm.bcolor ? new_prm.bcolor : ""; + if (bcolor_ != new_bcolor) surface_changed = true; + std::string new_color = new_prm.color ? new_prm.color : ""; + if (color_ != new_color) surface_changed = true; + for (size_t i = 0; i < gradient_colors_.size(); ++i) { + std::string new_grad = new_prm.gradient_colors[i] ? new_prm.gradient_colors[i] : ""; + if (gradient_colors_[i] != new_grad) { + surface_changed = true; + break; } - }); + } + + cacheConfigParams(new_prm); + + if ((dimensions_changed || shaders_changed) && gl_area_.get_realized()) { + gl_area_.make_current(); + cleanupGL(); + initShaders(); + if (shaderProgram_ != 0) { + initGLSL(); + initSurface(); + } + } else if (surface_changed && gl_area_.get_realized()) { + gl_area_.make_current(); + glUseProgram(shaderProgram_); + initSurface(); + } + + int length{0}; + if (config_["min-length"].isUInt()) + length = config_["min-length"].asUInt(); + else if (config_["max-length"].isUInt()) + length = config_["max-length"].asUInt(); + else + length = sdl_width_; + + gl_area_.set_size_request(length, sdl_height_); } bool waybar::modules::cava::CavaGLSL::onRender(const Glib::RefPtr& context) { - if (!m_data_) return true; + if (m_data_.bars_raw.empty() || shaderProgram_ == 0) return true; + + glUseProgram(shaderProgram_); + glBindVertexArray(vao_); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture_); - glUniform1i(glGetUniformLocation(shaderProgram_, "inputTexture"), 0); + glUniform1i(uniform_input_texture_, 0); - glUniform1fv(uniform_bars_, m_data_->number_of_bars, m_data_->bars_raw); - glUniform1fv(uniform_previous_bars_, m_data_->number_of_bars, m_data_->previous_bars_raw); - glUniform1i(uniform_bars_count_, m_data_->number_of_bars); - ++frame_counter; - glUniform1f(uniform_time_, (frame_counter / backend_->getFrameTimeMilsec().count()) / 1e3); + glUniform1fv(uniform_bars_, m_data_.number_of_bars, m_data_.bars_raw.data()); + glUniform1fv(uniform_previous_bars_, m_data_.number_of_bars, m_data_.previous_bars_raw.data()); + glUniform1i(uniform_bars_count_, m_data_.number_of_bars); + ++frame_counter_; + glUniform1f(uniform_time_, + static_cast(frame_counter_) * backend_->getFrameTimeMilsec().count() / 1000.0f); - // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, nullptr); glBindFramebuffer(GL_FRAMEBUFFER, fbo_); @@ -94,21 +220,35 @@ bool waybar::modules::cava::CavaGLSL::onRender(const Glib::RefPtrR, &color->G, &color->B); +static void parse_color(const char* color_string, struct Colors* color) { + if (color_string == nullptr) { + return; + } + if (color_string[0] != '#') { + return; + } + if (std::strlen(color_string) < 7) { + spdlog::warn("Invalid color string '{}': expected #RRGGBB", color_string); + return; + } + if (std::sscanf(color_string + 1, "%02hx%02hx%02hx", &color->R, &color->G, &color->B) != 3) { + spdlog::warn("Failed to parse color string '{}'", color_string); } } @@ -123,49 +263,44 @@ void waybar::modules::cava::CavaGLSL::initGLSL() { GLfloat vertexData[]{-1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f}; GLint indexData[]{0, 1, 2, 3}; - GLuint gVBO{0}; - glGenBuffers(1, &gVBO); - glBindBuffer(GL_ARRAY_BUFFER, gVBO); + glGenBuffers(1, &vbo_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); glBufferData(GL_ARRAY_BUFFER, 2 * 4 * sizeof(GLfloat), vertexData, GL_STATIC_DRAW); - GLuint gIBO{0}; - glGenBuffers(1, &gIBO); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gIBO); + glGenBuffers(1, &ibo_); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_); glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4 * sizeof(GLuint), indexData, GL_STATIC_DRAW); - GLuint gVAO{0}; - glGenVertexArrays(1, &gVAO); - glBindVertexArray(gVAO); + glGenVertexArrays(1, &vao_); + glBindVertexArray(vao_); glEnableVertexAttribArray(gVertexPos2DLocation); - glBindBuffer(GL_ARRAY_BUFFER, gVBO); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); glVertexAttribPointer(gVertexPos2DLocation, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gIBO); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_); glGenFramebuffers(1, &fbo_); glBindFramebuffer(GL_FRAMEBUFFER, fbo_); - // Create a texture to attach the framebuffer glGenTextures(1, &texture_); glBindTexture(GL_TEXTURE_2D, texture_); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, prm_.sdl_width, prm_.sdl_height, 0, GL_RGBA, + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, sdl_width_, sdl_height_, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_, 0); - // Check is framebuffer is complete if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { spdlog::error("{0}. Framebuffer not complete", name_); } - // Unbind the framebuffer glBindFramebuffer(GL_FRAMEBUFFER, 0); uniform_bars_ = glGetUniformLocation(shaderProgram_, "bars"); uniform_previous_bars_ = glGetUniformLocation(shaderProgram_, "previous_bars"); uniform_bars_count_ = glGetUniformLocation(shaderProgram_, "bars_count"); uniform_time_ = glGetUniformLocation(shaderProgram_, "shader_time"); + uniform_input_texture_ = glGetUniformLocation(shaderProgram_, "inputTexture"); GLuint err{glGetError()}; if (err != 0) { @@ -174,32 +309,32 @@ void waybar::modules::cava::CavaGLSL::initGLSL() { } void waybar::modules::cava::CavaGLSL::initSurface() { - colors color = {0}; + Colors color = {0}; GLint uniform_bg_col{glGetUniformLocation(shaderProgram_, "bg_color")}; - parse_color(prm_.bcolor, &color); - glUniform3f(uniform_bg_col, (float)color.R / 255.0, (float)color.G / 255.0, - (float)color.B / 255.0); + parse_color(bcolor_.c_str(), &color); + glUniform3f(uniform_bg_col, static_cast(color.R) / 255.0f, static_cast(color.G) / 255.0f, + static_cast(color.B) / 255.0f); GLint uniform_fg_col{glGetUniformLocation(shaderProgram_, "fg_color")}; - parse_color(prm_.color, &color); - glUniform3f(uniform_fg_col, (float)color.R / 255.0, (float)color.G / 255.0, - (float)color.B / 255.0); + parse_color(color_.c_str(), &color); + glUniform3f(uniform_fg_col, static_cast(color.R) / 255.0f, static_cast(color.G) / 255.0f, + static_cast(color.B) / 255.0f); GLint uniform_res{glGetUniformLocation(shaderProgram_, "u_resolution")}; - glUniform3f(uniform_res, (float)prm_.sdl_width, (float)prm_.sdl_height, 0.0f); + glUniform3f(uniform_res, static_cast(sdl_width_), static_cast(sdl_height_), 0.0f); GLint uniform_bar_width{glGetUniformLocation(shaderProgram_, "bar_width")}; - glUniform1i(uniform_bar_width, prm_.bar_width); + glUniform1i(uniform_bar_width, bar_width_); GLint uniform_bar_spacing{glGetUniformLocation(shaderProgram_, "bar_spacing")}; - glUniform1i(uniform_bar_spacing, prm_.bar_spacing); + glUniform1i(uniform_bar_spacing, bar_spacing_); GLint uniform_gradient_count{glGetUniformLocation(shaderProgram_, "gradient_count")}; - glUniform1i(uniform_gradient_count, prm_.gradient_count); + glUniform1i(uniform_gradient_count, std::max(0, gradient_count_)); GLint uniform_gradient_colors{glGetUniformLocation(shaderProgram_, "gradient_colors")}; - GLfloat gradient_colors[8][3]; - for (int i{0}; i < prm_.gradient_count; ++i) { - parse_color(prm_.gradient_colors[i], &color); - gradient_colors[i][0] = (float)color.R / 255.0; - gradient_colors[i][1] = (float)color.G / 255.0; - gradient_colors[i][2] = (float)color.B / 255.0; + GLfloat gradient_colors[8][3] = {}; + for (int i{0}; i < gradient_count_; ++i) { + parse_color(gradient_colors_[i].c_str(), &color); + gradient_colors[i][0] = static_cast(color.R) / 255.0f; + gradient_colors[i][1] = static_cast(color.G) / 255.0f; + gradient_colors[i][2] = static_cast(color.B) / 255.0f; } - glUniform3fv(uniform_gradient_colors, 8, (const GLfloat*)gradient_colors); + glUniform3fv(uniform_gradient_colors, std::max(0, std::min(gradient_count_, 8)), &gradient_colors[0][0]); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, nullptr); @@ -207,9 +342,23 @@ void waybar::modules::cava::CavaGLSL::initSurface() { void waybar::modules::cava::CavaGLSL::initShaders() { shaderProgram_ = glCreateProgram(); + if (shaderProgram_ == 0) { + spdlog::error("{0}. Failed to create shader program", name_); + gl_area_.hide(); + return; + } - GLuint vertexShader{loadShader(prm_.vertex_shader, GL_VERTEX_SHADER)}; - GLuint fragmentShader{loadShader(prm_.fragment_shader, GL_FRAGMENT_SHADER)}; + GLuint vertexShader{loadShader(vertex_shader_, GL_VERTEX_SHADER)}; + GLuint fragmentShader{loadShader(fragment_shader_, GL_FRAGMENT_SHADER)}; + + if (vertexShader == 0 || fragmentShader == 0) { + if (vertexShader != 0) glDeleteShader(vertexShader); + if (fragmentShader != 0) glDeleteShader(fragmentShader); + glDeleteProgram(shaderProgram_); + shaderProgram_ = 0; + gl_area_.hide(); + return; + } glAttachShader(shaderProgram_, vertexShader); glAttachShader(shaderProgram_, fragmentShader); @@ -219,14 +368,18 @@ void waybar::modules::cava::CavaGLSL::initShaders() { glDeleteShader(vertexShader); glDeleteShader(fragmentShader); - // Check for linking errors - GLint success, len; + GLint success{0}; glGetProgramiv(shaderProgram_, GL_LINK_STATUS, &success); if (!success) { + GLint len{0}; glGetProgramiv(shaderProgram_, GL_INFO_LOG_LENGTH, &len); - GLchar* infoLog{(char*)'\0'}; - glGetProgramInfoLog(shaderProgram_, len, &len, infoLog); - spdlog::error("{0}. Shader linking error: {1}", name_, infoLog); + std::vector infoLog(len + 1); + glGetProgramInfoLog(shaderProgram_, len, nullptr, infoLog.data()); + spdlog::error("{0}. Shader linking error: {1}", name_, infoLog.data()); + glDeleteProgram(shaderProgram_); + shaderProgram_ = 0; + gl_area_.hide(); + return; } glReleaseShaderCompiler(); @@ -236,35 +389,37 @@ void waybar::modules::cava::CavaGLSL::initShaders() { GLuint waybar::modules::cava::CavaGLSL::loadShader(const std::string& fileName, GLenum type) { spdlog::debug("{0}. loadShader: {1}", name_, fileName); - // Read shader source code from the file std::ifstream shaderFile{fileName}; - if (!shaderFile.is_open()) { spdlog::error("{0}. Could not open shader file: {1}", name_, fileName); + return 0; } std::ostringstream buffer; - buffer << shaderFile.rdbuf(); // read file content into stringstream + buffer << shaderFile.rdbuf(); std::string str{buffer.str()}; - const char* shaderSource = str.c_str(); shaderFile.close(); GLuint shaderID{glCreateShader(type)}; - if (shaderID == 0) spdlog::error("{0}. Error creating shader type: {0}", type); + if (shaderID == 0) { + spdlog::error("{0}. Error creating shader type: {1}", name_, type); + return 0; + } + + const char* shaderSource = str.c_str(); glShaderSource(shaderID, 1, &shaderSource, nullptr); glCompileShader(shaderID); - // Check for compilation errors - GLint success, len; - + GLint success{0}; glGetShaderiv(shaderID, GL_COMPILE_STATUS, &success); - if (!success) { + GLint len{0}; glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &len); - - GLchar* infoLog{(char*)'\0'}; - glGetShaderInfoLog(shaderID, len, nullptr, infoLog); - spdlog::error("{0}. Shader compilation error in {1}: {2}", name_, fileName, infoLog); + std::vector infoLog(len + 1); + glGetShaderInfoLog(shaderID, len, nullptr, infoLog.data()); + spdlog::error("{0}. Shader compilation error in {1}: {2}", name_, fileName, infoLog.data()); + glDeleteShader(shaderID); + return 0; } return shaderID; diff --git a/src/modules/cava/cavaRaw.cpp b/src/modules/cava/cavaRaw.cpp index 2b0aca72..65ce093d 100644 --- a/src/modules/cava/cavaRaw.cpp +++ b/src/modules/cava/cavaRaw.cpp @@ -2,59 +2,68 @@ #include -waybar::modules::cava::Cava::Cava(const std::string& id, const Json::Value& config) +const std::map + waybar::modules::cava::CavaRaw::actionMap_{{"mode", &CavaRaw::pauseResume}}; + +waybar::modules::cava::CavaRaw::CavaRaw(const std::string& id, const Json::Value& config) : ALabel(config, "cava", id, "{}", 60, false, false, false), backend_{waybar::modules::cava::CavaBackend::inst(config)} { if (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool(); if (config_["format_silent"].isString()) format_silent_ = config_["format_silent"].asString(); - ascii_range_ = backend_->getAsciiRange(); - backend_->signal_update().connect(sigc::mem_fun(*this, &Cava::onUpdate)); - backend_->signal_silence().connect(sigc::mem_fun(*this, &Cava::onSilence)); - backend_->Update(); + update_conn_ = backend_->signalUpdate().connect(sigc::mem_fun(*this, &CavaRaw::onUpdate)); + silence_conn_ = backend_->signalSilence().connect(sigc::mem_fun(*this, &CavaRaw::onSilence)); + backend_->update(); } -auto waybar::modules::cava::Cava::doAction(const std::string& name) -> void { - if ((actionMap_[name])) { - (this->*actionMap_[name])(); - } else +waybar::modules::cava::CavaRaw::~CavaRaw() { + update_conn_.disconnect(); + silence_conn_.disconnect(); +} + +auto waybar::modules::cava::CavaRaw::doAction(const std::string& name) -> void { + auto it = actionMap_.find(name); + if (it != actionMap_.end() && it->second) { + (this->*it->second)(); + } else { spdlog::error("Cava. Unsupported action \"{0}\"", name); + } } // Cava actions -void waybar::modules::cava::Cava::pause_resume() { backend_->doPauseResume(); } -auto waybar::modules::cava::Cava::onUpdate(const std::string& input) -> void { - Glib::signal_idle().connect_once([this, input]() { - if (silence_) { - silence_ = false; - label_.get_style_context()->remove_class("silent"); - if (!label_.get_style_context()->has_class("updated")) - label_.get_style_context()->add_class("updated"); - } - label_text_.clear(); - for (auto& ch : input) - label_text_.append(getIcon((ch > ascii_range_) ? ascii_range_ : ch, "", ascii_range_ + 1)); +void waybar::modules::cava::CavaRaw::pauseResume() { backend_->doPauseResume(); } +auto waybar::modules::cava::CavaRaw::onUpdate(const std::string& input) -> void { + if (silence_) { + silence_ = false; + label_.get_style_context()->remove_class("silent"); + if (!label_.get_style_context()->has_class("updated")) + label_.get_style_context()->add_class("updated"); + } + label_text_.clear(); + auto ascii_range = backend_->getAsciiRange(); + for (auto& ch : input) { + auto uch = static_cast(ch); + label_text_.append(getIcon((uch > ascii_range) ? ascii_range : uch, "", ascii_range + 1)); + } - label_.set_markup(label_text_); - label_.show(); - ALabel::update(); - }); + label_.set_markup(label_text_); + label_.show(); + ALabel::update(); } -auto waybar::modules::cava::Cava::onSilence() -> void { - Glib::signal_idle().connect_once([this]() { - if (!silence_) { - if (label_.get_style_context()->has_class("updated")) - label_.get_style_context()->remove_class("updated"); +auto waybar::modules::cava::CavaRaw::onSilence() -> void { + if (!silence_) { + if (label_.get_style_context()->has_class("updated")) + label_.get_style_context()->remove_class("updated"); - if (hide_on_silence_) { - // Clear the label markup before hiding to prevent GTK from rendering a NULL Pango layout - label_.set_markup(""); - label_.hide(); - } else if (config_["format_silent"].isString()) - label_.set_markup(format_silent_); - silence_ = true; - label_.get_style_context()->add_class("silent"); + if (hide_on_silence_) { + // Clear the label markup before hiding to prevent GTK from rendering a NULL Pango layout + label_.set_markup(""); + label_.hide(); + } else if (!format_silent_.empty()) { + label_.set_markup(format_silent_); } - }); + silence_ = true; + label_.get_style_context()->add_class("silent"); + } } diff --git a/src/modules/cava/cava_backend.cpp b/src/modules/cava/cava_backend.cpp index 99f03e16..00239131 100644 --- a/src/modules/cava/cava_backend.cpp +++ b/src/modules/cava/cava_backend.cpp @@ -2,8 +2,23 @@ #include +#include #include +namespace { +struct CavaConfigGuard { + ::cava::config_params* prm; + bool released = false; + explicit CavaConfigGuard(::cava::config_params* p) : prm(p) {} + ~CavaConfigGuard() { + if (!released && prm) { + free_config(prm); + } + } + void release() { released = true; } +}; +} // namespace + std::shared_ptr waybar::modules::cava::CavaBackend::inst( const Json::Value& config) { static auto* backend = new CavaBackend(config); @@ -12,69 +27,84 @@ std::shared_ptr waybar::modules::cava::CavaB } waybar::modules::cava::CavaBackend::CavaBackend(const Json::Value& config) : config_(config) { - // Load waybar module config loadConfig(); - // Read audio source trough cava API. Cava orginizes this process via infinity loop read_thread_ = [this] { - try { - input_source_(&audio_data_); - } catch (const std::runtime_error& e) { - spdlog::warn("Cava backend. Read source error: {0}", e.what()); + while (read_thread_.isRunning()) { + try { + if (input_source_) { + input_source_(&audio_data_); + } + } catch (const std::exception& e) { + spdlog::warn("Cava backend. Read source error: {0}", e.what()); + } + if (!read_thread_.isRunning()) break; + read_thread_.sleep_for(fetch_input_delay_); + if (!read_thread_.isRunning()) break; + try { + loadConfig(); + } catch (const std::exception& e) { + spdlog::error("{}", e.what()); + } } - read_thread_.sleep_for(fetch_input_delay_); - // loadConfig() now throws on failure; contain it so a runtime config error - // logs instead of terminating the process (#4456). - try { - loadConfig(); - } catch (const std::exception& e) { - spdlog::error("{}", e.what()); + { + std::lock_guard lk(read_thread_exit_mutex_); + read_thread_exited_ = true; } + read_thread_exit_cv_.notify_one(); }; - // Write outcoming data. Emit signals out_thread_ = [this] { - doUpdate(false); - out_thread_.sleep_for(frame_time_milsec_); + try { + doUpdate(false); + } catch (const std::exception& e) { + spdlog::error("Cava backend. Output thread error: {0}", e.what()); + } + out_thread_.sleep_for(adaptive_delay_.current()); }; } waybar::modules::cava::CavaBackend::~CavaBackend() { + { + std::lock_guard lock(state_mutex_); + shutdown_ = true; + } + + pthread_mutex_lock(&audio_data_.lock); + audio_data_.terminate = 1; + pthread_mutex_unlock(&audio_data_.lock); + out_thread_.stop(); read_thread_.stop(); + std::unique_lock lk(read_thread_exit_mutex_); + if (!read_thread_exit_cv_.wait_for(lk, std::chrono::milliseconds(100), + [this] { return read_thread_exited_; })) { + spdlog::debug( + "Cava backend: read thread still blocked in input_source() on shutdown. " + "Proceeding with cleanup."); + } + + std::lock_guard lock(state_mutex_); freeBackend(); } -static bool upThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { - if (delta == std::chrono::seconds{0}) { - delta += std::chrono::seconds{1}; - delay += delta; - return true; - } - return false; -} - -static bool downThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { - if (delta > std::chrono::seconds{0}) { - delay -= delta; - delta -= std::chrono::seconds{1}; - return true; - } - return false; -} - -bool waybar::modules::cava::CavaBackend::isSilence() { +bool waybar::modules::cava::CavaBackend::isSilent() { + pthread_mutex_lock(&audio_data_.lock); + bool silent = true; for (int i{0}; i < audio_data_.input_buffer_size; ++i) { if (audio_data_.cava_in[i]) { - return false; + silent = false; + break; } } - - return true; + pthread_mutex_unlock(&audio_data_.lock); + return silent; } -int waybar::modules::cava::CavaBackend::getAsciiRange() { return prm_.ascii_range; } +int waybar::modules::cava::CavaBackend::getAsciiRange() const { + std::lock_guard lock(state_mutex_); + return prm_.ascii_range; +} -// Process: execute cava void waybar::modules::cava::CavaBackend::invoke() { pthread_mutex_lock(&audio_data_.lock); ::cava::cava_execute(audio_data_.cava_in, audio_data_.samples_counter, audio_raw_.cava_out, @@ -83,7 +113,6 @@ void waybar::modules::cava::CavaBackend::invoke() { pthread_mutex_unlock(&audio_data_.lock); } -// Do transformation under raw data void waybar::modules::cava::CavaBackend::execute() { invoke(); audio_raw_fetch(&audio_raw_, &prm_, &re_paint_, plan_); @@ -99,183 +128,228 @@ void waybar::modules::cava::CavaBackend::execute() { } void waybar::modules::cava::CavaBackend::doPauseResume() { + std::lock_guard lock(state_mutex_); pthread_mutex_lock(&audio_data_.lock); if (audio_data_.suspendFlag) { audio_data_.suspendFlag = false; pthread_cond_broadcast(&audio_data_.resumeCond); - downThreadDelay(frame_time_milsec_, suspend_silence_delay_); + adaptive_delay_.decrease(); } else { audio_data_.suspendFlag = true; - upThreadDelay(frame_time_milsec_, suspend_silence_delay_); + adaptive_delay_.increase(); } pthread_mutex_unlock(&audio_data_.lock); - Update(); + update(); } -waybar::modules::cava::CavaBackend::type_signal_update -waybar::modules::cava::CavaBackend::signal_update() { +waybar::modules::cava::CavaBackend::SignalUpdate& +waybar::modules::cava::CavaBackend::signalUpdate() { return m_signal_update_; } -waybar::modules::cava::CavaBackend::type_signal_audio_raw_update -waybar::modules::cava::CavaBackend::signal_audio_raw_update() { +waybar::modules::cava::CavaBackend::SignalAudioRawUpdate& +waybar::modules::cava::CavaBackend::signalAudioRawUpdate() { return m_signal_audio_raw_; } -waybar::modules::cava::CavaBackend::type_signal_silence -waybar::modules::cava::CavaBackend::signal_silence() { +waybar::modules::cava::CavaBackend::SignalSilence& +waybar::modules::cava::CavaBackend::signalSilence() { return m_signal_silence_; } -void waybar::modules::cava::CavaBackend::Update() { doUpdate(true); } +waybar::modules::cava::CavaBackend::SignalConfigChanged& +waybar::modules::cava::CavaBackend::signalConfigChanged() { + return m_signal_config_changed_; +} + +void waybar::modules::cava::CavaBackend::update() { doUpdate(true); } void waybar::modules::cava::CavaBackend::doUpdate(bool force) { + std::lock_guard lock(state_mutex_); + if (!plan_ || !input_source_) return; if (audio_data_.suspendFlag && !force) return; - silence_ = isSilence(); + silence_ = isSilent(); if (!silence_) sleep_counter_ = 0; if (silence_ && prm_.sleep_timer != 0) { if (sleep_counter_ <= - (int)(std::chrono::milliseconds(prm_.sleep_timer * 1s) / frame_time_milsec_)) { + static_cast(std::chrono::milliseconds(prm_.sleep_timer * std::chrono::seconds(1)) / + adaptive_delay_.current())) { ++sleep_counter_; silence_ = false; } } if (!silence_ || prm_.sleep_timer == 0) { - if (downThreadDelay(frame_time_milsec_, suspend_silence_delay_)) Update(); + while (adaptive_delay_.decrease()) {} execute(); if (re_paint_ == 1 || force || prm_.continuous_rendering) { m_signal_update_.emit(output_); - m_signal_audio_raw_.emit(audio_raw_); + m_signal_audio_raw_.emit(AudioRaw{audio_raw_}); } } else { - if (upThreadDelay(frame_time_milsec_, suspend_silence_delay_)) Update(); + while (adaptive_delay_.increase()) {} if (silence_ != silence_prev_ || force) m_signal_silence_.emit(); } silence_prev_ = silence_; } void waybar::modules::cava::CavaBackend::freeBackend() { - if (plan_ != NULL) { + input_source_ = nullptr; + + if (plan_ != nullptr) { cava_destroy(plan_); - plan_ = NULL; + plan_ = nullptr; } - audio_raw_clean(&audio_raw_); + if (audio_raw_initialized_) { + audio_raw_clean(&audio_raw_); + audio_raw_initialized_ = false; + } pthread_mutex_lock(&audio_data_.lock); audio_data_.terminate = 1; pthread_mutex_unlock(&audio_data_.lock); free_config(&prm_); + prm_ = {}; free(audio_data_.source); + audio_data_.source = nullptr; free(audio_data_.cava_in); + audio_data_.cava_in = nullptr; } void waybar::modules::cava::CavaBackend::loadConfig() { - freeBackend(); - // Load waybar module config - char cfgPath[PATH_MAX]; - cfgPath[0] = '\0'; + std::lock_guard lock(state_mutex_); + if (shutdown_.load()) { + return; + } - if (config_["cava_config"].isString()) strcpy(cfgPath, config_["cava_config"].asString().data()); - // Load cava config + const Json::Value& cfg = config_; + + struct ::cava::config_params new_prm{}; + CavaConfigGuard new_prm_guard(&new_prm); + std::vector cfgPath(PATH_MAX, '\0'); + if (cfg["cava_config"].isString()) { + const std::string& s = cfg["cava_config"].asString(); + auto len = std::min(s.size(), static_cast(PATH_MAX - 1)); + std::copy_n(s.begin(), len, cfgPath.begin()); + cfgPath[len] = '\0'; + } error_.length = 0; - if (!load_config(cfgPath, &prm_, &error_)) { - // Throw, don't exit(): a bad config must disable only the cava module, not - // kill the whole bar (#4456). Caught by the factory (ctor) / read_thread_. + if (!load_config(cfgPath.data(), &new_prm, &error_)) { throw std::runtime_error(std::string{"cava backend: error loading config: "} + error_.message); } - // Override cava parameters by the user config - prm_.inAtty = 0; - auto const output{prm_.output}; - // prm_.output = ::cava::output_method::OUTPUT_RAW; - if (prm_.data_format) free(prm_.data_format); - // Default to ascii for format-icons output; allow user override - prm_.data_format = strdup( - config_["data_format"].isString() ? config_["data_format"].asString().c_str() : "ascii"); - if (config_["raw_target"].isString()) { - if (prm_.raw_target) free(prm_.raw_target); - prm_.raw_target = strdup(config_["raw_target"].asString().c_str()); + new_prm.inAtty = 0; + auto const output{new_prm.output}; + if (new_prm.data_format) free(new_prm.data_format); + new_prm.data_format = strdup( + cfg["data_format"].isString() ? cfg["data_format"].asString().c_str() : "ascii"); + if (cfg["raw_target"].isString()) { + if (new_prm.raw_target) free(new_prm.raw_target); + new_prm.raw_target = strdup(cfg["raw_target"].asString().c_str()); } - prm_.ascii_range = config_["format-icons"].size() - 1; - - if (config_["bar_spacing"].isInt()) prm_.bar_spacing = config_["bar_spacing"].asInt(); - if (config_["bar_width"].isInt()) prm_.bar_width = config_["bar_width"].asInt(); - if (config_["bar_height"].isInt()) prm_.bar_height = config_["bar_height"].asInt(); - prm_.orientation = ::cava::ORIENT_TOP; - prm_.xaxis = ::cava::xaxis_scale::NONE; - prm_.mono_opt = ::cava::AVERAGE; - prm_.autobars = 0; - if (config_["gravity"].isInt()) prm_.gravity = config_["gravity"].asInt(); - if (config_["integral"].isInt()) prm_.integral = config_["integral"].asInt(); - - if (config_["framerate"].isInt()) prm_.framerate = config_["framerate"].asInt(); - // Calculate delay for Update() thread - frame_time_milsec_ = std::chrono::milliseconds((int)(1e3 / prm_.framerate)); - if (config_["autosens"].isInt()) prm_.autosens = config_["autosens"].asInt(); - if (config_["sensitivity"].isInt()) prm_.sens = config_["sensitivity"].asInt(); - if (config_["bars"].isInt()) prm_.fixedbars = config_["bars"].asInt(); - if (config_["lower_cutoff_freq"].isNumeric()) - prm_.lower_cut_off = config_["lower_cutoff_freq"].asLargestInt(); - if (config_["higher_cutoff_freq"].isNumeric()) - prm_.upper_cut_off = config_["higher_cutoff_freq"].asLargestInt(); - if (config_["sleep_timer"].isInt()) prm_.sleep_timer = config_["sleep_timer"].asInt(); - if (config_["method"].isString()) - prm_.input = ::cava::input_method_by_name(config_["method"].asString().c_str()); - if (config_["source"].isString()) { - if (prm_.audio_source) free(prm_.audio_source); - prm_.audio_source = strdup(config_["source"].asString().c_str()); - } - if (config_["sample_rate"].isNumeric()) prm_.samplerate = config_["sample_rate"].asLargestInt(); - if (config_["sample_bits"].isInt()) prm_.samplebits = config_["sample_bits"].asInt(); - if (config_["stereo"].isBool()) prm_.stereo = config_["stereo"].asBool(); - if (config_["reverse"].isBool()) prm_.reverse = config_["reverse"].asBool(); - if (config_["bar_delimiter"].isInt()) prm_.bar_delim = config_["bar_delimiter"].asInt(); - if (config_["monstercat"].isBool()) prm_.monstercat = config_["monstercat"].asBool(); - if (config_["waves"].isBool()) prm_.waves = config_["waves"].asBool(); - if (config_["noise_reduction"].isDouble()) - prm_.noise_reduction = config_["noise_reduction"].asDouble(); - if (config_["input_delay"].isInt()) - fetch_input_delay_ = std::chrono::seconds(config_["input_delay"].asInt()); - if (config_["gradient"].isInt()) prm_.gradient = config_["gradient"].asInt(); - if (prm_.gradient == 0) - prm_.gradient_count = 0; - else if (config_["gradient_count"].isInt()) - prm_.gradient_count = config_["gradient_count"].asInt(); - if (config_["sdl_width"].isInt()) prm_.sdl_width = config_["sdl_width"].asInt(); - if (config_["sdl_height"].isInt()) prm_.sdl_height = config_["sdl_height"].asInt(); - - audio_raw_.height = prm_.ascii_range; - audio_data_.format = -1; - audio_data_.rate = 0; - audio_data_.samples_counter = 0; - audio_data_.channels = 2; - audio_data_.IEEE_FLOAT = 0; - audio_data_.input_buffer_size = BUFFER_SIZE * audio_data_.channels; - audio_data_.cava_buffer_size = audio_data_.input_buffer_size * 8; - audio_data_.terminate = 0; - audio_data_.suspendFlag = false; - input_source_ = get_input(&audio_data_, &prm_); - - if (!input_source_) { - throw std::runtime_error("cava backend: API didn't provide an input audio source"); + { + auto icon_count = cfg["format-icons"].size(); + new_prm.ascii_range = (icon_count > 0) ? static_cast(icon_count) - 1 : 0; } - prm_.output = ::cava::output_method::OUTPUT_RAW; + if (cfg["bar_spacing"].isInt()) new_prm.bar_spacing = cfg["bar_spacing"].asInt(); + if (cfg["bar_width"].isInt()) new_prm.bar_width = cfg["bar_width"].asInt(); + if (cfg["bar_height"].isInt()) new_prm.bar_height = cfg["bar_height"].asInt(); + new_prm.orientation = ::cava::ORIENT_TOP; + new_prm.xaxis = ::cava::xaxis_scale::NONE; + new_prm.mono_opt = ::cava::AVERAGE; + new_prm.autobars = 0; + if (cfg["gravity"].isInt()) new_prm.gravity = cfg["gravity"].asInt(); + if (cfg["integral"].isInt()) new_prm.integral = cfg["integral"].asInt(); - // Make cava parameters configuration - // Init cava plan, audio_raw structure - audio_raw_init(&audio_data_, &audio_raw_, &prm_, &plan_); - if (!plan_) spdlog::error("cava backend plan is not provided"); - audio_raw_.previous_frame[0] = -1; // For first Update() call need to rePaint text message + if (cfg["framerate"].isInt()) new_prm.framerate = cfg["framerate"].asInt(); + if (cfg["autosens"].isInt()) new_prm.autosens = cfg["autosens"].asInt(); + if (cfg["sensitivity"].isInt()) new_prm.sens = cfg["sensitivity"].asInt(); + if (cfg["bars"].isInt()) new_prm.fixedbars = cfg["bars"].asInt(); + if (cfg["lower_cutoff_freq"].isNumeric()) + new_prm.lower_cut_off = cfg["lower_cutoff_freq"].asLargestInt(); + if (cfg["higher_cutoff_freq"].isNumeric()) + new_prm.upper_cut_off = cfg["higher_cutoff_freq"].asLargestInt(); + if (cfg["sleep_timer"].isInt()) new_prm.sleep_timer = cfg["sleep_timer"].asInt(); + if (cfg["method"].isString()) + new_prm.input = ::cava::input_method_by_name(cfg["method"].asString().c_str()); + if (cfg["source"].isString()) { + if (new_prm.audio_source) free(new_prm.audio_source); + new_prm.audio_source = strdup(cfg["source"].asString().c_str()); + } + if (cfg["sample_rate"].isNumeric()) new_prm.samplerate = cfg["sample_rate"].asLargestInt(); + if (cfg["sample_bits"].isInt()) new_prm.samplebits = cfg["sample_bits"].asInt(); + if (cfg["stereo"].isBool()) new_prm.stereo = cfg["stereo"].asBool(); + if (cfg["reverse"].isBool()) new_prm.reverse = cfg["reverse"].asBool(); + if (cfg["bar_delimiter"].isInt()) new_prm.bar_delim = cfg["bar_delimiter"].asInt(); + if (cfg["monstercat"].isBool()) new_prm.monstercat = cfg["monstercat"].asBool(); + if (cfg["waves"].isBool()) new_prm.waves = cfg["waves"].asBool(); + if (cfg["noise_reduction"].isDouble()) + new_prm.noise_reduction = cfg["noise_reduction"].asDouble(); + if (cfg["input_delay"].isInt()) + fetch_input_delay_ = std::chrono::seconds(cfg["input_delay"].asInt()); + if (cfg["gradient"].isInt()) new_prm.gradient = cfg["gradient"].asInt(); + if (new_prm.gradient == 0) + new_prm.gradient_count = 0; + else if (cfg["gradient_count"].isInt()) + new_prm.gradient_count = cfg["gradient_count"].asInt(); + if (cfg["sdl_width"].isInt()) new_prm.sdl_width = cfg["sdl_width"].asInt(); + if (cfg["sdl_height"].isInt()) new_prm.sdl_height = cfg["sdl_height"].asInt(); + if (new_prm.framerate <= 0) { + throw std::runtime_error(std::string{"cava backend: framerate must be positive, got: "} + + std::to_string(new_prm.framerate)); + } + + adaptive_delay_.reset(std::chrono::milliseconds(static_cast(1e3 / new_prm.framerate))); + freeBackend(); + + try { + audio_raw_.height = new_prm.ascii_range; + audio_data_.format = -1; + audio_data_.rate = 0; + audio_data_.samples_counter = 0; + audio_data_.channels = 2; + audio_data_.IEEE_FLOAT = 0; + audio_data_.input_buffer_size = BUFFER_SIZE * audio_data_.channels; + audio_data_.cava_buffer_size = audio_data_.input_buffer_size * 8; + audio_data_.terminate = 0; + audio_data_.suspendFlag = false; + + input_source_ = get_input(&audio_data_, &new_prm); + if (!input_source_) { + throw std::runtime_error("cava backend: API didn't provide an input audio source"); + } + + new_prm.output = ::cava::output_method::OUTPUT_RAW; + + audio_raw_init(&audio_data_, &audio_raw_, &new_prm, &plan_); + if (!plan_) { + throw std::runtime_error("cava backend plan is not provided"); + } + audio_raw_.previous_frame[0] = -1; + audio_raw_initialized_ = true; + } catch (...) { + freeBackend(); + throw; + } + + prm_ = new_prm; + new_prm_guard.release(); prm_.output = output; + + m_signal_config_changed_.emit(); } -const struct ::cava::config_params* waybar::modules::cava::CavaBackend::getPrm() { return &prm_; } -std::chrono::milliseconds waybar::modules::cava::CavaBackend::getFrameTimeMilsec() { - return frame_time_milsec_; -}; +const ::cava::config_params& waybar::modules::cava::CavaBackend::getPrm() const { + std::lock_guard lock(state_mutex_); + return prm_; +} + +std::chrono::milliseconds waybar::modules::cava::CavaBackend::getFrameTimeMilsec() const { + std::lock_guard lock(state_mutex_); + return adaptive_delay_.current(); +}