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;
58 shuttle_max_speed = 8.0f;
59 shuttle_style_menu = 0;
60 shuttle_unit_menu = 0;
61 shuttle_context_menu = 0;
63 set_flags (CAN_FOCUS);
64 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);
65 set_size_request (100, 15);
66 set_name (X_("ShuttleControl"));
68 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, ui_bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
70 /* gtkmm 2.4: the C++ wrapper doesn't work */
71 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
72 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
75 ShuttleControl::~ShuttleControl ()
77 cairo_pattern_destroy (pattern);
81 ShuttleControl::set_session (Session *s)
83 SessionHandlePtr::set_session (s);
87 _session->add_controllable (_controllable);
89 set_sensitive (false);
94 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
97 cairo_pattern_destroy (pattern);
101 pattern = cairo_pattern_create_linear (0, 0, alloc.get_width(), alloc.get_height());
103 /* add 3 color stops */
105 cairo_pattern_add_color_stop_rgb (pattern, 0.0, 0, 0, 0);
106 cairo_pattern_add_color_stop_rgb (pattern, 0.5, 0.0, 0.0, 1.0);
107 cairo_pattern_add_color_stop_rgb (pattern, 1.0, 0, 0, 0);
109 DrawingArea::on_size_allocate (alloc);
113 ShuttleControl::map_transport_state ()
115 float speed = _session->transport_speed ();
117 if (fabs(speed) <= (2*DBL_EPSILON)) {
120 if (Config->get_shuttle_units() == Semitones) {
122 int semi = speed_as_semitones (speed, reverse);
123 shuttle_fract = semitones_as_fract (semi, reverse);
125 shuttle_fract = speed/shuttle_max_speed;
133 ShuttleControl::build_shuttle_context_menu ()
135 using namespace Menu_Helpers;
137 shuttle_context_menu = new Menu();
138 MenuList& items = shuttle_context_menu->items();
140 Menu* speed_menu = manage (new Menu());
141 MenuList& speed_items = speed_menu->items();
143 Menu* units_menu = manage (new Menu);
144 MenuList& units_items = units_menu->items();
145 RadioMenuItem::Group units_group;
147 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
148 if (Config->get_shuttle_units() == Percentage) {
149 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
151 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
152 if (Config->get_shuttle_units() == Semitones) {
153 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
155 items.push_back (MenuElem (_("Units"), *units_menu));
157 Menu* style_menu = manage (new Menu);
158 MenuList& style_items = style_menu->items();
159 RadioMenuItem::Group style_group;
161 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
162 if (Config->get_shuttle_behaviour() == Sprung) {
163 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
165 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
166 if (Config->get_shuttle_behaviour() == Wheel) {
167 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
170 items.push_back (MenuElem (_("Mode"), *style_menu));
172 RadioMenuItem::Group speed_group;
174 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
175 if (shuttle_max_speed == 8.0) {
176 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
178 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
179 if (shuttle_max_speed == 6.0) {
180 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
182 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
183 if (shuttle_max_speed == 4.0) {
184 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
186 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
187 if (shuttle_max_speed == 3.0) {
188 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
190 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
191 if (shuttle_max_speed == 2.0) {
192 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
194 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
195 if (shuttle_max_speed == 1.5) {
196 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
199 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
204 ShuttleControl::show_shuttle_context_menu ()
206 if (shuttle_context_menu == 0) {
207 build_shuttle_context_menu ();
210 shuttle_context_menu->popup (1, gtk_get_current_event_time());
214 ShuttleControl::set_shuttle_max_speed (float speed)
216 shuttle_max_speed = speed;
220 ShuttleControl::on_button_press_event (GdkEventButton* ev)
226 if (binding_proxy.button_press_handler (ev)) {
230 if (Keyboard::is_context_menu_event (ev)) {
231 show_shuttle_context_menu ();
235 switch (ev->button) {
238 shuttle_grabbed = true;
239 mouse_shuttle (ev->x, true);
252 ShuttleControl::on_button_release_event (GdkEventButton* ev)
258 switch (ev->button) {
260 shuttle_grabbed = false;
261 remove_modal_grab ();
263 if (Config->get_shuttle_behaviour() == Sprung) {
264 if (_session->config.get_auto_play()) {
265 _session->request_transport_speed (1.0);
267 _session->request_transport_speed (0.0);
270 mouse_shuttle (ev->x, true);
276 if (_session->transport_rolling()) {
277 _session->request_transport_speed (1.0);
291 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
297 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
299 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
303 switch (ev->direction) {
305 case GDK_SCROLL_RIGHT:
306 shuttle_fract += 0.005;
308 case GDK_SCROLL_DOWN:
309 case GDK_SCROLL_LEFT:
310 shuttle_fract -= 0.005;
316 if (Config->get_shuttle_units() == Semitones) {
318 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
319 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
321 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
322 to the far side of it.
325 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
326 switch (ev->direction) {
328 case GDK_SCROLL_RIGHT:
329 shuttle_fract = upper_side_of_dead_zone;
331 case GDK_SCROLL_DOWN:
332 case GDK_SCROLL_LEFT:
333 shuttle_fract = lower_side_of_dead_zone;
336 /* impossible, checked above */
342 use_shuttle_fract (true);
348 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
350 if (!_session || !shuttle_grabbed) {
354 return mouse_shuttle (ev->x, false);
358 ShuttleControl::mouse_shuttle (double x, bool force)
360 double const center = get_width() / 2.0;
361 double distance_from_center = x - center;
363 if (distance_from_center > 0) {
364 distance_from_center = min (distance_from_center, center);
366 distance_from_center = max (distance_from_center, -center);
369 /* compute shuttle fract as expressing how far between the center
370 and the edge we are. positive values indicate we are right of
371 center, negative values indicate left of center
374 shuttle_fract = distance_from_center / center; // center == half the width
375 use_shuttle_fract (force);
380 ShuttleControl::set_shuttle_fract (double f)
383 use_shuttle_fract (false);
387 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
389 assert (speed != 0.0);
393 return (int) round (12.0 * fast_log2 (-speed));
396 return (int) round (12.0 * fast_log2 (speed));
401 ShuttleControl::semitones_as_speed (int semi, bool reverse)
404 return -pow (2.0, (semi / 12.0));
406 return pow (2.0, (semi / 12.0));
411 ShuttleControl::semitones_as_fract (int semi, bool reverse)
413 float speed = semitones_as_speed (semi, reverse);
414 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
418 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
420 assert (fract != 0.0);
421 return speed_as_semitones (fract * 4.0, reverse);
425 ShuttleControl::use_shuttle_fract (bool force)
427 microseconds_t now = get_microseconds();
429 shuttle_fract = max (-1.0f, shuttle_fract);
430 shuttle_fract = min (1.0f, shuttle_fract);
432 /* do not attempt to submit a motion-driven transport speed request
433 more than once per process cycle.
436 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
440 last_shuttle_request = now;
444 if (Config->get_shuttle_units() == Semitones) {
445 if (shuttle_fract != 0.0) {
447 int semi = fract_as_semitones (shuttle_fract, reverse);
448 speed = semitones_as_speed (semi, reverse);
453 speed = shuttle_max_speed * shuttle_fract;
456 _session->request_transport_speed_nonzero (speed);
460 ShuttleControl::on_expose_event (GdkEventExpose* event)
462 cairo_text_extents_t extents;
463 Glib::RefPtr<Gdk::Window> win (get_window());
464 Glib::RefPtr<Gtk::Style> style (get_style());
466 cairo_t* cr = gdk_cairo_create (win->gobj());
468 cairo_set_source (cr, pattern);
469 cairo_rectangle (cr, 0.0, 0.0, get_width(), get_height());
470 cairo_fill_preserve (cr);
472 cairo_set_source_rgb (cr, 0, 0, 0.0);
478 speed = _session->transport_speed ();
483 double visual_fraction = std::min (1.0f, speed/shuttle_max_speed);
484 double x = (get_width() / 2.0) + (0.5 * (get_width() * visual_fraction));
485 cairo_move_to (cr, x, 1);
486 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
487 cairo_line_to (cr, x, get_height()-1);
496 if (Config->get_shuttle_units() == Percentage) {
499 snprintf (buf, sizeof (buf), _("Playing"));
502 snprintf (buf, sizeof (buf), "<<< %d%%", (int) round (-speed * 100));
504 snprintf (buf, sizeof (buf), ">>> %d%%", (int) round (speed * 100));
511 int semi = speed_as_semitones (speed, reversed);
514 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
516 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
521 snprintf (buf, sizeof (buf), _("Stopped"));
524 last_speed_displayed = speed;
526 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
527 cairo_text_extents (cr, buf, &extents);
528 cairo_move_to (cr, 10, extents.height + 2);
529 cairo_show_text (cr, buf);
534 switch (Config->get_shuttle_behaviour()) {
536 snprintf (buf, sizeof (buf), _("Sprung"));
539 snprintf (buf, sizeof (buf), _("Wheel"));
543 cairo_text_extents (cr, buf, &extents);
545 cairo_move_to (cr, get_width() - (fabs(extents.x_advance) + 5), extents.height + 2);
546 cairo_show_text (cr, buf);
554 ShuttleControl::shuttle_unit_clicked ()
556 if (shuttle_unit_menu == 0) {
557 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
559 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
563 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
565 Config->set_shuttle_behaviour (s);
569 ShuttleControl::set_shuttle_units (ShuttleUnits s)
571 Config->set_shuttle_units (s);
575 ShuttleControl::update_speed_display ()
577 if (_session->transport_speed() != last_speed_displayed) {
582 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
583 : PBD::Controllable (X_("Shuttle"))
589 ShuttleControl::ShuttleControllable::set_id (const std::string& str)
595 ShuttleControl::ShuttleControllable::set_value (double val)
603 fract = -((0.5 - val)/0.5);
605 fract = ((val - 0.5)/0.5);
609 sc.set_shuttle_fract (fract);
613 ShuttleControl::ShuttleControllable::get_value () const
615 return sc.get_shuttle_fract ();
619 ShuttleControl::parameter_changed (std::string p)
621 if (p == "shuttle-behaviour") {
622 switch (Config->get_shuttle_behaviour ()) {
624 /* back to Sprung - reset to speed = 1.0 if playing
627 if (_session->transport_rolling()) {
628 if (_session->transport_speed() == 1.0) {
631 _session->request_transport_speed (1.0);
632 /* redraw when speed changes */
645 } else if (p == "shuttle-units") {