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));
240 ShuttleControl::show_shuttle_context_menu ()
242 if (shuttle_context_menu == 0) {
243 build_shuttle_context_menu ();
246 shuttle_context_menu->popup (1, gtk_get_current_event_time());
250 ShuttleControl::set_shuttle_max_speed (float speed)
252 Config->set_shuttle_max_speed (speed);
253 shuttle_max_speed = speed;
254 last_speed_displayed = -99999999;
258 ShuttleControl::on_button_press_event (GdkEventButton* ev)
264 if (binding_proxy.button_press_handler (ev)) {
268 if (Keyboard::is_context_menu_event (ev)) {
269 show_shuttle_context_menu ();
273 switch (ev->button) {
275 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
276 if (_session->transport_rolling()) {
277 _session->request_transport_speed (1.0);
281 shuttle_grabbed = true;
282 shuttle_speed_on_grab = _session->transport_speed ();
283 mouse_shuttle (ev->x, true);
284 gdk_pointer_grab(ev->window,false,
285 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
300 ShuttleControl::on_button_release_event (GdkEventButton* ev)
306 switch (ev->button) {
308 if (shuttle_grabbed) {
309 shuttle_grabbed = false;
310 remove_modal_grab ();
311 gdk_pointer_ungrab (GDK_CURRENT_TIME);
313 if (Config->get_shuttle_behaviour() == Sprung) {
314 if (shuttle_speed_on_grab == 0 ) {
315 _session->request_stop ();
317 _session->request_transport_speed (shuttle_speed_on_grab);
320 mouse_shuttle (ev->x, true);
326 if (_session->transport_rolling()) {
327 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
341 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
347 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
349 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
353 bool semis = (Config->get_shuttle_units() == Semitones);
355 switch (ev->direction) {
357 case GDK_SCROLL_RIGHT:
359 if (shuttle_fract == 0) {
360 shuttle_fract = semitones_as_fract (1, false);
363 int st = fract_as_semitones (shuttle_fract, rev);
364 shuttle_fract = semitones_as_fract (st + 1, rev);
367 shuttle_fract += 0.00125;
370 case GDK_SCROLL_DOWN:
371 case GDK_SCROLL_LEFT:
373 if (shuttle_fract == 0) {
374 shuttle_fract = semitones_as_fract (1, true);
377 int st = fract_as_semitones (shuttle_fract, rev);
378 shuttle_fract = semitones_as_fract (st - 1, rev);
381 shuttle_fract -= 0.00125;
390 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
391 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
393 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
394 to the far side of it.
397 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
398 switch (ev->direction) {
400 case GDK_SCROLL_RIGHT:
401 shuttle_fract = upper_side_of_dead_zone;
403 case GDK_SCROLL_DOWN:
404 case GDK_SCROLL_LEFT:
405 shuttle_fract = lower_side_of_dead_zone;
408 /* impossible, checked above */
414 use_shuttle_fract (true);
420 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
422 if (!_session || !shuttle_grabbed) {
426 return mouse_shuttle (ev->x, false);
430 ShuttleControl::mouse_shuttle (double x, bool force)
432 double const center = get_width() / 2.0;
433 double distance_from_center = x - center;
435 if (distance_from_center > 0) {
436 distance_from_center = min (distance_from_center, center);
438 distance_from_center = max (distance_from_center, -center);
441 /* compute shuttle fract as expressing how far between the center
442 and the edge we are. positive values indicate we are right of
443 center, negative values indicate left of center
446 shuttle_fract = distance_from_center / center; // center == half the width
447 use_shuttle_fract (force);
452 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
455 use_shuttle_fract (false, zero_ok);
459 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
461 assert (speed != 0.0);
465 return (int) round (12.0 * fast_log2 (-speed));
468 return (int) round (12.0 * fast_log2 (speed));
473 ShuttleControl::semitones_as_speed (int semi, bool reverse)
476 return -pow (2.0, (semi / 12.0));
478 return pow (2.0, (semi / 12.0));
483 ShuttleControl::semitones_as_fract (int semi, bool reverse)
485 float speed = semitones_as_speed (semi, reverse);
486 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
490 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
492 assert (fract != 0.0);
493 return speed_as_semitones (fract * 4.0, reverse);
497 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
499 microseconds_t now = get_microseconds();
501 shuttle_fract = max (-1.0f, shuttle_fract);
502 shuttle_fract = min (1.0f, shuttle_fract);
504 /* do not attempt to submit a motion-driven transport speed request
505 more than once per process cycle.
508 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
512 last_shuttle_request = now;
516 if (Config->get_shuttle_units() == Semitones) {
517 if (shuttle_fract != 0.0) {
519 int semi = fract_as_semitones (shuttle_fract, reverse);
520 speed = semitones_as_speed (semi, reverse);
525 speed = shuttle_max_speed * shuttle_fract;
529 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
531 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
536 ShuttleControl::render (cairo_t* cr, cairo_rectangle_t*)
538 cairo_text_extents_t extents;
541 cairo_set_source_rgb (cr, 0, 0.0, 0.0);
542 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 4);
548 speed = _session->transport_speed ();
552 float visual_fraction = std::min (1.0f, speed / shuttle_max_speed);
553 float marker_size = get_height() - 5.0;
554 float avail_width = get_width() - marker_size - 4;
555 float x = get_width() * 0.5 + visual_fraction * avail_width * 0.5;
556 // cairo_set_source_rgb (cr, 0, 1, 0.0);
557 cairo_set_source (cr, pattern);
559 cairo_move_to( cr, x, 2.5);
560 cairo_line_to( cr, x + marker_size * .577, 2.5 + marker_size * 0.5);
561 cairo_line_to( cr, x, 2.5 + marker_size);
562 cairo_close_path(cr);
563 } else if ( speed ==0.0 )
564 rounded_rectangle (cr, x, 2.5, marker_size, marker_size, 1);
566 cairo_arc (cr, x, 2.5 + marker_size * .5, marker_size * 0.47, 0, 2.0 * M_PI);
567 cairo_set_line_width (cr, 1.75);
576 if (Config->get_shuttle_units() == Percentage) {
579 snprintf (buf, sizeof (buf), "%s", _("Playing"));
582 snprintf (buf, sizeof (buf), "<<< %.1f%%", -speed * 100.f);
584 snprintf (buf, sizeof (buf), ">>> %.1f%%", speed * 100.f);
591 int semi = speed_as_semitones (speed, reversed);
594 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
596 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
601 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
604 last_speed_displayed = speed;
606 // TODO use a proper pango layout, scale font
607 cairo_set_source_rgb (cr, 0.6, 0.6, 0.6);
608 cairo_set_font_size (cr, 13.0);
609 cairo_text_extents (cr, "0|", &extents); // note the descender
610 const float text_ypos = (get_height() + extents.height - 1.) * .5;
612 cairo_move_to (cr, 10, text_ypos);
613 cairo_show_text (cr, buf);
618 switch (Config->get_shuttle_behaviour()) {
620 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
623 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
627 cairo_text_extents (cr, buf, &extents);
628 cairo_move_to (cr, get_width() - (fabs(extents.x_advance) + 5), text_ypos);
629 cairo_show_text (cr, buf);
631 if (ARDOUR_UI::config()->get_widget_prelight()) {
633 rounded_rectangle (cr, 1, 1, get_width()-2, get_height()-2, 4.0);
634 cairo_set_source_rgba (cr, 1, 1, 1, 0.2);
641 ShuttleControl::shuttle_unit_clicked ()
643 if (shuttle_unit_menu == 0) {
644 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
646 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
650 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
652 Config->set_shuttle_behaviour (s);
656 ShuttleControl::set_shuttle_units (ShuttleUnits s)
658 Config->set_shuttle_units (s);
661 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
662 : PBD::Controllable (X_("Shuttle"))
668 ShuttleControl::ShuttleControllable::set_value (double val)
670 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
674 ShuttleControl::ShuttleControllable::get_value () const
676 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
680 ShuttleControl::parameter_changed (std::string p)
682 if (p == "shuttle-behaviour") {
683 switch (Config->get_shuttle_behaviour ()) {
685 /* back to Sprung - reset to speed = 1.0 if playing
688 if (_session->transport_rolling()) {
689 if (_session->transport_speed() == 1.0) {
692 /* reset current speed and
693 revert to 1.0 as the default
695 _session->request_transport_speed (1.0);
696 /* redraw when speed changes */
709 } else if (p == "shuttle-units") {
716 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
720 if (ARDOUR_UI::config()->get_widget_prelight()) {
724 return CairoWidget::on_enter_notify_event (ev);
728 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
732 if (ARDOUR_UI::config()->get_widget_prelight()) {
736 return CairoWidget::on_leave_notify_event (ev);