From 47bbfdbf1e2a6193157397938e76b16a1f60e789 Mon Sep 17 00:00:00 2001 From: Vasile Vilvoiu Date: Fri, 16 Jul 2021 18:32:27 +0300 Subject: Add support for arbitrary scales, with custom units. Add support for linear scales. Logging of scale to stderr. Closes #9. --- src/configuration.cpp | 86 +++++++++++++++++++++++++++++++++++++++------------ src/configuration.hpp | 12 +++++-- src/renderer.cpp | 24 ++++++-------- src/renderer.hpp | 1 + src/specgram.cpp | 17 +++++----- src/value-map.cpp | 59 ++++++++++++++++++++++++++++++----- src/value-map.hpp | 28 +++++++++++------ 7 files changed, 167 insertions(+), 60 deletions(-) (limited to 'src') diff --git a/src/configuration.cpp b/src/configuration.cpp index abfecfa..85b1faa 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -13,6 +13,7 @@ #include #include +#include Configuration::Configuration() { @@ -35,8 +36,10 @@ Configuration::Configuration() this->width_ = 512; this->min_freq_ = 0; this->max_freq_ = this->rate_ / 2; - this->scale_ = ValueMapType::kdBFS; + this->scale_type_ = ValueMapType::kDecibel; + this->scale_unit_ = "FS"; this->scale_lower_bound_ = -120.0f; + this->scale_upper_bound_ = 0.0f; this->color_map_ = ColorMapType::kJet; this->background_color_ = sf::Color(0, 0, 0); this->foreground_color_ = sf::Color(255, 255, 255); @@ -107,6 +110,40 @@ Configuration::StringToColor(const std::string& str) } } +Configuration::ScaleProperties +Configuration::StringToScale(const std::string &str) +{ + ScaleProperties props; + + auto first_comma_pos = str.find(','); + auto second_comma_pos = str.find(',', first_comma_pos + 1); + + std::get<2>(props) = str.substr(0, first_comma_pos); + if (first_comma_pos == std::string::npos) { + /* simple case, only unit */ + return props; + } + + std::string lower_bound_str = str.substr(first_comma_pos+1, second_comma_pos); + try { + std::get<0>(props) = std::stod(lower_bound_str); + } catch (const std::exception& e) { + throw std::runtime_error("Invalid lower bound for scale '" + lower_bound_str + "'"); + } + if (second_comma_pos == std::string::npos) { + /* unit + lower bound */ + return props; + } + + std::string upper_bound_str = str.substr(second_comma_pos+1, str.size()); + try { + std::get<1>(props) = std::stod(upper_bound_str); + } catch (const std::exception& e) { + throw std::runtime_error("Invalid upper bound for scale '" + upper_bound_str + "'"); + } + return props; +} + std::tuple Configuration::FromArgs(int argc, char **argv) { @@ -156,7 +193,7 @@ Configuration::FromArgs(int argc, char **argv) args::ValueFlag fmax(display_opts, "float", "Maximum frequency in Hz (default: 0.5 * rate)", {'y', "fmax"}); args::ValueFlag - scale(display_opts, "string", "Display scale (default: dBFS)", {'s', "scale"}); + scale(display_opts, "string", "Display scale (default: dBFS,-120,0)", {'s', "scale"}); args::ValueFlag colormap(display_opts, "string", "Colormap (default: jet)", {'c', "colormap"}); args::ValueFlag @@ -353,24 +390,35 @@ Configuration::FromArgs(int argc, char **argv) } if (scale) { auto& scale_str = args::get(scale); - if (scale_str.starts_with("dbfs") || scale_str.starts_with("dBFS")) { - conf.scale_ = ValueMapType::kdBFS; - auto value_str = scale_str.substr(4, scale_str.size() - 4); - if (value_str.size() > 0) { - try { - conf.scale_lower_bound_ = std::stod(value_str); - } catch (const std::exception& e) { - std::cerr << "Invalid lower bound for dBFS scale '" << value_str << "'" << std::endl; - return std::make_tuple(conf, 1, true); - } - if (conf.scale_lower_bound_ >= 0.0f) { - std::cerr << "Lower bound for dBFS scale must be negative, " - << conf.scale_lower_bound_ << " was received" << std::endl; - return std::make_tuple(conf, 1, true); - } - } + ScaleProperties props; + try { + props = StringToScale(scale_str); + } catch (std::runtime_error& e) { + std::cerr << e.what() << std::endl; + return std::make_tuple(conf, 1, true); + } + + const auto& unit_str = std::get<2>(props); + if (unit_str.starts_with("db") || unit_str.starts_with("DB") + || unit_str.starts_with("dB") || unit_str.starts_with("Db")) { + /* decibel scale - defaults stay the same */ + conf.scale_unit_ = unit_str.substr(2, scale_str.size()-2); } else { - std::cerr << "Unknown scale '" << scale_str << "'" << std::endl; + /* linear scale - defaults change */ + conf.scale_type_ = ValueMapType::kLinear; + conf.scale_unit_ = unit_str; + conf.scale_lower_bound_ = 0.0f; + conf.scale_upper_bound_ = 1.0f; + } + + if (std::get<0>(props).has_value()) { + conf.scale_lower_bound_ = *std::get<0>(props); + } + if (std::get<1>(props).has_value()) { + conf.scale_upper_bound_ = *std::get<1>(props); + } + if (conf.scale_lower_bound_ >= conf.scale_upper_bound_) { + std::cerr << "Scale upper bound must be larger than lower bound" << std::endl; return std::make_tuple(conf, 1, true); } } diff --git a/src/configuration.hpp b/src/configuration.hpp index db3be73..f6fff18 100644 --- a/src/configuration.hpp +++ b/src/configuration.hpp @@ -39,8 +39,10 @@ private: std::size_t width_; double min_freq_; double max_freq_; - ValueMapType scale_; + ValueMapType scale_type_; + std::string scale_unit_; double scale_lower_bound_; + double scale_upper_bound_; ColorMapType color_map_; sf::Color color_map_custom_color_; sf::Color background_color_; @@ -69,6 +71,10 @@ private: static sf::Color StringToColor(const std::string& str); + using OptionalBound = std::optional; + using ScaleProperties = std::tuple; + static ScaleProperties StringToScale(const std::string& str); + public: /* parse command line arguments and return a configuration object */ static std::tuple FromArgs(int argc, char **argv); @@ -98,8 +104,10 @@ public: auto GetWidth() const { return width_; } auto GetMinFreq() const { return min_freq_; } auto GetMaxFreq() const { return max_freq_; } - auto GetScale() const { return scale_; } + auto GetScaleType() const { return scale_type_; } + auto GetScaleUnit() const { return scale_unit_; } auto GetScaleLowerBound() const { return scale_lower_bound_; } + auto GetScaleUpperBound() const { return scale_upper_bound_; } auto GetColorMap() const { return color_map_; } auto GetColorMapCustomColor() const { return color_map_custom_color_; } auto GetBackgroundColor() const { return background_color_; } diff --git a/src/renderer.cpp b/src/renderer.cpp index 1a92ec4..05fea43 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -57,18 +57,10 @@ Renderer::Renderer(const Configuration& conf, const ColorMap& cmap, const ValueM Renderer::GetNiceTicks(0.0f, (double)fft_count * this->configuration_.GetAverageCount() * this->configuration_.GetFFTStride() / this->configuration_.GetRate(), "s", fft_count, 50); - std::list legend_ticks; - if (vmap.GetName() == "dBFS") { - unsigned int lticks = 1 + this->configuration_.GetWidth() / 60; - lticks = std::clamp(lticks, 2, 13); /* at maximum 10dBFS spacing */ - legend_ticks = Renderer::GetLinearTicks(vmap.GetLowerBound(), vmap.GetUpperBound(), vmap.GetUnit(), lticks); - this->live_ticks_ = Renderer::GetLinearTicks(vmap.GetLowerBound(), vmap.GetUpperBound(), "", 5); /* no unit, keep it short */ - } else { - legend_ticks = Renderer::GetNiceTicks(vmap.GetLowerBound(), vmap.GetUpperBound(), - vmap.GetUnit(), this->configuration_.GetWidth(), 60); - this->live_ticks_ = Renderer::GetNiceTicks(vmap.GetLowerBound(), vmap.GetUpperBound(), - "", this->configuration_.GetLiveFFTHeight(), 30); /* no unit, keep it short */ - } + auto legend_ticks = Renderer::GetNiceTicks(vmap.GetLowerBound(), vmap.GetUpperBound(), + vmap.GetUnit(), this->configuration_.GetWidth(), 60); + this->live_ticks_ = Renderer::GetNiceTicks(vmap.GetLowerBound(), vmap.GetUpperBound(), + "", this->configuration_.GetLiveFFTHeight(), 30); /* no unit, keep it short */ typeof(this->frequency_ticks_) freq_no_text_ticks; for (auto& t : this->frequency_ticks_) { @@ -252,7 +244,7 @@ Renderer::Renderer(const Configuration& conf, const ColorMap& cmap, const ValueM } } -std::list +[[maybe_unused]] std::list Renderer::GetLinearTicks(double v_min, double v_max, const std::string& v_unit, unsigned int num_ticks) { if (num_ticks <= 1) { @@ -339,8 +331,12 @@ Renderer::GetNiceTicks(double v_min, double v_max, const std::string& v_unit, un assert(v_min <= fval); assert(fval <= v_max); + /* adjust upper limit with a slight epsilon so that we have tickmark for v_max in most "nice" cases */ + /* otherwise we might miss it because of representation errors */ + double upper_limit = v_max + (v_max - v_min) * 1e-6; + /* add ticks */ - for (double value = fval; value < v_max; value += mfact) { + for (double value = fval; value <= upper_limit; value += mfact) { double k = (value - v_min) / (v_max - v_min); ticks.emplace_back(std::make_tuple(k, ::ValueToShortString(value, prec, v_unit))); } diff --git a/src/renderer.hpp b/src/renderer.hpp index b2002c2..a473a2a 100644 --- a/src/renderer.hpp +++ b/src/renderer.hpp @@ -46,6 +46,7 @@ private: std::list frequency_ticks_; std::list live_ticks_; + [[maybe_unused]] /* need for this method disappeared when fixing #9, might be useful in the future */ static std::list GetLinearTicks(double v_min, double v_max, const std::string& v_unit, unsigned int num_ticks); static std::list GetNiceTicks(double v_min, double v_max, const std::string& v_unit, diff --git a/src/specgram.cpp b/src/specgram.cpp index 6e209af..cb646fc 100644 --- a/src/specgram.cpp +++ b/src/specgram.cpp @@ -146,14 +146,15 @@ main(int argc, char** argv) FFT fft(conf.GetFFTWidth(), win_function); /* create value map */ - std::unique_ptr value_map = nullptr; - if (conf.GetScale() == ValueMapType::kdBFS) { - value_map = std::make_unique(conf.GetScaleLowerBound()); - } else { - assert(false); - spdlog::error("Internal error: unknown scale"); - return 1; - } + spdlog::info("Scale {}, unit {}, bounds [{}, {}]", + conf.GetScaleType() == ValueMapType::kLinear ? "linear" : "decibel", + conf.GetScaleUnit(), + conf.GetScaleLowerBound(), + conf.GetScaleUpperBound()); + std::unique_ptr value_map = ValueMap::Build(conf.GetScaleType(), + conf.GetScaleLowerBound(), + conf.GetScaleUpperBound(), + conf.GetScaleUnit()); /* create color map */ auto color_map = ColorMap::FromType(conf.GetColorMap(), diff --git a/src/value-map.cpp b/src/value-map.cpp index c3ad28b..8a547c4 100644 --- a/src/value-map.cpp +++ b/src/value-map.cpp @@ -6,26 +6,69 @@ */ #include "value-map.hpp" -ValueMap::ValueMap(double lower, double upper) : lower_(lower), upper_(upper) +ValueMap::ValueMap(double lower, double upper, const std::string& unit) : lower_(lower), upper_(upper), unit_(unit) { } -dBFSValueMap::dBFSValueMap(double mindb) : ValueMap(mindb, 0) +std::string +ValueMap::GetUnit() const { + return unit_; } -RealWindow -dBFSValueMap::Map(const RealWindow& input) +std::unique_ptr +ValueMap::Build(ValueMapType type, double lower, double upper, std::string unit) +{ + switch (type) { + case ValueMapType::kLinear: + return std::make_unique(lower, upper, unit); + + case ValueMapType::kDecibel: + return std::make_unique(lower, upper, unit); + + default: + throw std::runtime_error("unknown value map type"); + } +} + +LinearValueMap::LinearValueMap(double lower, double upper, const std::string &unit) + : ValueMap(lower, upper, unit) +{ +} + +RealWindow LinearValueMap::Map(const RealWindow &input) +{ + auto n = input.size(); + RealWindow output(n); + + for (unsigned int i = 0; i < n; i ++) { + output[i] = std::clamp(input[i] / n, this->lower_, this->upper_); + output[i] = (output[i] - this->lower_) / (this->upper_ - this->lower_); + } + + return output; +} + +DecibelValueMap::DecibelValueMap(double lower, double upper, const std::string &unit) + : ValueMap(lower, upper, unit) +{ +} + +RealWindow DecibelValueMap::Map(const RealWindow &input) { auto n = input.size(); - RealWindow output; - output.resize(n); + RealWindow output(n); for (unsigned int i = 0; i < n; i ++) { output[i] = 20.0 * std::log10(input[i] / n); - output[i] = std::clamp(output[i], this->lower_, 0.0f); - output[i] = 1.0f - output[i] / this->lower_; + output[i] = std::clamp(output[i], this->lower_, this->upper_); + output[i] = (output[i] - this->lower_) / (this->upper_ - this->lower_); } return output; +} + +std::string DecibelValueMap::GetUnit() const +{ + return "dB" + unit_; } \ No newline at end of file diff --git a/src/value-map.hpp b/src/value-map.hpp index d162f45..2f32d92 100644 --- a/src/value-map.hpp +++ b/src/value-map.hpp @@ -15,15 +15,18 @@ #include enum class ValueMapType { - kdBFS + kLinear, + kDecibel }; class ValueMap { protected: const double lower_; const double upper_; + const std::string unit_; + + ValueMap(double lower_, double upper, const std::string& unit); - ValueMap(double lower_, double upper); public: ValueMap() = delete; @@ -31,18 +34,25 @@ public: auto GetUpperBound() const { return upper_; } virtual RealWindow Map(const RealWindow& input) = 0; - virtual std::string GetUnit() const = 0; - virtual std::string GetName() const = 0; + virtual std::string GetUnit() const; + + static std::unique_ptr Build(ValueMapType type, double lower, double upper, std::string unit); +}; + +class LinearValueMap : public ValueMap { +public: + LinearValueMap(double lower, double upper, const std::string& unit); + + RealWindow Map(const RealWindow& input) override; }; -class dBFSValueMap : public ValueMap { -private: +class DecibelValueMap : public ValueMap { public: - explicit dBFSValueMap(double mindb); + /* unit parameter should NOT contain "dB" prefix */ + DecibelValueMap(double lower, double upper, const std::string& unit); RealWindow Map(const RealWindow& input) override; - std::string GetUnit() const override { return "dBFS"; } - std::string GetName() const override { return "dBFS"; } + std::string GetUnit() const override; }; #endif \ No newline at end of file -- cgit v1.2.3