summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVasile Vilvoiu <vasi@vilvoiu.ro>2021-07-16 18:32:27 +0300
committerVasile Vilvoiu <vasi@vilvoiu.ro>2021-07-16 18:32:27 +0300
commit47bbfdbf1e2a6193157397938e76b16a1f60e789 (patch)
tree5f90ac568bcd0ddfa2e885bacf4e4e996395d249
parent82c81858c65c80fb667e73ffdcc4ff69007cfa17 (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.120
-rw-r--r--man/specgram.1.html35
-rw-r--r--man/specgram.1.pdfbin37118 -> 37617 bytes
-rw-r--r--src/configuration.cpp86
-rw-r--r--src/configuration.hpp12
-rw-r--r--src/renderer.cpp24
-rw-r--r--src/renderer.hpp1
-rw-r--r--src/specgram.cpp17
-rw-r--r--src/value-map.cpp59
-rw-r--r--src/value-map.hpp28
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>&minus;s</b>,
<b>&minus;&minus;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>&minus;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>&minus;p,
+&minus;&minus;prescale</b>), the following transformations
+are applied to the input: <br>
+&bull; if <i>unit</i> starts with &quot;dB&quot;, then a
+logarithmic decibel scale is assumed: Y=20*log10(X) <br>
+&bull; 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
index 4b9db2d..f393414 100644
--- a/man/specgram.1.pdf
+++ b/man/specgram.1.pdf
Binary files differ
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