1 /*
2  * Copyright (C) 2017-2018 Robin Gareus <robin@gareus.org>
3  * Copyright (C) 2018 Ben Loftis <ben@harrisonconsoles.com>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19 
20 /* Faderport 8 Control Surface
21  * This is the button "Controller" of the MVC surface inteface,
22  * see callbacks.cc for the "View".
23  */
24 
25 #include "ardour/dB.h"
26 #include "ardour/plugin_insert.h"
27 #include "ardour/session.h"
28 #include "ardour/session_configuration.h"
29 #include "ardour/track.h"
30 #include "ardour/types.h"
31 
32 #include "gtkmm2ext/actions.h"
33 
34 #include "faderport8.h"
35 
36 #include "pbd/i18n.h"
37 
38 using namespace std;
39 using namespace ARDOUR;
40 using namespace ArdourSurface::FP_NAMESPACE;
41 using namespace ArdourSurface::FP_NAMESPACE::FP8Types;
42 
43 #define BindMethod(ID, CB) \
44 	_ctrls.button (FP8Controls::ID).released.connect_same_thread (button_connections, boost::bind (&FaderPort8:: CB, this));
45 
46 #define BindMethod2(ID, ACT, CB) \
47 	_ctrls.button (FP8Controls::ID). ACT .connect_same_thread (button_connections, boost::bind (&FaderPort8:: CB, this));
48 
49 #define BindFunction(ID, ACT, CB, ...) \
50 	_ctrls.button (FP8Controls::ID). ACT .connect_same_thread (button_connections, boost::bind (&FaderPort8:: CB, this, __VA_ARGS__));
51 
52 #define BindAction(ID, GRP, ITEM) \
53 	_ctrls.button (FP8Controls::ID).released.connect_same_thread (button_connections, boost::bind (&FaderPort8::button_action, this, GRP, ITEM));
54 
55 #define BindUserAction(ID) \
56 	_ctrls.button (ID).pressed.connect_same_thread (button_connections, boost::bind (&FaderPort8::button_user, this, true, ID)); \
57 _ctrls.button (ID).released.connect_same_thread (button_connections, boost::bind (&FaderPort8::button_user, this, false, ID));
58 
59 
60 /* Bind button signals (press, release) to callback methods
61  * (called once after constructing buttons).
62  * Bound actions are handled the the ctrl-surface thread.
63  */
64 void
setup_actions()65 FaderPort8::setup_actions ()
66 {
67 	BindMethod2 (BtnPlay, pressed, button_play);
68 	BindMethod2 (BtnStop, pressed, button_stop);
69 	BindMethod2 (BtnLoop, pressed, button_loop);
70 	BindMethod2 (BtnRecord, pressed, button_record);
71 	BindMethod2 (BtnClick, pressed, button_metronom);
72 	BindAction (BtnRedo, "Editor", "redo");
73 
74 	BindAction (BtnSave, "Common", "Save");
75 	BindAction (BtnUndo, "Editor", "undo");
76 	BindAction (BtnRedo, "Editor", "redo");
77 
78 #ifdef FP8_MUTESOLO_UNDO
79 	BindMethod (BtnSoloClear, button_solo_clear);
80 #else
81 	BindAction (BtnSoloClear, "Main", "cancel-solo");
82 #endif
83 	BindMethod (BtnMuteClear, button_mute_clear);
84 
85 	BindMethod (FP8Controls::BtnArmAll, button_arm_all);
86 
87 	BindFunction (BtnRewind, pressed, button_varispeed, false);
88 	BindFunction (BtnFastForward, pressed, button_varispeed, true);
89 
90 	BindFunction (BtnPrev, released, button_prev_next, false);
91 	BindFunction (BtnNext, released, button_prev_next, true);
92 
93 	BindFunction (BtnArm, pressed, button_arm, true);
94 	BindFunction (BtnArm, released, button_arm, false);
95 
96 	BindFunction (BtnAOff, released, button_automation, ARDOUR::Off);
97 	BindFunction (BtnATouch, released, button_automation, ARDOUR::Touch);
98 	BindFunction (BtnARead, released, button_automation, ARDOUR::Play);
99 	BindFunction (BtnAWrite, released, button_automation, ARDOUR::Write);
100 	BindFunction (BtnALatch, released, button_automation, ARDOUR::Latch);
101 
102 	_ctrls.button (FP8Controls::BtnEncoder).pressed.connect_same_thread (button_connections, boost::bind (&FaderPort8::button_encoder, this));
103 #ifdef FADERPORT2
104 	_ctrls.button (FP8Controls::BtnParam).pressed.connect_same_thread (button_connections, boost::bind (&FaderPort8::button_encoder, this));
105 #else
106 	_ctrls.button (FP8Controls::BtnParam).pressed.connect_same_thread (button_connections, boost::bind (&FaderPort8::button_parameter, this));
107 #endif
108 
109 
110 	BindMethod (BtnBypass, button_bypass);
111 	BindAction (BtnBypassAll, "Mixer", "ab-plugins");
112 
113 	BindAction (BtnMacro, "Common", "toggle-editor-and-mixer");
114 	BindMethod (BtnOpen, button_open);
115 
116 	BindMethod (BtnLink, button_link);
117 	BindMethod (BtnLock, button_lock);
118 
119 #ifdef FADERPORT2
120 	BindMethod (BtnChanLock, button_chanlock);
121 	BindMethod (BtnFlip, button_flip);
122 #endif
123 
124 	// user-specific
125 	for (FP8Controls::UserButtonMap::const_iterator i = _ctrls.user_buttons ().begin ();
126 			i != _ctrls.user_buttons ().end (); ++i) {
127 		BindUserAction ((*i).first);
128 	}
129 }
130 
131 /* ****************************************************************************
132  * Direct control callback Actions
133  */
134 
135 void
button_play()136 FaderPort8::button_play ()
137 {
138 	if (transport_rolling ()) {
139 		if (get_transport_speed() != 1.0) {
140 			session->request_roll (TRS_UI);
141 		} else {
142 			transport_stop ();
143 		}
144 	} else {
145 		transport_play ();
146 	}
147 }
148 
149 void
button_stop()150 FaderPort8::button_stop ()
151 {
152 	if (transport_rolling ()) {
153 		transport_stop ();
154 	} else {
155 		AccessAction ("Transport", "GotoStart");
156 	}
157 }
158 
159 void
button_record()160 FaderPort8::button_record ()
161 {
162 	set_record_enable (!get_record_enabled ());
163 }
164 
165 void
button_loop()166 FaderPort8::button_loop ()
167 {
168 	loop_toggle ();
169 }
170 
171 void
button_metronom()172 FaderPort8::button_metronom ()
173 {
174 	Config->set_clicking (!Config->get_clicking ());
175 }
176 
177 void
button_bypass()178 FaderPort8::button_bypass ()
179 {
180 	boost::shared_ptr<PluginInsert> pi = _plugin_insert.lock();
181 	if (pi) {
182 		pi->enable (! pi->enabled ());
183 	} else {
184 		AccessAction ("Mixer", "ab-plugins");
185 	}
186 }
187 
188 void
button_open()189 FaderPort8::button_open ()
190 {
191 	boost::shared_ptr<PluginInsert> pi = _plugin_insert.lock();
192 	if (pi) {
193 		pi->ToggleUI (); /* EMIT SIGNAL */
194 	} else {
195 		AccessAction ("Common", "addExistingAudioFiles");
196 	}
197 }
198 
199 void
button_chanlock()200 FaderPort8::button_chanlock ()
201 {
202 	_chan_locked = !_chan_locked;
203 
204 	_ctrls.button (FP8Controls::BtnChannel).set_blinking (_chan_locked);
205 }
206 
207 void
button_flip()208 FaderPort8::button_flip ()
209 {
210 }
211 
212 void
button_lock()213 FaderPort8::button_lock ()
214 {
215 	if (!_link_enabled) {
216 		AccessAction ("Editor", "lock");
217 		return;
218 	}
219 	if (_link_locked) {
220 		unlock_link ();
221 	} else if (!_link_control.expired ()) {
222 		lock_link ();
223 	}
224 }
225 
226 void
button_link()227 FaderPort8::button_link ()
228 {
229 	switch (_ctrls.fader_mode()) {
230 		case ModeTrack:
231 		case ModePan:
232 			if (_link_enabled) {
233 				stop_link ();
234 			} else {
235 				start_link ();
236 			}
237 			break;
238 		default:
239 			//AccessAction ("Window", "show-mixer");
240 			break;
241 	}
242 }
243 
244 void
button_automation(ARDOUR::AutoState as)245 FaderPort8::button_automation (ARDOUR::AutoState as)
246 {
247 	FaderMode fadermode = _ctrls.fader_mode ();
248 	switch (fadermode) {
249 		case ModePlugins:
250 #if 0 // Plugin Control Automation Mode
251 			for (std::list <ProcessorCtrl>::iterator i = _proc_params.begin(); i != _proc_params.end(); ++i) {
252 				((*i).ac)->set_automation_state (as);
253 			}
254 #endif
255 			return;
256 		case ModeSend:
257 			if (first_selected_stripable()) {
258 #if 0 // Send Level Automation
259 				boost::shared_ptr<Stripable> s = first_selected_stripable();
260 				boost::shared_ptr<AutomationControl> send;
261 				uint32_t i = 0;
262 				while (0 != (send = s->send_level_controllable (i))) {
263 					send->set_automation_state (as);
264 					++i;
265 				}
266 #endif
267 			}
268 			return;
269 		default:
270 			break;
271 	}
272 
273 	// TODO link/lock control automation?
274 
275 	// apply to all selected tracks
276 	StripableList all;
277 	session->get_stripables (all);
278 	for (StripableList::const_iterator i = all.begin(); i != all.end(); ++i) {
279 		if ((*i)->is_master() || (*i)->is_monitor()) {
280 			continue;
281 		}
282 		if (!(*i)->is_selected()) {
283 			continue;
284 		}
285 		boost::shared_ptr<AutomationControl> ac;
286 		switch (fadermode) {
287 			case ModeTrack:
288 				ac = (*i)->gain_control ();
289 				break;
290 			case ModePan:
291 				ac = (*i)->pan_azimuth_control ();
292 				break;
293 			default:
294 				break;
295 		}
296 		if (ac) {
297 			ac->set_automation_state (as);
298 		}
299 	}
300 }
301 
302 void
button_varispeed(bool ffw)303 FaderPort8::button_varispeed (bool ffw)
304 {
305 	/* pressing both rew + ffwd -> return to zero */
306 	FP8ButtonInterface& b_rew = _ctrls.button (FP8Controls::BtnRewind);
307 	FP8ButtonInterface& b_ffw = _ctrls.button (FP8Controls::BtnFastForward);
308 	if (b_rew.is_pressed () && b_ffw.is_pressed ()){
309 		// stop key-repeat
310 		dynamic_cast<FP8RepeatButton*>(&b_ffw)->stop_repeat();
311 		dynamic_cast<FP8RepeatButton*>(&b_rew)->stop_repeat();
312 		session->request_locate (0, MustStop);
313 		return;
314 	}
315 
316 	BasicUI::button_varispeed (ffw);
317 }
318 
319 #ifdef FP8_MUTESOLO_UNDO
320 void
button_solo_clear()321 FaderPort8::button_solo_clear ()
322 {
323 	bool soloing = session->soloing() || session->listening();
324 #ifdef MIXBUS
325 	soloing |= session->mixbus_soloed();
326 #endif
327 	if (soloing) {
328 		StripableList all;
329 		session->get_stripables (all);
330 		for (StripableList::const_iterator i = all.begin(); i != all.end(); ++i) {
331 			if ((*i)->is_master() || (*i)->is_auditioner() || (*i)->is_monitor()) {
332 				continue;
333 			}
334 			boost::shared_ptr<SoloControl> sc = (*i)->solo_control();
335 			if (sc && sc->self_soloed ()) {
336 				_solo_state.push_back (boost::weak_ptr<AutomationControl>(sc));
337 			}
338 		}
339 		cancel_all_solo (); // AccessAction ("Main", "cancel-solo");
340 	} else {
341 		/* restore solo */
342 		boost::shared_ptr<ControlList> cl (new ControlList);
343 		for (std::vector <boost::weak_ptr<AutomationControl> >::const_iterator i = _solo_state.begin(); i != _solo_state.end(); ++i) {
344 			boost::shared_ptr<AutomationControl> ac = (*i).lock();
345 			if (!ac) {
346 				continue;
347 			}
348 			ac->start_touch (ac->session().transport_sample());
349 			cl->push_back (ac);
350 		}
351 		if (!cl->empty()) {
352 			session->set_controls (cl, 1.0, PBD::Controllable::NoGroup);
353 		}
354 	}
355 }
356 #endif
357 
358 void
button_mute_clear()359 FaderPort8::button_mute_clear ()
360 {
361 #ifdef FP8_MUTESOLO_UNDO
362 	if (session->muted ()) {
363 		_mute_state = session->cancel_all_mute ();
364 	} else {
365 		/* restore mute */
366 		boost::shared_ptr<ControlList> cl (new ControlList);
367 		for (std::vector <boost::weak_ptr<AutomationControl> >::const_iterator i = _mute_state.begin(); i != _mute_state.end(); ++i) {
368 			boost::shared_ptr<AutomationControl> ac = (*i).lock();
369 			if (!ac) {
370 				continue;
371 			}
372 			cl->push_back (ac);
373 			ac->start_touch (ac->session().transport_sample());
374 		}
375 		if (!cl->empty()) {
376 			session->set_controls (cl, 1.0, PBD::Controllable::NoGroup);
377 		}
378 	}
379 #else
380 	session->cancel_all_mute ();
381 #endif
382 }
383 
384 void
button_arm_all()385 FaderPort8::button_arm_all ()
386 {
387 	BasicUI::all_tracks_rec_in ();
388 }
389 
390 /* access generic action */
391 void
button_action(const std::string & group,const std::string & item)392 FaderPort8::button_action (const std::string& group, const std::string& item)
393 {
394 	AccessAction (group, item);
395 }
396 
397 /* ****************************************************************************
398  * Control Interaction (encoder)
399  */
400 
401 void
handle_encoder_pan(int steps)402 FaderPort8::handle_encoder_pan (int steps)
403 {
404 	boost::shared_ptr<Stripable> s = first_selected_stripable();
405 	if (s) {
406 		boost::shared_ptr<AutomationControl> ac;
407 		if (shift_mod () || _ctrls.fader_mode() == ModePan) {
408 			ac = s->pan_width_control ();
409 		} else {
410 			ac = s->pan_azimuth_control ();
411 		}
412 		if (ac) {
413 			ac->start_touch (ac->session().transport_sample());
414 			if (steps == 0) {
415 				ac->set_value (ac->normal(), PBD::Controllable::UseGroup);
416 			} else {
417 				double v = ac->internal_to_interface (ac->get_value(), true);
418 				v = std::max (0.0, std::min (1.0, v + steps * .01));
419 				ac->set_value (ac->interface_to_internal(v, true), PBD::Controllable::UseGroup);
420 			}
421 		}
422 	}
423 }
424 
425 void
handle_encoder_link(int steps)426 FaderPort8::handle_encoder_link (int steps)
427 {
428 	if (_link_control.expired ()) {
429 		return;
430 	}
431 	boost::shared_ptr<AutomationControl> ac = boost::dynamic_pointer_cast<AutomationControl> (_link_control.lock ());
432 	if (!ac) {
433 		return;
434 	}
435 
436 	double v = ac->internal_to_interface (ac->get_value(), true);
437 	ac->start_touch (ac->session().transport_sample());
438 
439 	if (steps == 0) {
440 		ac->set_value (ac->normal(), PBD::Controllable::UseGroup);
441 		return;
442 	}
443 
444 	if (ac->desc().toggled) {
445 		v = v > 0 ? 0. : 1.;
446 	} else if (ac->desc().integer_step) {
447 		v += steps / (1.f + ac->desc().upper - ac->desc().lower);
448 	} else if (ac->desc().enumeration) {
449 		ac->set_value (ac->desc().step_enum (ac->get_value(), steps < 0), PBD::Controllable::UseGroup);
450 		return;
451 	} else {
452 		v = std::max (0.0, std::min (1.0, v + steps * .01));
453 	}
454 	ac->set_value (ac->interface_to_internal(v, true), PBD::Controllable::UseGroup);
455 }
456 
457 
458 /* ****************************************************************************
459  * Mode specific and internal callbacks
460  */
461 
462 /* handle "ARM" press -- act like shift, change "Select" button mode */
463 void
button_arm(bool press)464 FaderPort8::button_arm (bool press)
465 {
466 #ifdef FADERPORT2
467 	boost::shared_ptr<Stripable> s = first_selected_stripable();
468 	if (press && s) {
469 		boost::shared_ptr<Track> t = boost::dynamic_pointer_cast<Track>(s);
470 		if (t) {
471 			t->rec_enable_control()->set_value (!t->rec_enable_control()->get_value(), PBD::Controllable::UseGroup);
472 		}
473 	}
474 #else
475 	FaderMode fadermode = _ctrls.fader_mode ();
476 	if (fadermode == ModeTrack || fadermode == ModePan) {
477 		_ctrls.button (FP8Controls::BtnArm).set_active (press);
478 		ARMButtonChange (press); /* EMIT SIGNAL */
479 	}
480 #endif
481 }
482 
483 void
button_prev_next(bool next)484 FaderPort8::button_prev_next (bool next)
485 {
486 	switch (_ctrls.nav_mode()) {
487 		case NavChannel:
488 #ifndef FADERPORT2
489 			select_prev_next (next);
490 			break;
491 #endif
492 		case NavMaster:
493 		case NavScroll:
494 		case NavPan:
495 			bank (!next, false);
496 			break;
497 		case NavBank:
498 			bank (!next, true);
499 			break;
500 		case NavZoom:
501 			if (next) {
502 				VerticalZoomInSelected ();
503 			} else {
504 				VerticalZoomOutSelected ();
505 			}
506 			break;
507 		case NavSection:
508 			if (next) {
509 				AccessAction ("Region", "nudge-forward");
510 			} else {
511 				AccessAction ("Region", "nudge-backward");
512 			}
513 			break;
514 		case NavMarker:
515 			if (next) {
516 				next_marker ();
517 			} else {
518 				prev_marker ();
519 			}
520 			break;
521 	}
522 }
523 
524 /* handle navigation encoder press */
525 void
button_encoder()526 FaderPort8::button_encoder ()
527 {
528 	/* special-case metronome level */
529 	if (_ctrls.button (FP8Controls::BtnClick).is_pressed ()) {
530 		Config->set_click_gain (1.0);
531 		_ctrls.button (FP8Controls::BtnClick).ignore_release();
532 		return;
533 	}
534 	switch (_ctrls.nav_mode()) {
535 		case NavZoom:
536 			ZoomToSession (); // XXX undo zoom
537 			break;
538 		case NavScroll:
539 			ZoomToSession ();
540 			break;
541 		case NavChannel:
542 			AccessAction ("Editor", "select-topmost");
543 			break;
544 		case NavBank:
545 			move_selected_into_view ();
546 			break;
547 		case NavMaster:
548 			{
549 				/* master || monitor level -- reset to 0dB */
550 				boost::shared_ptr<AutomationControl> ac;
551 				if (session->monitor_active() && !_ctrls.button (FP8Controls::BtnMaster).is_pressed ()) {
552 					ac = session->monitor_out()->gain_control ();
553 				} else if (session->master_out()) {
554 					ac = session->master_out()->gain_control ();
555 				}
556 				if (ac) {
557 					ac->start_touch (ac->session().transport_sample());
558 					ac->set_value (ac->normal(), PBD::Controllable::NoGroup);
559 				}
560 			}
561 			break;
562 		case NavPan:
563 			break;
564 		case NavSection:
565 			// TODO nudge
566 			break;
567 		case NavMarker:
568 			{
569 				string markername;
570 				/* Don't add another mark if one exists within 1/100th of a second of
571 				 * the current position and we're not rolling.
572 				 */
573 				samplepos_t where = session->audible_sample();
574 				if (session->transport_stopped_or_stopping() && session->locations()->mark_at (where, session->sample_rate() / 100.0)) {
575 					return;
576 				}
577 
578 				session->locations()->next_available_name (markername,"mark");
579 				add_marker (markername);
580 			}
581 			break;
582 	}
583 }
584 
585 /* handle navigation encoder turn */
586 void
encoder_navigate(bool neg,int steps)587 FaderPort8::encoder_navigate (bool neg, int steps)
588 {
589 	/* special-case metronome level */
590 	if (_ctrls.button (FP8Controls::BtnClick).is_pressed ()) {
591 		// compare to ARDOUR_UI::click_button_scroll()
592 		gain_t gain = Config->get_click_gain();
593 		float gain_db = accurate_coefficient_to_dB (gain);
594 		gain_db += (neg ? -1.f : 1.f) * steps;
595 		gain_db = std::max (-60.f, gain_db);
596 		gain = dB_to_coefficient (gain_db);
597 		gain = std::min (gain, Config->get_max_gain());
598 		Config->set_click_gain (gain);
599 		_ctrls.button (FP8Controls::BtnClick).ignore_release();
600 		return;
601 	}
602 
603 	switch (_ctrls.nav_mode()) {
604 		case NavChannel:
605 			if (neg) {
606 				AccessAction ("Mixer", "scroll-left");
607 				AccessAction ("Editor", "step-tracks-up");
608 			} else {
609 				AccessAction ("Mixer", "scroll-right");
610 				AccessAction ("Editor", "step-tracks-down");
611 			}
612 			break;
613 		case NavZoom:
614 			if (neg) {
615 				ZoomOut ();
616 			} else {
617 				ZoomIn ();
618 			}
619 			break;
620 		case NavMarker:
621 		case NavScroll:
622 			ScrollTimeline ((neg ? -1.f : 1.f) * steps / (shift_mod() ? 1024.f : 256.f));
623 			break;
624 		case NavBank:
625 			bank (neg, false);
626 			break;
627 		case NavMaster:
628 			{
629 				/* master || monitor level */
630 				boost::shared_ptr<AutomationControl> ac;
631 				if (session->monitor_active() && !_ctrls.button (FP8Controls::BtnMaster).is_pressed ()) {
632 					ac = session->monitor_out()->gain_control ();
633 				} else if (session->master_out()) {
634 					ac = session->master_out()->gain_control ();
635 				}
636 				if (ac) {
637 					double v = ac->internal_to_interface (ac->get_value());
638 					v = std::max (0.0, std::min (1.0, v + steps * (neg ? -.01 : .01)));
639 					ac->start_touch (ac->session().transport_sample());
640 					ac->set_value (ac->interface_to_internal(v), PBD::Controllable::NoGroup);
641 				}
642 			}
643 			break;
644 		case NavSection:
645 			if (neg) {
646 				AccessAction ("Common", "nudge-playhead-backward");
647 			} else {
648 				AccessAction ("Common", "nudge-playhead-forward");
649 			}
650 			break;
651 		case NavPan:
652 			abort(); /*NOTREACHED*/
653 			break;
654 	}
655 }
656 
657 /* handle pan/param encoder press */
658 void
button_parameter()659 FaderPort8::button_parameter ()
660 {
661 	switch (_ctrls.fader_mode()) {
662 		case ModeTrack:
663 		case ModePan:
664 			if (_link_enabled || _link_locked) {
665 				handle_encoder_link (0);
666 			} else {
667 				handle_encoder_pan (0);
668 			}
669 			break;
670 		case ModePlugins:
671 			toggle_preset_param_mode ();
672 			break;
673 		case ModeSend:
674 			break;
675 	}
676 }
677 
678 /* handle pan/param encoder turn */
679 void
encoder_parameter(bool neg,int steps)680 FaderPort8::encoder_parameter (bool neg, int steps)
681 {
682 	switch (_ctrls.fader_mode()) {
683 		case ModeTrack:
684 		case ModePan:
685 			if (steps != 0) {
686 				if (_link_enabled || _link_locked) {
687 					handle_encoder_link (neg ? -steps : steps);
688 				} else {
689 					handle_encoder_pan (neg ? -steps : steps);
690 				}
691 			}
692 			break;
693 		case ModePlugins:
694 		case ModeSend:
695 			while (steps > 0) {
696 				bank_param (neg, shift_mod());
697 				--steps;
698 			}
699 			break;
700 	}
701 }
702 
703 /* handle user-specific actions */
704 void
button_user(bool press,FP8Controls::ButtonId btn)705 FaderPort8::button_user (bool press, FP8Controls::ButtonId btn)
706 {
707 	_user_action_map[btn].call (*this, press);
708 }
709