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"
35 using namespace Gtkmm2ext;
36 using namespace ARDOUR;
40 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
45 ShuttleControl::ShuttleControl ()
46 : _controllable (new ShuttleControllable (*this))
47 , binding_proxy (_controllable)
49 ARDOUR_UI::instance()->set_tip (*this, _("Shuttle speed control (Context-click for options)"));
52 last_shuttle_request = 0;
53 last_speed_displayed = -99999999;
54 shuttle_grabbed = false;
56 shuttle_max_speed = 8.0f;
57 shuttle_style_menu = 0;
58 shuttle_unit_menu = 0;
59 shuttle_context_menu = 0;
61 set_flags (CAN_FOCUS);
62 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);
63 set_size_request (100, 15);
64 set_name (X_("ShuttleControl"));
66 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, ui_bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
68 /* gtkmm 2.4: the C++ wrapper doesn't work */
69 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
70 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
73 ShuttleControl::~ShuttleControl ()
75 cairo_pattern_destroy (pattern);
79 ShuttleControl::set_session (Session *s)
81 SessionHandlePtr::set_session (s);
85 _session->add_controllable (_controllable);
87 set_sensitive (false);
92 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
95 cairo_pattern_destroy (pattern);
99 pattern = cairo_pattern_create_linear (0, 0, alloc.get_width(), alloc.get_height());
101 /* add 3 color stops */
103 cairo_pattern_add_color_stop_rgb (pattern, 0.0, 0, 0, 0);
104 cairo_pattern_add_color_stop_rgb (pattern, 0.5, 0.0, 0.0, 1.0);
105 cairo_pattern_add_color_stop_rgb (pattern, 1.0, 0, 0, 0);
107 DrawingArea::on_size_allocate (alloc);
111 ShuttleControl::map_transport_state ()
113 float speed = _session->transport_speed ();
115 if (fabs(speed) <= (2*DBL_EPSILON)) {
118 if (Config->get_shuttle_units() == Semitones) {
120 int semi = speed_as_semitones (speed, reverse);
121 shuttle_fract = semitones_as_fract (semi, reverse);
123 shuttle_fract = speed/shuttle_max_speed;
131 ShuttleControl::build_shuttle_context_menu ()
133 using namespace Menu_Helpers;
135 shuttle_context_menu = new Menu();
136 MenuList& items = shuttle_context_menu->items();
138 Menu* speed_menu = manage (new Menu());
139 MenuList& speed_items = speed_menu->items();
141 Menu* units_menu = manage (new Menu);
142 MenuList& units_items = units_menu->items();
143 RadioMenuItem::Group units_group;
145 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
146 if (Config->get_shuttle_units() == Percentage) {
147 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
149 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
150 if (Config->get_shuttle_units() == Semitones) {
151 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
153 items.push_back (MenuElem (_("Units"), *units_menu));
155 Menu* style_menu = manage (new Menu);
156 MenuList& style_items = style_menu->items();
157 RadioMenuItem::Group style_group;
159 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
160 if (Config->get_shuttle_behaviour() == Sprung) {
161 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
163 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
164 if (Config->get_shuttle_behaviour() == Wheel) {
165 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
168 items.push_back (MenuElem (_("Mode"), *style_menu));
170 RadioMenuItem::Group speed_group;
172 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
173 if (shuttle_max_speed == 8.0) {
174 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
176 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
177 if (shuttle_max_speed == 6.0) {
178 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
180 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
181 if (shuttle_max_speed == 4.0) {
182 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
184 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
185 if (shuttle_max_speed == 3.0) {
186 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
188 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
189 if (shuttle_max_speed == 2.0) {
190 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
192 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
193 if (shuttle_max_speed == 1.5) {
194 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
197 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
202 ShuttleControl::show_shuttle_context_menu ()
204 if (shuttle_context_menu == 0) {
205 build_shuttle_context_menu ();
208 shuttle_context_menu->popup (1, gtk_get_current_event_time());
212 ShuttleControl::set_shuttle_max_speed (float speed)
214 shuttle_max_speed = speed;
218 ShuttleControl::on_button_press_event (GdkEventButton* ev)
224 if (binding_proxy.button_press_handler (ev)) {
228 if (Keyboard::is_context_menu_event (ev)) {
229 show_shuttle_context_menu ();
233 switch (ev->button) {
236 shuttle_grabbed = true;
237 mouse_shuttle (ev->x, true);
250 ShuttleControl::on_button_release_event (GdkEventButton* ev)
256 switch (ev->button) {
258 shuttle_grabbed = false;
259 remove_modal_grab ();
261 if (Config->get_shuttle_behaviour() == Sprung) {
262 if (_session->config.get_auto_play()) {
263 _session->request_transport_speed (1.0);
265 _session->request_transport_speed (0.0);
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") {