2 * Copyright (C) 2011-2016 Paul Davis <paul@linuxaudiosystems.com>
3 * Copyright (C) 2011 Carl Hetherington <carl@carlh.net>
4 * Copyright (C) 2011 David Robillard <d@drobilla.net>
5 * Copyright (C) 2012-2019 Robin Gareus <robin@gareus.org>
6 * Copyright (C) 2015 Tim Mayberry <mojofunk@gmail.com>
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 #define BASELINESTRETCH (1.25)
29 #include "ardour/ardour.h"
30 #include "ardour/audioengine.h"
31 #include "ardour/rc_configuration.h"
32 #include "ardour/session.h"
34 #include "gtkmm2ext/colors.h"
35 #include "gtkmm2ext/keyboard.h"
36 #include "gtkmm2ext/gui_thread.h"
37 #include "gtkmm2ext/utils.h"
38 #include "gtkmm2ext/rgb_macros.h"
40 #include "widgets/tooltips.h"
43 #include "rgb_macros.h"
44 #include "shuttle_control.h"
49 using namespace Gtkmm2ext;
50 using namespace ARDOUR;
51 using namespace ArdourWidgets;
55 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
60 ShuttleControl::ShuttleControl ()
61 : _controllable (new ShuttleControllable (*this))
62 , binding_proxy (_controllable)
64 _info_button.set_layout_font (UIConfiguration::instance().get_NormalFont());
65 _info_button.set_sizing_text (S_("LogestShuttle|< +00 st"));
66 _info_button.set_name ("shuttle text");
67 _info_button.set_sensitive (false);
68 _info_button.set_visual_state (Gtkmm2ext::NoVisualState);
69 _info_button.set_elements (ArdourButton::Text);
71 set_tooltip (*this, _("Shuttle speed control (Context-click for options)"));
75 last_shuttle_request = 0;
76 last_speed_displayed = -99999999;
77 shuttle_grabbed = false;
78 shuttle_speed_on_grab = 0;
80 shuttle_max_speed = Config->get_max_transport_speed();
81 shuttle_context_menu = 0;
84 set_flags (CAN_FOCUS);
85 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);
86 set_name (X_("ShuttleControl"));
90 shuttle_max_speed = Config->get_shuttle_max_speed();
92 if (shuttle_max_speed >= Config->get_max_transport_speed()) { shuttle_max_speed = Config->get_max_transport_speed(); }
93 else if (shuttle_max_speed >= 6.f) { shuttle_max_speed = 6.0f; }
94 else if (shuttle_max_speed >= 4.f) { shuttle_max_speed = 4.0f; }
95 else if (shuttle_max_speed >= 3.f) { shuttle_max_speed = 3.0f; }
96 else if (shuttle_max_speed >= 2.f) { shuttle_max_speed = 2.0f; }
97 else { shuttle_max_speed = 1.5f; }
99 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
100 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &ShuttleControl::set_colors));
104 /* gtkmm 2.4: the C++ wrapper doesn't work */
105 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
106 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
109 ShuttleControl::~ShuttleControl ()
111 cairo_pattern_destroy (pattern);
112 cairo_pattern_destroy (shine_pattern);
113 delete shuttle_context_menu;
117 ShuttleControl::set_session (Session *s)
119 SessionHandlePtr::set_session (s);
122 set_sensitive (true);
123 _session->add_controllable (_controllable);
125 set_sensitive (false);
130 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
133 cairo_pattern_destroy (pattern);
135 cairo_pattern_destroy (shine_pattern);
139 CairoWidget::on_size_allocate ( alloc);
142 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
143 uint32_t col = UIConfiguration::instance().color ("shuttle");
145 UINT_TO_RGBA(col, &r, &g, &b, &a);
146 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
147 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
148 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
151 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
152 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
153 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
154 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
158 ShuttleControl::map_transport_state ()
165 speed = _session->actual_speed ();
168 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
169 && !( speed == 1.f && last_speed_displayed != 1.f)
170 && !( speed == 0.f && last_speed_displayed != 0.f)
173 return; // nothing to see here, move along.
176 // Q: is there a good reason why we re-calculate this every time?
177 if (fabs(speed) <= (2*DBL_EPSILON)) {
180 if (Config->get_shuttle_units() == Semitones) {
182 int semi = speed_as_semitones (speed, reverse);
183 shuttle_fract = semitones_as_fract (semi, reverse);
185 shuttle_fract = speed/shuttle_max_speed;
193 ShuttleControl::build_shuttle_context_menu ()
195 using namespace Menu_Helpers;
197 shuttle_context_menu = new Menu();
198 MenuList& items = shuttle_context_menu->items();
200 Menu* speed_menu = manage (new Menu());
201 MenuList& speed_items = speed_menu->items();
203 Menu* units_menu = manage (new Menu);
204 MenuList& units_items = units_menu->items();
205 RadioMenuItem::Group units_group;
207 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
208 if (Config->get_shuttle_units() == Percentage) {
209 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
211 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
212 if (Config->get_shuttle_units() == Semitones) {
213 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
215 items.push_back (MenuElem (_("Units"), *units_menu));
217 Menu* style_menu = manage (new Menu);
218 MenuList& style_items = style_menu->items();
219 RadioMenuItem::Group style_group;
221 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
222 if (Config->get_shuttle_behaviour() == Sprung) {
223 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
225 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
226 if (Config->get_shuttle_behaviour() == Wheel) {
227 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
230 items.push_back (MenuElem (_("Mode"), *style_menu));
232 RadioMenuItem::Group speed_group;
234 /* XXX this code assumes that Config->get_max_transport_speed() returns 8 */
236 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
237 if (shuttle_max_speed == 8.0) {
238 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
240 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
241 if (shuttle_max_speed == 6.0) {
242 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
244 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
245 if (shuttle_max_speed == 4.0) {
246 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
248 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
249 if (shuttle_max_speed == 3.0) {
250 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
252 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
253 if (shuttle_max_speed == 2.0) {
254 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
256 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
257 if (shuttle_max_speed == 1.5) {
258 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
261 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
263 items.push_back (SeparatorElem ());
264 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
268 ShuttleControl::reset_speed ()
274 if (_session->transport_rolling()) {
275 _session->request_transport_speed (1.0, true);
277 _session->request_transport_speed (0.0, true);
282 ShuttleControl::set_shuttle_max_speed (float speed)
284 Config->set_shuttle_max_speed (speed);
285 shuttle_max_speed = speed;
286 last_speed_displayed = -99999999;
290 ShuttleControl::on_button_press_event (GdkEventButton* ev)
296 if (binding_proxy.button_press_handler (ev)) {
300 if (Keyboard::is_context_menu_event (ev)) {
301 if (shuttle_context_menu == 0) {
302 build_shuttle_context_menu ();
304 shuttle_context_menu->popup (ev->button, ev->time);
308 switch (ev->button) {
310 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
311 if (_session->transport_rolling()) {
312 _session->request_transport_speed (1.0);
316 shuttle_grabbed = true;
317 shuttle_speed_on_grab = _session->actual_speed ();
318 requested_speed = shuttle_speed_on_grab;
319 mouse_shuttle (ev->x, true);
320 gdk_pointer_grab(ev->window,false,
321 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
336 ShuttleControl::on_button_release_event (GdkEventButton* ev)
342 switch (ev->button) {
344 if (shuttle_grabbed) {
345 shuttle_grabbed = false;
346 remove_modal_grab ();
347 gdk_pointer_ungrab (GDK_CURRENT_TIME);
349 if (Config->get_shuttle_behaviour() == Sprung) {
350 if (shuttle_speed_on_grab == 0 ) {
351 _session->request_stop ();
353 _session->request_transport_speed (shuttle_speed_on_grab);
356 mouse_shuttle (ev->x, true);
362 if (_session->transport_rolling()) {
363 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
377 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
383 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
385 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
389 bool semis = (Config->get_shuttle_units() == Semitones);
391 switch (ev->direction) {
393 case GDK_SCROLL_RIGHT:
395 if (shuttle_fract == 0) {
396 shuttle_fract = semitones_as_fract (1, false);
399 int st = fract_as_semitones (shuttle_fract, rev);
400 shuttle_fract = semitones_as_fract (st + 1, rev);
403 shuttle_fract += 0.00125;
406 case GDK_SCROLL_DOWN:
407 case GDK_SCROLL_LEFT:
409 if (shuttle_fract == 0) {
410 shuttle_fract = semitones_as_fract (1, true);
413 int st = fract_as_semitones (shuttle_fract, rev);
414 shuttle_fract = semitones_as_fract (st - 1, rev);
417 shuttle_fract -= 0.00125;
426 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
427 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
429 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
430 to the far side of it.
433 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
434 switch (ev->direction) {
436 case GDK_SCROLL_RIGHT:
437 shuttle_fract = upper_side_of_dead_zone;
439 case GDK_SCROLL_DOWN:
440 case GDK_SCROLL_LEFT:
441 shuttle_fract = lower_side_of_dead_zone;
444 /* impossible, checked above */
450 use_shuttle_fract (true);
456 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
458 if (!_session || !shuttle_grabbed) {
462 return mouse_shuttle (ev->x, false);
466 ShuttleControl::mouse_shuttle (double x, bool force)
468 double const center = get_width() / 2.0;
469 double distance_from_center = x - center;
471 if (distance_from_center > 0) {
472 distance_from_center = min (distance_from_center, center);
474 distance_from_center = max (distance_from_center, -center);
477 /* compute shuttle fract as expressing how far between the center
478 and the edge we are. positive values indicate we are right of
479 center, negative values indicate left of center
482 shuttle_fract = distance_from_center / center; // center == half the width
483 use_shuttle_fract (force);
488 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
491 use_shuttle_fract (false, zero_ok);
495 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
497 assert (speed != 0.0);
501 return (int) round (12.0 * fast_log2 (-speed));
504 return (int) round (12.0 * fast_log2 (speed));
509 ShuttleControl::semitones_as_speed (int semi, bool reverse)
512 return -pow (2.0, (semi / 12.0));
514 return pow (2.0, (semi / 12.0));
519 ShuttleControl::semitones_as_fract (int semi, bool reverse)
521 float speed = semitones_as_speed (semi, reverse);
522 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
526 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
528 assert (fract != 0.0);
529 return speed_as_semitones (fract * 4.0, reverse);
533 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
535 microseconds_t now = get_microseconds();
537 shuttle_fract = max (-1.0f, shuttle_fract);
538 shuttle_fract = min (1.0f, shuttle_fract);
540 /* do not attempt to submit a motion-driven transport speed request
541 more than once per process cycle.
544 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
548 last_shuttle_request = now;
552 if (Config->get_shuttle_units() == Semitones) {
553 if (shuttle_fract != 0.0) {
555 int semi = fract_as_semitones (shuttle_fract, reverse);
556 speed = semitones_as_speed (semi, reverse);
561 speed = shuttle_max_speed * shuttle_fract;
564 requested_speed = speed;
568 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
570 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
576 ShuttleControl::set_colors ()
580 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
582 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
589 ShuttleControl::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
591 cairo_t* cr = ctx->cobj();
592 // center slider line
593 float yc = get_height() / 2;
595 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
596 cairo_set_line_width (cr, 3);
597 cairo_move_to (cr, lw, yc);
598 cairo_line_to (cr, get_width () - lw, yc);
599 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
600 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
601 cairo_stroke_preserve (cr);
602 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
607 float acutal_speed = 0.0;
611 speed = _session->actual_speed ();
612 acutal_speed = speed;
613 if (shuttle_grabbed) {
614 speed = requested_speed;
619 float visual_fraction = std::max (-1.0f, std::min (1.0f, speed / shuttle_max_speed));
620 float marker_size = round (get_height() * 0.66);
621 float avail_width = get_width() - marker_size;
622 float x = 0.5 * (get_width() + visual_fraction * avail_width - marker_size);
624 rounded_rectangle (cr, x, 0, marker_size, get_height(), 5);
625 cairo_set_source_rgba (cr, 0, 0, 0, 1);
627 rounded_rectangle (cr, x + 1, 1, marker_size - 2, get_height() - 2, 3.5);
629 uint32_t col = UIConfiguration::instance().color ("shuttle");
630 Gtkmm2ext::set_source_rgba (cr, col);
632 cairo_set_source (cr, pattern);
634 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
635 cairo_fill_preserve (cr);
636 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
641 if (acutal_speed != 0) {
642 if (Config->get_shuttle_units() == Percentage) {
643 if (acutal_speed == 1.0) {
644 snprintf (buf, sizeof (buf), "%s", _("Play"));
646 if (acutal_speed < 0.0) {
647 snprintf (buf, sizeof (buf), "< %.1f%%", -acutal_speed * 100.f);
649 snprintf (buf, sizeof (buf), "> %.1f%%", acutal_speed * 100.f);
654 int semi = speed_as_semitones (acutal_speed, reversed);
656 snprintf (buf, sizeof (buf), _("< %+2d st"), semi);
658 snprintf (buf, sizeof (buf), _("> %+2d st"), semi);
662 snprintf (buf, sizeof (buf), "%s", _("Stop"));
665 last_speed_displayed = acutal_speed;
667 _info_button.set_text (buf);
670 if (UIConfiguration::instance().get_widget_prelight()) {
672 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3.5);
673 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
681 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
683 Config->set_shuttle_behaviour (s);
687 ShuttleControl::set_shuttle_units (ShuttleUnits s)
689 Config->set_shuttle_units (s);
692 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
693 : PBD::Controllable (X_("Shuttle"))
699 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
701 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
705 ShuttleControl::ShuttleControllable::get_value () const
707 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
711 ShuttleControl::parameter_changed (std::string p)
713 if (p == "shuttle-behaviour") {
714 switch (Config->get_shuttle_behaviour ()) {
716 /* back to Sprung - reset to speed = 1.0 if playing
719 if (_session->transport_rolling()) {
720 if (_session->actual_speed() == 1.0) {
723 /* reset current speed and
724 revert to 1.0 as the default
726 _session->request_transport_speed (1.0);
727 /* redraw when speed changes */
740 } else if (p == "shuttle-max-speed") {
742 } else if (p == "shuttle-units") {
749 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
753 if (UIConfiguration::instance().get_widget_prelight()) {
757 return CairoWidget::on_enter_notify_event (ev);
761 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
765 if (UIConfiguration::instance().get_widget_prelight()) {
769 return CairoWidget::on_leave_notify_event (ev);