diff --git a/gtk2_ardour/rta_manager.cc b/gtk2_ardour/rta_manager.cc index 51aab16fa9..aebc8d6821 100644 --- a/gtk2_ardour/rta_manager.cc +++ b/gtk2_ardour/rta_manager.cc @@ -20,6 +20,8 @@ #include "gtk2ardour-config.h" #endif +#include "pbd/types_convert.h" + #include "ardour/rt_safe_delete.h" #include "ardour/session.h" @@ -28,6 +30,8 @@ #include "timers.h" #include "ui_config.h" +#include "pbd/i18n.h" + using namespace ARDOUR; RTAManager* RTAManager::_instance = 0; @@ -42,6 +46,9 @@ RTAManager::instance () } RTAManager::RTAManager () + : _active (false) + , _speed (ARDOUR::DSP::PerceptualAnalyzer::Moderate) + , _warp (ARDOUR::DSP::PerceptualAnalyzer::Medium) { } @@ -53,6 +60,11 @@ XMLNode& RTAManager::get_state () const { XMLNode* node = new XMLNode ("RTAManager"); + node->set_property (X_("rta-warp"), _warp); + node->set_property (X_("rta-speed"), _speed); + for (auto& r : _rta) { + node->add_child (X_("RouteID"))->set_property (X_("id"), r.route ()->id ()); + } return *node; } @@ -64,9 +76,33 @@ RTAManager::set_session (ARDOUR::Session* s) } SessionHandlePtr::set_session (s); + XMLNode* node = _session->instant_xml (X_("RTAManager")); + if (node) { + node->get_property ("rta-warp", _warp); + node->get_property ("rta-speed", _speed); + SettingsChanged (); /* EMIT SIGNAL */ + } + if (_session->master_out ()) { attach (_session->master_out ()); } + + if (node) { + for (auto const& n : node->children ()) { + if (n->name () != X_("RouteID")) { + continue; + } + PBD::ID id; + if (!n->get_property (X_("id"), id)) { + continue; + } + std::shared_ptr route = _session->route_by_id (id); + if (route) { + attach (route); + } + } + } + _update_connection = Timers::super_rapid_connect (sigc::mem_fun (*this, &RTAManager::run_rta)); } @@ -75,6 +111,7 @@ RTAManager::session_going_away () { ENSURE_GUI_THREAD (*this, &RTAManager::session_going_away); _update_connection.disconnect (); + _rta.clear (); SessionHandlePtr::session_going_away (); _session = 0; @@ -83,25 +120,243 @@ RTAManager::session_going_away () void RTAManager::set_active (bool en) { + if (_active == en) { + return; + } + _active = en; + if (_active) { + for (auto& r : _rta) { + r.reset (); + r.delivery ()->set_analysis_active (true); + } + } else { + for (auto const& r : _rta) { + r.delivery ()->set_analysis_active (false); + } + } +} + +void +RTAManager::set_rta_speed (DSP::PerceptualAnalyzer::Speed s) +{ + if (_speed == s) { + return; + } + _speed = s; + for (auto& r : _rta) { + r.set_rta_speed (s); + } + SettingsChanged (); /* EMIT SIGNAL */ +} + +void +RTAManager::set_rta_warp (DSP::PerceptualAnalyzer::Warp w) +{ + if (_warp == w) { + return; + } + _warp = w; + for (auto& r : _rta) { + r.set_rta_warp (w); + } + SettingsChanged (); /* EMIT SIGNAL */ } void RTAManager::attach (std::shared_ptr route) { + for (auto const& r : _rta) { + if (r.route () == route) { + return; + } + } + try { + _rta.emplace_back (route); + } catch (...) { + return; + } + + _rta.back ().set_rta_speed (_speed); + _rta.back ().set_rta_warp (_warp); + _rta.back ().delivery ()->set_analysis_active (_active); + + route->DropReferences.connect (*this, invalidator (*this), std::bind (&RTAManager::route_removed, this, std::weak_ptr (route)), gui_context ()); } void RTAManager::remove (std::shared_ptr route) { + _rta.remove_if ([route] (RTAManager::RTA const& r) { return r.route () == route; }); } bool RTAManager::attached (std::shared_ptr route) const { - return false; + return std::find_if (_rta.begin (), _rta.end (), [route] (RTAManager::RTA const& r) { return r.route () == route; }) != _rta.end (); +} + +void +RTAManager::route_removed (std::weak_ptr wr) +{ + if (!_session || _session->inital_connect_or_deletion_in_progress ()) { + return; + } + std::shared_ptr route (wr.lock ()); + if (route) { + remove (route); + } } void RTAManager::run_rta () { + bool have_new_data = false; + + for (auto& r : _rta) { + have_new_data |= r.run (); + } + + if (have_new_data) { + SignalReady (); /* EMIT SIGNAL */ + } +} + +/* ****************************************************************************/ + +RTAManager::RTA::RTA (std::shared_ptr r) + : _route (r) + , _rate (r->session ().nominal_sample_rate ()) + , _blocksize (_rate > 64000 ? 16384 : 8192) + , _stepsize (_blocksize / 4) // must be >= PerceptualAnalyzer::fftlen (512) and <= blocksize and a power of two + , _offset (0) + , _speed (ARDOUR::DSP::PerceptualAnalyzer::Moderate) + , _warp (ARDOUR::DSP::PerceptualAnalyzer::Medium) +{ + if (!init ()) { + throw failed_constructor (); + } + _route->io_changed.connect (_route_connections, invalidator (*this), std::bind (&RTAManager::RTA::route_io_changed, this), gui_context ()); + _route->processors_changed.connect (_route_connections, invalidator (*this), std::bind (&RTAManager::RTA::route_io_changed, this), gui_context ()); +} + +RTAManager::RTA::~RTA () +{ + _route_connections.drop_connections (); + delivery ()->set_analysis_active (false); + RTABufferListPtr unset; + delivery ()->set_analysis_buffers (unset); +} + +std::shared_ptr +RTAManager::RTA::route () const +{ + return _route; +} + +std::shared_ptr +RTAManager::RTA::delivery () const +{ + return _route->main_outs (); +} + +std::vector const& +RTAManager::RTA::analyzers () const +{ + return _analyzers; +} + +bool +RTAManager::RTA::init () +{ + for (auto a : _analyzers) { + delete a; + } + _analyzers.clear (); + _ringbuffers.reset (); + + uint32_t n_audio = delivery ()->input_streams ().n_audio (); + bool was_active = delivery ()->analysis_active (); + + delivery ()->set_analysis_active (false); + + if (n_audio == 0) { + return false; + } + + _ringbuffers = RTABufferListPtr (new RTABufferList (), std::bind (&rt_safe_delete, &_route->session (), _1)); + for (uint32_t n = 0; n < n_audio; ++n) { + _ringbuffers->emplace_back (new RTARingBuffer (_rate), std::bind (&rt_safe_delete, &_route->session (), _1)); + _analyzers.emplace_back (new PerceptualAnalyzer (_rate, _blocksize)); + _analyzers.back ()->set_speed (_speed); + _analyzers.back ()->set_speed (_warp); + } + + assert (_analyzers.size () == _ringbuffers->size ()); + + delivery ()->set_analysis_buffers (_ringbuffers); + delivery ()->set_analysis_active (was_active); + return true; +} + +void +RTAManager::RTA::route_io_changed () +{ + if (_analyzers.size () != delivery ()->input_streams ().n_audio ()) { + init (); + } +} + +void +RTAManager::RTA::reset () +{ + for (auto const& a : _analyzers) { + a->reset (); + memset (a->ipdata (), 0, _blocksize * sizeof (float)); + } + for (auto& r : *_ringbuffers) { + r->increment_read_idx (r->read_space ()); + } +} + +void +RTAManager::RTA::set_rta_speed (ARDOUR::DSP::PerceptualAnalyzer::Speed s) +{ + _speed = s; + for (auto& a : _analyzers) { + a->set_speed (s); + } +} + +void +RTAManager::RTA::set_rta_warp (ARDOUR::DSP::PerceptualAnalyzer::Warp w) +{ + _warp = w; + for (auto& a : _analyzers) { + a->set_wfact (w); + } +} + +bool +RTAManager::RTA::run () +{ + bool have_new_data = false; + + while (true) { + for (auto& rb : *_ringbuffers) { + if (rb->read_space () < _stepsize) { + goto out; + } + } + /* we can process all channels */ + auto rtaiter = _analyzers.begin (); + for (auto& rb : *_ringbuffers) { + rb->read ((*rtaiter)->ipdata () + _offset, _stepsize); + (*rtaiter)->process (_stepsize); + have_new_data |= (*rtaiter)->power ()->_valid; + ++rtaiter; + } + _offset = (_offset + _stepsize) % _blocksize; + } +out: + return have_new_data; } diff --git a/gtk2_ardour/rta_manager.h b/gtk2_ardour/rta_manager.h index fe9950ccaf..8ba8abc75d 100644 --- a/gtk2_ardour/rta_manager.h +++ b/gtk2_ardour/rta_manager.h @@ -17,34 +17,108 @@ */ #pragma once +#include "pbd/ringbuffer.h" + #include "ardour/ardour.h" #include "ardour/dsp_filter.h" #include "ardour/session_handle.h" #include "ardour/types.h" +namespace ARDOUR { + class Delivery; +} + class RTAManager : public ARDOUR::SessionHandlePtr , public PBD::ScopedConnectionList + , public sigc::trackable { public: static RTAManager* instance (); ~RTAManager (); + class RTA : public sigc::trackable + { + public: + RTA (std::shared_ptr); + ~RTA (); + + RTA (RTA const&) = delete; + + bool init (); + void reset (); + bool run (); + + void set_rta_speed (ARDOUR::DSP::PerceptualAnalyzer::Speed); + void set_rta_warp (ARDOUR::DSP::PerceptualAnalyzer::Warp); + + using PerceptualAnalyzer = ARDOUR::DSP::PerceptualAnalyzer; + + std::shared_ptr route () const; + std::shared_ptr delivery () const; + std::vector const& analyzers () const; + + private: + using RTARingBuffer = PBD::RingBuffer; + using RTARingBufferPtr = std::shared_ptr; + using RTABufferList = std::vector; + using RTABufferListPtr = std::shared_ptr; + + void route_io_changed (); + + std::shared_ptr _route; + std::vector _analyzers; + ARDOUR::samplecnt_t _rate; + size_t _blocksize; + size_t _stepsize; + size_t _offset; + RTABufferListPtr _ringbuffers; + PerceptualAnalyzer::Speed _speed; + PerceptualAnalyzer::Warp _warp; + PBD::ScopedConnectionList _route_connections; + }; + void set_session (ARDOUR::Session*); + XMLNode& get_state () const; void attach (std::shared_ptr); void remove (std::shared_ptr); bool attached (std::shared_ptr) const; - void run_rta (); + std::list const& rta () const + { + return _rta; + } + void set_active (bool); + void set_rta_speed (ARDOUR::DSP::PerceptualAnalyzer::Speed); + void set_rta_warp (ARDOUR::DSP::PerceptualAnalyzer::Warp); + + ARDOUR::DSP::PerceptualAnalyzer::Speed rta_speed () const + { + return _speed; + } + ARDOUR::DSP::PerceptualAnalyzer::Warp rta_warp () const + { + return _warp; + } + + PBD::Signal SignalReady; + PBD::Signal SettingsChanged; private: RTAManager (); static RTAManager* _instance; + void run_rta (); void session_going_away (); + void route_removed (std::weak_ptr); + + std::list _rta; + bool _active; + ARDOUR::DSP::PerceptualAnalyzer::Speed _speed; + ARDOUR::DSP::PerceptualAnalyzer::Warp _warp; sigc::connection _update_connection; }; diff --git a/gtk2_ardour/rta_window.cc b/gtk2_ardour/rta_window.cc index c87e8ad94f..d12d4a5c65 100644 --- a/gtk2_ardour/rta_window.cc +++ b/gtk2_ardour/rta_window.cc @@ -20,14 +20,21 @@ #include "gtk2ardour-config.h" #endif +#include "ardour/route_group.h" #include "ardour/session.h" +#include "gtkmm2ext/colors.h" +#include "gtkmm2ext/menu_elems.h" +#include "gtkmm2ext/rgb_macros.h" +#include "gtkmm2ext/utils.h" #include "gtkmm2ext/window_title.h" +#include "ardour_ui.h" #include "gui_thread.h" +#include "rta_manager.h" #include "rta_window.h" #include "timers.h" -#include "utils.h" +#include "ui_config.h" #include "pbd/i18n.h" @@ -35,29 +42,95 @@ using namespace ARDOUR; RTAWindow::RTAWindow () : ArdourWindow (_("Realtime Perceptual Analyzer")) + , _pause (_("Freeze"), ArdourWidgets::ArdourButton::led_default_elements, true) + , _visible (false) + , _margin (20) + , _min_dB (-60) + , _max_dB (0) + , _hovering_dB (false) + , _dragging_dB (DragNone) + , _cursor_x (-1) + , _cursor_y (-1) { + _pause.signal_clicked.connect (mem_fun (*this, &RTAWindow::pause_toggled)); - _darea.add_events (Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::POINTER_MOTION_MASK); + _darea.add_events (Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::POINTER_MOTION_MASK | Gdk::LEAVE_NOTIFY_MASK); _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)); -#if 0 _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)); -#endif + _darea.signal_leave_notify_event ().connect (sigc::mem_fun (*this, &RTAWindow::darea_leave_notify_event), false); + + _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; + _speed_dropdown.AddMenuElem (MenuElemNoMnemonic (_speed_strings[(int)PA::Rapid], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_speed), PA::Rapid))); + _speed_dropdown.AddMenuElem (MenuElemNoMnemonic (_speed_strings[(int)PA::Fast], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_speed), PA::Fast))); + _speed_dropdown.AddMenuElem (MenuElemNoMnemonic (_speed_strings[(int)PA::Moderate], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_speed), PA::Moderate))); + _speed_dropdown.AddMenuElem (MenuElemNoMnemonic (_speed_strings[(int)PA::Slow], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_speed), PA::Slow))); + _speed_dropdown.AddMenuElem (MenuElemNoMnemonic (_speed_strings[(int)PA::Noise], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_speed), PA::Noise))); + _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")); + + _warp_dropdown.AddMenuElem (MenuElemNoMnemonic (_warp_strings[(int)PA::Bark], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_warp), PA::Bark))); + _warp_dropdown.AddMenuElem (MenuElemNoMnemonic (_warp_strings[(int)PA::Medium], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_warp), PA::Medium))); + _warp_dropdown.AddMenuElem (MenuElemNoMnemonic (_warp_strings[(int)PA::High], sigc::bind (sigc::mem_fun (*this, &RTAWindow::set_rta_warp), PA::High))); + _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); _vpacker.pack_start (_darea, true, true); + _vpacker.pack_start (_ctrlbox, false, false, 5); add (_vpacker); set_border_width (4); _vpacker.show_all (); + + 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")); + + _margin = 2 * ceilf (10.f * UIConfiguration::instance ().get_ui_scale ()); + + _grid.clear (); + _xpos.clear (); + _darea.queue_draw (); } XMLNode& RTAWindow::get_state () const { XMLNode* node = new XMLNode ("RTAWindow"); + node->set_property (X_("min-dB"), _min_dB); + node->set_property (X_("max-dB"), _max_dB); return *node; } @@ -72,8 +145,30 @@ RTAWindow::set_session (ARDOUR::Session* s) */ ArdourWindow::set_session (s); + 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; + } + } + update_title (); _session->DirtyChanged.connect (_session_connections, invalidator (*this), std::bind (&RTAWindow::update_title, this), gui_context ()); + + _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 (); }); } void @@ -81,9 +176,13 @@ RTAWindow::session_going_away () { ENSURE_GUI_THREAD (*this, &RTAWindow::session_going_away); + _rta_connections.drop_connections (); + ArdourWindow::session_going_away (); _session = 0; + update_title (); + _darea.queue_draw (); } void @@ -114,28 +213,532 @@ RTAWindow::update_title () } } +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 (); +} + +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) +{ + if (ev->button != 1 || !_hovering_dB || ev->type != GDK_BUTTON_PRESS) { + return false; + } + + 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; + + gdk_pointer_grab (ev->window, false, GdkEventMask (Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK), NULL, NULL, ev->time); + + return true; +} + +bool +RTAWindow::darea_button_release_event (GdkEventButton* ev) +{ + if (ev->button != 1) { + return false; + } + + if (_dragging_dB != DragNone) { + _dragging_dB = DragNone; + gdk_pointer_ungrab (ev->time); + } + + if (_hovering_dB) { + _hovering_dB = false; + _darea.queue_draw (); + } + + return true; +} + +bool +RTAWindow::darea_leave_notify_event (GdkEventCrossing*) +{ + if (_hovering_dB) { + _dragging_dB = DragNone; + _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_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 (dbd, max_dB - _max_dB); + + changed = dbd != 0; + _max_dB += dbd; + _min_dB += dbd; + } + + if (changed) { + _grid.clear (); + _xpos.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; +} + void RTAWindow::darea_size_allocate (Gtk::Allocation&) { + _grid.clear (); + _xpos.clear (); } void RTAWindow::darea_size_request (Gtk::Requisition* req) { - req->width = 300; - req->height = 300; + req->width = 512 + 2 * _margin; + req->height = req->width * 9 / 17; } bool RTAWindow::darea_expose_event (GdkEventExpose* ev) { - Gtk::Allocation a = _darea.get_allocation (); - double const width = a.get_width (); - double const height = a.get_height (); - Cairo::RefPtr cr = _darea.get_window()->create_cairo_context (); + 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 cr = Cairo::Context::create (_grid); + Gtkmm2ext::set_source_rgb_a (cr, _basec, 1); + + cr->paint (); + cr->set_line_width (1.0); + + std::map 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 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 (); + + 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); + layout->show_in_cairo_context (cr); + } + + /* dB grid */ + + std::vector dashes35; + dashes35.push_back (3.0); + dashes35.push_back (5.0); + + std::vector dashes2; + dashes2.push_back (2.0); + + for (int dB = min_dB; dB <= max_dB; ++dB) { + bool lbl = 0 == (dB % 18); + 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; + } + + layout->set_text (string_compose ("%1", dB)); + layout->set_alignment (Pango::ALIGN_CENTER); + int tw, th; + layout->get_pixel_size (tw, th); + cr->save (); + cr->move_to (x1 + 5, y + tw / 2); + cr->rotate (1.5 * M_PI); + layout->show_in_cairo_context (cr); + cr->restore (); + cr->save (); + cr->move_to (x0 - 5, y - tw / 2); + cr->rotate (0.5 * M_PI); + 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 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 cr = _darea.get_window ()->create_cairo_context (); cr->rectangle (ev->area.x, ev->area.y, ev->area.width, ev->area.height); - cr->clip (); + cr->clip (); + + cr->set_source (_grid, 0, 0); + cr->paint (); + + cr->save (); + cr->rectangle (x0 + 1, y0, ww - 1, hh); + cr->clip (); + + std::vector> 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) { + 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 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; + } return true; } diff --git a/gtk2_ardour/rta_window.h b/gtk2_ardour/rta_window.h index f272651106..3698e55922 100644 --- a/gtk2_ardour/rta_window.h +++ b/gtk2_ardour/rta_window.h @@ -23,29 +23,81 @@ #include #include "ardour/ardour.h" +#include "ardour/dsp_filter.h" #include "ardour/session_handle.h" #include "ardour/types.h" +#include "widgets/ardour_button.h" +#include "widgets/ardour_dropdown.h" + #include "ardour_window.h" -class RTAWindow - : public ArdourWindow +class RTAWindow : public ArdourWindow { public: RTAWindow (); - void set_session (ARDOUR::Session*); + void set_session (ARDOUR::Session*); XMLNode& get_state () const; private: + void on_map (); + void on_unmap (); + void session_going_away (); void update_title (); void on_theme_changed (); + void rta_settings_changed (); void darea_size_request (Gtk::Requisition*); void darea_size_allocate (Gtk::Allocation&); bool darea_expose_event (GdkEventExpose*); - Gtk::VBox _vpacker; - Gtk::DrawingArea _darea; + bool darea_button_press_event (GdkEventButton*); + bool darea_button_release_event (GdkEventButton*); + bool darea_motion_notify_event (GdkEventMotion*); + bool darea_leave_notify_event (GdkEventCrossing*); + + void set_rta_speed (ARDOUR::DSP::PerceptualAnalyzer::Speed); + void set_rta_warp (ARDOUR::DSP::PerceptualAnalyzer::Warp); + + void pause_toggled (); + + enum DragStatus { + DragNone, + DragUpper, + DragLower, + DragRange + }; + + const float _dB_range = 86; // +6 .. -80 dB + const float _dB_span = 24; + const float _dB_min = -80; + + Gtk::VBox _vpacker; + Gtk::HBox _ctrlbox; + Gtk::DrawingArea _darea; + Gtk::Label _pointer_info; + ArdourWidgets::ArdourButton _pause; + ArdourWidgets::ArdourDropdown _speed_dropdown; + ArdourWidgets::ArdourDropdown _warp_dropdown; + Cairo::RefPtr _grid; + bool _visible; + std::vector _speed_strings; + std::vector _warp_strings; + std::map _xpos; + Gtkmm2ext::Color _basec; + Gtkmm2ext::Color _gridc; + Gtkmm2ext::Color _textc; + int _margin; + int _min_dB; + int _max_dB; + bool _hovering_dB; + DragStatus _dragging_dB; + float _dragstart_y; + float _dragstart_dB; + int _cursor_x; + int _cursor_y; + + PBD::ScopedConnectionList _rta_connections; };