/* * Copyright (c) 2020-2021 Vasile Vilvoiu * * specgram is free software; you can redistribute it and/or modify * it under the terms of the MIT license. See LICENSE for details. */ #include "renderer.hpp" #include "share-tech-mono.hpp" #include "fft.hpp" #include #include #include #include static std::string ValueToShortString(double value, int prec, const std::string& unit) { static const std::vector PREFIXES = { "p", "n", "u", "m", "", "k", "M", "G", "T" }; std::size_t pidx = 4; while ((prec >= 3) && (pidx > 0)) { prec -= 3; pidx--; value *= 1000.0f; } while ((prec <= -3) && (pidx < PREFIXES.size() - 1)) { prec += 3; pidx++; value /= 1000.0f; } prec++; std::stringstream ss; ss << std::fixed << std::setprecision(prec > 0 ? prec : 0) << value << PREFIXES[pidx] << unit; return ss.str(); } Renderer::Renderer(const Configuration& conf, const ColorMap& cmap, const ValueMap& vmap, std::size_t fft_count) : configuration_(conf), fft_count_(fft_count), color_map_(cmap.Copy()) { if (color_map_ == nullptr) { throw std::runtime_error("failed to copy color map"); } if (fft_count == 0) { throw std::runtime_error("positive number of FFT windows required by rendere"); } /* load font */ if (!this->font_.loadFromMemory(ShareTechMono_Regular_ttf, ShareTechMono_Regular_ttf_len)) { throw std::runtime_error("unable to load font"); } /* compute tickmarks */ this->frequency_ticks_ = Renderer::GetNiceTicks(this->configuration_.GetMinFreq(), this->configuration_.GetMaxFreq(), "Hz", this->configuration_.GetWidth(), 75); auto time_ticks = Renderer::GetNiceTicks(0.0f, (double)fft_count * this->configuration_.GetAverageCount() * this->configuration_.GetFFTStride() / this->configuration_.GetRate(), "s", fft_count, 50); 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 */ std::list freq_no_text_ticks; for (auto& t : this->frequency_ticks_) { freq_no_text_ticks.emplace_back(std::make_tuple(std::get<0>(t), "")); } /* get maximum text widths */ double max_freq_ticks_width = 0.0f; double max_freq_ticks_height = 0.0f; double max_time_ticks_width = 0.0f; double max_time_ticks_height = 0.0f; double max_legend_ticks_width = 0.0f; double max_legend_ticks_height = 0.0f; double max_live_ticks_width = 0.0f; double max_live_ticks_height = 0.0f; if (this->configuration_.HasAxes()) { for (auto &t : this->frequency_ticks_) { sf::Text text; text.setCharacterSize(this->configuration_.GetAxisFontSize()); text.setFont(this->font_); text.setString(std::get<1>(t)); if (text.getLocalBounds().width > max_freq_ticks_width) { max_freq_ticks_width = text.getLocalBounds().width; } if (text.getLocalBounds().height > max_freq_ticks_height) { max_freq_ticks_height = text.getLocalBounds().height; } } for (auto &t : time_ticks) { sf::Text text; text.setCharacterSize(this->configuration_.GetAxisFontSize()); text.setFont(this->font_); text.setString(std::get<1>(t)); if (text.getLocalBounds().width > max_time_ticks_width) { max_time_ticks_width = text.getLocalBounds().width; } if (text.getLocalBounds().height > max_time_ticks_height) { max_time_ticks_height = text.getLocalBounds().height; } } for (auto &t : legend_ticks) { sf::Text text; text.setCharacterSize(this->configuration_.GetAxisFontSize()); text.setFont(this->font_); text.setString(std::get<1>(t)); if (text.getLocalBounds().width > max_legend_ticks_width) { max_legend_ticks_width = text.getLocalBounds().width; } if (text.getLocalBounds().height > max_legend_ticks_height) { max_legend_ticks_height = text.getLocalBounds().height; } } for (auto &t : this->live_ticks_) { sf::Text text; text.setCharacterSize(this->configuration_.GetAxisFontSize()); text.setFont(this->font_); text.setString(std::get<1>(t)); if (text.getLocalBounds().width > max_live_ticks_width) { max_live_ticks_width = text.getLocalBounds().width; } if (text.getLocalBounds().height > max_live_ticks_height) { max_live_ticks_height = text.getLocalBounds().height; } } } double freq_axis_spacing = (this->configuration_.IsHorizontal() ? max_freq_ticks_width : max_freq_ticks_height); double time_axis_spacing = (this->configuration_.IsHorizontal() ? max_time_ticks_height : max_time_ticks_width); double legend_axis_spacing = (this->configuration_.IsHorizontal() ? max_legend_ticks_width : max_legend_ticks_height); double live_axis_spacing = (this->configuration_.IsHorizontal() ? max_live_ticks_height : max_live_ticks_width); double horizontal_extra_spacing = std::max({ time_axis_spacing, live_axis_spacing }); horizontal_extra_spacing = std::max({ 0.0f, horizontal_extra_spacing - this->configuration_.GetMarginSize() + this->configuration_.GetMinimumMarginSize() }); /* compute various sizes */ this->width_ = conf.GetWidth(); this->width_ += conf.HasAxes() ? 2 * conf.GetMarginSize() + static_cast(horizontal_extra_spacing) : 0; this->height_ = 0; /* compute transforms */ if (this->configuration_.HasLegend()) { if (this->configuration_.HasAxes()) { this->legend_transform_.translate(conf.GetMarginSize() + horizontal_extra_spacing, conf.GetMarginSize() + legend_axis_spacing); this->height_ += conf.GetMarginSize() + legend_axis_spacing; } this->height_ += conf.GetLegendHeight(); } this->fft_live_transform_.translate(0.0f, this->height_); if (conf.HasLiveWindow()) { if (this->configuration_.HasAxes()) { this->fft_live_transform_.translate(conf.GetMarginSize() + horizontal_extra_spacing, conf.GetMarginSize()); this->height_ += conf.GetLiveMarginSize(); } this->height_ += conf.GetLiveFFTHeight(); } this->fft_area_transform_.translate(0.0f, this->height_); if (conf.HasAxes()) { this->fft_area_transform_.translate(conf.GetMarginSize() + horizontal_extra_spacing, conf.GetMarginSize() + freq_axis_spacing); this->height_ += conf.GetMarginSize() * 2 + freq_axis_spacing; } this->height_ += this->fft_count_; /* allocate canvas render texture */ this->canvas_.create(this->width_, this->height_); this->canvas_.clear(this->configuration_.GetBackgroundColor()); /* allocate FFT area texture */ this->fft_area_texture_.create(conf.GetWidth(), fft_count); /* * render UI */ /* render FFT area axes */ if (this->configuration_.HasAxes()) { /* FFT area box */ sf::RectangleShape fft_area_box(sf::Vector2f(this->configuration_.GetWidth(), this->fft_count_)); fft_area_box.setFillColor(this->configuration_.GetBackgroundColor()); fft_area_box.setOutlineColor(this->configuration_.GetForegroundColor()); fft_area_box.setOutlineThickness(1); this->canvas_.draw(fft_area_box, this->fft_area_transform_); /* frequency axis */ this->RenderAxis(this->canvas_, this->fft_area_transform_, true, this->configuration_.IsHorizontal() ? Orientation::k90CW : Orientation::kNormal, this->configuration_.GetWidth(), this->frequency_ticks_); /* time axis */ this->RenderAxis(this->canvas_, this->fft_area_transform_ * sf::Transform().rotate(90.0f), false, this->configuration_.IsHorizontal() ? Orientation::kNormal : Orientation::k90CCW, fft_count, time_ticks); } if (this->configuration_.HasLegend()) { /* legend box */ sf::RectangleShape legend_box(sf::Vector2f(this->configuration_.GetWidth(), this->configuration_.GetLegendHeight())); legend_box.setFillColor(this->configuration_.GetBackgroundColor()); legend_box.setOutlineColor(this->configuration_.GetForegroundColor()); legend_box.setOutlineThickness(1); this->canvas_.draw(legend_box, this->legend_transform_); /* legend gradient */ auto memory = cmap.Gradient(this->configuration_.GetWidth()); sf::Texture tex; tex.create(this->configuration_.GetWidth(), 1); tex.update(reinterpret_cast(memory.data())); this->canvas_.draw(sf::Sprite(tex), this->legend_transform_ * sf::Transform().scale(1.0f, this->configuration_.GetLegendHeight())); if (this->configuration_.HasAxes()) { this->RenderAxis(this->canvas_, this->legend_transform_, true, this->configuration_.IsHorizontal() ? Orientation::k90CW : Orientation::kNormal, this->configuration_.GetWidth(), legend_ticks); } } if (this->configuration_.HasLiveWindow() && this->configuration_.HasAxes()) { /* value axis */ this->RenderAxis(this->canvas_, this->fft_live_transform_ * sf::Transform().translate(0.0f, this->configuration_.GetLiveFFTHeight()).rotate(-90.0f), true, this->configuration_.IsHorizontal() ? Orientation::k180 : Orientation::k90CW, this->configuration_.GetLiveFFTHeight(), this->live_ticks_); /* frequency axis */ this->RenderAxis(this->canvas_, this->fft_live_transform_ * sf::Transform().translate(0.0f, this->configuration_.GetLiveFFTHeight()), false, this->configuration_.IsHorizontal() ? Orientation::k90CW : Orientation::kNormal, this->configuration_.GetWidth(), freq_no_text_ticks); } } [[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) { throw std::runtime_error("GetLinearTicks() requires at least two ticks"); } if (v_min >= v_max) { throw std::runtime_error("minimum and maximum values are not in order"); } int prec = 0; double dist = (v_max - v_min) / ((double) num_ticks - 1); while (dist >= 10.0f) { prec--; dist /= 10.0f; } while (dist < 1.0f) { prec++; dist *= 10.0f; } std::list ticks; for (unsigned int i = 0; i < num_ticks; i ++) { double k = (double) i / (double)(num_ticks - 1); double v = v_min + k * (v_max - v_min); ticks.emplace_back(std::make_tuple(k, ::ValueToShortString(v, prec, v_unit))); } return ticks; } std::list Renderer::GetNiceTicks(double v_min, double v_max, const std::string& v_unit, unsigned int length_px, unsigned int est_tick_length_px) { if (v_min >= v_max) { throw std::runtime_error("minimum and maximum values are not in order"); } if (length_px == 0) { throw std::runtime_error("length in pixels must be positive"); } if (est_tick_length_px == 0) { throw std::runtime_error("estimate tick length in pixels must be positive"); } std::list ticks; /* find a factor that brings some span close to est_tick_length to a nice value */ double v_diff = v_max - v_min; double px_per_v = length_px / v_diff; double mdist = 1e12, mfact; constexpr double NICE_FACTORS[] = { 0.15f, 0.2f, 0.25f, 0.3f, 0.5f }; for (double f = 1e-15; f < 1e+15; f *= 10.0f) { for (double nf : NICE_FACTORS) { double factor = f * nf; double dist = std::abs(est_tick_length_px - px_per_v * factor); if (dist < mdist) { mdist = dist; mfact = factor; } } } /* compute precision */ int prec = 0; double dist = mfact; while (dist > 10.0f) { prec--; dist /= 10.0f; } while (dist < 1.0f) { prec++; dist *= 10.0f; } /* find the first nice value */ double fval = v_min / mfact; constexpr double ROUND_EPSILON = 1e-6; if (std::abs(fval - std::floor(fval)) > ROUND_EPSILON) { fval = std::floor(fval) + 1.0f; } fval *= mfact; 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 <= 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))); } return ticks; } void Renderer::RenderAxis(sf::RenderTexture& texture, const sf::Transform& t, bool lhs, Orientation orientation, double length, const std::list& ticks) { if (length <= 0.0f) { throw std::runtime_error("positive axis length required for rendering"); } for (auto& tick : ticks) { /* draw tick line */ double x = (length - 1) * std::get<0>(tick); sf::RectangleShape tick_shape(sf::Vector2f(1.0, (lhs ? -5.0f : 5.0f))); tick_shape.setFillColor(this->configuration_.GetForegroundColor()); texture.draw(tick_shape, t * sf::Transform().translate(x, 0.0f)); /* draw text */ sf::Text text; text.setFillColor(this->configuration_.GetForegroundColor()); text.setCharacterSize(this->configuration_.GetAxisFontSize()); text.setFont(this->font_); text.setString(std::get<1>(tick)); sf::Vector2f pos; switch (orientation) { case Orientation::k90CCW: pos = sf::Vector2f(sf::Vector2f(length * std::get<0>(tick) - text.getLocalBounds().height, (lhs ? -10.0f : text.getLocalBounds().width + 10.0f))); text.setRotation(-90.0f); break; case Orientation::k90CW: pos = sf::Vector2f(sf::Vector2f(length * std::get<0>(tick) + text.getLocalBounds().height, (lhs ? -text.getLocalBounds().width - 10.0f : 10.0f))); text.setRotation(90.0f); break; case Orientation::kNormal: pos = sf::Vector2f(sf::Vector2f(length * std::get<0>(tick) - text.getLocalBounds().width / 2, (lhs ? -2.0f * text.getLocalBounds().height - 3.0f : 3.0f))); break; case Orientation::k180: pos = sf::Vector2f(sf::Vector2f(length * std::get<0>(tick) + text.getLocalBounds().width / 2, (lhs ? -3.0f : 2.0f * text.getLocalBounds().height + 3.0f))); text.setRotation(180.0f); break; default: throw std::runtime_error("unknown orientation"); } text.setPosition(std::round(pos.x), std::round(pos.y)); /* avoid interpolation on text, looks yuck */ texture.draw(text, t); } } void Renderer::RenderFFTArea(const std::vector& memory) { if (memory.size() != configuration_.GetWidth() * this->fft_count_ * 4) { throw std::runtime_error("bad memory size"); } /* update FFT area texture */ this->fft_area_texture_.update(reinterpret_cast(memory.data())); /* render FFT area on canvas */ this->canvas_.draw(sf::Sprite(this->fft_area_texture_), this->fft_area_transform_); } void Renderer::RenderFFTArea(const std::list>& history) { if (history.size() != this->fft_count_) { throw std::runtime_error("bad history size"); } std::vector memory; memory.resize(this->fft_count_ * this->configuration_.GetWidth() * 4); std::size_t i = 0; for (auto& win : history) { std::memcpy(reinterpret_cast(memory.data() + i * this->configuration_.GetWidth() * 4), reinterpret_cast(win.data()), this->configuration_.GetWidth() * 4); i++; } return this->RenderFFTArea(memory); } std::vector Renderer::RenderLiveFFT(const RealWindow& window) { if (window.size() != this->configuration_.GetWidth()) { throw std::runtime_error("incorrect window size to be rendered"); } if (!this->configuration_.HasLiveWindow()) { throw std::runtime_error("asked to render live window for non-live configuration"); } std::vector colors = this->color_map_->Map(window); /* FFT live box (so we overwrite old one */ sf::RectangleShape fft_live_box(sf::Vector2f(this->configuration_.GetWidth(), this->configuration_.GetLiveFFTHeight() + 1.0f)); fft_live_box.setFillColor(this->configuration_.GetBackgroundColor()); fft_live_box.setOutlineColor(this->configuration_.GetForegroundColor()); fft_live_box.setOutlineThickness(1); this->canvas_.draw(fft_live_box, this->fft_live_transform_); /* horizontal live guidelines */ for (std::size_t i = 1; i < this->live_ticks_.size() - 1; i ++) { sf::RectangleShape hline(sf::Vector2f(this->configuration_.GetWidth(), 1.0f)); hline.setFillColor(this->configuration_.GetLiveGuidelinesColor()); sf::Transform tran; tran.translate(0.0f, std::get<0>(*std::next(this->live_ticks_.begin(), i)) * (this->configuration_.GetLiveFFTHeight() - 1.0f)); this->canvas_.draw(hline, this->fft_live_transform_ * tran); } /* vertical live guidelines */ for (std::size_t i = 1; i < this->frequency_ticks_.size(); i ++) { sf::RectangleShape vline(sf::Vector2f(1.0f, this->configuration_.GetLiveFFTHeight())); vline.setFillColor(this->configuration_.GetLiveGuidelinesColor()); sf::Transform tran; tran.translate(std::get<0>(*std::next(this->frequency_ticks_.begin(), i)) * (this->configuration_.GetWidth() - 1.0f), 0.0f); this->canvas_.draw(vline, this->fft_live_transform_ * tran); } /* plot */ std::vector vertices; vertices.resize(window.size()); for (std::size_t i = 0; i < window.size(); i++) { double x = i; double y = (1.0f - window[i]) * this->configuration_.GetLiveFFTHeight(); vertices[i] = sf::Vertex(sf::Vector2f(x, y), sf::Color(colors[i * 4 + 0], colors[i * 4 + 1], colors[i * 4 + 2])); } this->canvas_.draw(reinterpret_cast(vertices.data()), vertices.size(), sf::LineStrip, this->fft_live_transform_); return colors; } sf::Texture Renderer::GetCanvas() { this->canvas_.display(); return this->canvas_.getTexture(); }