diff options
| author | Vasile Vilvoiu <vasi@vilvoiu.ro> | 2021-07-16 18:32:27 +0300 |
|---|---|---|
| committer | Vasile Vilvoiu <vasi@vilvoiu.ro> | 2021-07-16 18:32:27 +0300 |
| commit | 47bbfdbf1e2a6193157397938e76b16a1f60e789 (patch) | |
| tree | 5f90ac568bcd0ddfa2e885bacf4e4e996395d249 | |
| parent | 82c81858c65c80fb667e73ffdcc4ff69007cfa17 (diff) | |
Add support for arbitrary scales, with custom units.
Add support for linear scales.
Logging of scale to stderr.
Closes #9.
| -rw-r--r-- | man/specgram.1 | 20 | ||||
| -rw-r--r-- | man/specgram.1.html | 35 | ||||
| -rw-r--r-- | man/specgram.1.pdf | bin | 37118 -> 37617 bytes | |||
| -rw-r--r-- | src/configuration.cpp | 86 | ||||
| -rw-r--r-- | src/configuration.hpp | 12 | ||||
| -rw-r--r-- | src/renderer.cpp | 24 | ||||
| -rw-r--r-- | src/renderer.hpp | 1 | ||||
| -rw-r--r-- | src/specgram.cpp | 17 | ||||
| -rw-r--r-- | src/value-map.cpp | 59 | ||||
| -rw-r--r-- | src/value-map.hpp | 28 |
10 files changed, 205 insertions, 77 deletions
diff --git a/man/specgram.1 b/man/specgram.1 index 971af9d..6e60aaa 100644 --- a/man/specgram.1 +++ b/man/specgram.1 @@ -1,4 +1,4 @@ -.TH SPECGRAM 1 "2021-07-15" +.TH SPECGRAM 1 "2021-07-16" .SH NAME specgram \- create spectrograms from raw files or standard input @@ -192,13 +192,19 @@ Default is \fIRATE\fR/2. .TP .BR \-s ", " \-\-scale =\fISCALE\fR -Spectrogram scale. -Valid values are: dBFS. +Spectrogram scale, specified with the following format: \fIunit\fR[,\fIlower\fR[,\fIupper\fR]] -Default is dBFS. +\fIunit\fR is an arbitrary string representing the unit of measurement (e.g. \fBV\fR). +\fIlower\fR is an optional numeric value representing the lower bound of the scale. +\fIupper\fR is an optional numeric value representing the upper bound of the scale. -\fB[dBFS] NOTE:\fR By default, the scale has a -120dB lower bound. -You can adjust it by appending the custom lower bound after the scale string (e.g. \fB\-s dBFS-60\fR for a -60dB lower bound). +Valid values for \fISCALE\fR specify either just the unit, the unit and the lower bound, or all three values. + +After normalization and prescaling (see \fB\-p, \-\-prescale\fR), the following transformations are applied to the input: + \(bu if \fIunit\fR starts with "dB", then a logarithmic decibel scale is assumed: Y=20*log10(X) + \(bu the values are clamped between \fIlower\fR and \fIupper\fR: Y=clamp(X, \fIlower\fR, \fIupper\fR) + +Default is dBFS,-120,0. \fB[dBFS] NOTE:\fR The peak amplitude assumed for dBFS, after normalization and prescaling (see \fB\-p, \-\-prescale\fR), is 1.0. Thus, the correct input domains are: @@ -360,4 +366,4 @@ Program icon by Flavia Fabian, released under the CC-BY-SA 4.0 license. Share Tech Mono font by Carrois Type Design, released under Open Font License. -Special thanks to Eugen Stoianovici for code review and various fixes.
\ No newline at end of file +Special thanks to Eugen Stoianovici for code review and various fixes. diff --git a/man/specgram.1.html b/man/specgram.1.html index 41cce87..6cc0a1c 100644 --- a/man/specgram.1.html +++ b/man/specgram.1.html @@ -1,5 +1,5 @@ <!-- Creator : groff version 1.22.4 --> -<!-- CreationDate: Thu Jul 15 19:45:50 2021 --> +<!-- CreationDate: Fri Jul 16 11:40:40 2021 --> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> @@ -334,17 +334,32 @@ frequency spectrum, in Hz.</p> <p style="margin-left:11%;"><b>−s</b>, <b>−−scale</b>=<i>SCALE</i></p> -<p style="margin-left:22%;">Spectrogram scale. Valid values -are: dBFS.</p> +<p style="margin-left:22%;">Spectrogram scale, specified +with the following format: +<i>unit</i>[,<i>lower</i>[,<i>upper</i>]]</p> -<p style="margin-left:22%; margin-top: 1em">Default is -dBFS.</p> +<p style="margin-left:22%; margin-top: 1em"><i>unit</i> is +an arbitrary string representing the unit of measurement +(e.g. <b>V</b>). <i>lower</i> is an optional numeric value +representing the lower bound of the scale. <i>upper</i> is +an optional numeric value representing the upper bound of +the scale.</p> -<p style="margin-left:22%; margin-top: 1em"><b>[dBFS] -NOTE:</b> By default, the scale has a -120dB lower bound. -You can adjust it by appending the custom lower bound after -the scale string (e.g. <b>−s dBFS-60</b> for a -60dB -lower bound).</p> +<p style="margin-left:22%; margin-top: 1em">Valid values +for <i>SCALE</i> specify either just the unit, the unit and +the lower bound, or all three values.</p> + +<p style="margin-left:22%; margin-top: 1em">After +normalization and prescaling (see <b>−p, +−−prescale</b>), the following transformations +are applied to the input: <br> +• if <i>unit</i> starts with "dB", then a +logarithmic decibel scale is assumed: Y=20*log10(X) <br> +• the values are clamped between <i>lower</i> and +<i>upper</i>: Y=clamp(X, <i>lower</i>, <i>upper</i>)</p> + +<p style="margin-left:22%; margin-top: 1em">Default is +dBFS,-120,0.</p> <p style="margin-left:22%; margin-top: 1em"><b>[dBFS] NOTE:</b> The peak amplitude assumed for dBFS, after diff --git a/man/specgram.1.pdf b/man/specgram.1.pdf Binary files differindex 4b9db2d..f393414 100644 --- a/man/specgram.1.pdf +++ b/man/specgram.1.pdf 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 <tuple> #include <regex> +#include <vector> 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, int, bool> Configuration::FromArgs(int argc, char **argv) { @@ -156,7 +193,7 @@ Configuration::FromArgs(int argc, char **argv) args::ValueFlag<float> fmax(display_opts, "float", "Maximum frequency in Hz (default: 0.5 * rate)", {'y', "fmax"}); args::ValueFlag<std::string> - scale(display_opts, "string", "Display scale (default: dBFS)", {'s', "scale"}); + scale(display_opts, "string", "Display scale (default: dBFS,-120,0)", {'s', "scale"}); args::ValueFlag<std::string> colormap(display_opts, "string", "Colormap (default: jet)", {'c', "colormap"}); args::ValueFlag<std::string> @@ -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<double>; + using ScaleProperties = std::tuple<OptionalBound, OptionalBound, std::string>; + static ScaleProperties StringToScale(const std::string& str); + public: /* parse command line arguments and return a configuration object */ static std::tuple<Configuration, int, bool> 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<AxisTick> legend_ticks; - if (vmap.GetName() == "dBFS") { - unsigned int lticks = 1 + this->configuration_.GetWidth() / 60; - lticks = std::clamp<unsigned int>(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<AxisTick> +[[maybe_unused]] std::list<AxisTick> 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<AxisTick> frequency_ticks_; std::list<AxisTick> live_ticks_; + [[maybe_unused]] /* need for this method disappeared when fixing #9, might be useful in the future */ static std::list<AxisTick> GetLinearTicks(double v_min, double v_max, const std::string& v_unit, unsigned int num_ticks); static std::list<AxisTick> 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<ValueMap> value_map = nullptr; - if (conf.GetScale() == ValueMapType::kdBFS) { - value_map = std::make_unique<dBFSValueMap>(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<ValueMap> 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> +ValueMap::Build(ValueMapType type, double lower, double upper, std::string unit) +{ + switch (type) { + case ValueMapType::kLinear: + return std::make_unique<LinearValueMap>(lower, upper, unit); + + case ValueMapType::kDecibel: + return std::make_unique<DecibelValueMap>(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<double>(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<double>(output[i], this->lower_, 0.0f); - output[i] = 1.0f - output[i] / this->lower_; + output[i] = std::clamp<double>(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 <complex> 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<ValueMap> 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 |
