diff options
Diffstat (limited to 'src/configuration.cpp')
| -rw-r--r-- | src/configuration.cpp | 491 |
1 files changed, 491 insertions, 0 deletions
diff --git a/src/configuration.cpp b/src/configuration.cpp new file mode 100644 index 0000000..fd28f8e --- /dev/null +++ b/src/configuration.cpp @@ -0,0 +1,491 @@ +/* + * Copyright (c) 2020-2021 Vasile Vilvoiu <vasi.vilvoiu@gmail.com> + * + * specgram is free software; you can redistribute it and/or modify + * it under the terms of the MIT license. See LICENSE for details. + */ + +#include "configuration.hpp" +#include "args.hxx" +#include "input-parser.hpp" +#include "specgram.hpp" +#include "fft.hpp" + +#include <tuple> +#include <regex> + +Configuration::Configuration() +{ + this->input_filename_ = {}; + this->output_filename_ = {}; + + this->block_size_ = 256; + this->rate_ = 44100; + this->datatype_ = DataType::kSignedInt16; + this->prescale_factor_ = 1.0f; + + this->fft_width_ = 1024; + this->fft_stride_ = 1024; + this->alias_negative_ = true; + this->window_function_ = WindowFunctionType::kHann; + this->average_count_ = 1; + + this->no_resampling_ = false; + this->width_ = 512; + this->min_freq_ = 0; + this->max_freq_ = this->rate_ / 2; + this->scale_ = ValueMapType::kdBFS; + this->scale_lower_bound_ = -120.0f; + this->color_map_ = ColorMapType::kJet; + this->background_color_ = sf::Color(0, 0, 0); + this->foreground_color_ = sf::Color(255, 255, 255); + this->has_axes_ = false; + this->has_legend_ = false; + this->is_horizontal_ = false; + this->print_input_ = false; + this->print_fft_ = false; + this->print_output_ = false; + + this->live_ = false; + this->count_ = 512; + this->title_ = "Spectrogram"; + + + this->has_live_window_ = false; + this->margin_size_ = 30; + this->live_margin_size_ = 16; /* this is so the two axes share the same label... more or less */ + this->minimum_margin_size_ = 15; + this->legend_height_ = 20; + this->live_fft_height_ = 100; + this->axis_font_size_ = 12; +} + +sf::Color +Configuration::GetLiveGuidelinesColor() const +{ + sf::Color c = this->GetForegroundColor(); + c.a = 50; + return c; +} + +Configuration +Configuration::GetForLive() const +{ + Configuration c(*this); + + /* overridden configuration for live output */ + c.has_axes_ = true; + c.has_legend_ = true; + c.has_live_window_ = true; + + /* do not allow transparent background, will generate artifacts on live view */ + c.background_color_.a = 255; + + return c; +} + +sf::Color +Configuration::StringToColor(const std::string& str) +{ + if (std::regex_match(str, std::regex("[0-9a-fA-F]{6}"))) { + unsigned int color = std::strtoul(str.c_str(), 0, 16); + return sf::Color( + ((color >> 16) & 0xff), + ((color >> 8) & 0xff), + (color & 0xff), + 255); + } else if (std::regex_match(str, std::regex("[0-9a-fA-F]{8}"))) { + unsigned int color = std::strtoul(str.c_str(), 0, 16); + return sf::Color( + ((color >> 24) & 0xff), + ((color >> 16) & 0xff), + ((color >> 8) & 0xff), + (color & 0xff)); + } else { + throw std::runtime_error("invalid hex color format"); + } +} + +std::tuple<Configuration, int, bool> +Configuration::FromArgs(int argc, char **argv) +{ + Configuration conf; + + /* build parser */ + args::ArgumentParser parser("Generate spectrogram from stdin.", "For more info see https://github.com/rimio/specgram"); + + args::Positional<std::string> outfile(parser, "outfile", "Output PNG file"); + + args::HelpFlag help(parser, "help", "Display this help menu", {'h', "help"}); + args::Flag version(parser, "version", "Display version", {'v', "version"}); + + args::Group input_opts(parser, "Input options:", args::Group::Validators::DontCare); + args::ValueFlag<std::string> + infile(input_opts, "string", "Input file name", {'i', "input"}); + args::ValueFlag<float> + rate(input_opts, "float", "Sampling rate of input in Hz (default: 44100)", {'r', "rate"}); + args::ValueFlag<std::string> + datatype(input_opts, "string", "Data type of input (default: s16)", {'d', "datatype"}); + args::ValueFlag<float> + prescale(input_opts, "float", "Prescaling factor (default: 1.0)", {'p', "prescale"}); + args::ValueFlag<int> + block_size(input_opts, "integer", "Block size when reading input, in data types (default: 256)", {'b', "block_size"}); + + args::Group fft_opts(parser, "FFT options:", args::Group::Validators::DontCare); + args::ValueFlag<int> + fft_width(fft_opts, "integer", "FFT window width (default: 1024)", {'f', "fft_width"}); + args::ValueFlag<int> + fft_stride(fft_opts, "integer", "FFT window stride (default: 1024)", {'g', "fft_stride"}); + args::ValueFlag<std::string> + win_func(fft_opts, "string", "ComplexWindow function (default: hann)", {'n', "window_function"}); + args::ValueFlag<bool> + alias(fft_opts, "boolean", "Alias negative and positive frequencies (default: 0 (no) for complex data types, 1 (yes) otherwise)", + {'m', "alias"}); + args::ValueFlag<int> + average(fft_opts, "integer", "Number of windows to average (default: 1)", {'A', "average"}); + + args::Group display_opts(parser, "Display options:", args::Group::Validators::DontCare); + args::Flag + no_resampling(display_opts, "no_resampling", "No resampling; width will be computed from FFT parameters and fmin/fmax", + {'q', "no_resampling"}); + args::ValueFlag<int> + width(display_opts, "integer", "Display width (default: 512)", {'w', "width"}); + args::ValueFlag<float> + fmin(display_opts, "float", "Minimum frequency in Hz (default: -0.5 * rate for complex data types, 0 otherwise)", {'x', "fmin"}); + 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"}); + args::ValueFlag<std::string> + colormap(display_opts, "string", "Colormap (default: jet)", {'c', "colormap"}); + args::ValueFlag<std::string> + bgcolor(display_opts, "string", "Background color (default: 000000)", {"bg-color"}); + args::ValueFlag<std::string> + fgcolor(display_opts, "string", "Foreground color (default: ffffff)", {"fg-color"}); + args::Flag + axes(display_opts, "axes", "Display axes (inferred for -e, --legend)", {'a', "axes"}); + args::Flag + legend(display_opts, "legend", "Display legend", {'e', "legend"}); + args::Flag + horizontal(display_opts, "horizontal", "Display horizontally", {'z', "horizontal"}); + args::Flag + print_input(display_opts, "print_input", "Print input window", {"print_input"}); + args::Flag + print_fft(display_opts, "print_fft", "Print FFT output", {"print_fft"}); + args::Flag + print_output(display_opts, "print_output", "Print resampled/cropped and normalized output", {"print_output"}); + + args::Group live_opts(parser, "Live options:", args::Group::Validators::DontCare); + args::Flag + live(live_opts, "live", "Display live spectrogram", {'l', "live"}); + args::ValueFlag<int> + count(live_opts, "integer", "Number of FFT windows in displayed history (default: 512)", {'k', "count"}); + args::ValueFlag<std::string> + title(live_opts, "string", "ComplexWindow title", {'t', "title"}); + + /* parse arguments */ + try { + parser.ParseCLI(argc, argv); + } catch (const args::Help&) { + std::cout << parser; + return std::make_tuple(conf, 0, true); + } catch (const args::ParseError& e) { + std::cerr << e.what() << std::endl; + std::cerr << parser; + return std::make_tuple(conf, 1, true); + } catch (args::ValidationError& e) { + std::cerr << e.what() << std::endl; + std::cerr << parser; + return std::make_tuple(conf, 1, true); + } + + /* version mode */ + if (version) { + std::cout << "specgram version " << SPECGRAM_VERSION << std::endl; + return std::make_tuple(conf, 0, true); + } + + /* check and store command line arguments */ + if (outfile) { + conf.output_filename_ = args::get(outfile); + } else if (!live) { + std::cerr << "Either specify file or '--live', otherwise nothing to do." << std::endl; + return std::make_tuple(conf, 1, true); + } + + if (infile) { + conf.input_filename_ = args::get(infile); + } + if (block_size) { + if (args::get(block_size) <= 0) { + std::cerr << "'block_size' must be positive." << std::endl; + return std::make_tuple(conf, 1, true); + } else { + conf.block_size_ = args::get(block_size); + } + } + if (rate) { + if (args::get(rate) <= 0) { + std::cerr << "'rate' must be positive." << std::endl; + return std::make_tuple(conf, 1, true); + } else { + conf.rate_ = args::get(rate); + } + } + if (datatype) { + auto dtype = args::get(datatype); + if ((dtype.size() > 0) && (dtype[0] == 'c')) { + conf.alias_negative_ = false; + conf.has_complex_input_ = true; + dtype = dtype.substr(1, dtype.size() - 1); + } else { + conf.alias_negative_ = true; + conf.has_complex_input_ = false; + } + + if (dtype == "s8") { + conf.datatype_ = DataType::kSignedInt8; + } else if (dtype == "s16") { + conf.datatype_ = DataType::kSignedInt16; + } else if (dtype == "s32") { + conf.datatype_ = DataType::kSignedInt32; + } else if (dtype == "s64") { + conf.datatype_ = DataType::kSignedInt64; + } else if (dtype == "u8") { + conf.datatype_ = DataType::kUnsignedInt8; + } else if (dtype == "u16") { + conf.datatype_ = DataType::kUnsignedInt16; + } else if (dtype == "u32") { + conf.datatype_ = DataType::kUnsignedInt32; + } else if (dtype == "u64") { + conf.datatype_ = DataType::kUnsignedInt64; + } else if (dtype == "f32") { + conf.datatype_ = DataType::kFloat32; + } else if (dtype == "f64") { + conf.datatype_ = DataType::kFloat64; + } else { + std::cerr << "Unknown data type '" << dtype << "'" << std::endl; + return std::make_tuple(conf, 1, true); + } + } + if (prescale) { + conf.prescale_factor_ = args::get(prescale); + } + + if (fft_width) { + if (args::get(fft_width) <= 0) { + std::cerr << "'fft_width' must be positive." << std::endl; + return std::make_tuple(conf, 1, true); + } else { + conf.fft_width_ = args::get(fft_width); + } + } + if (conf.fft_width_ % 2 == 0) { + double boundary = conf.rate_ * (conf.fft_width_ - 2.0f) / (2.0f * conf.fft_width_); + conf.min_freq_ = conf.alias_negative_ ? 0.0f : -boundary; + conf.max_freq_ = conf.rate_ / 2.0f; + } else { + double boundary = conf.rate_ * (conf.fft_width_ - 1.0f) / (2.0f * conf.fft_width_); + conf.min_freq_ = conf.alias_negative_ ? 0.0f : -boundary; + conf.max_freq_ = boundary; + } + if (fft_stride) { + if (args::get(fft_stride) <= 0) { + std::cerr << "'fft_width' must be positive." << std::endl; + return std::make_tuple(conf, 1, true); + } else { + conf.fft_stride_ = args::get(fft_stride); + } + } + if (win_func) { + auto& wf_str = args::get(win_func); + if (wf_str == "none") { + conf.window_function_ = WindowFunctionType::kNone; + } else if (wf_str == "hann") { + conf.window_function_ = WindowFunctionType::kHann; + } else if (wf_str == "hamming") { + conf.window_function_ = WindowFunctionType::kHamming; + } else if (wf_str == "blackman") { + conf.window_function_ = WindowFunctionType::kBlackman; + } else if (wf_str == "nuttall") { + conf.window_function_ = WindowFunctionType::kBlackman; + } else { + std::cerr << "Unknown window function '" << wf_str << "'" << std::endl; + return std::make_tuple(conf, 1, true); + } + } + if (alias) { + conf.alias_negative_ = args::get(alias); + } + if (average) { + conf.average_count_ = args::get(average); + if (conf.average_count_ == 0) { + conf.average_count_ = 1; + } + } + + if (no_resampling) { + conf.no_resampling_ = true; + } + if (width) { + if (args::get(width) <= 0) { + std::cerr << "'width' must be positive." << std::endl; + return std::make_tuple(conf, 1, true); + } else if (conf.no_resampling_) { + std::cerr << "'width' cannot be specified when not resampling (-q, --no_resampling)." << std::endl; + return std::make_tuple(conf, 1, true); + } else { + conf.width_ = args::get(width); + } + } + if (fmin) { + conf.min_freq_ = args::get(fmin); + } + if (fmax) { + conf.max_freq_ = args::get(fmax); + } + 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); + } + } + } else { + std::cerr << "Unknown scale '" << scale_str << "'" << std::endl; + return std::make_tuple(conf, 1, true); + } + } + if (colormap) { + auto& cmap_str = args::get(colormap); + if (cmap_str == "gray") { + conf.color_map_ = ColorMapType::kGray; + } else if (cmap_str == "jet") { + conf.color_map_ = ColorMapType::kJet; + } else if (cmap_str == "purple") { + conf.color_map_ = ColorMapType::kPurple; + } else if (cmap_str == "blue") { + conf.color_map_ = ColorMapType::kBlue; + } else if (cmap_str == "green") { + conf.color_map_ = ColorMapType::kGreen; + } else if (cmap_str == "orange") { + conf.color_map_ = ColorMapType::kOrange; + } else if (cmap_str == "red") { + conf.color_map_ = ColorMapType::kRed; + } else { + try { + auto color = Configuration::StringToColor(cmap_str); + conf.color_map_custom_color_ = color; + conf.color_map_ = ColorMapType::kCustom; + } catch (const std::exception& e) { + std::cerr << "Unknown colormap '" << cmap_str << "'" << std::endl; + return std::make_tuple(conf, 1, true); + } + } + } + if (bgcolor) { + auto& str = args::get(bgcolor); + try { + auto color = Configuration::StringToColor(str); + conf.background_color_ = color; + } catch (const std::exception& e) { + std::cerr << "Invalid background color '" << str << "'" << std::endl; + return std::make_tuple(conf, 1, true); + } + } + if (fgcolor) { + auto& str = args::get(fgcolor); + try { + auto color = Configuration::StringToColor(str); + conf.foreground_color_ = color; + } catch (const std::exception& e) { + std::cerr << "Invalid foreground color '" << str << "'" << std::endl; + return std::make_tuple(conf, 1, true); + } + } + if (axes) { + conf.has_axes_ = true; + } + if (legend) { + conf.has_legend_ = true; + } + if (horizontal) { + conf.is_horizontal_ = true; + } + if (print_input) { + conf.print_input_ = true; + } + if (print_fft) { + conf.print_fft_ = true; + } + if (print_output) { + conf.print_output_ = true; + } + + if (live) { + conf.live_ = true; + if (infile) { + std::cerr << "live view not allowed on file input (-i, --input)" << std::endl; + return std::make_tuple(conf, 1, true); + } + } + if (count) { + if (args::get(count) <= 0) { + std::cerr << "'count' must be positive." << std::endl; + return std::make_tuple(conf, 1, true); + } else { + conf.count_ = args::get(count); + } + } + if (title) { + conf.title_ = args::get(title); + } + + /* compute width for --no_resampling case */ + if (conf.no_resampling_) { + int mini = static_cast<int>(std::round(FFT::GetFrequencyIndex(conf.rate_, conf.fft_width_, conf.min_freq_))); + int maxi = static_cast<int>(std::round(FFT::GetFrequencyIndex(conf.rate_, conf.fft_width_, conf.max_freq_))); + + if (mini < 0) { + std::cerr + << "'fmin' is outside of FFT window, which is not allowed when not resampling (-q, --no_resampling)." + << std::endl; + return std::make_tuple(conf, 1, true); + } + if (maxi >= static_cast<int>(conf.fft_width_)) { + std::cerr + << "'fmax' is outside of FFT window, which is not allowed when not resampling (-q, --no_resampling)." + << std::endl; + return std::make_tuple(conf, 1, true); + } + + conf.width_ = maxi - mini; + if (conf.width_ == 0) { + std::cerr + << "'fmin' and 'fmax' are either equal or very close, which is not allowed when not resampling (-q, --no_resampling)." + << std::endl; + return std::make_tuple(conf, 1, true); + } + /* (maxi-mini) < 0 should be caught lower */ + } + + /* fmin/fmax checks */ + if (conf.min_freq_ >= conf.max_freq_) { + std::cerr << "'fmin' must be less than 'fmax'." << std::endl; + return std::make_tuple(conf, 1, true); + } + + /* normal usage mode, don't exit */ + return std::make_tuple(conf, 0, false); +} |
