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-09-02 20:43:13 +02:00
# include "widgets/tooltips.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"
2025-04-15 00:25:28 +02:00
# 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-08-20 12:50:16 +02:00
, _clear ( _ ( " Clear " ) , ArdourWidgets : : ArdourButton : : default_elements )
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-09-02 20:43:13 +02:00
_clear . signal_clicked . connect ( mem_fun ( * this , & RTAWindow : : clear_clicked ) ) ;
2025-08-20 12:50:16 +02:00
_clear . signal_clicked . connect ( mem_fun ( * this , & RTAWindow : : clear_clicked ) ) ;
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 ) ) ;
2025-04-10 15:08:25 +02:00
_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-08-20 12:50:16 +02:00
_ctrlbox . pack_end ( _clear , 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 ) ) ;
2025-09-02 20:43:13 +02:00
ArdourWidgets : : set_tooltip ( _clear , _ ( " Clear current set of analyzed signals (except for the master bus). " ) ) ;
2025-09-11 09:28:24 -05:00
ArdourWidgets : : set_tooltip ( _pause , _ ( " Toggle analysis enable, pause the visual update. Left mouse press on the analysis area can temporarily freeze the display " ) ) ;
2025-09-22 19:49:09 -04:00
ArdourWidgets : : set_tooltip ( _speed_dropdown , _ ( " Set analysis return time. Noise Measurement has a fallback time of 1dB in 2 seconds. Slow mode falls 5dB/s, Moderate 48dB/sec. Fast and Rapid mode are fast enough to be frequency dependent with Fast mode falling 96dB/s. " ) ) ;
2025-11-15 13:35:24 +01:00
ArdourWidgets : : set_tooltip ( _warp_dropdown , _ ( " Frequency warp the spectrum to focus on given range. A high warp factor increases resolution in the low frequency range, while the Bark scale is a frequency scale on which equal distances correspond with perceptually equal distances. " ) ) ;
2025-09-02 20:43:13 +02:00
2025-03-26 23:57:52 +01:00
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 ( ) ) ;
2025-04-06 01:16:49 +02:00
_uiscale = std : : max < float > ( 1.f , sqrtf ( UIConfiguration : : instance ( ) . get_ui_scale ( ) ) ) ;
2025-03-26 23:57:52 +01:00
_grid . clear ( ) ;
_xpos . clear ( ) ;
2025-04-06 01:16:49 +02:00
_darea . queue_resize ( ) ;
2025-03-26 23:57:52 +01:00
_darea . queue_draw ( ) ;
2025-03-24 20:51:13 +01:00
}
2025-08-24 06:51:44 +02:00
bool
RTAWindow : : on_key_press_event ( GdkEventKey * ev )
{
if ( gtk_window_propagate_key_event ( GTK_WINDOW ( gobj ( ) ) , ev ) ) {
return true ;
}
return ARDOUR_UI_UTILS : : relay_key_press ( ev , this ) ;
}
bool
RTAWindow : : on_key_release_event ( GdkEventKey * ev )
{
if ( gtk_window_propagate_key_event ( GTK_WINDOW ( gobj ( ) ) , ev ) ) {
return true ;
}
return ARDOUR_UI_UTILS : : relay_key_press ( ev , this ) ;
}
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 ( ) ) ;
}
2025-08-20 12:50:16 +02:00
void
RTAWindow : : clear_clicked ( )
{
RTAManager : : instance ( ) - > clear ( ) ;
}
2025-03-26 23:57:52 +01:00
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 ( ) ;
2025-04-09 18:41:45 +02:00
_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 ;
2025-04-10 15:08:25 +02:00
_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 ( ) ;
}
2025-04-10 15:08:25 +02:00
bool changed = false ;
2025-03-26 23:57:52 +01:00
if ( _dragging_dB ! = DragNone ) {
_dragging_dB = DragNone ;
2025-04-10 15:08:25 +02:00
changed = true ;
_darea . remove_modal_grab ( ) ;
2025-03-26 23:57:52 +01:00
}
if ( _hovering_dB ) {
_hovering_dB = false ;
2025-04-10 15:08:25 +02:00
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 ;
}
2025-04-10 15:08:25 +02:00
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 ;
}
2025-04-05 20:53:15 +02:00
bool
RTAWindow : : darea_scroll_event ( GdkEventScroll * ev )
{
if ( _dragging_dB ! = DragNone | | ! _hovering_dB ) {
return true ;
}
2025-04-15 00:25:28 +02:00
float delta = 0 ;
2025-04-05 20:53:15 +02:00
switch ( ev - > direction ) {
case GDK_SCROLL_UP :
2025-04-15 00:25:28 +02:00
delta = 1 ;
2025-04-05 20:53:15 +02:00
break ;
case GDK_SCROLL_DOWN :
2025-04-15 00:25:28 +02:00
delta = - 1 ;
2025-04-05 20:53:15 +02:00
break ;
default :
return true ;
}
2025-04-15 00:25:28 +02:00
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 ;
}
2025-04-05 20:53:15 +02:00
_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 )
{
2025-04-06 01:16:49 +02:00
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 ( ) ;
2025-04-10 15:08:25 +02:00
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 ;
}