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 (<memory>, <string>, <chrono>)
- 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.
This commit is contained in:
Viktar Lukashonak
2026-07-13 19:50:09 +03:00
parent cf19c836d3
commit 5c979152de
8 changed files with 692 additions and 339 deletions
+46 -10
View File
@@ -1,36 +1,64 @@
#pragma once #pragma once
#include <epoxy/gl.h> #include <epoxy/gl.h>
#include <gtkmm/glarea.h>
#include <map>
#include <string>
#include <array>
#include <sigc++/connection.h>
#include "AModule.hpp" #include "AModule.hpp"
#include "cava_backend.hpp" #include "cava_backend.hpp"
namespace waybar::modules::cava { namespace waybar::modules::cava {
class CavaGLSL final : public AModule, public Gtk::GLArea { class CavaGLSL final : public AModule {
public: public:
CavaGLSL(const std::string&, const Json::Value&); CavaGLSL(const std::string&, const Json::Value&);
~CavaGLSL() = default; ~CavaGLSL();
auto doAction(const std::string& name) -> void override;
private: private:
using Action = void (CavaGLSL::*)();
Gtk::GLArea gl_area_;
std::shared_ptr<CavaBackend> backend_; std::shared_ptr<CavaBackend> backend_;
struct ::cava::config_params prm_; // Cached config params (deep-copied strings to avoid dangling char* on backend reload)
int frame_counter{0}; 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<std::string, 8> gradient_colors_;
int frame_counter_{0};
bool silence_{false}; bool silence_{false};
bool hide_on_silence_{false}; bool hide_on_silence_{false};
bool mapped_{false};
// Cava method // Cava method
auto onUpdate(const ::cava::audio_raw& input) -> void; void pauseResume();
auto onUpdate(const CavaBackend::AudioRaw& input) -> void;
auto onSilence() -> void; auto onSilence() -> void;
// Member variable to store the shared pointer auto onBackendConfigChanged() -> void;
std::shared_ptr<::cava::audio_raw> m_data_; void cacheConfigParams(const ::cava::config_params& src);
GLuint shaderProgram_; // Member variable to store audio data
CavaBackend::AudioRaw m_data_;
GLuint shaderProgram_{0};
// OpenGL variables // OpenGL variables
GLuint fbo_; GLuint fbo_{0};
GLuint texture_; GLuint texture_{0};
GLuint vbo_{0};
GLuint ibo_{0};
GLuint vao_{0};
GLint uniform_bars_; GLint uniform_bars_;
GLint uniform_previous_bars_; GLint uniform_previous_bars_;
GLint uniform_bars_count_; GLint uniform_bars_count_;
GLint uniform_time_; GLint uniform_time_;
GLint uniform_input_texture_;
// Methods // Methods
void onRealize(); void onRealize();
bool onRender(const Glib::RefPtr<Gdk::GLContext>& context); bool onRender(const Glib::RefPtr<Gdk::GLContext>& context);
@@ -39,5 +67,13 @@ class CavaGLSL final : public AModule, public Gtk::GLArea {
void initSurface(); void initSurface();
void initGLSL(); void initGLSL();
GLuint loadShader(const std::string& fileName, GLenum type); GLuint loadShader(const std::string& fileName, GLenum type);
void cleanupGL();
// ModuleActionMap
static const std::map<std::string, Action> actionMap_;
sigc::connection audio_raw_update_conn_;
sigc::connection silence_conn_;
sigc::connection config_changed_conn_;
}; };
} // namespace waybar::modules::cava } // namespace waybar::modules::cava
+17 -9
View File
@@ -1,30 +1,38 @@
#pragma once #pragma once
#include <map>
#include <string>
#include <sigc++/connection.h>
#include "ALabel.hpp" #include "ALabel.hpp"
#include "cava_backend.hpp" #include "cava_backend.hpp"
namespace waybar::modules::cava { namespace waybar::modules::cava {
class Cava final : public ALabel, public sigc::trackable { class CavaRaw final : public ALabel {
public: public:
Cava(const std::string&, const Json::Value&); CavaRaw(const std::string&, const Json::Value&);
~Cava() = default; ~CavaRaw();
auto doAction(const std::string& name) -> void override; auto doAction(const std::string& name) -> void override;
private: private:
using Action = void (CavaRaw::*)();
std::shared_ptr<CavaBackend> backend_; std::shared_ptr<CavaBackend> backend_;
// Text to display // Text to display
Glib::ustring label_text_{""}; Glib::ustring label_text_;
bool silence_{false}; bool silence_{false};
bool hide_on_silence_{false}; bool hide_on_silence_{false};
std::string format_silent_{""}; std::string format_silent_;
int ascii_range_{0};
// Cava method // Cava method
void pause_resume(); void pauseResume();
auto onUpdate(const std::string& input) -> void; auto onUpdate(const std::string& input) -> void;
auto onSilence() -> void; auto onSilence() -> void;
// ModuleActionMap // ModuleActionMap
static inline std::map<const std::string, void (waybar::modules::cava::Cava::* const)()> static const std::map<std::string, Action> actionMap_;
actionMap_{{"mode", &waybar::modules::cava::Cava::pause_resume}};
sigc::connection update_conn_;
sigc::connection silence_conn_;
}; };
} // namespace waybar::modules::cava } // namespace waybar::modules::cava
+90 -21
View File
@@ -1,8 +1,17 @@
#pragma once #pragma once
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include <json/json.h> #include <json/json.h>
#include <sigc++/sigc++.h> #include <sigc++/sigc++.h>
#include "util/SafeSignal.hpp"
#include "util/sleeper_thread.hpp" #include "util/sleeper_thread.hpp"
namespace cava { namespace cava {
@@ -21,7 +30,6 @@ extern "C" {
} // namespace cava } // namespace cava
namespace waybar::modules::cava { namespace waybar::modules::cava {
using namespace std::literals::chrono_literals;
class CavaBackend final { class CavaBackend final {
public: public:
@@ -29,19 +37,38 @@ class CavaBackend final {
virtual ~CavaBackend(); virtual ~CavaBackend();
// Methods // Methods
int getAsciiRange(); int getAsciiRange() const;
void doPauseResume(); void doPauseResume();
void Update(); void update();
const struct ::cava::config_params* getPrm(); const ::cava::config_params& getPrm() const;
std::chrono::milliseconds getFrameTimeMilsec(); std::chrono::milliseconds getFrameTimeMilsec() const;
struct AudioRaw {
std::vector<float> bars_raw;
std::vector<float> 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 // Signal accessor
using type_signal_update = sigc::signal<void(const std::string&)>; using SignalUpdate = SafeSignal<const std::string&>;
type_signal_update signal_update(); SignalUpdate& signalUpdate();
using type_signal_audio_raw_update = sigc::signal<void(const ::cava::audio_raw&)>; using SignalAudioRawUpdate = SafeSignal<AudioRaw>;
type_signal_audio_raw_update signal_audio_raw_update(); SignalAudioRawUpdate& signalAudioRawUpdate();
using type_signal_silence = sigc::signal<void()>; using SignalSilence = SafeSignal<>;
type_signal_silence signal_silence(); SignalSilence& signalSilence();
using SignalConfigChanged = SafeSignal<>;
SignalConfigChanged& signalConfigChanged();
private: private:
CavaBackend(const Json::Value& config); CavaBackend(const Json::Value& config);
@@ -49,36 +76,78 @@ class CavaBackend final {
util::SleeperThread out_thread_; util::SleeperThread out_thread_;
// Cava API to read audio source // 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::error_s error_{}; // cava errors
struct ::cava::config_params prm_{}; // cava parameters 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_raw audio_raw_{}; // cava handled raw audio data(is based on audio_data)
struct ::cava::audio_data audio_data_{}; // cava 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}; 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}; int re_paint_{0};
bool silence_{false}; bool silence_{false};
bool silence_prev_{false}; bool silence_prev_{false};
std::chrono::seconds suspend_silence_delay_{0};
int sleep_counter_{0}; int sleep_counter_{0};
std::string output_{}; std::string output_{};
// Methods // Methods
void invoke(); void invoke();
void execute(); void execute();
bool isSilence(); bool isSilent();
void doUpdate(bool force = false); void doUpdate(bool force = false);
void loadConfig(); void loadConfig();
void freeBackend(); void freeBackend();
// Signal // Signal
type_signal_update m_signal_update_; SignalUpdate m_signal_update_;
type_signal_audio_raw_update m_signal_audio_raw_; SignalAudioRawUpdate m_signal_audio_raw_;
type_signal_silence m_signal_silence_; SignalSilence m_signal_silence_;
SignalConfigChanged m_signal_config_changed_;
std::atomic<bool> 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 } // namespace waybar::modules::cava
+6 -4
View File
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <memory>
#ifdef HAVE_LIBCAVA #ifdef HAVE_LIBCAVA
#include "cavaRaw.hpp" #include "cavaRaw.hpp"
#include "cava_backend.hpp" #include "cava_backend.hpp"
@@ -9,16 +11,16 @@
#endif #endif
namespace waybar::modules::cava { namespace waybar::modules::cava {
AModule* getModule(const std::string& id, const Json::Value& config) { inline std::unique_ptr<AModule> getModule(const std::string& id, const Json::Value& config) {
#ifdef HAVE_LIBCAVA #ifdef HAVE_LIBCAVA
const std::shared_ptr<CavaBackend> backend_{waybar::modules::cava::CavaBackend::inst(config)}; const std::shared_ptr<CavaBackend> backend_{waybar::modules::cava::CavaBackend::inst(config)};
switch (backend_->getPrm()->output) { switch (backend_->getPrm().output) {
#ifdef HAVE_LIBCAVAGLSL #ifdef HAVE_LIBCAVAGLSL
case ::cava::output_method::OUTPUT_SDL_GLSL: case ::cava::output_method::OUTPUT_SDL_GLSL:
return new waybar::modules::cava::CavaGLSL(id, config); return std::make_unique<waybar::modules::cava::CavaGLSL>(id, config);
#endif #endif
default: default:
return new waybar::modules::cava::Cava(id, config); return std::make_unique<waybar::modules::cava::CavaRaw>(id, config);
} }
#else #else
throw std::runtime_error("Unknown module"); throw std::runtime_error("Unknown module");
+1 -1
View File
@@ -372,7 +372,7 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name,
} }
#endif #endif
if (ref == "cava") { if (ref == "cava") {
return waybar::modules::cava::getModule(id, config_[name]); return waybar::modules::cava::getModule(id, config_[name]).release();
} }
#ifdef HAVE_SYSTEMD_MONITOR #ifdef HAVE_SYSTEMD_MONITOR
if (ref == "systemd-failed-units") { if (ref == "systemd-failed-units") {
+254 -99
View File
@@ -2,88 +2,214 @@
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <fstream> #include <fstream>
#include <sstream>
#include <vector>
const std::map<std::string, waybar::modules::cava::CavaGLSL::Action>
waybar::modules::cava::CavaGLSL::actionMap_{{"mode", &CavaGLSL::pauseResume}};
waybar::modules::cava::CavaGLSL::CavaGLSL(const std::string& id, const Json::Value& config) waybar::modules::cava::CavaGLSL::CavaGLSL(const std::string& id, const Json::Value& config)
: AModule(config, "cavaGLSL", id, false, false), : AModule(config, "cavaGLSL", id, false, false),
backend_{waybar::modules::cava::CavaBackend::inst(config)} { 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 (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool();
if (!id.empty()) { 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); gl_area_.set_use_es(true);
// set_auto_render(true); gl_area_.signal_realize().connect(sigc::mem_fun(*this, &CavaGLSL::onRealize));
signal_realize().connect(sigc::mem_fun(*this, &CavaGLSL::onRealize)); gl_area_.signal_render().connect(sigc::mem_fun(*this, &CavaGLSL::onRender), false);
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 cacheConfigParams(backend_->getPrm());
prm_ = *backend_->getPrm();
// Set widget length
int length{0}; int length{0};
if (config_["min-length"].isUInt()) if (config_["min-length"].isUInt())
length = config_["min-length"].asUInt(); length = config_["min-length"].asUInt();
else if (config_["max-length"].isUInt()) else if (config_["max-length"].isUInt())
length = config_["max-length"].asUInt(); length = config_["max-length"].asUInt();
else 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 audio_raw_update_conn_ =
backend_->signal_audio_raw_update().connect(sigc::mem_fun(*this, &CavaGLSL::onUpdate)); backend_->signalAudioRawUpdate().connect(sigc::mem_fun(*this, &CavaGLSL::onUpdate));
// Subscribe for silence silence_conn_ = backend_->signalSilence().connect(sigc::mem_fun(*this, &CavaGLSL::onSilence));
backend_->signal_silence().connect(sigc::mem_fun(*this, &CavaGLSL::onSilence)); config_changed_conn_ =
event_box_.add(*this); 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 { waybar::modules::cava::CavaGLSL::~CavaGLSL() {
Glib::signal_idle().connect_once([this, input]() { audio_raw_update_conn_.disconnect();
m_data_ = std::make_shared<::cava::audio_raw>(input); silence_conn_.disconnect();
config_changed_conn_.disconnect();
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_) { if (silence_) {
get_style_context()->remove_class("silent"); gl_area_.get_style_context()->remove_class("silent");
if (!get_style_context()->has_class("updated")) get_style_context()->add_class("updated"); if (!gl_area_.get_style_context()->has_class("updated"))
show(); gl_area_.get_style_context()->add_class("updated");
gl_area_.show();
silence_ = false; silence_ = false;
} }
queue_render(); if (mapped_) {
}); gl_area_.queue_render();
}
} }
auto waybar::modules::cava::CavaGLSL::onSilence() -> void { auto waybar::modules::cava::CavaGLSL::onSilence() -> void {
Glib::signal_idle().connect_once([this]() {
if (!silence_) { if (!silence_) {
if (get_style_context()->has_class("updated")) get_style_context()->remove_class("updated"); if (gl_area_.get_style_context()->has_class("updated"))
gl_area_.get_style_context()->remove_class("updated");
if (hide_on_silence_) hide(); if (hide_on_silence_) gl_area_.hide();
silence_ = true; silence_ = true;
get_style_context()->add_class("silent"); gl_area_.get_style_context()->add_class("silent");
// Set clear color to black
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
queue_render();
} }
}); }
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<Gdk::GLContext>& context) { bool waybar::modules::cava::CavaGLSL::onRender(const Glib::RefPtr<Gdk::GLContext>& 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); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture_); 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_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); glUniform1fv(uniform_previous_bars_, m_data_.number_of_bars, m_data_.previous_bars_raw.data());
glUniform1i(uniform_bars_count_, m_data_->number_of_bars); glUniform1i(uniform_bars_count_, m_data_.number_of_bars);
++frame_counter; ++frame_counter_;
glUniform1f(uniform_time_, (frame_counter / backend_->getFrameTimeMilsec().count()) / 1e3); glUniform1f(uniform_time_,
static_cast<float>(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); glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, nullptr);
glBindFramebuffer(GL_FRAMEBUFFER, fbo_); glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
@@ -94,21 +220,35 @@ bool waybar::modules::cava::CavaGLSL::onRender(const Glib::RefPtr<Gdk::GLContext
} }
void waybar::modules::cava::CavaGLSL::onRealize() { void waybar::modules::cava::CavaGLSL::onRealize() {
make_current(); gl_area_.make_current();
cleanupGL();
initShaders(); initShaders();
if (shaderProgram_ == 0) {
return;
}
initGLSL(); initGLSL();
initSurface(); initSurface();
} }
struct colors { struct Colors {
uint16_t R; uint16_t R;
uint16_t G; uint16_t G;
uint16_t B; uint16_t B;
}; };
static void parse_color(char* color_string, struct colors* color) { static void parse_color(const char* color_string, struct Colors* color) {
if (color_string[0] == '#') { if (color_string == nullptr) {
sscanf(++color_string, "%02hx%02hx%02hx", &color->R, &color->G, &color->B); 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}; GLfloat vertexData[]{-1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f};
GLint indexData[]{0, 1, 2, 3}; GLint indexData[]{0, 1, 2, 3};
GLuint gVBO{0}; glGenBuffers(1, &vbo_);
glGenBuffers(1, &gVBO); glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glBindBuffer(GL_ARRAY_BUFFER, gVBO);
glBufferData(GL_ARRAY_BUFFER, 2 * 4 * sizeof(GLfloat), vertexData, GL_STATIC_DRAW); glBufferData(GL_ARRAY_BUFFER, 2 * 4 * sizeof(GLfloat), vertexData, GL_STATIC_DRAW);
GLuint gIBO{0}; glGenBuffers(1, &ibo_);
glGenBuffers(1, &gIBO); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gIBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4 * sizeof(GLuint), indexData, GL_STATIC_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4 * sizeof(GLuint), indexData, GL_STATIC_DRAW);
GLuint gVAO{0}; glGenVertexArrays(1, &vao_);
glGenVertexArrays(1, &gVAO); glBindVertexArray(vao_);
glBindVertexArray(gVAO);
glEnableVertexAttribArray(gVertexPos2DLocation); glEnableVertexAttribArray(gVertexPos2DLocation);
glBindBuffer(GL_ARRAY_BUFFER, gVBO); glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glVertexAttribPointer(gVertexPos2DLocation, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr); 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_); glGenFramebuffers(1, &fbo_);
glBindFramebuffer(GL_FRAMEBUFFER, fbo_); glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
// Create a texture to attach the framebuffer
glGenTextures(1, &texture_); glGenTextures(1, &texture_);
glBindTexture(GL_TEXTURE_2D, 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); GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_, 0);
// Check is framebuffer is complete
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
spdlog::error("{0}. Framebuffer not complete", name_); spdlog::error("{0}. Framebuffer not complete", name_);
} }
// Unbind the framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
uniform_bars_ = glGetUniformLocation(shaderProgram_, "bars"); uniform_bars_ = glGetUniformLocation(shaderProgram_, "bars");
uniform_previous_bars_ = glGetUniformLocation(shaderProgram_, "previous_bars"); uniform_previous_bars_ = glGetUniformLocation(shaderProgram_, "previous_bars");
uniform_bars_count_ = glGetUniformLocation(shaderProgram_, "bars_count"); uniform_bars_count_ = glGetUniformLocation(shaderProgram_, "bars_count");
uniform_time_ = glGetUniformLocation(shaderProgram_, "shader_time"); uniform_time_ = glGetUniformLocation(shaderProgram_, "shader_time");
uniform_input_texture_ = glGetUniformLocation(shaderProgram_, "inputTexture");
GLuint err{glGetError()}; GLuint err{glGetError()};
if (err != 0) { if (err != 0) {
@@ -174,32 +309,32 @@ void waybar::modules::cava::CavaGLSL::initGLSL() {
} }
void waybar::modules::cava::CavaGLSL::initSurface() { void waybar::modules::cava::CavaGLSL::initSurface() {
colors color = {0}; Colors color = {0};
GLint uniform_bg_col{glGetUniformLocation(shaderProgram_, "bg_color")}; GLint uniform_bg_col{glGetUniformLocation(shaderProgram_, "bg_color")};
parse_color(prm_.bcolor, &color); parse_color(bcolor_.c_str(), &color);
glUniform3f(uniform_bg_col, (float)color.R / 255.0, (float)color.G / 255.0, glUniform3f(uniform_bg_col, static_cast<float>(color.R) / 255.0f, static_cast<float>(color.G) / 255.0f,
(float)color.B / 255.0); static_cast<float>(color.B) / 255.0f);
GLint uniform_fg_col{glGetUniformLocation(shaderProgram_, "fg_color")}; GLint uniform_fg_col{glGetUniformLocation(shaderProgram_, "fg_color")};
parse_color(prm_.color, &color); parse_color(color_.c_str(), &color);
glUniform3f(uniform_fg_col, (float)color.R / 255.0, (float)color.G / 255.0, glUniform3f(uniform_fg_col, static_cast<float>(color.R) / 255.0f, static_cast<float>(color.G) / 255.0f,
(float)color.B / 255.0); static_cast<float>(color.B) / 255.0f);
GLint uniform_res{glGetUniformLocation(shaderProgram_, "u_resolution")}; 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<float>(sdl_width_), static_cast<float>(sdl_height_), 0.0f);
GLint uniform_bar_width{glGetUniformLocation(shaderProgram_, "bar_width")}; 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")}; 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")}; 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")}; GLint uniform_gradient_colors{glGetUniformLocation(shaderProgram_, "gradient_colors")};
GLfloat gradient_colors[8][3]; GLfloat gradient_colors[8][3] = {};
for (int i{0}; i < prm_.gradient_count; ++i) { for (int i{0}; i < gradient_count_; ++i) {
parse_color(prm_.gradient_colors[i], &color); parse_color(gradient_colors_[i].c_str(), &color);
gradient_colors[i][0] = (float)color.R / 255.0; gradient_colors[i][0] = static_cast<float>(color.R) / 255.0f;
gradient_colors[i][1] = (float)color.G / 255.0; gradient_colors[i][1] = static_cast<float>(color.G) / 255.0f;
gradient_colors[i][2] = (float)color.B / 255.0; gradient_colors[i][2] = static_cast<float>(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); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, nullptr); 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() { void waybar::modules::cava::CavaGLSL::initShaders() {
shaderProgram_ = glCreateProgram(); 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 vertexShader{loadShader(vertex_shader_, GL_VERTEX_SHADER)};
GLuint fragmentShader{loadShader(prm_.fragment_shader, GL_FRAGMENT_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_, vertexShader);
glAttachShader(shaderProgram_, fragmentShader); glAttachShader(shaderProgram_, fragmentShader);
@@ -219,14 +368,18 @@ void waybar::modules::cava::CavaGLSL::initShaders() {
glDeleteShader(vertexShader); glDeleteShader(vertexShader);
glDeleteShader(fragmentShader); glDeleteShader(fragmentShader);
// Check for linking errors GLint success{0};
GLint success, len;
glGetProgramiv(shaderProgram_, GL_LINK_STATUS, &success); glGetProgramiv(shaderProgram_, GL_LINK_STATUS, &success);
if (!success) { if (!success) {
GLint len{0};
glGetProgramiv(shaderProgram_, GL_INFO_LOG_LENGTH, &len); glGetProgramiv(shaderProgram_, GL_INFO_LOG_LENGTH, &len);
GLchar* infoLog{(char*)'\0'}; std::vector<GLchar> infoLog(len + 1);
glGetProgramInfoLog(shaderProgram_, len, &len, infoLog); glGetProgramInfoLog(shaderProgram_, len, nullptr, infoLog.data());
spdlog::error("{0}. Shader linking error: {1}", name_, infoLog); spdlog::error("{0}. Shader linking error: {1}", name_, infoLog.data());
glDeleteProgram(shaderProgram_);
shaderProgram_ = 0;
gl_area_.hide();
return;
} }
glReleaseShaderCompiler(); glReleaseShaderCompiler();
@@ -236,35 +389,37 @@ void waybar::modules::cava::CavaGLSL::initShaders() {
GLuint waybar::modules::cava::CavaGLSL::loadShader(const std::string& fileName, GLenum type) { GLuint waybar::modules::cava::CavaGLSL::loadShader(const std::string& fileName, GLenum type) {
spdlog::debug("{0}. loadShader: {1}", name_, fileName); spdlog::debug("{0}. loadShader: {1}", name_, fileName);
// Read shader source code from the file
std::ifstream shaderFile{fileName}; std::ifstream shaderFile{fileName};
if (!shaderFile.is_open()) { if (!shaderFile.is_open()) {
spdlog::error("{0}. Could not open shader file: {1}", name_, fileName); spdlog::error("{0}. Could not open shader file: {1}", name_, fileName);
return 0;
} }
std::ostringstream buffer; std::ostringstream buffer;
buffer << shaderFile.rdbuf(); // read file content into stringstream buffer << shaderFile.rdbuf();
std::string str{buffer.str()}; std::string str{buffer.str()};
const char* shaderSource = str.c_str();
shaderFile.close(); shaderFile.close();
GLuint shaderID{glCreateShader(type)}; 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); glShaderSource(shaderID, 1, &shaderSource, nullptr);
glCompileShader(shaderID); glCompileShader(shaderID);
// Check for compilation errors GLint success{0};
GLint success, len;
glGetShaderiv(shaderID, GL_COMPILE_STATUS, &success); glGetShaderiv(shaderID, GL_COMPILE_STATUS, &success);
if (!success) { if (!success) {
GLint len{0};
glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &len); glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &len);
std::vector<GLchar> infoLog(len + 1);
GLchar* infoLog{(char*)'\0'}; glGetShaderInfoLog(shaderID, len, nullptr, infoLog.data());
glGetShaderInfoLog(shaderID, len, nullptr, infoLog); spdlog::error("{0}. Shader compilation error in {1}: {2}", name_, fileName, infoLog.data());
spdlog::error("{0}. Shader compilation error in {1}: {2}", name_, fileName, infoLog); glDeleteShader(shaderID);
return 0;
} }
return shaderID; return shaderID;
+28 -19
View File
@@ -2,29 +2,37 @@
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
waybar::modules::cava::Cava::Cava(const std::string& id, const Json::Value& config) const std::map<std::string, waybar::modules::cava::CavaRaw::Action>
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), : ALabel(config, "cava", id, "{}", 60, false, false, false),
backend_{waybar::modules::cava::CavaBackend::inst(config)} { backend_{waybar::modules::cava::CavaBackend::inst(config)} {
if (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool(); 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(); if (config_["format_silent"].isString()) format_silent_ = config_["format_silent"].asString();
ascii_range_ = backend_->getAsciiRange(); update_conn_ = backend_->signalUpdate().connect(sigc::mem_fun(*this, &CavaRaw::onUpdate));
backend_->signal_update().connect(sigc::mem_fun(*this, &Cava::onUpdate)); silence_conn_ = backend_->signalSilence().connect(sigc::mem_fun(*this, &CavaRaw::onSilence));
backend_->signal_silence().connect(sigc::mem_fun(*this, &Cava::onSilence)); backend_->update();
backend_->Update();
} }
auto waybar::modules::cava::Cava::doAction(const std::string& name) -> void { waybar::modules::cava::CavaRaw::~CavaRaw() {
if ((actionMap_[name])) { update_conn_.disconnect();
(this->*actionMap_[name])(); silence_conn_.disconnect();
} else }
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); spdlog::error("Cava. Unsupported action \"{0}\"", name);
} }
}
// Cava actions // Cava actions
void waybar::modules::cava::Cava::pause_resume() { backend_->doPauseResume(); } void waybar::modules::cava::CavaRaw::pauseResume() { backend_->doPauseResume(); }
auto waybar::modules::cava::Cava::onUpdate(const std::string& input) -> void { auto waybar::modules::cava::CavaRaw::onUpdate(const std::string& input) -> void {
Glib::signal_idle().connect_once([this, input]() {
if (silence_) { if (silence_) {
silence_ = false; silence_ = false;
label_.get_style_context()->remove_class("silent"); label_.get_style_context()->remove_class("silent");
@@ -32,17 +40,18 @@ auto waybar::modules::cava::Cava::onUpdate(const std::string& input) -> void {
label_.get_style_context()->add_class("updated"); label_.get_style_context()->add_class("updated");
} }
label_text_.clear(); label_text_.clear();
for (auto& ch : input) auto ascii_range = backend_->getAsciiRange();
label_text_.append(getIcon((ch > ascii_range_) ? ascii_range_ : ch, "", ascii_range_ + 1)); for (auto& ch : input) {
auto uch = static_cast<unsigned char>(ch);
label_text_.append(getIcon((uch > ascii_range) ? ascii_range : uch, "", ascii_range + 1));
}
label_.set_markup(label_text_); label_.set_markup(label_text_);
label_.show(); label_.show();
ALabel::update(); ALabel::update();
});
} }
auto waybar::modules::cava::Cava::onSilence() -> void { auto waybar::modules::cava::CavaRaw::onSilence() -> void {
Glib::signal_idle().connect_once([this]() {
if (!silence_) { if (!silence_) {
if (label_.get_style_context()->has_class("updated")) if (label_.get_style_context()->has_class("updated"))
label_.get_style_context()->remove_class("updated"); label_.get_style_context()->remove_class("updated");
@@ -51,10 +60,10 @@ auto waybar::modules::cava::Cava::onSilence() -> void {
// Clear the label markup before hiding to prevent GTK from rendering a NULL Pango layout // Clear the label markup before hiding to prevent GTK from rendering a NULL Pango layout
label_.set_markup(""); label_.set_markup("");
label_.hide(); label_.hide();
} else if (config_["format_silent"].isString()) } else if (!format_silent_.empty()) {
label_.set_markup(format_silent_); label_.set_markup(format_silent_);
}
silence_ = true; silence_ = true;
label_.get_style_context()->add_class("silent"); label_.get_style_context()->add_class("silent");
} }
});
} }
+203 -129
View File
@@ -2,8 +2,23 @@
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <algorithm>
#include <stdexcept> #include <stdexcept>
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> waybar::modules::cava::CavaBackend::inst( std::shared_ptr<waybar::modules::cava::CavaBackend> waybar::modules::cava::CavaBackend::inst(
const Json::Value& config) { const Json::Value& config) {
static auto* backend = new CavaBackend(config); static auto* backend = new CavaBackend(config);
@@ -12,69 +27,84 @@ std::shared_ptr<waybar::modules::cava::CavaBackend> waybar::modules::cava::CavaB
} }
waybar::modules::cava::CavaBackend::CavaBackend(const Json::Value& config) : config_(config) { waybar::modules::cava::CavaBackend::CavaBackend(const Json::Value& config) : config_(config) {
// Load waybar module config
loadConfig(); loadConfig();
// Read audio source trough cava API. Cava orginizes this process via infinity loop
read_thread_ = [this] { read_thread_ = [this] {
while (read_thread_.isRunning()) {
try { try {
if (input_source_) {
input_source_(&audio_data_); input_source_(&audio_data_);
} catch (const std::runtime_error& e) { }
} catch (const std::exception& e) {
spdlog::warn("Cava backend. Read source error: {0}", e.what()); spdlog::warn("Cava backend. Read source error: {0}", e.what());
} }
if (!read_thread_.isRunning()) break;
read_thread_.sleep_for(fetch_input_delay_); read_thread_.sleep_for(fetch_input_delay_);
// loadConfig() now throws on failure; contain it so a runtime config error if (!read_thread_.isRunning()) break;
// logs instead of terminating the process (#4456).
try { try {
loadConfig(); loadConfig();
} catch (const std::exception& e) { } catch (const std::exception& e) {
spdlog::error("{}", e.what()); spdlog::error("{}", e.what());
} }
}
{
std::lock_guard<std::mutex> lk(read_thread_exit_mutex_);
read_thread_exited_ = true;
}
read_thread_exit_cv_.notify_one();
}; };
// Write outcoming data. Emit signals
out_thread_ = [this] { out_thread_ = [this] {
try {
doUpdate(false); doUpdate(false);
out_thread_.sleep_for(frame_time_milsec_); } 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() { waybar::modules::cava::CavaBackend::~CavaBackend() {
{
std::lock_guard<std::recursive_mutex> lock(state_mutex_);
shutdown_ = true;
}
pthread_mutex_lock(&audio_data_.lock);
audio_data_.terminate = 1;
pthread_mutex_unlock(&audio_data_.lock);
out_thread_.stop(); out_thread_.stop();
read_thread_.stop(); read_thread_.stop();
std::unique_lock<std::mutex> 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<std::recursive_mutex> lock(state_mutex_);
freeBackend(); freeBackend();
} }
static bool upThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { bool waybar::modules::cava::CavaBackend::isSilent() {
if (delta == std::chrono::seconds{0}) { pthread_mutex_lock(&audio_data_.lock);
delta += std::chrono::seconds{1}; bool silent = true;
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() {
for (int i{0}; i < audio_data_.input_buffer_size; ++i) { for (int i{0}; i < audio_data_.input_buffer_size; ++i) {
if (audio_data_.cava_in[i]) { if (audio_data_.cava_in[i]) {
return false; silent = false;
break;
} }
} }
pthread_mutex_unlock(&audio_data_.lock);
return true; return silent;
} }
int waybar::modules::cava::CavaBackend::getAsciiRange() { return prm_.ascii_range; } int waybar::modules::cava::CavaBackend::getAsciiRange() const {
std::lock_guard<std::recursive_mutex> lock(state_mutex_);
return prm_.ascii_range;
}
// Process: execute cava
void waybar::modules::cava::CavaBackend::invoke() { void waybar::modules::cava::CavaBackend::invoke() {
pthread_mutex_lock(&audio_data_.lock); pthread_mutex_lock(&audio_data_.lock);
::cava::cava_execute(audio_data_.cava_in, audio_data_.samples_counter, audio_raw_.cava_out, ::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); pthread_mutex_unlock(&audio_data_.lock);
} }
// Do transformation under raw data
void waybar::modules::cava::CavaBackend::execute() { void waybar::modules::cava::CavaBackend::execute() {
invoke(); invoke();
audio_raw_fetch(&audio_raw_, &prm_, &re_paint_, plan_); audio_raw_fetch(&audio_raw_, &prm_, &re_paint_, plan_);
@@ -99,156 +128,187 @@ void waybar::modules::cava::CavaBackend::execute() {
} }
void waybar::modules::cava::CavaBackend::doPauseResume() { void waybar::modules::cava::CavaBackend::doPauseResume() {
std::lock_guard<std::recursive_mutex> lock(state_mutex_);
pthread_mutex_lock(&audio_data_.lock); pthread_mutex_lock(&audio_data_.lock);
if (audio_data_.suspendFlag) { if (audio_data_.suspendFlag) {
audio_data_.suspendFlag = false; audio_data_.suspendFlag = false;
pthread_cond_broadcast(&audio_data_.resumeCond); pthread_cond_broadcast(&audio_data_.resumeCond);
downThreadDelay(frame_time_milsec_, suspend_silence_delay_); adaptive_delay_.decrease();
} else { } else {
audio_data_.suspendFlag = true; audio_data_.suspendFlag = true;
upThreadDelay(frame_time_milsec_, suspend_silence_delay_); adaptive_delay_.increase();
} }
pthread_mutex_unlock(&audio_data_.lock); pthread_mutex_unlock(&audio_data_.lock);
Update(); update();
} }
waybar::modules::cava::CavaBackend::type_signal_update waybar::modules::cava::CavaBackend::SignalUpdate&
waybar::modules::cava::CavaBackend::signal_update() { waybar::modules::cava::CavaBackend::signalUpdate() {
return m_signal_update_; return m_signal_update_;
} }
waybar::modules::cava::CavaBackend::type_signal_audio_raw_update waybar::modules::cava::CavaBackend::SignalAudioRawUpdate&
waybar::modules::cava::CavaBackend::signal_audio_raw_update() { waybar::modules::cava::CavaBackend::signalAudioRawUpdate() {
return m_signal_audio_raw_; return m_signal_audio_raw_;
} }
waybar::modules::cava::CavaBackend::type_signal_silence waybar::modules::cava::CavaBackend::SignalSilence&
waybar::modules::cava::CavaBackend::signal_silence() { waybar::modules::cava::CavaBackend::signalSilence() {
return m_signal_silence_; 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) { void waybar::modules::cava::CavaBackend::doUpdate(bool force) {
std::lock_guard<std::recursive_mutex> lock(state_mutex_);
if (!plan_ || !input_source_) return;
if (audio_data_.suspendFlag && !force) return; if (audio_data_.suspendFlag && !force) return;
silence_ = isSilence(); silence_ = isSilent();
if (!silence_) sleep_counter_ = 0; if (!silence_) sleep_counter_ = 0;
if (silence_ && prm_.sleep_timer != 0) { if (silence_ && prm_.sleep_timer != 0) {
if (sleep_counter_ <= if (sleep_counter_ <=
(int)(std::chrono::milliseconds(prm_.sleep_timer * 1s) / frame_time_milsec_)) { static_cast<int>(std::chrono::milliseconds(prm_.sleep_timer * std::chrono::seconds(1)) /
adaptive_delay_.current())) {
++sleep_counter_; ++sleep_counter_;
silence_ = false; silence_ = false;
} }
} }
if (!silence_ || prm_.sleep_timer == 0) { if (!silence_ || prm_.sleep_timer == 0) {
if (downThreadDelay(frame_time_milsec_, suspend_silence_delay_)) Update(); while (adaptive_delay_.decrease()) {}
execute(); execute();
if (re_paint_ == 1 || force || prm_.continuous_rendering) { if (re_paint_ == 1 || force || prm_.continuous_rendering) {
m_signal_update_.emit(output_); m_signal_update_.emit(output_);
m_signal_audio_raw_.emit(audio_raw_); m_signal_audio_raw_.emit(AudioRaw{audio_raw_});
} }
} else { } else {
if (upThreadDelay(frame_time_milsec_, suspend_silence_delay_)) Update(); while (adaptive_delay_.increase()) {}
if (silence_ != silence_prev_ || force) m_signal_silence_.emit(); if (silence_ != silence_prev_ || force) m_signal_silence_.emit();
} }
silence_prev_ = silence_; silence_prev_ = silence_;
} }
void waybar::modules::cava::CavaBackend::freeBackend() { void waybar::modules::cava::CavaBackend::freeBackend() {
if (plan_ != NULL) { input_source_ = nullptr;
if (plan_ != nullptr) {
cava_destroy(plan_); cava_destroy(plan_);
plan_ = NULL; plan_ = nullptr;
} }
if (audio_raw_initialized_) {
audio_raw_clean(&audio_raw_); audio_raw_clean(&audio_raw_);
audio_raw_initialized_ = false;
}
pthread_mutex_lock(&audio_data_.lock); pthread_mutex_lock(&audio_data_.lock);
audio_data_.terminate = 1; audio_data_.terminate = 1;
pthread_mutex_unlock(&audio_data_.lock); pthread_mutex_unlock(&audio_data_.lock);
free_config(&prm_); free_config(&prm_);
prm_ = {};
free(audio_data_.source); free(audio_data_.source);
audio_data_.source = nullptr;
free(audio_data_.cava_in); free(audio_data_.cava_in);
audio_data_.cava_in = nullptr;
} }
void waybar::modules::cava::CavaBackend::loadConfig() { void waybar::modules::cava::CavaBackend::loadConfig() {
freeBackend(); std::lock_guard<std::recursive_mutex> lock(state_mutex_);
// Load waybar module config if (shutdown_.load()) {
char cfgPath[PATH_MAX]; return;
cfgPath[0] = '\0'; }
if (config_["cava_config"].isString()) strcpy(cfgPath, config_["cava_config"].asString().data()); const Json::Value& cfg = config_;
// Load cava config
struct ::cava::config_params new_prm{};
CavaConfigGuard new_prm_guard(&new_prm);
std::vector<char> 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<size_t>(PATH_MAX - 1));
std::copy_n(s.begin(), len, cfgPath.begin());
cfgPath[len] = '\0';
}
error_.length = 0; error_.length = 0;
if (!load_config(cfgPath, &prm_, &error_)) { if (!load_config(cfgPath.data(), &new_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_.
throw std::runtime_error(std::string{"cava backend: error loading config: "} + error_.message); throw std::runtime_error(std::string{"cava backend: error loading config: "} + error_.message);
} }
// Override cava parameters by the user config new_prm.inAtty = 0;
prm_.inAtty = 0; auto const output{new_prm.output};
auto const output{prm_.output}; if (new_prm.data_format) free(new_prm.data_format);
// prm_.output = ::cava::output_method::OUTPUT_RAW; new_prm.data_format = strdup(
if (prm_.data_format) free(prm_.data_format); cfg["data_format"].isString() ? cfg["data_format"].asString().c_str() : "ascii");
// Default to ascii for format-icons output; allow user override if (cfg["raw_target"].isString()) {
prm_.data_format = strdup( if (new_prm.raw_target) free(new_prm.raw_target);
config_["data_format"].isString() ? config_["data_format"].asString().c_str() : "ascii"); new_prm.raw_target = strdup(cfg["raw_target"].asString().c_str());
if (config_["raw_target"].isString()) {
if (prm_.raw_target) free(prm_.raw_target);
prm_.raw_target = strdup(config_["raw_target"].asString().c_str());
} }
prm_.ascii_range = config_["format-icons"].size() - 1; {
auto icon_count = cfg["format-icons"].size();
if (config_["bar_spacing"].isInt()) prm_.bar_spacing = config_["bar_spacing"].asInt(); new_prm.ascii_range = (icon_count > 0) ? static_cast<int>(icon_count) - 1 : 0;
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; 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();
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<int>(1e3 / new_prm.framerate)));
freeBackend();
try {
audio_raw_.height = new_prm.ascii_range;
audio_data_.format = -1; audio_data_.format = -1;
audio_data_.rate = 0; audio_data_.rate = 0;
audio_data_.samples_counter = 0; audio_data_.samples_counter = 0;
@@ -258,24 +318,38 @@ void waybar::modules::cava::CavaBackend::loadConfig() {
audio_data_.cava_buffer_size = audio_data_.input_buffer_size * 8; audio_data_.cava_buffer_size = audio_data_.input_buffer_size * 8;
audio_data_.terminate = 0; audio_data_.terminate = 0;
audio_data_.suspendFlag = false; audio_data_.suspendFlag = false;
input_source_ = get_input(&audio_data_, &prm_);
input_source_ = get_input(&audio_data_, &new_prm);
if (!input_source_) { if (!input_source_) {
throw std::runtime_error("cava backend: API didn't provide an input audio source"); throw std::runtime_error("cava backend: API didn't provide an input audio source");
} }
prm_.output = ::cava::output_method::OUTPUT_RAW; new_prm.output = ::cava::output_method::OUTPUT_RAW;
// Make cava parameters configuration audio_raw_init(&audio_data_, &audio_raw_, &new_prm, &plan_);
// Init cava plan, audio_raw structure if (!plan_) {
audio_raw_init(&audio_data_, &audio_raw_, &prm_, &plan_); throw std::runtime_error("cava backend plan is not provided");
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 audio_raw_.previous_frame[0] = -1;
audio_raw_initialized_ = true;
prm_.output = output; } catch (...) {
freeBackend();
throw;
} }
const struct ::cava::config_params* waybar::modules::cava::CavaBackend::getPrm() { return &prm_; } prm_ = new_prm;
std::chrono::milliseconds waybar::modules::cava::CavaBackend::getFrameTimeMilsec() { new_prm_guard.release();
return frame_time_milsec_; prm_.output = output;
};
m_signal_config_changed_.emit();
}
const ::cava::config_params& waybar::modules::cava::CavaBackend::getPrm() const {
std::lock_guard<std::recursive_mutex> lock(state_mutex_);
return prm_;
}
std::chrono::milliseconds waybar::modules::cava::CavaBackend::getFrameTimeMilsec() const {
std::lock_guard<std::recursive_mutex> lock(state_mutex_);
return adaptive_delay_.current();
}