diff --git a/include/factory.hpp b/include/factory.hpp index 89eea0e9..1848f6a7 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -80,6 +80,9 @@ #ifdef HAVE_LIBWIREPLUMBER #include "modules/wireplumber.hpp" #endif +#ifdef HAVE_LIBCAVA +#include "modules/cava.hpp" +#endif #include "bar.hpp" #include "modules/custom.hpp" #include "modules/image.hpp" diff --git a/include/modules/cava.hpp b/include/modules/cava.hpp new file mode 100644 index 00000000..b31711d8 --- /dev/null +++ b/include/modules/cava.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +extern "C" { +#include +} + +namespace waybar::modules { +using namespace std::literals::chrono_literals; + +class Cava final: public ALabel { + public: + Cava(const std::string&, const Json::Value&); + virtual ~Cava(); + auto update() -> void override; + auto doAction(const std::string& name) -> void override; + + private: + util::SleeperThread thread_; + util::SleeperThread thread_fetch_input_; + + struct error_s error_{}; //cava errors + struct config_params prm_{}; //cava parameters + struct audio_raw audio_raw_{}; //cava handled raw audio data(is based on audio_data) + struct audio_data audio_data_{}; //cava audio data + struct cava_plan* plan_;//{new cava_plan{}}; + // Cava API to read audio source + ptr input_source_; + // Delay to handle audio source + std::chrono::milliseconds frame_time_milsec_{1s}; + // Text to display + std::string text_{""}; + int rePaint_{1}; + std::chrono::seconds fetch_input_delay_{4}; + std::chrono::seconds suspend_silence_delay_{0}; + bool silence_{false}; + int sleep_counter_{0}; + // Cava method + void pause_resume(); + // ModuleActionMap + static inline std::map actionMap_{ + {"mode", &waybar::modules::Cava::pause_resume} + }; +}; +} diff --git a/meson.build b/meson.build index 58e1c672..2b018acd 100644 --- a/meson.build +++ b/meson.build @@ -2,7 +2,7 @@ project( 'waybar', 'cpp', 'c', version: '0.9.17', license: 'MIT', - meson_version: '>= 0.49.0', + meson_version: '>= 0.50.0', default_options : [ 'cpp_std=c++17', 'buildtype=release', @@ -175,6 +175,8 @@ src_files = files( 'src/util/rewrite_string.cpp' ) +inc_dirs = ['include'] + if is_linux add_project_arguments('-DHAVE_CPU_LINUX', language: 'cpp') add_project_arguments('-DHAVE_MEMORY_LINUX', language: 'cpp') @@ -334,6 +336,19 @@ if get_option('experimental') add_project_arguments('-DUSE_EXPERIMENTAL', language: 'cpp') endif +cava = compiler.find_library('cava', + required: get_option('cava')) +if not cava.found() and not get_option('cava').disabled() + cava = dependency('cava', + required: false, + fallback: [ 'cava', 'cava_dep' ]) +endif + +if cava.found() + add_project_arguments('-DHAVE_LIBCAVA', language: 'cpp') + src_files += 'src/modules/cava.cpp' +endif + subdir('protocol') executable( @@ -367,9 +382,10 @@ executable( gtk_layer_shell, libsndio, tz_dep, - xkbregistry + xkbregistry, + cava ], - include_directories: [include_directories('include')], + include_directories: inc_dirs, install: true, ) diff --git a/meson_options.txt b/meson_options.txt index 98cd4949..7dacf087 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -17,4 +17,5 @@ option('logind', type: 'feature', value: 'auto', description: 'Enable support fo option('tests', type: 'feature', value: 'auto', description: 'Enable tests') option('experimental', type : 'boolean', value : false, description: 'Enable experimental features') option('jack', type: 'feature', value: 'auto', description: 'Enable support for JACK') -option('wireplumber', type: 'feature', value: 'auto', description: 'Enable support for WirePlumber') \ No newline at end of file +option('wireplumber', type: 'feature', value: 'auto', description: 'Enable support for WirePlumber') +option('cava', type: 'feature', value: 'auto', description: 'Enable support for Cava') diff --git a/src/factory.cpp b/src/factory.cpp index dd5c142d..618483f4 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -156,6 +156,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "wireplumber") { return new waybar::modules::Wireplumber(id, config_[name]); } +#endif +#ifdef HAVE_LIBCAVA + if (ref == "cava") { + return new waybar::modules::Cava(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); diff --git a/src/modules/cava.cpp b/src/modules/cava.cpp new file mode 100644 index 00000000..8e788f5b --- /dev/null +++ b/src/modules/cava.cpp @@ -0,0 +1,203 @@ +#include "modules/cava.hpp" + +#include + +waybar::modules::Cava::Cava(const std::string& id, const Json::Value& config) + : ALabel(config, "cava", id, "{}", 60, false, false, false) { + // Load waybar module config + char cfgPath[PATH_MAX]; + cfgPath[0] = '\0'; + + if (config_["cava_config"].isString()) { + std::string strPath{config_["cava_config"].asString()}; + const std::string fnd{"XDG_CONFIG_HOME"}; + const std::string::size_type npos{strPath.find("$" + fnd)}; + if (npos != std::string::npos) strPath.replace(npos, fnd.length() + 1, getenv(fnd.c_str())); + strcpy(cfgPath, strPath.data()); + } + // Load cava config + error_.length = 0; + + if (!load_config(cfgPath, &prm_, false, &error_)) { + spdlog::error("Error loading config. {0}", error_.message); + exit(EXIT_FAILURE); + } + + // Override cava parameters by the user config + prm_.inAtty = 0; + prm_.output = output_method::OUTPUT_RAW; + strcpy(prm_.data_format, "ascii"); + strcpy(prm_.raw_target, "/dev/stdout"); + prm_.ascii_range = config_["format-icons"].size() - 1; + + prm_.bar_width = 2; + prm_.bar_spacing = 0; + prm_.bar_height = 32; + prm_.bar_width = 1; + prm_.orientation = ORIENT_TOP; + prm_.xaxis = xaxis_scale::NONE; + prm_.mono_opt = AVERAGE; + prm_.autobars = 0; + prm_.gravity = 0; + prm_.integral = 1; + + if (config_["framerate"].isInt()) prm_.framerate = config_["framerate"].asInt(); + 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 = input_method_by_name(config_["method"].asString().c_str()); + if (config_["source"].isString()) prm_.audio_source = config_["source"].asString().data(); + if (config_["sample_rate"].isNumeric()) prm_.fifoSample = config_["sample_rate"].asLargestInt(); + if (config_["sample_bits"].isInt()) prm_.fifoSampleBits = 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()); + // Make cava parameters configuration + plan_ = new cava_plan{}; + + audio_raw_.height = prm_.ascii_range; + audio_data_.format = -1; + audio_data_.source = new char[1 + strlen(prm_.audio_source)]; + audio_data_.source[0] = '\0'; + strcpy(audio_data_.source, prm_.audio_source); + + 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_.cava_in = new double[audio_data_.cava_buffer_size]{0.0}; + + audio_data_.terminate = 0; + audio_data_.suspendFlag = false; + input_source_ = get_input(&audio_data_, &prm_); + + if (!input_source_) { + spdlog::error("cava API didn't provide input audio source method"); + exit(EXIT_FAILURE); + } + // Calculate delay for Update() thread + frame_time_milsec_ = std::chrono::milliseconds((int)(1e3 / prm_.framerate)); + + // Init cava plan, audio_raw structure + audio_raw_init(&audio_data_, &audio_raw_, &prm_, plan_); + if (!plan_) spdlog::error("cava plan is not provided"); + audio_raw_.previous_frame[0] = -1; // For first Update() call need to rePaint text message + // Read audio source trough cava API. Cava orginizes this process via infinity loop + thread_fetch_input_ = [this] { + thread_fetch_input_.sleep_for(fetch_input_delay_); + input_source_(&audio_data_); + dp.emit(); + }; + + thread_ = [this] { + dp.emit(); + thread_.sleep_for(frame_time_milsec_); + }; +} + +waybar::modules::Cava::~Cava() { + thread_fetch_input_.stop(); + thread_.stop(); + delete plan_; + plan_ = nullptr; +} + +void upThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { + if (delta == std::chrono::seconds{0}) { + delta += std::chrono::seconds{1}; + delay += delta; + } +} + +void downThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { + if (delta > std::chrono::seconds{0}) { + delay -= delta; + delta -= std::chrono::seconds{1}; + } +} + +auto waybar::modules::Cava::update() -> void { + if (audio_data_.suspendFlag) return; + silence_ = true; + + for (int i{0}; i < audio_data_.input_buffer_size; ++i) { + if (audio_data_.cava_in[i]) { + silence_ = false; + sleep_counter_ = 0; + break; + } + } + + if (silence_ && prm_.sleep_timer) { + if (sleep_counter_ <= + (int)(std::chrono::milliseconds(prm_.sleep_timer * 1s) / frame_time_milsec_)) { + ++sleep_counter_; + silence_ = false; + } + } + + if (!silence_) { + downThreadDelay(frame_time_milsec_, suspend_silence_delay_); + // Process: execute cava + pthread_mutex_lock(&audio_data_.lock); + cava_execute(audio_data_.cava_in, audio_data_.samples_counter, audio_raw_.cava_out, plan_); + if (audio_data_.samples_counter > 0) audio_data_.samples_counter = 0; + pthread_mutex_unlock(&audio_data_.lock); + + // Do transformation under raw data + audio_raw_fetch(&audio_raw_, &prm_, &rePaint_); + + if (rePaint_ == 1) { + text_.clear(); + + for (int i{0}; i < audio_raw_.number_of_bars; ++i) { + audio_raw_.previous_frame[i] = audio_raw_.bars[i]; + text_.append( + getIcon((audio_raw_.bars[i] > prm_.ascii_range) ? prm_.ascii_range : audio_raw_.bars[i], + "", prm_.ascii_range + 1)); + if (prm_.bar_delim != 0) text_.push_back(prm_.bar_delim); + } + + label_.set_markup(text_); + ALabel::update(); + } + } else + upThreadDelay(frame_time_milsec_, suspend_silence_delay_); +} + +auto waybar::modules::Cava::doAction(const std::string& name) -> void { + if ((actionMap_[name])) { + (this->*actionMap_[name])(); + } else + spdlog::error("Cava. Unsupported action \"{0}\"", name); +} + +// Cava actions +void waybar::modules::Cava::pause_resume() { + 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_); + } else { + audio_data_.suspendFlag = true; + upThreadDelay(frame_time_milsec_, suspend_silence_delay_); + } + pthread_mutex_unlock(&audio_data_.lock); +} diff --git a/subprojects/cava.wrap b/subprojects/cava.wrap new file mode 100644 index 00000000..95a9c8fe --- /dev/null +++ b/subprojects/cava.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = cava-0.8.3 +source_url = https://github.com/LukashonakV/cava/archive/0.8.3.tar.gz +source_filename = cava-0.8.3.tar.gz +source_hash = 10f9ec910682102c47dc39d684fd3fc90d38a4d1c2e5a310f132f70ad0e00850 +[provide] +cava = cava_dep