3a0572cd69ec17fdd9043db2b1d1bf790213e930
[ardour.git] / gtk2_ardour / shuttle_control.cc
1 /*
2     Copyright (C) 2011 Paul Davis
3
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.
8
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.
13
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.
17 */
18
19 #include <algorithm>
20
21 #include <cairo/cairo.h>
22
23 #include "ardour/ardour.h"
24 #include "ardour/audioengine.h"
25 #include "ardour/rc_configuration.h"
26 #include "ardour/session.h"
27
28 #include "gtkmm2ext/keyboard.h"
29 #include "gtkmm2ext/gui_thread.h"
30
31 #include "ardour_ui.h"
32 #include "shuttle_control.h"
33
34 #include "i18n.h"
35
36 using namespace Gtk;
37 using namespace Gtkmm2ext;
38 using namespace ARDOUR;
39 using std::min;
40 using std::max;
41
42 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
43 {
44         return FALSE;
45 }
46
47 ShuttleControl::ShuttleControl ()
48         : _controllable (new ShuttleControllable (*this))
49         , binding_proxy (_controllable)
50 {
51         ARDOUR_UI::instance()->set_tip (*this, _("Shuttle speed control (Context-click for options)"));
52
53         pattern = 0;
54         last_shuttle_request = 0;
55         last_speed_displayed = -99999999;
56         shuttle_grabbed = false;
57         shuttle_speed_on_grab = 0;
58         shuttle_fract = 0.0;
59         shuttle_max_speed = 8.0f;
60         shuttle_style_menu = 0;
61         shuttle_unit_menu = 0;
62         shuttle_context_menu = 0;
63
64         set_flags (CAN_FOCUS);
65         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);
66         set_size_request (100, 15);
67         set_name (X_("ShuttleControl"));
68
69         Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, ui_bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
70
71         /* gtkmm 2.4: the C++ wrapper doesn't work */
72         g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
73         // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
74 }
75
76 ShuttleControl::~ShuttleControl ()
77 {
78         cairo_pattern_destroy (pattern);
79 }
80
81 void
82 ShuttleControl::set_session (Session *s)
83 {
84         SessionHandlePtr::set_session (s);
85
86         if (_session) {
87                 set_sensitive (true);
88                 _session->add_controllable (_controllable);
89         } else {
90                 set_sensitive (false);
91         }
92 }
93
94 void
95 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
96 {
97         if (pattern) {
98                 cairo_pattern_destroy (pattern);
99                 pattern = 0;
100         }
101
102         pattern = cairo_pattern_create_linear (0, 0, alloc.get_width(), alloc.get_height());
103         
104         /* add 3 color stops */
105
106         cairo_pattern_add_color_stop_rgb (pattern, 0.0, 0, 0, 0);
107         cairo_pattern_add_color_stop_rgb (pattern, 0.5, 0.0, 0.0, 1.0);
108         cairo_pattern_add_color_stop_rgb (pattern, 1.0, 0, 0, 0);
109
110         DrawingArea::on_size_allocate (alloc);
111 }
112
113 void
114 ShuttleControl::map_transport_state ()
115 {
116         float speed = _session->transport_speed ();
117
118         if (fabs(speed) <= (2*DBL_EPSILON)) {
119                 shuttle_fract = 0;
120         } else {
121                 if (Config->get_shuttle_units() == Semitones) {
122                         bool reverse;
123                         int semi = speed_as_semitones (speed, reverse);
124                         shuttle_fract = semitones_as_fract (semi, reverse);
125                 } else {
126                         shuttle_fract = speed/shuttle_max_speed;
127                 }
128         }
129
130         queue_draw ();
131 }
132
133 void
134 ShuttleControl::build_shuttle_context_menu ()
135 {
136         using namespace Menu_Helpers;
137
138         shuttle_context_menu = new Menu();
139         MenuList& items = shuttle_context_menu->items();
140
141         Menu* speed_menu = manage (new Menu());
142         MenuList& speed_items = speed_menu->items();
143
144         Menu* units_menu = manage (new Menu);
145         MenuList& units_items = units_menu->items();
146         RadioMenuItem::Group units_group;
147         
148         units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
149         if (Config->get_shuttle_units() == Percentage) {
150                 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
151         }
152         units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
153         if (Config->get_shuttle_units() == Semitones) {
154                 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
155         }
156         items.push_back (MenuElem (_("Units"), *units_menu));
157         
158         Menu* style_menu = manage (new Menu);
159         MenuList& style_items = style_menu->items();
160         RadioMenuItem::Group style_group;
161
162         style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
163         if (Config->get_shuttle_behaviour() == Sprung) {
164                 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
165         }
166         style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
167         if (Config->get_shuttle_behaviour() == Wheel) {
168                 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
169         }
170         
171         items.push_back (MenuElem (_("Mode"), *style_menu));
172
173         RadioMenuItem::Group speed_group;
174
175         speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
176         if (shuttle_max_speed == 8.0) {
177                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
178         }
179         speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
180         if (shuttle_max_speed == 6.0) {
181                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
182         }
183         speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
184         if (shuttle_max_speed == 4.0) {
185                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
186         }
187         speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
188         if (shuttle_max_speed == 3.0) {
189                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
190         }
191         speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
192         if (shuttle_max_speed == 2.0) {
193                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
194         }
195         speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
196         if (shuttle_max_speed == 1.5) {
197                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
198         }
199
200         items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
201         
202 }
203
204 void
205 ShuttleControl::show_shuttle_context_menu ()
206 {
207         if (shuttle_context_menu == 0) {
208                 build_shuttle_context_menu ();
209         }
210
211         shuttle_context_menu->popup (1, gtk_get_current_event_time());
212 }
213
214 void
215 ShuttleControl::set_shuttle_max_speed (float speed)
216 {
217         shuttle_max_speed = speed;
218 }
219
220 bool
221 ShuttleControl::on_button_press_event (GdkEventButton* ev)
222 {
223         if (!_session) {
224                 return true;
225         }
226
227         if (binding_proxy.button_press_handler (ev)) {
228                 return true;
229         }
230
231         if (Keyboard::is_context_menu_event (ev)) {
232                 show_shuttle_context_menu ();
233                 return true;
234         }
235
236         switch (ev->button) {
237         case 1:
238                 add_modal_grab ();
239                 shuttle_grabbed = true;
240                 shuttle_speed_on_grab = _session->transport_speed ();
241                 mouse_shuttle (ev->x, true);
242                 break;
243
244         case 2:
245         case 3:
246                 return true;
247                 break;
248         }
249
250         return true;
251 }
252
253 bool
254 ShuttleControl::on_button_release_event (GdkEventButton* ev)
255 {
256         if (!_session) {
257                 return true;
258         }
259
260         switch (ev->button) {
261         case 1:
262                 shuttle_grabbed = false;
263                 remove_modal_grab ();
264
265                 if (Config->get_shuttle_behaviour() == Sprung) {
266                         _session->request_transport_speed (shuttle_speed_on_grab);
267                 } else {
268                         mouse_shuttle (ev->x, true);
269                 }
270
271                 return true;
272
273         case 2:
274                 if (_session->transport_rolling()) {
275                         _session->request_transport_speed (1.0);
276                 } 
277                 return true;
278
279         case 3:
280         default:
281                 return true;
282
283         }
284
285         return true;
286 }
287
288 bool
289 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
290 {
291         return false;
292 }
293
294 bool
295 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
296 {
297         if (!_session || Config->get_shuttle_behaviour() != Wheel) {
298                 return true;
299         }
300
301         switch (ev->direction) {
302         case GDK_SCROLL_UP:
303         case GDK_SCROLL_RIGHT:
304                 shuttle_fract += 0.005;
305                 break;
306         case GDK_SCROLL_DOWN:
307         case GDK_SCROLL_LEFT:
308                 shuttle_fract -= 0.005;
309                 break;
310         default:
311                 return false;
312         }
313         
314         if (Config->get_shuttle_units() == Semitones) {
315
316                 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
317                 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
318
319                 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
320                    to the far side of it.
321                 */
322
323                 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
324                         switch (ev->direction) {
325                         case GDK_SCROLL_UP:
326                         case GDK_SCROLL_RIGHT:
327                                 shuttle_fract = upper_side_of_dead_zone;
328                                 break;
329                         case GDK_SCROLL_DOWN:
330                         case GDK_SCROLL_LEFT:
331                                 shuttle_fract = lower_side_of_dead_zone;
332                                 break;
333                         default:
334                                 /* impossible, checked above */
335                                 return false;
336                         }
337                 }
338         }
339
340         use_shuttle_fract (true);
341
342         return true;
343 }
344
345 bool
346 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
347 {
348         if (!_session || !shuttle_grabbed) {
349                 return true;
350         }
351
352         return mouse_shuttle (ev->x, false);
353 }
354
355 gint
356 ShuttleControl::mouse_shuttle (double x, bool force)
357 {
358         double const center = get_width() / 2.0;
359         double distance_from_center = x - center;
360
361         if (distance_from_center > 0) {
362                 distance_from_center = min (distance_from_center, center);
363         } else {
364                 distance_from_center = max (distance_from_center, -center);
365         }
366
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
370         */
371
372         shuttle_fract = distance_from_center / center; // center == half the width
373         use_shuttle_fract (force);
374         return true;
375 }
376
377 void
378 ShuttleControl::set_shuttle_fract (double f)
379 {
380         shuttle_fract = f;
381         use_shuttle_fract (false);
382 }
383
384 int
385 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
386 {
387         assert (speed != 0.0);
388
389         if (speed < 0.0) {
390                 reverse = true;
391                 return (int) round (12.0 * fast_log2 (-speed));
392         } else {
393                 reverse = false;
394                 return (int) round (12.0 * fast_log2 (speed));
395         }
396 }        
397
398 float
399 ShuttleControl::semitones_as_speed (int semi, bool reverse)
400 {
401         if (reverse) {
402                 return -pow (2.0, (semi / 12.0));
403         } else {
404                 return pow (2.0, (semi / 12.0));
405         }
406 }
407
408 float
409 ShuttleControl::semitones_as_fract (int semi, bool reverse)
410 {
411         float speed = semitones_as_speed (semi, reverse);
412         return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
413 }
414
415 int
416 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
417 {
418         assert (fract != 0.0);
419         return speed_as_semitones (fract * 4.0, reverse);
420 }
421
422 void
423 ShuttleControl::use_shuttle_fract (bool force)
424 {
425         microseconds_t now = get_microseconds();
426
427         shuttle_fract = max (-1.0f, shuttle_fract);
428         shuttle_fract = min (1.0f, shuttle_fract);
429
430         /* do not attempt to submit a motion-driven transport speed request
431            more than once per process cycle.
432         */
433
434         if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
435                 return;
436         }
437
438         last_shuttle_request = now;
439
440         double speed = 0;
441
442         if (Config->get_shuttle_units() == Semitones) {
443                 if (shuttle_fract != 0.0) {
444                         bool reverse;
445                         int semi = fract_as_semitones (shuttle_fract, reverse);
446                         speed = semitones_as_speed (semi, reverse);
447                 } else {
448                         speed = 0.0;
449                 }
450         } else {
451                 speed = shuttle_max_speed * shuttle_fract;
452         }
453
454         _session->request_transport_speed_nonzero (speed);
455 }
456
457 bool
458 ShuttleControl::on_expose_event (GdkEventExpose* event)
459 {
460         cairo_text_extents_t extents;
461         Glib::RefPtr<Gdk::Window> win (get_window());
462         Glib::RefPtr<Gtk::Style> style (get_style());
463
464         cairo_t* cr = gdk_cairo_create (win->gobj());   
465
466         cairo_set_source (cr, pattern);
467         cairo_rectangle (cr, 0.0, 0.0, get_width(), get_height());
468         cairo_fill_preserve (cr);
469
470         cairo_set_source_rgb (cr, 0, 0, 0.0);
471         cairo_stroke (cr);
472
473         float speed = 0.0;
474
475         if (_session) {
476                 speed = _session->transport_speed ();
477         }
478
479         /* Marker */
480
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);
486         cairo_stroke (cr);
487
488         /* speed text */
489
490         char buf[32];
491
492         if (speed != 0) {
493
494                 if (Config->get_shuttle_units() == Percentage) {
495
496                         if (speed == 1.0) {
497                                 snprintf (buf, sizeof (buf), _("Playing"));
498                         } else {
499                                 if (speed < 0.0) {
500                                         snprintf (buf, sizeof (buf), "<<< %d%%", (int) round (-speed * 100));
501                                 } else {
502                                         snprintf (buf, sizeof (buf), ">>> %d%%", (int) round (speed * 100));
503                                 }
504                         }
505
506                 } else {
507
508                         bool reversed;
509                         int semi = speed_as_semitones (speed, reversed);
510
511                         if (reversed) {
512                                 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
513                         } else {
514                                 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
515                         }
516                 }
517
518         } else {
519                 snprintf (buf, sizeof (buf), _("Stopped"));
520         }
521
522         last_speed_displayed = speed;
523
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);
528
529         /* style text */
530
531
532         switch (Config->get_shuttle_behaviour()) {
533         case Sprung:
534                 snprintf (buf, sizeof (buf), _("Sprung"));
535                 break;
536         case Wheel:
537                 snprintf (buf, sizeof (buf), _("Wheel"));
538                 break;
539         }
540
541         cairo_text_extents (cr, buf, &extents);
542
543         cairo_move_to (cr, get_width() - (fabs(extents.x_advance) + 5), extents.height + 2);
544         cairo_show_text (cr, buf);
545
546         cairo_destroy (cr);
547
548         return true;
549 }
550
551 void
552 ShuttleControl::shuttle_unit_clicked ()
553 {
554         if (shuttle_unit_menu == 0) {
555                 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
556         }
557         shuttle_unit_menu->popup (1, gtk_get_current_event_time());
558 }
559
560 void
561 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
562 {
563         Config->set_shuttle_behaviour (s);
564 }
565
566 void
567 ShuttleControl::set_shuttle_units (ShuttleUnits s)
568 {
569         Config->set_shuttle_units (s);
570 }
571
572 void
573 ShuttleControl::update_speed_display ()
574 {
575         if (_session->transport_speed() != last_speed_displayed) {
576                 queue_draw ();
577         }
578 }
579       
580 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
581         : PBD::Controllable (X_("Shuttle")) 
582         , sc (s)
583 {
584 }
585
586 void
587 ShuttleControl::ShuttleControllable::set_id (const std::string& str)
588 {
589         _id = str;
590 }
591
592 void
593 ShuttleControl::ShuttleControllable::set_value (double val)
594 {
595         double fract;
596         
597         if (val == 0.5) {
598                 fract = 0.0;
599         } else {
600                 if (val < 0.5) {
601                         fract = -((0.5 - val)/0.5);
602                 } else {
603                         fract = ((val - 0.5)/0.5);
604                 }
605         }
606
607         sc.set_shuttle_fract (fract);
608 }
609
610 double 
611 ShuttleControl::ShuttleControllable::get_value () const
612 {
613         return sc.get_shuttle_fract ();
614 }
615
616 void
617 ShuttleControl::parameter_changed (std::string p)
618 {
619         if (p == "shuttle-behaviour") {
620                 switch (Config->get_shuttle_behaviour ()) {
621                 case Sprung:
622                         /* back to Sprung - reset to speed = 1.0 if playing
623                          */
624                         if (_session) {
625                                 if (_session->transport_rolling()) {
626                                         if (_session->transport_speed() == 1.0) {
627                                                 queue_draw ();
628                                         } else {
629                                                 _session->request_transport_speed (1.0);
630                                                 /* redraw when speed changes */
631                                         }
632                                 } else {
633                                         queue_draw ();
634                                 }
635                         }
636                         break;
637
638                 case Wheel:
639                         queue_draw ();
640                         break;
641                 }
642                         
643         } else if (p == "shuttle-units") {
644                 queue_draw ();
645         }
646 }