2 Copyright (C) 2011 Paul Davis
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21 #include <cairo/cairo.h>
23 #include "ardour/ardour.h"
24 #include "ardour/audioengine.h"
25 #include "ardour/rc_configuration.h"
26 #include "ardour/session.h"
28 #include "gtkmm2ext/keyboard.h"
29 #include "gtkmm2ext/gui_thread.h"
31 #include "ardour_ui.h"
32 #include "rgb_macros.h"
33 #include "shuttle_control.h"
38 using namespace Gtkmm2ext
;
39 using namespace ARDOUR
;
43 gboolean
qt (gboolean
, gint
, gint
, gboolean
, Gtk::Tooltip
*, gpointer
)
48 ShuttleControl::ShuttleControl ()
49 : _controllable (new ShuttleControllable (*this))
50 , binding_proxy (_controllable
)
52 ARDOUR_UI::instance()->set_tip (*this, _("Shuttle speed control (Context-click for options)"));
55 last_shuttle_request
= 0;
56 last_speed_displayed
= -99999999;
57 shuttle_grabbed
= false;
58 shuttle_speed_on_grab
= 0;
60 shuttle_max_speed
= 8.0f
;
61 shuttle_style_menu
= 0;
62 shuttle_unit_menu
= 0;
63 shuttle_context_menu
= 0;
65 set_flags (CAN_FOCUS
);
66 add_events (Gdk::ENTER_NOTIFY_MASK
|Gdk::LEAVE_NOTIFY_MASK
|Gdk::BUTTON_RELEASE_MASK
|Gdk::BUTTON_PRESS_MASK
|Gdk::POINTER_MOTION_MASK
|Gdk::SCROLL_MASK
);
67 set_size_request (100, 15);
68 set_name (X_("ShuttleControl"));
70 Config
->ParameterChanged
.connect (parameter_connection
, MISSING_INVALIDATOR
, ui_bind (&ShuttleControl::parameter_changed
, this, _1
), gui_context());
72 /* gtkmm 2.4: the C++ wrapper doesn't work */
73 g_signal_connect ((GObject
*) gobj(), "query-tooltip", G_CALLBACK (qt
), NULL
);
74 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
77 ShuttleControl::~ShuttleControl ()
79 cairo_pattern_destroy (pattern
);
83 ShuttleControl::set_session (Session
*s
)
85 SessionHandlePtr::set_session (s
);
89 _session
->add_controllable (_controllable
);
91 set_sensitive (false);
96 ShuttleControl::on_size_allocate (Gtk::Allocation
& alloc
)
99 cairo_pattern_destroy (pattern
);
103 pattern
= cairo_pattern_create_linear (0, 0, alloc
.get_width(), alloc
.get_height());
105 /* add 3 color stops */
107 uint32_t col
= ARDOUR_UI::config()->canvasvar_Shuttle
.get();
110 UINT_TO_RGBA(col
, &r
, &g
, &b
, &a
);
112 cairo_pattern_add_color_stop_rgb (pattern
, 0.0, 0, 0, 0);
113 cairo_pattern_add_color_stop_rgb (pattern
, 0.5, r
/255.0, g
/255.0, b
/255.0);
114 cairo_pattern_add_color_stop_rgb (pattern
, 1.0, 0, 0, 0);
116 DrawingArea::on_size_allocate (alloc
);
120 ShuttleControl::map_transport_state ()
122 float speed
= _session
->transport_speed ();
124 if (fabs(speed
) <= (2*DBL_EPSILON
)) {
127 if (Config
->get_shuttle_units() == Semitones
) {
129 int semi
= speed_as_semitones (speed
, reverse
);
130 shuttle_fract
= semitones_as_fract (semi
, reverse
);
132 shuttle_fract
= speed
/shuttle_max_speed
;
140 ShuttleControl::build_shuttle_context_menu ()
142 using namespace Menu_Helpers
;
144 shuttle_context_menu
= new Menu();
145 MenuList
& items
= shuttle_context_menu
->items();
147 Menu
* speed_menu
= manage (new Menu());
148 MenuList
& speed_items
= speed_menu
->items();
150 Menu
* units_menu
= manage (new Menu
);
151 MenuList
& units_items
= units_menu
->items();
152 RadioMenuItem::Group units_group
;
154 units_items
.push_back (RadioMenuElem (units_group
, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units
), Percentage
)));
155 if (Config
->get_shuttle_units() == Percentage
) {
156 static_cast<RadioMenuItem
*>(&units_items
.back())->set_active();
158 units_items
.push_back (RadioMenuElem (units_group
, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units
), Semitones
)));
159 if (Config
->get_shuttle_units() == Semitones
) {
160 static_cast<RadioMenuItem
*>(&units_items
.back())->set_active();
162 items
.push_back (MenuElem (_("Units"), *units_menu
));
164 Menu
* style_menu
= manage (new Menu
);
165 MenuList
& style_items
= style_menu
->items();
166 RadioMenuItem::Group style_group
;
168 style_items
.push_back (RadioMenuElem (style_group
, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style
), Sprung
)));
169 if (Config
->get_shuttle_behaviour() == Sprung
) {
170 static_cast<RadioMenuItem
*>(&style_items
.back())->set_active();
172 style_items
.push_back (RadioMenuElem (style_group
, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style
), Wheel
)));
173 if (Config
->get_shuttle_behaviour() == Wheel
) {
174 static_cast<RadioMenuItem
*>(&style_items
.back())->set_active();
177 items
.push_back (MenuElem (_("Mode"), *style_menu
));
179 RadioMenuItem::Group speed_group
;
181 speed_items
.push_back (RadioMenuElem (speed_group
, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 8.0f
)));
182 if (shuttle_max_speed
== 8.0) {
183 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
185 speed_items
.push_back (RadioMenuElem (speed_group
, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 6.0f
)));
186 if (shuttle_max_speed
== 6.0) {
187 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
189 speed_items
.push_back (RadioMenuElem (speed_group
, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 4.0f
)));
190 if (shuttle_max_speed
== 4.0) {
191 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
193 speed_items
.push_back (RadioMenuElem (speed_group
, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 3.0f
)));
194 if (shuttle_max_speed
== 3.0) {
195 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
197 speed_items
.push_back (RadioMenuElem (speed_group
, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 2.0f
)));
198 if (shuttle_max_speed
== 2.0) {
199 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
201 speed_items
.push_back (RadioMenuElem (speed_group
, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 1.5f
)));
202 if (shuttle_max_speed
== 1.5) {
203 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
206 items
.push_back (MenuElem (_("Maximum speed"), *speed_menu
));
211 ShuttleControl::show_shuttle_context_menu ()
213 if (shuttle_context_menu
== 0) {
214 build_shuttle_context_menu ();
217 shuttle_context_menu
->popup (1, gtk_get_current_event_time());
221 ShuttleControl::set_shuttle_max_speed (float speed
)
223 shuttle_max_speed
= speed
;
227 ShuttleControl::on_button_press_event (GdkEventButton
* ev
)
233 if (binding_proxy
.button_press_handler (ev
)) {
237 if (Keyboard::is_context_menu_event (ev
)) {
238 show_shuttle_context_menu ();
242 switch (ev
->button
) {
245 shuttle_grabbed
= true;
246 shuttle_speed_on_grab
= _session
->transport_speed ();
247 mouse_shuttle (ev
->x
, true);
260 ShuttleControl::on_button_release_event (GdkEventButton
* ev
)
266 switch (ev
->button
) {
268 shuttle_grabbed
= false;
269 remove_modal_grab ();
271 if (Config
->get_shuttle_behaviour() == Sprung
) {
272 _session
->request_transport_speed (shuttle_speed_on_grab
);
274 mouse_shuttle (ev
->x
, true);
280 if (_session
->transport_rolling()) {
281 _session
->request_transport_speed (1.0);
295 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr
<Gtk::Tooltip
>&)
301 ShuttleControl::on_scroll_event (GdkEventScroll
* ev
)
303 if (!_session
|| Config
->get_shuttle_behaviour() != Wheel
) {
307 bool semis
= (Config
->get_shuttle_units() == Semitones
);
309 switch (ev
->direction
) {
311 case GDK_SCROLL_RIGHT
:
313 if (shuttle_fract
== 0) {
314 shuttle_fract
= semitones_as_fract (1, false);
317 int st
= fract_as_semitones (shuttle_fract
, rev
);
318 shuttle_fract
= semitones_as_fract (st
+ 1, rev
);
321 shuttle_fract
+= 0.00125;
324 case GDK_SCROLL_DOWN
:
325 case GDK_SCROLL_LEFT
:
327 if (shuttle_fract
== 0) {
328 shuttle_fract
= semitones_as_fract (1, true);
331 int st
= fract_as_semitones (shuttle_fract
, rev
);
332 shuttle_fract
= semitones_as_fract (st
- 1, rev
);
335 shuttle_fract
-= 0.00125;
344 float lower_side_of_dead_zone
= semitones_as_fract (-24, true);
345 float upper_side_of_dead_zone
= semitones_as_fract (-24, false);
347 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
348 to the far side of it.
351 if (shuttle_fract
> lower_side_of_dead_zone
&& shuttle_fract
< upper_side_of_dead_zone
) {
352 switch (ev
->direction
) {
354 case GDK_SCROLL_RIGHT
:
355 shuttle_fract
= upper_side_of_dead_zone
;
357 case GDK_SCROLL_DOWN
:
358 case GDK_SCROLL_LEFT
:
359 shuttle_fract
= lower_side_of_dead_zone
;
362 /* impossible, checked above */
368 use_shuttle_fract (true);
374 ShuttleControl::on_motion_notify_event (GdkEventMotion
* ev
)
376 if (!_session
|| !shuttle_grabbed
) {
380 return mouse_shuttle (ev
->x
, false);
384 ShuttleControl::mouse_shuttle (double x
, bool force
)
386 double const center
= get_width() / 2.0;
387 double distance_from_center
= x
- center
;
389 if (distance_from_center
> 0) {
390 distance_from_center
= min (distance_from_center
, center
);
392 distance_from_center
= max (distance_from_center
, -center
);
395 /* compute shuttle fract as expressing how far between the center
396 and the edge we are. positive values indicate we are right of
397 center, negative values indicate left of center
400 shuttle_fract
= distance_from_center
/ center
; // center == half the width
401 use_shuttle_fract (force
);
406 ShuttleControl::set_shuttle_fract (double f
)
409 use_shuttle_fract (false);
413 ShuttleControl::speed_as_semitones (float speed
, bool& reverse
)
415 assert (speed
!= 0.0);
419 return (int) round (12.0 * fast_log2 (-speed
));
422 return (int) round (12.0 * fast_log2 (speed
));
427 ShuttleControl::semitones_as_speed (int semi
, bool reverse
)
430 return -pow (2.0, (semi
/ 12.0));
432 return pow (2.0, (semi
/ 12.0));
437 ShuttleControl::semitones_as_fract (int semi
, bool reverse
)
439 float speed
= semitones_as_speed (semi
, reverse
);
440 return speed
/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
444 ShuttleControl::fract_as_semitones (float fract
, bool& reverse
)
446 assert (fract
!= 0.0);
447 return speed_as_semitones (fract
* 4.0, reverse
);
451 ShuttleControl::use_shuttle_fract (bool force
)
453 microseconds_t now
= get_microseconds();
455 shuttle_fract
= max (-1.0f
, shuttle_fract
);
456 shuttle_fract
= min (1.0f
, shuttle_fract
);
458 /* do not attempt to submit a motion-driven transport speed request
459 more than once per process cycle.
462 if (!force
&& (last_shuttle_request
- now
) < (microseconds_t
) AudioEngine::instance()->usecs_per_cycle()) {
466 last_shuttle_request
= now
;
470 if (Config
->get_shuttle_units() == Semitones
) {
471 if (shuttle_fract
!= 0.0) {
473 int semi
= fract_as_semitones (shuttle_fract
, reverse
);
474 speed
= semitones_as_speed (semi
, reverse
);
479 speed
= shuttle_max_speed
* shuttle_fract
;
482 _session
->request_transport_speed_nonzero (speed
);
486 ShuttleControl::on_expose_event (GdkEventExpose
* event
)
488 cairo_text_extents_t extents
;
489 Glib::RefPtr
<Gdk::Window
> win (get_window());
490 Glib::RefPtr
<Gtk::Style
> style (get_style());
492 cairo_t
* cr
= gdk_cairo_create (win
->gobj());
494 cairo_set_source (cr
, pattern
);
495 cairo_rectangle (cr
, 0.0, 0.0, get_width(), get_height());
496 cairo_fill_preserve (cr
);
498 cairo_set_source_rgb (cr
, 0, 0, 0.0);
504 speed
= _session
->transport_speed ();
509 double visual_fraction
= std::min (1.0f
, speed
/shuttle_max_speed
);
510 double x
= (get_width() / 2.0) + (0.5 * (get_width() * visual_fraction
));
511 cairo_move_to (cr
, x
, 1);
512 cairo_set_source_rgb (cr
, 1.0, 1.0, 1.0);
513 cairo_line_to (cr
, x
, get_height()-1);
522 if (Config
->get_shuttle_units() == Percentage
) {
525 snprintf (buf
, sizeof (buf
), _("Playing"));
528 snprintf (buf
, sizeof (buf
), "<<< %d%%", (int) round (-speed
* 100));
530 snprintf (buf
, sizeof (buf
), ">>> %d%%", (int) round (speed
* 100));
537 int semi
= speed_as_semitones (speed
, reversed
);
540 snprintf (buf
, sizeof (buf
), _("<<< %+d semitones"), semi
);
542 snprintf (buf
, sizeof (buf
), _(">>> %+d semitones"), semi
);
547 snprintf (buf
, sizeof (buf
), _("Stopped"));
550 last_speed_displayed
= speed
;
552 cairo_set_source_rgb (cr
, 1.0, 1.0, 1.0);
553 cairo_text_extents (cr
, buf
, &extents
);
554 cairo_move_to (cr
, 10, extents
.height
+ 2);
555 cairo_show_text (cr
, buf
);
560 switch (Config
->get_shuttle_behaviour()) {
562 snprintf (buf
, sizeof (buf
), _("Sprung"));
565 snprintf (buf
, sizeof (buf
), _("Wheel"));
569 cairo_text_extents (cr
, buf
, &extents
);
571 cairo_move_to (cr
, get_width() - (fabs(extents
.x_advance
) + 5), extents
.height
+ 2);
572 cairo_show_text (cr
, buf
);
580 ShuttleControl::shuttle_unit_clicked ()
582 if (shuttle_unit_menu
== 0) {
583 shuttle_unit_menu
= dynamic_cast<Menu
*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
585 shuttle_unit_menu
->popup (1, gtk_get_current_event_time());
589 ShuttleControl::set_shuttle_style (ShuttleBehaviour s
)
591 Config
->set_shuttle_behaviour (s
);
595 ShuttleControl::set_shuttle_units (ShuttleUnits s
)
597 Config
->set_shuttle_units (s
);
601 ShuttleControl::update_speed_display ()
603 if (_session
->transport_speed() != last_speed_displayed
) {
608 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl
& s
)
609 : PBD::Controllable (X_("Shuttle"))
615 ShuttleControl::ShuttleControllable::set_id (const std::string
& str
)
621 ShuttleControl::ShuttleControllable::set_value (double val
)
629 fract
= -((0.5 - val
)/0.5);
631 fract
= ((val
- 0.5)/0.5);
635 sc
.set_shuttle_fract (fract
);
639 ShuttleControl::ShuttleControllable::get_value () const
641 return sc
.get_shuttle_fract ();
645 ShuttleControl::parameter_changed (std::string p
)
647 if (p
== "shuttle-behaviour") {
648 switch (Config
->get_shuttle_behaviour ()) {
650 /* back to Sprung - reset to speed = 1.0 if playing
653 if (_session
->transport_rolling()) {
654 if (_session
->transport_speed() == 1.0) {
657 _session
->request_transport_speed (1.0);
658 /* redraw when speed changes */
671 } else if (p
== "shuttle-units") {