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 "shuttle_control.h"
37 using namespace Gtkmm2ext
;
38 using namespace ARDOUR
;
42 gboolean
qt (gboolean
, gint
, gint
, gboolean
, Gtk::Tooltip
*, gpointer
)
47 ShuttleControl::ShuttleControl ()
48 : _controllable (new ShuttleControllable (*this))
49 , binding_proxy (_controllable
)
51 ARDOUR_UI::instance()->set_tip (*this, _("Shuttle speed control (Context-click for options)"));
54 last_shuttle_request
= 0;
55 last_speed_displayed
= -99999999;
56 shuttle_grabbed
= false;
57 shuttle_speed_on_grab
= 0;
59 shuttle_max_speed
= 8.0f
;
60 shuttle_style_menu
= 0;
61 shuttle_unit_menu
= 0;
62 shuttle_context_menu
= 0;
64 set_flags (CAN_FOCUS
);
65 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
);
66 set_size_request (100, 15);
67 set_name (X_("ShuttleControl"));
69 Config
->ParameterChanged
.connect (parameter_connection
, MISSING_INVALIDATOR
, ui_bind (&ShuttleControl::parameter_changed
, this, _1
), gui_context());
71 /* gtkmm 2.4: the C++ wrapper doesn't work */
72 g_signal_connect ((GObject
*) gobj(), "query-tooltip", G_CALLBACK (qt
), NULL
);
73 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
76 ShuttleControl::~ShuttleControl ()
78 cairo_pattern_destroy (pattern
);
82 ShuttleControl::set_session (Session
*s
)
84 SessionHandlePtr::set_session (s
);
88 _session
->add_controllable (_controllable
);
90 set_sensitive (false);
95 ShuttleControl::on_size_allocate (Gtk::Allocation
& alloc
)
98 cairo_pattern_destroy (pattern
);
102 pattern
= cairo_pattern_create_linear (0, 0, alloc
.get_width(), alloc
.get_height());
104 /* add 3 color stops */
106 cairo_pattern_add_color_stop_rgb (pattern
, 0.0, 0, 0, 0);
107 cairo_pattern_add_color_stop_rgb (pattern
, 0.5, 0.0, 0.0, 1.0);
108 cairo_pattern_add_color_stop_rgb (pattern
, 1.0, 0, 0, 0);
110 DrawingArea::on_size_allocate (alloc
);
114 ShuttleControl::map_transport_state ()
116 float speed
= _session
->transport_speed ();
118 if (fabs(speed
) <= (2*DBL_EPSILON
)) {
121 if (Config
->get_shuttle_units() == Semitones
) {
123 int semi
= speed_as_semitones (speed
, reverse
);
124 shuttle_fract
= semitones_as_fract (semi
, reverse
);
126 shuttle_fract
= speed
/shuttle_max_speed
;
134 ShuttleControl::build_shuttle_context_menu ()
136 using namespace Menu_Helpers
;
138 shuttle_context_menu
= new Menu();
139 MenuList
& items
= shuttle_context_menu
->items();
141 Menu
* speed_menu
= manage (new Menu());
142 MenuList
& speed_items
= speed_menu
->items();
144 Menu
* units_menu
= manage (new Menu
);
145 MenuList
& units_items
= units_menu
->items();
146 RadioMenuItem::Group units_group
;
148 units_items
.push_back (RadioMenuElem (units_group
, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units
), Percentage
)));
149 if (Config
->get_shuttle_units() == Percentage
) {
150 static_cast<RadioMenuItem
*>(&units_items
.back())->set_active();
152 units_items
.push_back (RadioMenuElem (units_group
, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units
), Semitones
)));
153 if (Config
->get_shuttle_units() == Semitones
) {
154 static_cast<RadioMenuItem
*>(&units_items
.back())->set_active();
156 items
.push_back (MenuElem (_("Units"), *units_menu
));
158 Menu
* style_menu
= manage (new Menu
);
159 MenuList
& style_items
= style_menu
->items();
160 RadioMenuItem::Group style_group
;
162 style_items
.push_back (RadioMenuElem (style_group
, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style
), Sprung
)));
163 if (Config
->get_shuttle_behaviour() == Sprung
) {
164 static_cast<RadioMenuItem
*>(&style_items
.back())->set_active();
166 style_items
.push_back (RadioMenuElem (style_group
, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style
), Wheel
)));
167 if (Config
->get_shuttle_behaviour() == Wheel
) {
168 static_cast<RadioMenuItem
*>(&style_items
.back())->set_active();
171 items
.push_back (MenuElem (_("Mode"), *style_menu
));
173 RadioMenuItem::Group speed_group
;
175 speed_items
.push_back (RadioMenuElem (speed_group
, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 8.0f
)));
176 if (shuttle_max_speed
== 8.0) {
177 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
179 speed_items
.push_back (RadioMenuElem (speed_group
, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 6.0f
)));
180 if (shuttle_max_speed
== 6.0) {
181 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
183 speed_items
.push_back (RadioMenuElem (speed_group
, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 4.0f
)));
184 if (shuttle_max_speed
== 4.0) {
185 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
187 speed_items
.push_back (RadioMenuElem (speed_group
, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 3.0f
)));
188 if (shuttle_max_speed
== 3.0) {
189 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
191 speed_items
.push_back (RadioMenuElem (speed_group
, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 2.0f
)));
192 if (shuttle_max_speed
== 2.0) {
193 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
195 speed_items
.push_back (RadioMenuElem (speed_group
, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed
), 1.5f
)));
196 if (shuttle_max_speed
== 1.5) {
197 static_cast<RadioMenuItem
*>(&speed_items
.back())->set_active ();
200 items
.push_back (MenuElem (_("Maximum speed"), *speed_menu
));
205 ShuttleControl::show_shuttle_context_menu ()
207 if (shuttle_context_menu
== 0) {
208 build_shuttle_context_menu ();
211 shuttle_context_menu
->popup (1, gtk_get_current_event_time());
215 ShuttleControl::set_shuttle_max_speed (float speed
)
217 shuttle_max_speed
= speed
;
221 ShuttleControl::on_button_press_event (GdkEventButton
* ev
)
227 if (binding_proxy
.button_press_handler (ev
)) {
231 if (Keyboard::is_context_menu_event (ev
)) {
232 show_shuttle_context_menu ();
236 switch (ev
->button
) {
239 shuttle_grabbed
= true;
240 shuttle_speed_on_grab
= _session
->transport_speed ();
241 mouse_shuttle (ev
->x
, true);
254 ShuttleControl::on_button_release_event (GdkEventButton
* ev
)
260 switch (ev
->button
) {
262 shuttle_grabbed
= false;
263 remove_modal_grab ();
265 if (Config
->get_shuttle_behaviour() == Sprung
) {
266 _session
->request_transport_speed (shuttle_speed_on_grab
);
268 mouse_shuttle (ev
->x
, true);
274 if (_session
->transport_rolling()) {
275 _session
->request_transport_speed (1.0);
289 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr
<Gtk::Tooltip
>&)
295 ShuttleControl::on_scroll_event (GdkEventScroll
* ev
)
297 if (!_session
|| Config
->get_shuttle_behaviour() != Wheel
) {
301 switch (ev
->direction
) {
303 case GDK_SCROLL_RIGHT
:
304 shuttle_fract
+= 0.005;
306 case GDK_SCROLL_DOWN
:
307 case GDK_SCROLL_LEFT
:
308 shuttle_fract
-= 0.005;
314 if (Config
->get_shuttle_units() == Semitones
) {
316 float lower_side_of_dead_zone
= semitones_as_fract (-24, true);
317 float upper_side_of_dead_zone
= semitones_as_fract (-24, false);
319 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
320 to the far side of it.
323 if (shuttle_fract
> lower_side_of_dead_zone
&& shuttle_fract
< upper_side_of_dead_zone
) {
324 switch (ev
->direction
) {
326 case GDK_SCROLL_RIGHT
:
327 shuttle_fract
= upper_side_of_dead_zone
;
329 case GDK_SCROLL_DOWN
:
330 case GDK_SCROLL_LEFT
:
331 shuttle_fract
= lower_side_of_dead_zone
;
334 /* impossible, checked above */
340 use_shuttle_fract (true);
346 ShuttleControl::on_motion_notify_event (GdkEventMotion
* ev
)
348 if (!_session
|| !shuttle_grabbed
) {
352 return mouse_shuttle (ev
->x
, false);
356 ShuttleControl::mouse_shuttle (double x
, bool force
)
358 double const center
= get_width() / 2.0;
359 double distance_from_center
= x
- center
;
361 if (distance_from_center
> 0) {
362 distance_from_center
= min (distance_from_center
, center
);
364 distance_from_center
= max (distance_from_center
, -center
);
367 /* compute shuttle fract as expressing how far between the center
368 and the edge we are. positive values indicate we are right of
369 center, negative values indicate left of center
372 shuttle_fract
= distance_from_center
/ center
; // center == half the width
373 use_shuttle_fract (force
);
378 ShuttleControl::set_shuttle_fract (double f
)
381 use_shuttle_fract (false);
385 ShuttleControl::speed_as_semitones (float speed
, bool& reverse
)
387 assert (speed
!= 0.0);
391 return (int) round (12.0 * fast_log2 (-speed
));
394 return (int) round (12.0 * fast_log2 (speed
));
399 ShuttleControl::semitones_as_speed (int semi
, bool reverse
)
402 return -pow (2.0, (semi
/ 12.0));
404 return pow (2.0, (semi
/ 12.0));
409 ShuttleControl::semitones_as_fract (int semi
, bool reverse
)
411 float speed
= semitones_as_speed (semi
, reverse
);
412 return speed
/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
416 ShuttleControl::fract_as_semitones (float fract
, bool& reverse
)
418 assert (fract
!= 0.0);
419 return speed_as_semitones (fract
* 4.0, reverse
);
423 ShuttleControl::use_shuttle_fract (bool force
)
425 microseconds_t now
= get_microseconds();
427 shuttle_fract
= max (-1.0f
, shuttle_fract
);
428 shuttle_fract
= min (1.0f
, shuttle_fract
);
430 /* do not attempt to submit a motion-driven transport speed request
431 more than once per process cycle.
434 if (!force
&& (last_shuttle_request
- now
) < (microseconds_t
) AudioEngine::instance()->usecs_per_cycle()) {
438 last_shuttle_request
= now
;
442 if (Config
->get_shuttle_units() == Semitones
) {
443 if (shuttle_fract
!= 0.0) {
445 int semi
= fract_as_semitones (shuttle_fract
, reverse
);
446 speed
= semitones_as_speed (semi
, reverse
);
451 speed
= shuttle_max_speed
* shuttle_fract
;
454 _session
->request_transport_speed_nonzero (speed
);
458 ShuttleControl::on_expose_event (GdkEventExpose
* event
)
460 cairo_text_extents_t extents
;
461 Glib::RefPtr
<Gdk::Window
> win (get_window());
462 Glib::RefPtr
<Gtk::Style
> style (get_style());
464 cairo_t
* cr
= gdk_cairo_create (win
->gobj());
466 cairo_set_source (cr
, pattern
);
467 cairo_rectangle (cr
, 0.0, 0.0, get_width(), get_height());
468 cairo_fill_preserve (cr
);
470 cairo_set_source_rgb (cr
, 0, 0, 0.0);
476 speed
= _session
->transport_speed ();
481 double visual_fraction
= std::min (1.0f
, speed
/shuttle_max_speed
);
482 double x
= (get_width() / 2.0) + (0.5 * (get_width() * visual_fraction
));
483 cairo_move_to (cr
, x
, 1);
484 cairo_set_source_rgb (cr
, 1.0, 1.0, 1.0);
485 cairo_line_to (cr
, x
, get_height()-1);
494 if (Config
->get_shuttle_units() == Percentage
) {
497 snprintf (buf
, sizeof (buf
), _("Playing"));
500 snprintf (buf
, sizeof (buf
), "<<< %d%%", (int) round (-speed
* 100));
502 snprintf (buf
, sizeof (buf
), ">>> %d%%", (int) round (speed
* 100));
509 int semi
= speed_as_semitones (speed
, reversed
);
512 snprintf (buf
, sizeof (buf
), _("<<< %+d semitones"), semi
);
514 snprintf (buf
, sizeof (buf
), _(">>> %+d semitones"), semi
);
519 snprintf (buf
, sizeof (buf
), _("Stopped"));
522 last_speed_displayed
= speed
;
524 cairo_set_source_rgb (cr
, 1.0, 1.0, 1.0);
525 cairo_text_extents (cr
, buf
, &extents
);
526 cairo_move_to (cr
, 10, extents
.height
+ 2);
527 cairo_show_text (cr
, buf
);
532 switch (Config
->get_shuttle_behaviour()) {
534 snprintf (buf
, sizeof (buf
), _("Sprung"));
537 snprintf (buf
, sizeof (buf
), _("Wheel"));
541 cairo_text_extents (cr
, buf
, &extents
);
543 cairo_move_to (cr
, get_width() - (fabs(extents
.x_advance
) + 5), extents
.height
+ 2);
544 cairo_show_text (cr
, buf
);
552 ShuttleControl::shuttle_unit_clicked ()
554 if (shuttle_unit_menu
== 0) {
555 shuttle_unit_menu
= dynamic_cast<Menu
*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
557 shuttle_unit_menu
->popup (1, gtk_get_current_event_time());
561 ShuttleControl::set_shuttle_style (ShuttleBehaviour s
)
563 Config
->set_shuttle_behaviour (s
);
567 ShuttleControl::set_shuttle_units (ShuttleUnits s
)
569 Config
->set_shuttle_units (s
);
573 ShuttleControl::update_speed_display ()
575 if (_session
->transport_speed() != last_speed_displayed
) {
580 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl
& s
)
581 : PBD::Controllable (X_("Shuttle"))
587 ShuttleControl::ShuttleControllable::set_id (const std::string
& str
)
593 ShuttleControl::ShuttleControllable::set_value (double val
)
601 fract
= -((0.5 - val
)/0.5);
603 fract
= ((val
- 0.5)/0.5);
607 sc
.set_shuttle_fract (fract
);
611 ShuttleControl::ShuttleControllable::get_value () const
613 return sc
.get_shuttle_fract ();
617 ShuttleControl::parameter_changed (std::string p
)
619 if (p
== "shuttle-behaviour") {
620 switch (Config
->get_shuttle_behaviour ()) {
622 /* back to Sprung - reset to speed = 1.0 if playing
625 if (_session
->transport_rolling()) {
626 if (_session
->transport_speed() == 1.0) {
629 _session
->request_transport_speed (1.0);
630 /* redraw when speed changes */
643 } else if (p
== "shuttle-units") {