ardour/gtk2_ardour/rta_window.cc

835 lines
22 KiB
C++
Raw Normal View History

2025-03-24 20:51:13 +01:00
/*
* Copyright (C) 2025 Robin Gareus <robin@gareus.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifdef WAF_BUILD
#include "gtk2ardour-config.h"
#endif
2025-03-26 23:57:52 +01:00
#include "ardour/route_group.h"
2025-03-24 20:51:13 +01:00
#include "ardour/session.h"
2025-03-26 23:57:52 +01:00
#include "gtkmm2ext/colors.h"
#include "gtkmm2ext/menu_elems.h"
#include "gtkmm2ext/rgb_macros.h"
#include "gtkmm2ext/utils.h"
2025-03-24 20:51:13 +01:00
#include "gtkmm2ext/window_title.h"
2025-03-26 23:57:52 +01:00
#include "ardour_ui.h"
2025-03-24 20:51:13 +01:00
#include "gui_thread.h"
#include "keyboard.h"
2025-03-26 23:57:52 +01:00
#include "rta_manager.h"
2025-03-24 20:51:13 +01:00
#include "rta_window.h"
#include "timers.h"
2025-03-26 23:57:52 +01:00
#include "ui_config.h"
2025-03-24 20:51:13 +01:00
#include "pbd/i18n.h"
using namespace ARDOUR;
RTAWindow::RTAWindow ()
: ArdourWindow (_("Realtime Perceptual Analyzer"))
2025-04-14 23:45:51 +02:00
, _pause (_("Freeze"), ArdourWidgets::ArdourButton::default_elements, true)
2025-03-26 23:57:52 +01:00
, _visible (false)
2025-04-15 00:00:20 +02:00
, _margin (24)
2025-03-26 23:57:52 +01:00
, _min_dB (-60)
, _max_dB (0)
, _hovering_dB (false)
, _dragging_dB (DragNone)
, _cursor_x (-1)
, _cursor_y (-1)
2025-03-24 20:51:13 +01:00
{
2025-03-26 23:57:52 +01:00
_pause.signal_clicked.connect (mem_fun (*this, &RTAWindow::pause_toggled));
2025-04-14 23:45:51 +02:00
_pause.set_name ("rta freeze button");
2025-03-24 20:51:13 +01:00
2025-03-26 23:57:52 +01:00
_darea.add_events (Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::POINTER_MOTION_MASK | Gdk::LEAVE_NOTIFY_MASK);
2025-03-24 20:51:13 +01:00
_darea.signal_size_request ().connect (sigc::mem_fun (*this, &RTAWindow::darea_size_request));
_darea.signal_size_allocate ().connect (sigc::mem_fun (*this, &RTAWindow::darea_size_allocate));
_darea.signal_expose_event ().connect (sigc::mem_fun (*this, &RTAWindow::darea_expose_event));
_darea.signal_button_press_event ().connect (sigc::mem_fun (*this, &RTAWindow::darea_button_press_event));
_darea.signal_button_release_event ().connect (sigc::mem_fun (*this, &RTAWindow::darea_button_release_event));
_darea.signal_motion_notify_event ().connect (sigc::mem_fun (*this, &RTAWindow::darea_motion_notify_event));
_darea.signal_scroll_event ().connect (sigc::mem_fun (*this, &RTAWindow::darea_scroll_event));
_darea.signal_leave_notify_event ().connect (sigc::mem_fun (*this, &RTAWindow::darea_leave_notify_event), true);
_darea.signal_grab_broken_event ().connect (sigc::mem_fun (*this, &RTAWindow::darea_grab_broken_event), true);
_darea.signal_grab_notify ().connect (sigc::mem_fun (*this, &RTAWindow::darea_grab_notify), true);
2025-03-26 23:57:52 +01:00
_speed_strings.push_back (_("Rapid"));
_speed_strings.push_back (_("Fast"));
_speed_strings.push_back (_("Moderate"));
_speed_strings.push_back (_("Slow"));
_speed_strings.push_back (_("Noise Measurement"));
using namespace Gtkmm2ext;
using PA = ARDOUR::DSP::PerceptualAnalyzer;
2025-07-12 10:51:54 -06:00
_speed_dropdown.add_menu_elem (MenuElemNoMnemonic (_speed_strings[(int)PA::Rapid], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_speed), PA::Rapid)));
_speed_dropdown.add_menu_elem (MenuElemNoMnemonic (_speed_strings[(int)PA::Fast], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_speed), PA::Fast)));
_speed_dropdown.add_menu_elem (MenuElemNoMnemonic (_speed_strings[(int)PA::Moderate], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_speed), PA::Moderate)));
_speed_dropdown.add_menu_elem (MenuElemNoMnemonic (_speed_strings[(int)PA::Slow], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_speed), PA::Slow)));
_speed_dropdown.add_menu_elem (MenuElemNoMnemonic (_speed_strings[(int)PA::Noise], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_speed), PA::Noise)));
2025-03-26 23:57:52 +01:00
_speed_dropdown.set_sizing_texts (_speed_strings);
_speed_dropdown.set_text (_speed_strings[(int)RTAManager::instance ()->rta_speed ()]);
_warp_strings.push_back (_("Bark"));
_warp_strings.push_back (_("Medium"));
_warp_strings.push_back (_("High"));
2025-07-12 10:51:54 -06:00
_warp_dropdown.add_menu_elem (MenuElemNoMnemonic (_warp_strings[(int)PA::Bark], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_warp), PA::Bark)));
_warp_dropdown.add_menu_elem (MenuElemNoMnemonic (_warp_strings[(int)PA::Medium], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_warp), PA::Medium)));
_warp_dropdown.add_menu_elem (MenuElemNoMnemonic (_warp_strings[(int)PA::High], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_warp), PA::High)));
2025-03-26 23:57:52 +01:00
_warp_dropdown.set_sizing_texts (_warp_strings);
_warp_dropdown.set_text (_warp_strings[(int)RTAManager::instance ()->rta_warp ()]);
_ctrlbox.set_spacing (4);
_ctrlbox.pack_start (*manage (new Gtk::Label (_("Speed:"))), false, false);
_ctrlbox.pack_start (_speed_dropdown, false, false);
_ctrlbox.pack_start (*manage (new Gtk::Label (_("Warp:"))), false, false);
_ctrlbox.pack_start (_warp_dropdown, false, false);
_ctrlbox.pack_start (_pointer_info, false, false, 5);
_ctrlbox.pack_end (_pause, false, false);
2025-03-24 20:51:13 +01:00
_vpacker.pack_start (_darea, true, true);
2025-03-26 23:57:52 +01:00
_vpacker.pack_start (_ctrlbox, false, false, 5);
2025-03-24 20:51:13 +01:00
add (_vpacker);
set_border_width (4);
_vpacker.show_all ();
2025-03-26 23:57:52 +01:00
Gtkmm2ext::UI::instance ()->theme_changed.connect (sigc::mem_fun (*this, &RTAWindow::on_theme_changed));
UIConfiguration::instance ().ColorsChanged.connect (sigc::mem_fun (*this, &RTAWindow::on_theme_changed));
UIConfiguration::instance ().DPIReset.connect (sigc::mem_fun (*this, &RTAWindow::on_theme_changed));
on_theme_changed ();
}
void
RTAWindow::on_theme_changed ()
{
_basec = UIConfiguration::instance ().color (X_("gtk_bases")); // gtk_darkest
_gridc = UIConfiguration::instance ().color (X_("gtk_background"));
_textc = UIConfiguration::instance ().color (X_("gtk_foreground"));
2025-04-15 00:00:20 +02:00
_margin = 2 * ceilf (12.f * UIConfiguration::instance ().get_ui_scale ());
_uiscale = std::max<float> (1.f, sqrtf (UIConfiguration::instance ().get_ui_scale ()));
2025-03-26 23:57:52 +01:00
_grid.clear ();
_xpos.clear ();
_darea.queue_resize ();
2025-03-26 23:57:52 +01:00
_darea.queue_draw ();
2025-03-24 20:51:13 +01:00
}
XMLNode&
RTAWindow::get_state () const
{
XMLNode* node = new XMLNode ("RTAWindow");
2025-03-26 23:57:52 +01:00
node->set_property (X_("min-dB"), _min_dB);
node->set_property (X_("max-dB"), _max_dB);
2025-03-24 20:51:13 +01:00
return *node;
}
void
RTAWindow::set_session (ARDOUR::Session* s)
{
if (!s) {
return;
}
/* only call SessionHandlePtr::set_session if session is not NULL,
* otherwise RTAWindow::session_going_away will never be invoked.
*/
ArdourWindow::set_session (s);
2025-03-26 23:57:52 +01:00
XMLNode* node = _session->instant_xml (X_("RTAWindow"));
if (node) {
node->get_property ("min-dB", _min_dB);
node->get_property ("max-dB", _max_dB);
if (_max_dB > _dB_min + _dB_range) {
_max_dB = _dB_min + _dB_range;
}
if (_min_dB < _dB_min) {
_min_dB = _dB_min;
}
if (_max_dB - _min_dB < _dB_span) {
_max_dB = 0;
_min_dB = -60;
}
}
2025-03-24 20:51:13 +01:00
update_title ();
_session->DirtyChanged.connect (_session_connections, invalidator (*this), std::bind (&RTAWindow::update_title, this), gui_context ());
2025-03-26 23:57:52 +01:00
_pause.set_active (false);
RTAManager::instance ()->SignalReady.connect_same_thread (_rta_connections, [this] () { _darea.queue_draw (); });
RTAManager::instance ()->SettingsChanged.connect_same_thread (_rta_connections, [this] () { rta_settings_changed (); });
2025-03-24 20:51:13 +01:00
}
void
RTAWindow::session_going_away ()
{
ENSURE_GUI_THREAD (*this, &RTAWindow::session_going_away);
2025-03-26 23:57:52 +01:00
_rta_connections.drop_connections ();
2025-03-24 20:51:13 +01:00
ArdourWindow::session_going_away ();
_session = 0;
2025-03-26 23:57:52 +01:00
2025-03-24 20:51:13 +01:00
update_title ();
2025-03-26 23:57:52 +01:00
_darea.queue_draw ();
2025-03-24 20:51:13 +01:00
}
void
RTAWindow::update_title ()
{
if (_session) {
std::string n;
if (_session->snap_name () != _session->name ()) {
n = _session->snap_name ();
} else {
n = _session->name ();
}
if (_session->dirty ()) {
n = "*" + n;
}
Gtkmm2ext::WindowTitle title (n);
title += _("Realtime Perceptual Analyzer");
title += Glib::get_application_name ();
set_title (title.get_string ());
} else {
Gtkmm2ext::WindowTitle title (_("Realtime Perceptual Analyzer"));
title += Glib::get_application_name ();
set_title (title.get_string ());
}
}
2025-03-26 23:57:52 +01:00
void
RTAWindow::on_map ()
{
_visible = true;
RTAManager::instance ()->set_active (!_pause.get_active ());
ArdourWindow::on_map ();
}
void
RTAWindow::on_unmap ()
{
_visible = false;
RTAManager::instance ()->set_active (false);
ArdourWindow::on_unmap ();
}
void
RTAWindow::pause_toggled ()
{
RTAManager::instance ()->set_active (_visible && !_pause.get_active ());
}
void
RTAWindow::rta_settings_changed ()
{
_speed_dropdown.set_text (_speed_strings[(int)RTAManager::instance ()->rta_speed ()]);
_warp_dropdown.set_text (_warp_strings[(int)RTAManager::instance ()->rta_warp ()]);
_xpos.clear ();
_darea.queue_draw ();
2025-03-26 23:57:52 +01:00
}
void
RTAWindow::set_rta_speed (DSP::PerceptualAnalyzer::Speed s)
{
RTAManager::instance ()->set_rta_speed (s);
}
void
RTAWindow::set_rta_warp (DSP::PerceptualAnalyzer::Warp w)
{
RTAManager::instance ()->set_rta_warp (w);
}
/* log scale grid 20Hz .. 20kHz */
static float
x_at_freq (const float f, const int width)
{
return width * logf (f / 20.f) / logf (1000.f);
}
static float
freq_at_x (const int x, const int width)
{
return 20.f * powf (1000.f, x / (float)width);
}
bool
RTAWindow::darea_button_press_event (GdkEventButton* ev)
{
2025-04-14 23:45:51 +02:00
if (ev->button != 1 || ev->type != GDK_BUTTON_PRESS) {
2025-03-26 23:57:52 +01:00
return false;
}
2025-04-14 23:45:51 +02:00
if (!_hovering_dB) {
if (!_pause.get_active ()) {
_pause.set_active_state (Gtkmm2ext::ImplicitActive);
pause_toggled ();
}
return true;
}
2025-03-26 23:57:52 +01:00
assert (_dragging_dB == DragNone);
Gtk::Allocation a = _darea.get_allocation ();
float const height = a.get_height ();
const float y0 = _margin;
const float y1 = height - _margin;
const float hh = y1 - y0;
const float dBy0 = (y1 - hh * (_max_dB - _dB_min) / _dB_range);
const float dBy1 = (y1 - hh * (_min_dB - _dB_min) / _dB_range);
const float dByc = (dBy1 + dBy0) / 2;
const float dByr = (dBy1 - dBy0) / 2;
if (ev->y < dByc - dByr * .8) {
_dragging_dB = DragUpper;
_dragstart_dB = _max_dB;
} else if (ev->y > dByc + dByr * .8) {
_dragging_dB = DragLower;
_dragstart_dB = _min_dB;
} else {
_dragging_dB = DragRange;
_dragstart_dB = _min_dB;
}
_dragstart_y = ev->y;
_darea.add_modal_grab ();
_darea.queue_draw ();
2025-03-26 23:57:52 +01:00
return true;
}
bool
RTAWindow::darea_button_release_event (GdkEventButton* ev)
{
if (ev->button != 1) {
return false;
}
2025-04-14 23:45:51 +02:00
if (_pause.active_state () == Gtkmm2ext::ImplicitActive) {
_pause.set_active_state (Gtkmm2ext::Off);
pause_toggled ();
}
bool changed = false;
2025-03-26 23:57:52 +01:00
if (_dragging_dB != DragNone) {
_dragging_dB = DragNone;
changed = true;
_darea.remove_modal_grab ();
2025-03-26 23:57:52 +01:00
}
if (_hovering_dB) {
_hovering_dB = false;
changed = true;
}
if (changed) {
2025-03-26 23:57:52 +01:00
_darea.queue_draw ();
}
return true;
}
bool
RTAWindow::darea_leave_notify_event (GdkEventCrossing*)
{
if (_hovering_dB) {
_hovering_dB = false;
_darea.queue_draw ();
} else if (_cursor_x >= 0 || _cursor_y >= 0) {
_darea.queue_draw ();
}
_pointer_info.set_text ("");
_cursor_x = _cursor_y = -1;
return false;
}
bool
RTAWindow::darea_grab_broken_event (GdkEventGrabBroken*)
{
if (_dragging_dB != DragNone) {
_darea.remove_modal_grab ();
_dragging_dB = DragNone;
_darea.queue_draw ();
return true;
}
return false;
}
void
RTAWindow::darea_grab_notify (bool was_grabbed)
{
if (!was_grabbed) {
_darea.remove_modal_grab ();
_dragging_dB = DragNone;
_darea.queue_draw ();
}
}
2025-03-26 23:57:52 +01:00
bool
RTAWindow::darea_motion_notify_event (GdkEventMotion* ev)
{
Gtk::Allocation a = _darea.get_allocation ();
float const width = a.get_width ();
float const height = a.get_height ();
if (_dragging_dB != DragNone) {
const float hh = height - 2 * _margin;
const float dx = _dragstart_y - ev->y;
const float dBpx = _dB_range / hh;
const float dB = dx * dBpx;
float new_dB = _dragstart_dB + dB;
bool changed = false;
if (_dragging_dB == DragUpper) {
new_dB = rintf (std::min (_dB_min + _dB_range, std::max (new_dB, _min_dB + _dB_span)));
changed = _max_dB != new_dB;
_max_dB = new_dB;
} else if (_dragging_dB == DragLower) {
new_dB = rintf (std::max (_dB_min, std::min (new_dB, _max_dB - _dB_span)));
changed = _min_dB != new_dB;
_min_dB = new_dB;
} else {
float min_dB = rintf (std::max (_dB_min, std::min (new_dB, _max_dB - _dB_span)));
float dbd = (min_dB - _min_dB);
float max_dB = rintf (std::min (_dB_min + _dB_range, std::max (_max_dB + dbd, _min_dB + _dB_span)));
dbd = std::min<float> (dbd, max_dB - _max_dB);
changed = dbd != 0;
_max_dB += dbd;
_min_dB += dbd;
}
if (changed) {
_grid.clear ();
_darea.queue_draw ();
}
return true;
}
bool queue_draw = false;
int twomargin = 2 * _margin;
if (ev->x > _margin && ev->x < width - _margin && ev->y > _margin && ev->y < height - _margin) {
float freq = freq_at_x (ev->x - _margin, width - twomargin);
std::stringstream ss;
ss << std::fixed;
if (freq >= 10000) {
ss << std::setprecision (1) << freq / 1000.0 << "kHz";
} else if (freq >= 1000) {
ss << std::setprecision (2) << freq / 1000.0 << "kHz";
} else {
ss << std::setprecision (0) << freq << "Hz";
}
float dB = _min_dB + (height - _margin - ev->y) * (_max_dB - _min_dB) / (height - twomargin);
ss << " " << std::setw (6) << std::setprecision (1) << std::showpos << dB;
ss << std::setw (0) << "dB";
_pointer_info.set_text (ss.str ());
if (ev->x != _cursor_x || ev->y != _cursor_y) {
queue_draw = true;
}
_cursor_x = ev->x;
_cursor_y = ev->y;
} else {
_pointer_info.set_text ("");
if (_cursor_x >= 0 || _cursor_y >= 0) {
queue_draw = true;
}
_cursor_x = _cursor_y = -1;
}
bool h = ev->x > (width - _margin);
if (h == _hovering_dB && !queue_draw) {
return true;
}
_hovering_dB = h;
_darea.queue_draw ();
return true;
}
bool
RTAWindow::darea_scroll_event (GdkEventScroll* ev)
{
if (_dragging_dB != DragNone || !_hovering_dB) {
return true;
}
float delta = 0;
switch (ev->direction) {
case GDK_SCROLL_UP:
delta = 1;
break;
case GDK_SCROLL_DOWN:
delta = -1;
break;
default:
return true;
}
using Gtkmm2ext::Keyboard;
if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollHorizontalModifier)) {
_min_dB = rintf (std::max (_dB_min, std::min (_min_dB - delta, _max_dB - _dB_span)));
_max_dB = rintf (std::min (_dB_min + _dB_range, std::max (_max_dB + delta, _min_dB + _dB_span)));
} else {
float new_dB = _min_dB + delta;
/* compare to DragRange */
float min_dB = rintf (std::max (_dB_min, std::min (new_dB, _max_dB - _dB_span)));
float dbd = (min_dB - _min_dB);
float max_dB = rintf (std::min (_dB_min + _dB_range, std::max (_max_dB + dbd, _min_dB + _dB_span)));
dbd = std::min<float> (dbd, max_dB - _max_dB);
_max_dB += dbd;
_min_dB += dbd;
}
_grid.clear ();
_darea.queue_draw ();
return true;
}
2025-03-24 20:51:13 +01:00
void
RTAWindow::darea_size_allocate (Gtk::Allocation&)
{
2025-03-26 23:57:52 +01:00
_grid.clear ();
_xpos.clear ();
2025-03-24 20:51:13 +01:00
}
void
RTAWindow::darea_size_request (Gtk::Requisition* req)
{
req->width = 512 *_uiscale + 2 * _margin;
2025-03-26 23:57:52 +01:00
req->height = req->width * 9 / 17;
2025-03-24 20:51:13 +01:00
}
bool
RTAWindow::darea_expose_event (GdkEventExpose* ev)
{
2025-03-26 23:57:52 +01:00
Gtk::Allocation a = _darea.get_allocation ();
float const width = a.get_width ();
float const height = a.get_height ();
const float min_dB = _min_dB;
const float max_dB = _max_dB;
const float x0 = _margin;
const float x1 = width - _margin;
const float ww = x1 - x0;
const float y0 = _margin;
const float y1 = height - _margin;
const float hh = y1 - y0;
if (!_grid) {
_grid = Cairo::ImageSurface::create (Cairo::FORMAT_RGB24, width, height);
Cairo::RefPtr<Cairo::Context> cr = Cairo::Context::create (_grid);
Gtkmm2ext::set_source_rgb_a (cr, _basec, 1);
cr->paint ();
cr->set_line_width (1.0);
std::map<float, std::string> grid;
grid[20] = "20";
grid[25] = "";
grid[31.5] = "";
grid[40] = "40";
grid[50] = "";
grid[63] = "";
grid[80] = "80";
grid[100] = "";
grid[125] = "";
grid[160] = "160";
grid[200] = "";
grid[250] = "";
grid[315] = "315";
grid[400] = "";
grid[500] = "";
grid[630] = "630";
grid[800] = "";
grid[1000] = "";
grid[1250] = "1K25";
grid[1600] = "";
grid[2000] = "";
grid[2500] = "2K5";
grid[3150] = "";
grid[4000] = "";
grid[5000] = "5K";
grid[6300] = "";
grid[8000] = "";
grid[10000] = "10K";
grid[12500] = "";
grid[16000] = "";
grid[20000] = "20K";
Gtkmm2ext::set_source_rgb_a (cr, _gridc, 1);
Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create (cr);
layout->set_font_description (UIConfiguration::instance ().get_SmallMonospaceFont ());
for (const auto& [f, txt] : grid) {
const float xx = rintf (x0 + x_at_freq (f, ww)) + .5f;
if (txt.empty ()) {
cr->move_to (xx, y1);
cr->line_to (xx, y1 + 4);
cr->stroke ();
continue;
}
cr->move_to (xx, y0);
cr->line_to (xx, y1 + 5);
cr->stroke ();
2025-04-05 18:24:23 +02:00
cr->save ();
2025-03-26 23:57:52 +01:00
layout->set_text (txt);
layout->set_alignment (Pango::ALIGN_CENTER);
int tw, th;
layout->get_pixel_size (tw, th);
cr->move_to (xx - tw / 2, y1 + 5);
2025-04-05 18:24:23 +02:00
Gtkmm2ext::set_source_rgb_a (cr, _textc, 0.75);
2025-03-26 23:57:52 +01:00
layout->show_in_cairo_context (cr);
2025-04-05 18:24:23 +02:00
cr->restore ();
2025-03-26 23:57:52 +01:00
}
/* dB grid */
std::vector<double> dashes35;
dashes35.push_back (3.0);
dashes35.push_back (5.0);
std::vector<double> dashes2;
dashes2.push_back (2.0);
for (int dB = min_dB; dB <= max_dB; ++dB) {
2025-04-15 00:00:20 +02:00
bool lbl = 0 == (dB % 12) || dB == max_dB;
2025-03-26 23:57:52 +01:00
if (dB % 6 != 0) {
continue;
}
float y = rintf (y1 - hh * (dB - min_dB) / (max_dB - min_dB)) + .5;
cr->save ();
cr->set_line_cap (Cairo::LINE_CAP_ROUND);
if (!lbl) {
cr->set_dash (dashes35, 0);
} else {
cr->set_dash (dashes2, 0);
}
cr->move_to (x0 - (lbl ? 5 : 0), y);
cr->line_to (x1 + (lbl ? 5 : 0), y);
cr->stroke ();
cr->restore ();
if (!lbl) {
continue;
}
cr->save ();
2025-04-05 18:24:23 +02:00
Gtkmm2ext::set_source_rgb_a (cr, _textc, 0.75);
2025-04-15 00:00:20 +02:00
layout->set_text (string_compose ("%1", abs (dB) >= 10 ? abs(dB) : dB));
layout->set_alignment (Pango::ALIGN_LEFT);
int tw, th;
layout->get_pixel_size (tw, th);
cr->move_to (x1 + 5, y - th / 2);
2025-03-26 23:57:52 +01:00
layout->show_in_cairo_context (cr);
2025-04-15 00:00:20 +02:00
layout->set_alignment (Pango::ALIGN_RIGHT);
cr->move_to (x0 - 5 - tw, y - th / 2);
2025-03-26 23:57:52 +01:00
layout->show_in_cairo_context (cr);
cr->restore ();
}
/* top/bottom border */
cr->move_to (x0, y0 + .5);
cr->line_to (x1, y0 + .5);
cr->stroke ();
cr->move_to (x0, y1 + .5);
cr->line_to (x1, y1 + .5);
cr->stroke ();
}
std::list<RTAManager::RTA> const& rta = RTAManager::instance ()->rta ();
/* cache x-axis deflection */
if (_xpos.empty () && !rta.empty ()) {
auto const& r = rta.front ();
auto const& a = r.analyzers ().front ();
const int n_bins = a->fftlen ();
for (int i = 0; i <= n_bins; ++i) {
float f = a->freq_at_bin (i);
if (f < 15 || f > 22000) {
_xpos[i] = -1;
} else {
_xpos[i] = x0 + x_at_freq (f, ww) + 0.5;
}
}
}
Cairo::RefPtr<Cairo::Context> cr = _darea.get_window ()->create_cairo_context ();
2025-03-24 20:51:13 +01:00
cr->rectangle (ev->area.x, ev->area.y, ev->area.width, ev->area.height);
2025-03-26 23:57:52 +01:00
cr->clip ();
cr->set_source (_grid, 0, 0);
cr->paint ();
cr->save ();
cr->rectangle (x0 + 1, y0, ww - 1, hh);
cr->clip ();
std::vector<std::pair<std::string, color_t>> legend;
cr->set_line_width (1.5);
for (auto const& r : rta) {
const int n_bins = r.analyzers ().front ()->fftlen ();
color_t color;
RouteGroup* group = r.route ()->route_group ();
if (r.route ()->is_singleton ()) {
color = 0xff | _textc;
} else if (group && group->is_color ()) {
color = group->rgba ();
} else {
color = r.route ()->presentation_info ().color ();
}
legend.emplace_back (r.route ()->name (), color);
float red = UINT_RGBA_R_FLT (color);
float grn = UINT_RGBA_G_FLT (color);
float blu = UINT_RGBA_B_FLT (color);
cr->set_source_rgba (red, grn, blu, 1);
float last_x = -1;
for (int i = 0; i <= n_bins; ++i) {
float x = _xpos[i];
if (x < 0) {
continue;
}
auto a = r.analyzers ().begin ();
float dB = (*a)->power_at_bin (i, 1.0, true);
for (++a; a != r.analyzers ().end (); ++a) {
dB = std::max (dB, (*a)->power_at_bin (i, 1.0, true));
}
float y = y1 - hh * (dB - min_dB) / (max_dB - min_dB);
x = std::max (std::min (x, x1), x0);
y = std::max (std::min (y, y1 + 1), y0 - 1);
if (last_x < 0) {
cr->move_to (x, y1 + 1);
}
cr->line_to (x, y);
last_x = x;
}
cr->stroke_preserve ();
assert (last_x > 0);
cr->line_to (ceil (last_x) + 0.5, y1 + 1);
cr->close_path ();
cr->set_source_rgba (red, grn, blu, 0.35);
cr->fill ();
}
cr->restore ();
if (_hovering_dB || _dragging_dB != DragNone) {
2025-03-26 23:57:52 +01:00
float m2 = _margin / 2.0;
float m4 = _margin / 4.0;
float m8 = _margin / 8.0;
cr->rectangle (x1 + m2, 0, m2, height);
Gtkmm2ext::set_source_rgb_a (cr, _textc, 0.3);
cr->fill ();
float dBy0 = rintf (y1 - hh * (max_dB - _dB_min) / _dB_range) + .5;
float dBy1 = rintf (y1 - hh * (min_dB - _dB_min) / _dB_range) + .5;
Gtkmm2ext::rounded_rectangle (cr, x1 + m2 + m8, dBy0, m4, (dBy1 - dBy0), m8);
Gtkmm2ext::set_source_rgb_a (cr, _textc, 0.5);
cr->fill ();
}
if (_cursor_x > 0 && _cursor_y > 0) {
Gtkmm2ext::set_source_rgb_a (cr, _textc, 0.75);
cr->set_line_width (1.0);
cr->move_to (_cursor_x + .5, y0);
cr->line_to (_cursor_x + .5, y1);
cr->stroke ();
cr->move_to (x0, _cursor_y + .5);
cr->line_to (x1, _cursor_y + .5);
cr->stroke ();
}
if (legend.empty ()) {
return true;
}
Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create (cr);
layout->set_font_description (UIConfiguration::instance ().get_SmallFont ());
layout->set_alignment (Pango::ALIGN_LEFT);
layout->set_text ("8|gGTrackorBusName");
int tw, th;
layout->get_pixel_size (tw, th);
layout->set_ellipsize (Pango::ELLIPSIZE_END);
layout->set_width (tw * PANGO_SCALE);
float lw = tw + 10;
float lh = 5 + (th + 5) * legend.size ();
float lx = x1 - _margin / 2 - lw;
float ly = y0 - _margin / 2;
Gtkmm2ext::rounded_rectangle (cr, lx, ly, lw, lh);
cr->set_line_width (1.0);
Gtkmm2ext::set_source_rgb_a (cr, _textc, 0.8);
cr->stroke_preserve ();
cr->set_source_rgba (0, 0, 0, 0.5);
cr->fill_preserve ();
cr->clip ();
ly += 5;
for (const auto& [name, color] : legend) {
Gtkmm2ext::set_source_rgb_a (cr, color, 1);
layout->set_text (name);
cr->move_to (lx + 5, ly);
layout->show_in_cairo_context (cr);
ly += 5 + th;
}
2025-03-24 20:51:13 +01:00
return true;
}