diff --git a/libtascar/include/levelmeter.h b/libtascar/include/levelmeter.h index ff9d0fe4..6f65335b 100644 --- a/libtascar/include/levelmeter.h +++ b/libtascar/include/levelmeter.h @@ -20,8 +20,8 @@ #ifndef LEVELMETER_H #define LEVELMETER_H -#include "tscconfig.h" #include "filterclass.h" +#include "tscconfig.h" namespace TASCAR { @@ -35,7 +35,7 @@ namespace TASCAR { \ingroup levels */ class levelmeter_t : public TASCAR::wave_t { - public: + public: /** \param fs Audio sampling rate in Hz \param tc Time constant in seconds @@ -46,7 +46,7 @@ namespace TASCAR { Change meter weighting during runtime */ void set_weight(levelmeter::weight_t weight); - levelmeter::weight_t get_weight() const { return w;}; + levelmeter::weight_t get_weight() const { return w; }; /** \brief Add audio samples to level meter container \param src Audio samples @@ -57,11 +57,14 @@ namespace TASCAR { \retval rms RMS value in dB SPL \retval peak Peak value in dB */ - void get_rms_and_peak( float& rms, float& peak ) const; + void get_rms_and_peak(float& rms, float& peak) const; /** - \brief Return percentile levels, similar to ISMADHA standard for hearing aids analysis. + \brief Return percentile levels, similar to ISMADHA standard for hearing + aids analysis. */ - void get_percentile_levels( float& q30, float& q50, float& q65, float& g95, float& q99 ) const; + void get_percentile_levels(float& q30, float& q50, float& q65, float& g95, + float& q99) const; + private: TASCAR::levelmeter::weight_t w; uint32_t segment_length; @@ -72,12 +75,16 @@ namespace TASCAR { uint32_t i65; uint32_t i95; uint32_t i99; + + public: bandpass_t bp; + + private: bandpass_t bp_C; aweighting_t flt_A; }; -} +} // namespace TASCAR #endif diff --git a/manual/apmodlevel2hsv.tex b/manual/apmodlevel2hsv.tex new file mode 100644 index 00000000..e5c0e6fd --- /dev/null +++ b/manual/apmodlevel2hsv.tex @@ -0,0 +1,35 @@ +Convert sound pressure level to value component of a lamp. + +\definecolor{shadecolor}{RGB}{255,230,204}\begin{snugshade} +{\footnotesize +\label{attrtab:level2hsv} +Attributes of element {\bf level2hsv}\nopagebreak + +\begin{tabularx}{\textwidth}{lXl} +\hline +name & description (type, unit) & def.\\ +\hline +\hline +\indattr{frange} & Frequency range in bandpass mode (float array, Hz) & 62.5 4000\\ +\hline +\indattr{hue} & Hue component (0-360) (float, degree) & 0\\ +\hline +\indattr{lrange} & Level range (float array, dB) & 40 90\\ +\hline +\indattr{mode} & Level mode [dbspl|rms|max] (string) & dbspl\\ +\hline +\indattr{path} & Target path (string) & /hsv\\ +\hline +\indattr{saturation} & Saturation component (0-1) (float) & 1\\ +\hline +\indattr{skip} & Skip frames (uint32) & 0\\ +\hline +\indattr{tau} & Leq duration, or 0 to use block size (float, s) & 0\\ +\hline +\indattr{url} & Target URL (string) & {\tiny osc.udp://localhost:9999/}\\ +\hline +\indattr{weight} & Level meter weight (f-weight) & Z\\ +\hline +\end{tabularx} +} +\end{snugshade} diff --git a/manual/documentation.tsc b/manual/documentation.tsc index 92782148..d45884f2 100644 --- a/manual/documentation.tsc +++ b/manual/documentation.tsc @@ -50,6 +50,7 @@ + diff --git a/plugins/Makefile b/plugins/Makefile index a72567c0..7da51a65 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -29,7 +29,7 @@ AUDIOPLUGINS = delay dummy gate hannenv identity lipsync \ gainramp pulse pink dumplevels feedbackdelay bandpass filter \ level2osc const noise loopmachine pos2osc gain sndfileasync \ addchannel tubesim spkcalib sessiontime flanger timestamp \ - burglatticelpc fence transportramp allpass bandlevel2osc + burglatticelpc fence transportramp allpass bandlevel2osc level2hsv MASKPLUGINS = fig8 multibeam sampledgain diff --git a/plugins/src/tascar_ap_level2hsv.cc b/plugins/src/tascar_ap_level2hsv.cc new file mode 100644 index 00000000..a7aaa4bf --- /dev/null +++ b/plugins/src/tascar_ap_level2hsv.cc @@ -0,0 +1,200 @@ +/* + * This file is part of the TASCAR software, see + * + * Copyright (c) 2024 Giso Grimm + */ +/* + * TASCAR is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation, version 3 of the License. + * + * TASCAR is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHATABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License, version 3 for more details. + * + * You should have received a copy of the GNU General Public License, + * Version 3 along with TASCAR. If not, see . + */ + +#include "audioplugin.h" +#include "errorhandling.h" +#include "levelmeter.h" +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +enum levelmode_t { dbspl, rms, max }; + +class level2hsv_t : public TASCAR::audioplugin_base_t { +public: + level2hsv_t(const TASCAR::audioplugin_cfg_t& cfg); + void ap_process(std::vector& chunk, const TASCAR::pos_t& pos, + const TASCAR::zyx_euler_t&, const TASCAR::transport_t& tp); + void configure(); + void release(); + ~level2hsv_t(); + +private: + uint32_t skip = 0; + float tau = 0; + std::string url = "osc.udp://localhost:9999/"; + std::string path = "/hsv"; + float hue = 0; + float saturation = 1; + TASCAR::levelmeter::weight_t weight = TASCAR::levelmeter::Z; + std::vector frange = {62.5f, 4000.0f}; + std::vector lrange = {40.0f, 90.0f}; + // float value = 0; + // derived variables: + levelmode_t imode = dbspl; + lo_address lo_addr; + uint32_t skipcnt = 0; + lo_message msg; + std::thread thread; + std::atomic_bool run_thread = true; + void sendthread(); + std::mutex mtx; + std::condition_variable cond; + std::atomic_bool has_data = false; + TASCAR::levelmeter_t* lmeter = NULL; + double currenttime = 0; + float* p_value = NULL; +}; + +level2hsv_t::level2hsv_t(const TASCAR::audioplugin_cfg_t& cfg) + : audioplugin_base_t(cfg) +{ + GET_ATTRIBUTE(skip, "", "Skip frames"); + GET_ATTRIBUTE(url, "", "Target URL"); + GET_ATTRIBUTE(path, "", "Target path"); + GET_ATTRIBUTE(tau, "s", "Leq duration, or 0 to use block size"); + GET_ATTRIBUTE(hue, "degree", "Hue component (0-360)"); + GET_ATTRIBUTE(saturation, "", "Saturation component (0-1)"); + std::string mode("dbspl"); + GET_ATTRIBUTE(mode, "", "Level mode [dbspl|rms|max]"); + if(mode == "dbspl") + imode = dbspl; + else if(mode == "rms") + imode = rms; + else if(mode == "max") + imode = max; + else + throw TASCAR::ErrMsg("Invalid level mode: " + mode); + GET_ATTRIBUTE_NOUNIT(weight, "Level meter weight"); + GET_ATTRIBUTE(frange, "Hz", "Frequency range in bandpass mode"); + if(weight == TASCAR::levelmeter::bandpass) + if(frange.size() != 2) + throw TASCAR::ErrMsg( + "Frequency range requires exactly two entries (min max)"); + GET_ATTRIBUTE(lrange, "dB", "Level range"); + if((lrange.size() != 2) || (lrange[0] == lrange[1])) + throw TASCAR::ErrMsg( + "Level range requires exactly two different entries (min max)"); + lo_addr = lo_address_new_from_url(url.c_str()); + msg = lo_message_new(); + lo_message_add_float(msg, hue); + lo_message_add_float(msg, saturation); + lo_message_add_float(msg, 0); + lo_message_add_float(msg, 0.01); + auto oscmsgargv = lo_message_get_argv(msg); + p_value = &(oscmsgargv[2]->f); + thread = std::thread(&level2hsv_t::sendthread, this); +} + +void level2hsv_t::sendthread() +{ + std::unique_lock lk(mtx); + while(run_thread) { + cond.wait_for(lk, 100ms); + if(has_data) { + float l = 0.0f; + switch(imode) { + case dbspl: + l = lmeter->spldb(); + break; + case rms: + l = lmeter->rms(); + break; + case max: + l = lmeter->maxabsdb(); + break; + } + l = std::min(1.0f, + std::max(0.0f, (l - lrange[0]) / (lrange[1] - lrange[0]))); + *p_value = l; + lo_send_message(lo_addr, path.c_str(), msg); + has_data = false; + } + } +} + +void level2hsv_t::configure() +{ + audioplugin_base_t::configure(); + float tau_ = tau; + if(tau_ == 0) + tau_ = t_fragment; + if(lmeter) + delete lmeter; + lmeter = NULL; + lmeter = new TASCAR::levelmeter_t(f_sample, tau_, weight); + if(weight == TASCAR::levelmeter::bandpass) + lmeter->bp.set_range(frange[0], frange[1]); +} + +void level2hsv_t::release() +{ + audioplugin_base_t::release(); +} + +level2hsv_t::~level2hsv_t() +{ + run_thread = false; + thread.join(); + lo_address_free(lo_addr); + if(lmeter) + delete lmeter; + lo_message_free(msg); +} + +void level2hsv_t::ap_process(std::vector& chunk, + const TASCAR::pos_t&, const TASCAR::zyx_euler_t&, + const TASCAR::transport_t&) +{ + if(chunk.size() != n_channels) + throw TASCAR::ErrMsg( + "Programming error (invalid channel number, expected " + + TASCAR::to_string(n_channels) + ", got " + + std::to_string(chunk.size()) + ")."); + if(chunk.size() == 0) + return; + lmeter->update(chunk[0]); + if(skipcnt) { + skipcnt--; + } else { + // data will be sent in extra thread: + if(mtx.try_lock()) { + has_data = true; + mtx.unlock(); + cond.notify_one(); + } + skipcnt = skip; + } +} + +REGISTER_AUDIOPLUGIN(level2hsv_t); + +/* + * Local Variables: + * mode: c++ + * c-basic-offset: 2 + * indent-tabs-mode: nil + * compile-command: "make -C .." + * End: + */