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.
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"
30 #include "gtkmm2ext/cairocell.h"
31 #include "gtkmm2ext/utils.h"
32 #include "gtkmm2ext/rgb_macros.h"
35 #include "ardour_ui.h"
36 #include "rgb_macros.h"
37 #include "shuttle_control.h"
42 using namespace Gtkmm2ext;
43 using namespace ARDOUR;
47 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
52 ShuttleControl::ShuttleControl ()
53 : _controllable (new ShuttleControllable (*this))
54 , binding_proxy (_controllable)
56 ARDOUR_UI::instance()->set_tip (*this, _("Shuttle speed control (Context-click for options)"));
60 last_shuttle_request = 0;
61 last_speed_displayed = -99999999;
62 shuttle_grabbed = false;
63 shuttle_speed_on_grab = 0;
65 shuttle_max_speed = 8.0f;
66 shuttle_style_menu = 0;
67 shuttle_unit_menu = 0;
68 shuttle_context_menu = 0;
71 set_flags (CAN_FOCUS);
72 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);
73 set_size_request (85, 20);
74 set_name (X_("ShuttleControl"));
76 shuttle_max_speed = Config->get_shuttle_max_speed();
78 if (shuttle_max_speed >= 8.f) { shuttle_max_speed = 8.0f; }
79 else if (shuttle_max_speed >= 6.f) { shuttle_max_speed = 6.0f; }
80 else if (shuttle_max_speed >= 4.f) { shuttle_max_speed = 4.0f; }
81 else if (shuttle_max_speed >= 3.f) { shuttle_max_speed = 3.0f; }
82 else if (shuttle_max_speed >= 2.f) { shuttle_max_speed = 2.0f; }
83 else { shuttle_max_speed = 1.5f; }
85 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
87 /* gtkmm 2.4: the C++ wrapper doesn't work */
88 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
89 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
92 ShuttleControl::~ShuttleControl ()
94 cairo_pattern_destroy (pattern);
95 cairo_pattern_destroy (shine_pattern);
99 ShuttleControl::set_session (Session *s)
101 SessionHandlePtr::set_session (s);
104 set_sensitive (true);
105 _session->add_controllable (_controllable);
107 set_sensitive (false);
112 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
115 cairo_pattern_destroy (pattern);
117 cairo_pattern_destroy (shine_pattern);
121 CairoWidget::on_size_allocate ( alloc);
124 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
125 uint32_t col = ARDOUR_UI::config()->color ("shuttle");
127 UINT_TO_RGBA(col, &r, &g, &b, &a);
128 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
129 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
130 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
133 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
134 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
135 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
136 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
140 ShuttleControl::map_transport_state ()
142 float speed = _session->transport_speed ();
144 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
145 && !( speed == 1.f && last_speed_displayed != 1.f)
146 && !( speed == 0.f && last_speed_displayed != 0.f)
149 return; // nothing to see here, move along.
152 // Q: is there a good reason why we re-calculate this every time?
153 if (fabs(speed) <= (2*DBL_EPSILON)) {
156 if (Config->get_shuttle_units() == Semitones) {
158 int semi = speed_as_semitones (speed, reverse);
159 shuttle_fract = semitones_as_fract (semi, reverse);
161 shuttle_fract = speed/shuttle_max_speed;
169 ShuttleControl::build_shuttle_context_menu ()
171 using namespace Menu_Helpers;
173 shuttle_context_menu = new Menu();
174 MenuList& items = shuttle_context_menu->items();
176 Menu* speed_menu = manage (new Menu());
177 MenuList& speed_items = speed_menu->items();
179 Menu* units_menu = manage (new Menu);
180 MenuList& units_items = units_menu->items();
181 RadioMenuItem::Group units_group;
183 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
184 if (Config->get_shuttle_units() == Percentage) {
185 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
187 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
188 if (Config->get_shuttle_units() == Semitones) {
189 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
191 items.push_back (MenuElem (_("Units"), *units_menu));
193 Menu* style_menu = manage (new Menu);
194 MenuList& style_items = style_menu->items();
195 RadioMenuItem::Group style_group;
197 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
198 if (Config->get_shuttle_behaviour() == Sprung) {
199 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
201 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
202 if (Config->get_shuttle_behaviour() == Wheel) {
203 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
206 items.push_back (MenuElem (_("Mode"), *style_menu));
208 RadioMenuItem::Group speed_group;
210 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
211 if (shuttle_max_speed == 8.0) {
212 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
214 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
215 if (shuttle_max_speed == 6.0) {
216 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
218 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
219 if (shuttle_max_speed == 4.0) {
220 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
222 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
223 if (shuttle_max_speed == 3.0) {
224 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
226 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
227 if (shuttle_max_speed == 2.0) {
228 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
230 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
231 if (shuttle_max_speed == 1.5) {
232 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
235 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
237 items.push_back (SeparatorElem ());
238 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
242 ShuttleControl::show_shuttle_context_menu ()
244 if (shuttle_context_menu == 0) {
245 build_shuttle_context_menu ();
248 shuttle_context_menu->popup (1, gtk_get_current_event_time());
252 ShuttleControl::reset_speed ()
254 if (_session->transport_rolling()) {
255 _session->request_transport_speed (1.0, true);
257 _session->request_transport_speed (0.0, true);
262 ShuttleControl::set_shuttle_max_speed (float speed)
264 Config->set_shuttle_max_speed (speed);
265 shuttle_max_speed = speed;
266 last_speed_displayed = -99999999;
270 ShuttleControl::on_button_press_event (GdkEventButton* ev)
276 if (binding_proxy.button_press_handler (ev)) {
280 if (Keyboard::is_context_menu_event (ev)) {
281 show_shuttle_context_menu ();
285 switch (ev->button) {
287 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
288 if (_session->transport_rolling()) {
289 _session->request_transport_speed (1.0);
293 shuttle_grabbed = true;
294 shuttle_speed_on_grab = _session->transport_speed ();
295 mouse_shuttle (ev->x, true);
296 gdk_pointer_grab(ev->window,false,
297 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
312 ShuttleControl::on_button_release_event (GdkEventButton* ev)
318 switch (ev->button) {
320 if (shuttle_grabbed) {
321 shuttle_grabbed = false;
322 remove_modal_grab ();
323 gdk_pointer_ungrab (GDK_CURRENT_TIME);
325 if (Config->get_shuttle_behaviour() == Sprung) {
326 if (shuttle_speed_on_grab == 0 ) {
327 _session->request_stop ();
329 _session->request_transport_speed (shuttle_speed_on_grab);
332 mouse_shuttle (ev->x, true);
338 if (_session->transport_rolling()) {
339 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
353 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
359 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
361 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
365 bool semis = (Config->get_shuttle_units() == Semitones);
367 switch (ev->direction) {
369 case GDK_SCROLL_RIGHT:
371 if (shuttle_fract == 0) {
372 shuttle_fract = semitones_as_fract (1, false);
375 int st = fract_as_semitones (shuttle_fract, rev);
376 shuttle_fract = semitones_as_fract (st + 1, rev);
379 shuttle_fract += 0.00125;
382 case GDK_SCROLL_DOWN:
383 case GDK_SCROLL_LEFT:
385 if (shuttle_fract == 0) {
386 shuttle_fract = semitones_as_fract (1, true);
389 int st = fract_as_semitones (shuttle_fract, rev);
390 shuttle_fract = semitones_as_fract (st - 1, rev);
393 shuttle_fract -= 0.00125;
402 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
403 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
405 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
406 to the far side of it.
409 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
410 switch (ev->direction) {
412 case GDK_SCROLL_RIGHT:
413 shuttle_fract = upper_side_of_dead_zone;
415 case GDK_SCROLL_DOWN:
416 case GDK_SCROLL_LEFT:
417 shuttle_fract = lower_side_of_dead_zone;
420 /* impossible, checked above */
426 use_shuttle_fract (true);
432 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
434 if (!_session || !shuttle_grabbed) {
438 return mouse_shuttle (ev->x, false);
442 ShuttleControl::mouse_shuttle (double x, bool force)
444 double const center = get_width() / 2.0;
445 double distance_from_center = x - center;
447 if (distance_from_center > 0) {
448 distance_from_center = min (distance_from_center, center);
450 distance_from_center = max (distance_from_center, -center);
453 /* compute shuttle fract as expressing how far between the center
454 and the edge we are. positive values indicate we are right of
455 center, negative values indicate left of center
458 shuttle_fract = distance_from_center / center; // center == half the width
459 use_shuttle_fract (force);
464 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
467 use_shuttle_fract (false, zero_ok);
471 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
473 assert (speed != 0.0);
477 return (int) round (12.0 * fast_log2 (-speed));
480 return (int) round (12.0 * fast_log2 (speed));
485 ShuttleControl::semitones_as_speed (int semi, bool reverse)
488 return -pow (2.0, (semi / 12.0));
490 return pow (2.0, (semi / 12.0));
495 ShuttleControl::semitones_as_fract (int semi, bool reverse)
497 float speed = semitones_as_speed (semi, reverse);
498 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
502 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
504 assert (fract != 0.0);
505 return speed_as_semitones (fract * 4.0, reverse);
509 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
511 microseconds_t now = get_microseconds();
513 shuttle_fract = max (-1.0f, shuttle_fract);
514 shuttle_fract = min (1.0f, shuttle_fract);
516 /* do not attempt to submit a motion-driven transport speed request
517 more than once per process cycle.
520 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
524 last_shuttle_request = now;
528 if (Config->get_shuttle_units() == Semitones) {
529 if (shuttle_fract != 0.0) {
531 int semi = fract_as_semitones (shuttle_fract, reverse);
532 speed = semitones_as_speed (semi, reverse);
537 speed = shuttle_max_speed * shuttle_fract;
541 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
543 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
548 ShuttleControl::render (cairo_t* cr, cairo_rectangle_t*)
550 cairo_text_extents_t extents;
553 cairo_set_source_rgb (cr, 0, 0.0, 0.0);
554 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 4);
560 speed = _session->transport_speed ();
564 float visual_fraction = std::min (1.0f, speed / shuttle_max_speed);
565 float marker_size = get_height() - 5.0;
566 float avail_width = get_width() - marker_size - 4;
567 float x = get_width() * 0.5 + visual_fraction * avail_width * 0.5;
568 // cairo_set_source_rgb (cr, 0, 1, 0.0);
569 cairo_set_source (cr, pattern);
571 cairo_move_to( cr, x, 2.5);
572 cairo_line_to( cr, x + marker_size * .577, 2.5 + marker_size * 0.5);
573 cairo_line_to( cr, x, 2.5 + marker_size);
574 cairo_close_path(cr);
575 } else if ( speed ==0.0 )
576 rounded_rectangle (cr, x, 2.5, marker_size, marker_size, 1);
578 cairo_arc (cr, x, 2.5 + marker_size * .5, marker_size * 0.47, 0, 2.0 * M_PI);
579 cairo_set_line_width (cr, 1.75);
588 if (Config->get_shuttle_units() == Percentage) {
591 snprintf (buf, sizeof (buf), "%s", _("Playing"));
594 snprintf (buf, sizeof (buf), "<<< %.1f%%", -speed * 100.f);
596 snprintf (buf, sizeof (buf), ">>> %.1f%%", speed * 100.f);
603 int semi = speed_as_semitones (speed, reversed);
606 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
608 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
613 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
616 last_speed_displayed = speed;
618 // TODO use a proper pango layout, scale font
619 cairo_set_source_rgb (cr, 0.6, 0.6, 0.6);
620 cairo_set_font_size (cr, 13.0);
621 cairo_text_extents (cr, "0|", &extents); // note the descender
622 const float text_ypos = (get_height() + extents.height - 1.) * .5;
624 cairo_move_to (cr, 10, text_ypos);
625 cairo_show_text (cr, buf);
630 switch (Config->get_shuttle_behaviour()) {
632 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
635 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
639 cairo_text_extents (cr, buf, &extents);
640 cairo_move_to (cr, get_width() - (fabs(extents.x_advance) + 5), text_ypos);
641 cairo_show_text (cr, buf);
643 if (ARDOUR_UI::config()->get_widget_prelight()) {
645 rounded_rectangle (cr, 1, 1, get_width()-2, get_height()-2, 4.0);
646 cairo_set_source_rgba (cr, 1, 1, 1, 0.2);
653 ShuttleControl::shuttle_unit_clicked ()
655 if (shuttle_unit_menu == 0) {
656 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
658 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
662 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
664 Config->set_shuttle_behaviour (s);
668 ShuttleControl::set_shuttle_units (ShuttleUnits s)
670 Config->set_shuttle_units (s);
673 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
674 : PBD::Controllable (X_("Shuttle"))
680 ShuttleControl::ShuttleControllable::set_value (double val)
682 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
686 ShuttleControl::ShuttleControllable::get_value () const
688 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
692 ShuttleControl::parameter_changed (std::string p)
694 if (p == "shuttle-behaviour") {
695 switch (Config->get_shuttle_behaviour ()) {
697 /* back to Sprung - reset to speed = 1.0 if playing
700 if (_session->transport_rolling()) {
701 if (_session->transport_speed() == 1.0) {
704 /* reset current speed and
705 revert to 1.0 as the default
707 _session->request_transport_speed (1.0);
708 /* redraw when speed changes */
721 } else if (p == "shuttle-units") {
728 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
732 if (ARDOUR_UI::config()->get_widget_prelight()) {
736 return CairoWidget::on_enter_notify_event (ev);
740 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
744 if (ARDOUR_UI::config()->get_widget_prelight()) {
748 return CairoWidget::on_leave_notify_event (ev);