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, boost::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) {
244 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
245 if (_session->transport_rolling()) {
246 _session->request_transport_speed (1.0);
250 shuttle_grabbed = true;
251 shuttle_speed_on_grab = _session->transport_speed ();
252 mouse_shuttle (ev->x, true);
266 ShuttleControl::on_button_release_event (GdkEventButton* ev)
272 switch (ev->button) {
274 if (shuttle_grabbed) {
275 shuttle_grabbed = false;
276 remove_modal_grab ();
278 if (Config->get_shuttle_behaviour() == Sprung) {
279 _session->request_transport_speed (shuttle_speed_on_grab);
281 mouse_shuttle (ev->x, true);
287 if (_session->transport_rolling()) {
288 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
302 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
308 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
310 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
314 bool semis = (Config->get_shuttle_units() == Semitones);
316 switch (ev->direction) {
318 case GDK_SCROLL_RIGHT:
320 if (shuttle_fract == 0) {
321 shuttle_fract = semitones_as_fract (1, false);
324 int st = fract_as_semitones (shuttle_fract, rev);
325 shuttle_fract = semitones_as_fract (st + 1, rev);
328 shuttle_fract += 0.00125;
331 case GDK_SCROLL_DOWN:
332 case GDK_SCROLL_LEFT:
334 if (shuttle_fract == 0) {
335 shuttle_fract = semitones_as_fract (1, true);
338 int st = fract_as_semitones (shuttle_fract, rev);
339 shuttle_fract = semitones_as_fract (st - 1, rev);
342 shuttle_fract -= 0.00125;
351 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
352 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
354 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
355 to the far side of it.
358 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
359 switch (ev->direction) {
361 case GDK_SCROLL_RIGHT:
362 shuttle_fract = upper_side_of_dead_zone;
364 case GDK_SCROLL_DOWN:
365 case GDK_SCROLL_LEFT:
366 shuttle_fract = lower_side_of_dead_zone;
369 /* impossible, checked above */
375 use_shuttle_fract (true);
381 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
383 if (!_session || !shuttle_grabbed) {
387 return mouse_shuttle (ev->x, false);
391 ShuttleControl::mouse_shuttle (double x, bool force)
393 double const center = get_width() / 2.0;
394 double distance_from_center = x - center;
396 if (distance_from_center > 0) {
397 distance_from_center = min (distance_from_center, center);
399 distance_from_center = max (distance_from_center, -center);
402 /* compute shuttle fract as expressing how far between the center
403 and the edge we are. positive values indicate we are right of
404 center, negative values indicate left of center
407 shuttle_fract = distance_from_center / center; // center == half the width
408 use_shuttle_fract (force);
413 ShuttleControl::set_shuttle_fract (double f)
416 use_shuttle_fract (false);
420 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
422 assert (speed != 0.0);
426 return (int) round (12.0 * fast_log2 (-speed));
429 return (int) round (12.0 * fast_log2 (speed));
434 ShuttleControl::semitones_as_speed (int semi, bool reverse)
437 return -pow (2.0, (semi / 12.0));
439 return pow (2.0, (semi / 12.0));
444 ShuttleControl::semitones_as_fract (int semi, bool reverse)
446 float speed = semitones_as_speed (semi, reverse);
447 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
451 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
453 assert (fract != 0.0);
454 return speed_as_semitones (fract * 4.0, reverse);
458 ShuttleControl::use_shuttle_fract (bool force)
460 microseconds_t now = get_microseconds();
462 shuttle_fract = max (-1.0f, shuttle_fract);
463 shuttle_fract = min (1.0f, shuttle_fract);
465 /* do not attempt to submit a motion-driven transport speed request
466 more than once per process cycle.
469 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
473 last_shuttle_request = now;
477 if (Config->get_shuttle_units() == Semitones) {
478 if (shuttle_fract != 0.0) {
480 int semi = fract_as_semitones (shuttle_fract, reverse);
481 speed = semitones_as_speed (semi, reverse);
486 speed = shuttle_max_speed * shuttle_fract;
489 _session->request_transport_speed_nonzero (speed, true);
493 ShuttleControl::on_expose_event (GdkEventExpose*)
495 cairo_text_extents_t extents;
496 Glib::RefPtr<Gdk::Window> win (get_window());
497 Glib::RefPtr<Gtk::Style> style (get_style());
499 cairo_t* cr = gdk_cairo_create (win->gobj());
501 cairo_set_source (cr, pattern);
502 cairo_rectangle (cr, 0.0, 0.0, get_width(), get_height());
503 cairo_fill_preserve (cr);
505 cairo_set_source_rgb (cr, 0, 0, 0.0);
511 speed = _session->transport_speed ();
516 double visual_fraction = std::min (1.0f, speed/shuttle_max_speed);
517 double x = (get_width() / 2.0) + (0.5 * (get_width() * visual_fraction));
518 cairo_move_to (cr, x, 1);
519 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
520 cairo_line_to (cr, x, get_height()-1);
529 if (Config->get_shuttle_units() == Percentage) {
532 snprintf (buf, sizeof (buf), "%s", _("Playing"));
535 snprintf (buf, sizeof (buf), "<<< %d%%", (int) round (-speed * 100));
537 snprintf (buf, sizeof (buf), ">>> %d%%", (int) round (speed * 100));
544 int semi = speed_as_semitones (speed, reversed);
547 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
549 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
554 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
557 last_speed_displayed = speed;
559 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
560 cairo_text_extents (cr, buf, &extents);
561 cairo_move_to (cr, 10, extents.height + 2);
562 cairo_show_text (cr, buf);
567 switch (Config->get_shuttle_behaviour()) {
569 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
572 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
576 cairo_text_extents (cr, buf, &extents);
578 cairo_move_to (cr, get_width() - (fabs(extents.x_advance) + 5), extents.height + 2);
579 cairo_show_text (cr, buf);
587 ShuttleControl::shuttle_unit_clicked ()
589 if (shuttle_unit_menu == 0) {
590 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
592 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
596 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
598 Config->set_shuttle_behaviour (s);
602 ShuttleControl::set_shuttle_units (ShuttleUnits s)
604 Config->set_shuttle_units (s);
608 ShuttleControl::update_speed_display ()
610 if (_session->transport_speed() != last_speed_displayed) {
615 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
616 : PBD::Controllable (X_("Shuttle"))
622 ShuttleControl::ShuttleControllable::set_value (double val)
630 fract = -((0.5 - val)/0.5);
632 fract = ((val - 0.5)/0.5);
636 sc.set_shuttle_fract (fract);
640 ShuttleControl::ShuttleControllable::get_value () const
642 return sc.get_shuttle_fract ();
646 ShuttleControl::parameter_changed (std::string p)
648 if (p == "shuttle-behaviour") {
649 switch (Config->get_shuttle_behaviour ()) {
651 /* back to Sprung - reset to speed = 1.0 if playing
654 if (_session->transport_rolling()) {
655 if (_session->transport_speed() == 1.0) {
658 /* reset current speed and
659 revert to 1.0 as the default
661 _session->request_transport_speed (1.0);
662 /* redraw when speed changes */
675 } else if (p == "shuttle-units") {